pg_reports 0.5.4 → 0.6.1
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 +69 -0
- data/README.md +123 -370
- data/app/controllers/pg_reports/dashboard_controller.rb +21 -21
- data/app/views/layouts/pg_reports/application.html.erb +135 -69
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +22 -22
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +105 -55
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +49 -11
- data/app/views/pg_reports/dashboard/index.html.erb +123 -114
- data/app/views/pg_reports/dashboard/show.html.erb +30 -26
- data/config/locales/en.yml +597 -0
- data/config/locales/ru.yml +562 -0
- data/config/locales/uk.yml +607 -0
- data/lib/pg_reports/compatibility.rb +63 -0
- data/lib/pg_reports/configuration.rb +2 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +112 -5
- data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
- data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
- data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
- data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
- data/lib/pg_reports/definitions/schema_analysis/always_null_columns.yml +31 -0
- data/lib/pg_reports/definitions/schema_analysis/unused_columns.yml +32 -0
- data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
- data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
- data/lib/pg_reports/definitions/tables/unused_tables.yml +30 -0
- data/lib/pg_reports/definitions/tables/update_hotspots.yml +32 -0
- data/lib/pg_reports/engine.rb +6 -0
- data/lib/pg_reports/module_generator.rb +2 -1
- data/lib/pg_reports/modules/indexes.rb +3 -0
- data/lib/pg_reports/modules/queries.rb +1 -0
- data/lib/pg_reports/modules/schema_analysis.rb +261 -2
- data/lib/pg_reports/modules/system.rb +27 -0
- data/lib/pg_reports/modules/tables.rb +1 -0
- data/lib/pg_reports/query_monitor.rb +64 -36
- data/lib/pg_reports/report_definition.rb +20 -24
- data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
- data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
- data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
- data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
- data/lib/pg_reports/sql/schema_analysis/always_null_columns.sql +25 -0
- data/lib/pg_reports/sql/schema_analysis/unused_columns.sql +36 -0
- data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
- data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
- data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
- data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
- data/lib/pg_reports/sql/tables/unused_tables.sql +19 -0
- data/lib/pg_reports/sql/tables/update_hotspots.sql +26 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +5 -0
- metadata +24 -1
|
@@ -19,7 +19,7 @@ module PgReports
|
|
|
19
19
|
|
|
20
20
|
def reset_statistics
|
|
21
21
|
PgReports.reset_statistics!
|
|
22
|
-
render json: {success: true, message: "
|
|
22
|
+
render json: {success: true, message: I18n.t("pg_reports.ui.success.statistics_reset")}
|
|
23
23
|
rescue => e
|
|
24
24
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
25
25
|
end
|
|
@@ -35,7 +35,7 @@ module PgReports
|
|
|
35
35
|
if data[:connections][:total].nil? && data[:transactions][:total].nil?
|
|
36
36
|
render json: {
|
|
37
37
|
success: false,
|
|
38
|
-
error: "
|
|
38
|
+
error: I18n.t("pg_reports.ui.errors.fetch_metrics_check_perms"),
|
|
39
39
|
available: false
|
|
40
40
|
}, status: :service_unavailable
|
|
41
41
|
return
|
|
@@ -50,7 +50,7 @@ module PgReports
|
|
|
50
50
|
rescue PG::InsufficientPrivilege
|
|
51
51
|
render json: {
|
|
52
52
|
success: false,
|
|
53
|
-
error: "
|
|
53
|
+
error: I18n.t("pg_reports.ui.errors.insufficient_database_perms"),
|
|
54
54
|
available: false
|
|
55
55
|
}, status: :forbidden
|
|
56
56
|
rescue => e
|
|
@@ -68,7 +68,7 @@ module PgReports
|
|
|
68
68
|
@report_info = Dashboard::ReportsRegistry.find(@category, @report_key)
|
|
69
69
|
|
|
70
70
|
if @report_info.nil?
|
|
71
|
-
redirect_to root_path, alert: "
|
|
71
|
+
redirect_to root_path, alert: I18n.t("pg_reports.ui.errors.report_not_found")
|
|
72
72
|
return
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -138,7 +138,7 @@ module PgReports
|
|
|
138
138
|
report.send_to_telegram
|
|
139
139
|
end
|
|
140
140
|
|
|
141
|
-
render json: {success: true, message: "
|
|
141
|
+
render json: {success: true, message: I18n.t("pg_reports.ui.success.telegram_sent")}
|
|
142
142
|
rescue => e
|
|
143
143
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
144
144
|
end
|
|
@@ -177,7 +177,7 @@ module PgReports
|
|
|
177
177
|
query_params = params[:params] || {}
|
|
178
178
|
|
|
179
179
|
if query_hash.blank?
|
|
180
|
-
render json: {success: false, error: "
|
|
180
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.query_hash_required")}, status: :unprocessable_entity
|
|
181
181
|
return
|
|
182
182
|
end
|
|
183
183
|
|
|
@@ -185,7 +185,7 @@ module PgReports
|
|
|
185
185
|
unless PgReports.config.allow_raw_query_execution
|
|
186
186
|
render json: {
|
|
187
187
|
success: false,
|
|
188
|
-
error: "
|
|
188
|
+
error: I18n.t("pg_reports.ui.errors.query_execution_disabled")
|
|
189
189
|
}, status: :forbidden
|
|
190
190
|
return
|
|
191
191
|
end
|
|
@@ -195,11 +195,11 @@ module PgReports
|
|
|
195
195
|
query = retrieve_query_by_hash(query_hash)
|
|
196
196
|
|
|
197
197
|
if query.nil?
|
|
198
|
-
render json: {success: false, error: "
|
|
198
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.query_not_found_expired")}, status: :not_found
|
|
199
199
|
return
|
|
200
200
|
end
|
|
201
201
|
rescue SecurityError => e
|
|
202
|
-
render json: {success: false, error: "
|
|
202
|
+
render json: {success: false, error: "#{I18n.t("pg_reports.ui.errors.security_violation_prefix")} #{e.message}"}, status: :forbidden
|
|
203
203
|
return
|
|
204
204
|
end
|
|
205
205
|
|
|
@@ -207,7 +207,7 @@ module PgReports
|
|
|
207
207
|
if query.match?(/\b(NEW|OLD)\./i)
|
|
208
208
|
render json: {
|
|
209
209
|
success: false,
|
|
210
|
-
error: "
|
|
210
|
+
error: I18n.t("pg_reports.ui.errors.trigger_variables_not_allowed")
|
|
211
211
|
}, status: :unprocessable_entity
|
|
212
212
|
return
|
|
213
213
|
end
|
|
@@ -219,7 +219,7 @@ module PgReports
|
|
|
219
219
|
if final_query.match?(/\$\d+/)
|
|
220
220
|
render json: {
|
|
221
221
|
success: false,
|
|
222
|
-
error: "
|
|
222
|
+
error: I18n.t("pg_reports.ui.errors.missing_parameter_values")
|
|
223
223
|
}, status: :unprocessable_entity
|
|
224
224
|
return
|
|
225
225
|
end
|
|
@@ -248,7 +248,7 @@ module PgReports
|
|
|
248
248
|
query_params = params[:params] || {}
|
|
249
249
|
|
|
250
250
|
if query_hash.blank?
|
|
251
|
-
render json: {success: false, error: "
|
|
251
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.query_hash_required")}, status: :unprocessable_entity
|
|
252
252
|
return
|
|
253
253
|
end
|
|
254
254
|
|
|
@@ -256,7 +256,7 @@ module PgReports
|
|
|
256
256
|
unless PgReports.config.allow_raw_query_execution
|
|
257
257
|
render json: {
|
|
258
258
|
success: false,
|
|
259
|
-
error: "
|
|
259
|
+
error: I18n.t("pg_reports.ui.errors.query_execution_disabled")
|
|
260
260
|
}, status: :forbidden
|
|
261
261
|
return
|
|
262
262
|
end
|
|
@@ -266,11 +266,11 @@ module PgReports
|
|
|
266
266
|
query = retrieve_query_by_hash(query_hash)
|
|
267
267
|
|
|
268
268
|
if query.nil?
|
|
269
|
-
render json: {success: false, error: "
|
|
269
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.query_not_found_expired")}, status: :not_found
|
|
270
270
|
return
|
|
271
271
|
end
|
|
272
272
|
rescue SecurityError => e
|
|
273
|
-
render json: {success: false, error: "
|
|
273
|
+
render json: {success: false, error: "#{I18n.t("pg_reports.ui.errors.security_violation_prefix")} #{e.message}"}, status: :forbidden
|
|
274
274
|
return
|
|
275
275
|
end
|
|
276
276
|
|
|
@@ -281,7 +281,7 @@ module PgReports
|
|
|
281
281
|
if final_query.match?(/\$\d+/)
|
|
282
282
|
render json: {
|
|
283
283
|
success: false,
|
|
284
|
-
error: "
|
|
284
|
+
error: I18n.t("pg_reports.ui.errors.missing_parameter_values")
|
|
285
285
|
}, status: :unprocessable_entity
|
|
286
286
|
return
|
|
287
287
|
end
|
|
@@ -326,7 +326,7 @@ module PgReports
|
|
|
326
326
|
unless Rails.env.development?
|
|
327
327
|
render json: {
|
|
328
328
|
success: false,
|
|
329
|
-
error: "
|
|
329
|
+
error: I18n.t("pg_reports.ui.errors.migration_dev_only")
|
|
330
330
|
}, status: :forbidden
|
|
331
331
|
return
|
|
332
332
|
end
|
|
@@ -335,28 +335,28 @@ module PgReports
|
|
|
335
335
|
code = params[:code]
|
|
336
336
|
|
|
337
337
|
if file_name.blank? || code.blank?
|
|
338
|
-
render json: {success: false, error: "
|
|
338
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.filename_code_required")}, status: :unprocessable_entity
|
|
339
339
|
return
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
# Sanitize file name
|
|
343
343
|
safe_file_name = file_name.gsub(/[^a-z0-9_.]/, "")
|
|
344
344
|
unless safe_file_name.match?(/\A\d{14}_\w+\.rb\z/)
|
|
345
|
-
render json: {success: false, error: "
|
|
345
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.invalid_filename_format")}, status: :unprocessable_entity
|
|
346
346
|
return
|
|
347
347
|
end
|
|
348
348
|
|
|
349
349
|
# Find migrations directory
|
|
350
350
|
migrations_path = Rails.root.join("db", "migrate")
|
|
351
351
|
unless migrations_path.exist?
|
|
352
|
-
render json: {success: false, error: "
|
|
352
|
+
render json: {success: false, error: I18n.t("pg_reports.ui.errors.migrations_dir_not_found")}, status: :unprocessable_entity
|
|
353
353
|
return
|
|
354
354
|
end
|
|
355
355
|
|
|
356
356
|
file_path = migrations_path.join(safe_file_name)
|
|
357
357
|
File.write(file_path, code)
|
|
358
358
|
|
|
359
|
-
render json: {success: true, file_path: file_path.to_s, message: "
|
|
359
|
+
render json: {success: true, file_path: file_path.to_s, message: I18n.t("pg_reports.ui.success.migration_created")}
|
|
360
360
|
rescue => e
|
|
361
361
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
362
362
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="<%= I18n.locale %>">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<meta name="pg-reports-root" content="<%= request.script_name.presence || PgReports::Engine.routes.url_helpers.root_path %>">
|
|
7
|
-
<title
|
|
7
|
+
<title><%= t("pg_reports.ui.branding.page_title") %></title>
|
|
8
8
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%234f46e5'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
|
|
9
9
|
<% if PgReports.config.load_external_fonts %>
|
|
10
10
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
@@ -13,22 +13,22 @@
|
|
|
13
13
|
<% end %>
|
|
14
14
|
<style>
|
|
15
15
|
:root {
|
|
16
|
-
--bg-primary: #
|
|
17
|
-
--bg-secondary: #
|
|
18
|
-
--bg-tertiary: #
|
|
19
|
-
--bg-card: #
|
|
20
|
-
--border-color: #
|
|
21
|
-
--text-primary: #
|
|
22
|
-
--text-secondary: #
|
|
23
|
-
--text-muted: #
|
|
24
|
-
--accent-purple: #
|
|
25
|
-
--accent-blue: #
|
|
26
|
-
--accent-green: #
|
|
27
|
-
--accent-amber: #
|
|
28
|
-
--accent-rose: #
|
|
29
|
-
--accent-indigo: #
|
|
30
|
-
--gradient-start: #
|
|
31
|
-
--gradient-end: #
|
|
16
|
+
--bg-primary: #151719;
|
|
17
|
+
--bg-secondary: #1c1f22;
|
|
18
|
+
--bg-tertiary: #232629;
|
|
19
|
+
--bg-card: #1c1f22;
|
|
20
|
+
--border-color: #2e3235;
|
|
21
|
+
--text-primary: #c9cbcd;
|
|
22
|
+
--text-secondary: #888d93;
|
|
23
|
+
--text-muted: #5e6369;
|
|
24
|
+
--accent-purple: #7c8af6;
|
|
25
|
+
--accent-blue: #5c9eed;
|
|
26
|
+
--accent-green: #45a87a;
|
|
27
|
+
--accent-amber: #c89030;
|
|
28
|
+
--accent-rose: #d45d6e;
|
|
29
|
+
--accent-indigo: #7c8af6;
|
|
30
|
+
--gradient-start: #7c8af6;
|
|
31
|
+
--gradient-end: #7c8af6;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
* {
|
|
@@ -68,27 +68,45 @@
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.logo-icon {
|
|
71
|
-
width:
|
|
72
|
-
height:
|
|
73
|
-
background:
|
|
71
|
+
width: 32px;
|
|
72
|
+
height: 32px;
|
|
73
|
+
background: var(--accent-purple);
|
|
74
74
|
border-radius: 6px;
|
|
75
75
|
display: flex;
|
|
76
76
|
align-items: center;
|
|
77
77
|
justify-content: center;
|
|
78
|
-
font-size:
|
|
78
|
+
font-size: 1rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.logo-text {
|
|
82
|
+
display: flex;
|
|
83
|
+
flex-direction: column;
|
|
84
|
+
gap: 5px;
|
|
85
|
+
line-height: 1.15;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
.logo-text h1 {
|
|
82
|
-
font-size: 1.
|
|
83
|
-
font-weight:
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
89
|
+
font-size: 1.1rem;
|
|
90
|
+
font-weight: 600;
|
|
91
|
+
color: var(--text-primary);
|
|
92
|
+
line-height: 1.15;
|
|
93
|
+
margin-top: -3px;
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: baseline;
|
|
96
|
+
gap: 0.5rem;
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
.logo-
|
|
90
|
-
font-size: 0.
|
|
99
|
+
.logo-version {
|
|
100
|
+
font-size: 0.7rem;
|
|
101
|
+
font-weight: 500;
|
|
102
|
+
color: var(--text-muted);
|
|
103
|
+
letter-spacing: 0.02em;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.logo-subtitle {
|
|
107
|
+
font-size: 0.7rem;
|
|
91
108
|
color: var(--text-muted);
|
|
109
|
+
line-height: 1.15;
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
.header-badge {
|
|
@@ -130,14 +148,12 @@
|
|
|
130
148
|
.category-card {
|
|
131
149
|
background: var(--bg-card);
|
|
132
150
|
border: 1px solid var(--border-color);
|
|
133
|
-
border-radius:
|
|
151
|
+
border-radius: 6px;
|
|
134
152
|
padding: 1rem;
|
|
135
|
-
transition: all 0.2s ease;
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
.category-card:hover {
|
|
139
|
-
border-color:
|
|
140
|
-
transform: translateY(-1px);
|
|
156
|
+
border-color: #3e4347;
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
.category-header {
|
|
@@ -148,13 +164,15 @@
|
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
.category-icon {
|
|
151
|
-
width:
|
|
152
|
-
height:
|
|
153
|
-
border-radius:
|
|
167
|
+
width: 28px;
|
|
168
|
+
height: 28px;
|
|
169
|
+
border-radius: 4px;
|
|
154
170
|
display: flex;
|
|
155
171
|
align-items: center;
|
|
156
172
|
justify-content: center;
|
|
157
|
-
font-size:
|
|
173
|
+
font-size: 0.95rem;
|
|
174
|
+
background: transparent !important;
|
|
175
|
+
color: var(--text-secondary) !important;
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
.category-title {
|
|
@@ -181,19 +199,18 @@
|
|
|
181
199
|
display: flex;
|
|
182
200
|
align-items: center;
|
|
183
201
|
justify-content: space-between;
|
|
184
|
-
padding: 0.
|
|
185
|
-
background:
|
|
202
|
+
padding: 0.45rem 0.7rem;
|
|
203
|
+
background: transparent;
|
|
186
204
|
border: 1px solid transparent;
|
|
187
|
-
border-radius:
|
|
205
|
+
border-radius: 4px;
|
|
188
206
|
color: var(--text-secondary);
|
|
189
207
|
text-decoration: none;
|
|
190
|
-
font-size: 0.
|
|
191
|
-
transition:
|
|
208
|
+
font-size: 0.82rem;
|
|
209
|
+
transition: background 0.1s, color 0.1s;
|
|
192
210
|
}
|
|
193
211
|
|
|
194
212
|
.report-link:hover {
|
|
195
|
-
background: var(--bg-
|
|
196
|
-
border-color: var(--border-color);
|
|
213
|
+
background: var(--bg-tertiary);
|
|
197
214
|
color: var(--text-primary);
|
|
198
215
|
}
|
|
199
216
|
|
|
@@ -208,6 +225,20 @@
|
|
|
208
225
|
transform: translateX(0);
|
|
209
226
|
}
|
|
210
227
|
|
|
228
|
+
.report-badge-new {
|
|
229
|
+
display: inline-block;
|
|
230
|
+
margin-left: 0.4rem;
|
|
231
|
+
padding: 0.1rem 0.35rem;
|
|
232
|
+
font-size: 0.55rem;
|
|
233
|
+
font-weight: 600;
|
|
234
|
+
letter-spacing: 0.04em;
|
|
235
|
+
line-height: 1.2;
|
|
236
|
+
color: var(--accent-green);
|
|
237
|
+
background: rgba(69, 168, 122, 0.12);
|
|
238
|
+
border-radius: 3px;
|
|
239
|
+
vertical-align: middle;
|
|
240
|
+
}
|
|
241
|
+
|
|
211
242
|
/* Report Detail Page */
|
|
212
243
|
.report-page {
|
|
213
244
|
display: flex;
|
|
@@ -230,7 +261,7 @@
|
|
|
230
261
|
}
|
|
231
262
|
|
|
232
263
|
.breadcrumb a:hover {
|
|
233
|
-
color: var(--
|
|
264
|
+
color: var(--text-primary);
|
|
234
265
|
}
|
|
235
266
|
|
|
236
267
|
.report-header {
|
|
@@ -264,25 +295,24 @@
|
|
|
264
295
|
height: 36px;
|
|
265
296
|
padding: 0 1rem;
|
|
266
297
|
border: 1px solid transparent;
|
|
267
|
-
border-radius:
|
|
298
|
+
border-radius: 4px;
|
|
268
299
|
font-family: inherit;
|
|
269
300
|
font-size: 0.8rem;
|
|
270
301
|
font-weight: 500;
|
|
271
302
|
cursor: pointer;
|
|
272
|
-
transition:
|
|
303
|
+
transition: background 0.1s;
|
|
273
304
|
white-space: nowrap;
|
|
274
305
|
text-decoration: none;
|
|
275
306
|
}
|
|
276
307
|
|
|
277
308
|
.btn-primary {
|
|
278
|
-
background:
|
|
279
|
-
color:
|
|
280
|
-
border-color:
|
|
309
|
+
background: var(--accent-purple);
|
|
310
|
+
color: #fff;
|
|
311
|
+
border-color: var(--accent-purple);
|
|
281
312
|
}
|
|
282
313
|
|
|
283
314
|
.btn-primary:hover {
|
|
284
|
-
|
|
285
|
-
transform: translateY(-1px);
|
|
315
|
+
background: #6b79e4;
|
|
286
316
|
}
|
|
287
317
|
|
|
288
318
|
.btn-secondary {
|
|
@@ -292,19 +322,19 @@
|
|
|
292
322
|
}
|
|
293
323
|
|
|
294
324
|
.btn-secondary:hover {
|
|
295
|
-
background:
|
|
325
|
+
background: #2a2d31;
|
|
296
326
|
color: var(--text-primary);
|
|
297
327
|
}
|
|
298
328
|
|
|
299
329
|
.btn-telegram {
|
|
300
|
-
background:
|
|
301
|
-
color:
|
|
302
|
-
border-color
|
|
330
|
+
background: var(--bg-tertiary);
|
|
331
|
+
color: var(--text-secondary);
|
|
332
|
+
border: 1px solid var(--border-color);
|
|
303
333
|
}
|
|
304
334
|
|
|
305
335
|
.btn-telegram:hover {
|
|
306
|
-
background: #
|
|
307
|
-
|
|
336
|
+
background: #2a2d31;
|
|
337
|
+
color: var(--text-primary);
|
|
308
338
|
}
|
|
309
339
|
|
|
310
340
|
.btn:disabled {
|
|
@@ -316,7 +346,7 @@
|
|
|
316
346
|
.results-container {
|
|
317
347
|
background: var(--bg-card);
|
|
318
348
|
border: 1px solid var(--border-color);
|
|
319
|
-
border-radius:
|
|
349
|
+
border-radius: 6px;
|
|
320
350
|
overflow: hidden;
|
|
321
351
|
}
|
|
322
352
|
|
|
@@ -462,22 +492,51 @@
|
|
|
462
492
|
.modal {
|
|
463
493
|
position: fixed;
|
|
464
494
|
inset: 0;
|
|
465
|
-
background: rgba(0, 0, 0, 0.
|
|
495
|
+
background: rgba(0, 0, 0, 0.6);
|
|
466
496
|
display: flex;
|
|
467
497
|
align-items: center;
|
|
468
498
|
justify-content: center;
|
|
469
499
|
z-index: 1000;
|
|
470
|
-
backdrop-filter: blur(4px);
|
|
471
500
|
}
|
|
472
501
|
|
|
473
502
|
.modal-content {
|
|
474
503
|
background: var(--bg-card);
|
|
475
504
|
border: 1px solid var(--border-color);
|
|
476
|
-
border-radius:
|
|
505
|
+
border-radius: 6px;
|
|
477
506
|
max-width: 900px;
|
|
478
507
|
width: 90%;
|
|
479
508
|
max-height: 90vh;
|
|
480
509
|
overflow: auto;
|
|
510
|
+
scrollbar-width: thin;
|
|
511
|
+
scrollbar-color: var(--border-color) var(--bg-secondary);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.modal-content::-webkit-scrollbar,
|
|
515
|
+
.modal-body::-webkit-scrollbar {
|
|
516
|
+
width: 8px;
|
|
517
|
+
height: 8px;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.modal-content::-webkit-scrollbar-track,
|
|
521
|
+
.modal-body::-webkit-scrollbar-track {
|
|
522
|
+
background: var(--bg-secondary);
|
|
523
|
+
border-radius: 4px;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.modal-content::-webkit-scrollbar-thumb,
|
|
527
|
+
.modal-body::-webkit-scrollbar-thumb {
|
|
528
|
+
background: var(--border-color);
|
|
529
|
+
border-radius: 4px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.modal-content::-webkit-scrollbar-thumb:hover,
|
|
533
|
+
.modal-body::-webkit-scrollbar-thumb:hover {
|
|
534
|
+
background: var(--text-muted);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.modal-content::-webkit-scrollbar-corner,
|
|
538
|
+
.modal-body::-webkit-scrollbar-corner {
|
|
539
|
+
background: var(--bg-secondary);
|
|
481
540
|
}
|
|
482
541
|
|
|
483
542
|
.modal-large {
|
|
@@ -553,7 +612,7 @@
|
|
|
553
612
|
margin-bottom: 1.5rem;
|
|
554
613
|
background: var(--bg-card);
|
|
555
614
|
border: 1px solid var(--border-color);
|
|
556
|
-
border-radius:
|
|
615
|
+
border-radius: 6px;
|
|
557
616
|
padding: 0.875rem 1rem;
|
|
558
617
|
}
|
|
559
618
|
|
|
@@ -899,7 +958,7 @@
|
|
|
899
958
|
padding: 0.75rem 1rem;
|
|
900
959
|
background: var(--bg-tertiary);
|
|
901
960
|
border: 1px solid var(--border-color);
|
|
902
|
-
border-radius:
|
|
961
|
+
border-radius: 6px;
|
|
903
962
|
cursor: pointer;
|
|
904
963
|
transition: all 0.15s;
|
|
905
964
|
}
|
|
@@ -945,6 +1004,13 @@
|
|
|
945
1004
|
|
|
946
1005
|
<script>
|
|
947
1006
|
const pgReportsRoot = document.querySelector('meta[name="pg-reports-root"]')?.content || '/pg_reports';
|
|
1007
|
+
window.PG_REPORTS_I18N = <%= raw I18n.t("pg_reports.ui").to_json %>;
|
|
1008
|
+
|
|
1009
|
+
// Format strings with %{var} placeholders
|
|
1010
|
+
window.pgReportsFormat = function(template, vars) {
|
|
1011
|
+
if (!template) return '';
|
|
1012
|
+
return template.replace(/%\{(\w+)\}/g, function(_, k) { return vars[k] != null ? vars[k] : ''; });
|
|
1013
|
+
};
|
|
948
1014
|
|
|
949
1015
|
function showToast(message, type = 'success') {
|
|
950
1016
|
const toast = document.getElementById('toast');
|
|
@@ -958,7 +1024,7 @@
|
|
|
958
1024
|
async function sendToTelegram(category, report, button) {
|
|
959
1025
|
if (button) {
|
|
960
1026
|
button.disabled = true;
|
|
961
|
-
button.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;vertical-align:middle;"></span>
|
|
1027
|
+
button.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;vertical-align:middle;"></span> ' + PG_REPORTS_I18N.actions.sending;
|
|
962
1028
|
}
|
|
963
1029
|
|
|
964
1030
|
try {
|
|
@@ -973,17 +1039,17 @@
|
|
|
973
1039
|
const data = await response.json();
|
|
974
1040
|
|
|
975
1041
|
if (data.success) {
|
|
976
|
-
showToast(
|
|
1042
|
+
showToast(PG_REPORTS_I18N.success.telegram_sent);
|
|
977
1043
|
} else {
|
|
978
|
-
showToast(data.error ||
|
|
1044
|
+
showToast(data.error || PG_REPORTS_I18N.errors.send_telegram_failed, 'error');
|
|
979
1045
|
}
|
|
980
1046
|
} catch (error) {
|
|
981
|
-
showToast(
|
|
1047
|
+
showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
|
|
982
1048
|
}
|
|
983
1049
|
|
|
984
1050
|
if (button) {
|
|
985
1051
|
button.disabled = false;
|
|
986
|
-
button.innerHTML =
|
|
1052
|
+
button.innerHTML = PG_REPORTS_I18N.actions.send_telegram;
|
|
987
1053
|
}
|
|
988
1054
|
}
|
|
989
1055
|
</script>
|
|
@@ -2,39 +2,39 @@
|
|
|
2
2
|
<div id="ide-settings-modal" class="modal" style="display: none;">
|
|
3
3
|
<div class="modal-content modal-small">
|
|
4
4
|
<div class="modal-header">
|
|
5
|
-
<h3
|
|
5
|
+
<h3><%= t("pg_reports.ui.modals.ide_settings_title") %></h3>
|
|
6
6
|
<button class="modal-close" onclick="closeIdeSettingsModal()">×</button>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="modal-body">
|
|
9
|
-
<p class="settings-label"
|
|
9
|
+
<p class="settings-label"><%= t("pg_reports.ui.settings.default_ide_label") %></p>
|
|
10
10
|
<div class="ide-options">
|
|
11
11
|
<label class="ide-option">
|
|
12
12
|
<input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
|
|
13
|
-
<span
|
|
13
|
+
<span><%= t("pg_reports.ui.settings.ide_show_menu") %></span>
|
|
14
14
|
</label>
|
|
15
15
|
<label class="ide-option">
|
|
16
16
|
<input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
|
|
17
|
-
<span
|
|
17
|
+
<span><%= t("pg_reports.ui.settings.ide_vscode_wsl") %></span>
|
|
18
18
|
</label>
|
|
19
19
|
<label class="ide-option">
|
|
20
20
|
<input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
|
|
21
|
-
<span
|
|
21
|
+
<span><%= t("pg_reports.ui.settings.ide_vscode") %></span>
|
|
22
22
|
</label>
|
|
23
23
|
<label class="ide-option">
|
|
24
24
|
<input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
|
|
25
|
-
<span
|
|
25
|
+
<span><%= t("pg_reports.ui.settings.ide_rubymine") %></span>
|
|
26
26
|
</label>
|
|
27
27
|
<label class="ide-option">
|
|
28
28
|
<input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
|
|
29
|
-
<span
|
|
29
|
+
<span><%= t("pg_reports.ui.settings.ide_intellij") %></span>
|
|
30
30
|
</label>
|
|
31
31
|
<label class="ide-option">
|
|
32
32
|
<input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
|
|
33
|
-
<span
|
|
33
|
+
<span><%= t("pg_reports.ui.settings.ide_cursor_wsl") %></span>
|
|
34
34
|
</label>
|
|
35
35
|
<label class="ide-option">
|
|
36
36
|
<input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
|
|
37
|
-
<span
|
|
37
|
+
<span><%= t("pg_reports.ui.settings.ide_cursor") %></span>
|
|
38
38
|
</label>
|
|
39
39
|
</div>
|
|
40
40
|
</div>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<div id="problem-modal" class="problem-modal" style="display: none;">
|
|
46
46
|
<div class="problem-modal-content">
|
|
47
47
|
<div class="problem-modal-header">
|
|
48
|
-
<h3
|
|
48
|
+
<h3><%= t("pg_reports.ui.modals.problem_detected_title") %></h3>
|
|
49
49
|
<button class="modal-close" onclick="closeProblemModal()">×</button>
|
|
50
50
|
</div>
|
|
51
51
|
<div class="problem-modal-body" id="problem-modal-body">
|
|
@@ -58,24 +58,24 @@
|
|
|
58
58
|
<div id="explain-modal" class="modal" style="display: none;">
|
|
59
59
|
<div class="modal-content modal-wide">
|
|
60
60
|
<div class="modal-header">
|
|
61
|
-
<h3
|
|
61
|
+
<h3><%= t("pg_reports.ui.modals.query_analyzer_title") %></h3>
|
|
62
62
|
<button class="modal-close" onclick="closeExplainModal()">×</button>
|
|
63
63
|
</div>
|
|
64
64
|
<div class="modal-body" id="explain-modal-body">
|
|
65
65
|
<div class="explain-query-section">
|
|
66
|
-
<label class="explain-label"
|
|
66
|
+
<label class="explain-label"><%= t("pg_reports.ui.modals.query_label") %></label>
|
|
67
67
|
<pre class="explain-query" id="explain-query-display"></pre>
|
|
68
68
|
</div>
|
|
69
69
|
<div id="explain-params-section" class="explain-params-section" style="display: none;">
|
|
70
|
-
<label class="explain-label"
|
|
70
|
+
<label class="explain-label"><%= t("pg_reports.ui.modals.parameters_label") %></label>
|
|
71
71
|
<div id="explain-params-inputs"></div>
|
|
72
72
|
</div>
|
|
73
73
|
<div class="explain-actions">
|
|
74
74
|
<button class="btn btn-secondary" onclick="executeExplainAnalyze()" id="btn-explain">
|
|
75
|
-
|
|
75
|
+
<%= t("pg_reports.ui.actions.explain_analyze") %>
|
|
76
76
|
</button>
|
|
77
77
|
<button class="btn btn-secondary" onclick="executeQuery()" id="btn-execute">
|
|
78
|
-
|
|
78
|
+
<%= t("pg_reports.ui.actions.execute_query") %>
|
|
79
79
|
</button>
|
|
80
80
|
</div>
|
|
81
81
|
<div id="explain-loading" class="loading" style="display: none;">
|
|
@@ -90,22 +90,22 @@
|
|
|
90
90
|
<div id="migration-modal" class="modal" style="display: none;">
|
|
91
91
|
<div class="modal-content">
|
|
92
92
|
<div class="modal-header">
|
|
93
|
-
<h3
|
|
93
|
+
<h3><%= t("pg_reports.ui.modals.migration_title") %></h3>
|
|
94
94
|
<button class="modal-close" onclick="closeMigrationModal()">×</button>
|
|
95
95
|
</div>
|
|
96
96
|
<div class="modal-body" id="migration-modal-body">
|
|
97
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;"
|
|
98
|
+
<p style="margin: 0 0 0.5rem 0; font-weight: 600; color: #ffb74d;"><%= t("pg_reports.ui.documentation.threshold_warning_label").chomp(":") %></p>
|
|
99
99
|
<p style="margin: 0; color: #ffcc80; font-size: 0.875rem; line-height: 1.5;">
|
|
100
|
-
|
|
101
|
-
<strong
|
|
100
|
+
<%= t("pg_reports.ui.modals.migration_warning") %>
|
|
101
|
+
<strong><%= t("pg_reports.ui.modals.migration_warning_dev_only") %></strong>
|
|
102
102
|
</p>
|
|
103
103
|
</div>
|
|
104
|
-
<p class="settings-label"
|
|
104
|
+
<p class="settings-label"><%= t("pg_reports.ui.modals.migration_subtitle") %></p>
|
|
105
105
|
<div id="migration-code" class="migration-code"></div>
|
|
106
106
|
<div class="migration-actions">
|
|
107
|
-
<button class="btn btn-secondary" onclick="copyMigrationCode()"
|
|
108
|
-
<button class="btn btn-primary" id="create-migration-btn" onclick="createMigrationFile()"
|
|
107
|
+
<button class="btn btn-secondary" onclick="copyMigrationCode()"><%= t("pg_reports.ui.actions.copy_code") %></button>
|
|
108
|
+
<button class="btn btn-primary" id="create-migration-btn" onclick="createMigrationFile()"><%= t("pg_reports.ui.actions.create_migration_file") %></button>
|
|
109
109
|
</div>
|
|
110
110
|
</div>
|
|
111
111
|
</div>
|