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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/README.md +123 -370
  4. data/app/controllers/pg_reports/dashboard_controller.rb +21 -21
  5. data/app/views/layouts/pg_reports/application.html.erb +135 -69
  6. data/app/views/pg_reports/dashboard/_show_modals.html.erb +22 -22
  7. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +105 -55
  8. data/app/views/pg_reports/dashboard/_show_styles.html.erb +49 -11
  9. data/app/views/pg_reports/dashboard/index.html.erb +123 -114
  10. data/app/views/pg_reports/dashboard/show.html.erb +30 -26
  11. data/config/locales/en.yml +597 -0
  12. data/config/locales/ru.yml +562 -0
  13. data/config/locales/uk.yml +607 -0
  14. data/lib/pg_reports/compatibility.rb +63 -0
  15. data/lib/pg_reports/configuration.rb +2 -0
  16. data/lib/pg_reports/dashboard/reports_registry.rb +112 -5
  17. data/lib/pg_reports/definitions/indexes/fk_without_indexes.yml +30 -0
  18. data/lib/pg_reports/definitions/indexes/index_correlation.yml +31 -0
  19. data/lib/pg_reports/definitions/indexes/inefficient_indexes.yml +45 -0
  20. data/lib/pg_reports/definitions/queries/temp_file_queries.yml +39 -0
  21. data/lib/pg_reports/definitions/schema_analysis/always_null_columns.yml +31 -0
  22. data/lib/pg_reports/definitions/schema_analysis/unused_columns.yml +32 -0
  23. data/lib/pg_reports/definitions/system/wraparound_risk.yml +31 -0
  24. data/lib/pg_reports/definitions/tables/tables_without_pk.yml +28 -0
  25. data/lib/pg_reports/definitions/tables/unused_tables.yml +30 -0
  26. data/lib/pg_reports/definitions/tables/update_hotspots.yml +32 -0
  27. data/lib/pg_reports/engine.rb +6 -0
  28. data/lib/pg_reports/module_generator.rb +2 -1
  29. data/lib/pg_reports/modules/indexes.rb +3 -0
  30. data/lib/pg_reports/modules/queries.rb +1 -0
  31. data/lib/pg_reports/modules/schema_analysis.rb +261 -2
  32. data/lib/pg_reports/modules/system.rb +27 -0
  33. data/lib/pg_reports/modules/tables.rb +1 -0
  34. data/lib/pg_reports/query_monitor.rb +64 -36
  35. data/lib/pg_reports/report_definition.rb +20 -24
  36. data/lib/pg_reports/sql/indexes/fk_without_indexes.sql +23 -0
  37. data/lib/pg_reports/sql/indexes/index_correlation.sql +27 -0
  38. data/lib/pg_reports/sql/indexes/inefficient_indexes.sql +22 -0
  39. data/lib/pg_reports/sql/queries/temp_file_queries.sql +16 -0
  40. data/lib/pg_reports/sql/schema_analysis/always_null_columns.sql +25 -0
  41. data/lib/pg_reports/sql/schema_analysis/unused_columns.sql +36 -0
  42. data/lib/pg_reports/sql/system/checkpoint_stats.sql +20 -0
  43. data/lib/pg_reports/sql/system/checkpoint_stats_legacy.sql +19 -0
  44. data/lib/pg_reports/sql/system/wraparound_risk.sql +21 -0
  45. data/lib/pg_reports/sql/tables/tables_without_pk.sql +20 -0
  46. data/lib/pg_reports/sql/tables/unused_tables.sql +19 -0
  47. data/lib/pg_reports/sql/tables/update_hotspots.sql +26 -0
  48. data/lib/pg_reports/version.rb +1 -1
  49. data/lib/pg_reports.rb +5 -0
  50. metadata +24 -1
@@ -4,36 +4,39 @@
4
4
  <div class="logo">
5
5
  <div class="logo-icon">🐘</div>
6
6
  <div class="logo-text">
7
- <h1>PgReports</h1>
8
- <span>PostgreSQL Analysis Dashboard</span>
7
+ <h1>
8
+ <%= t("pg_reports.ui.branding.title") %>
9
+ <span class="logo-version">v<%= PgReports::VERSION %></span>
10
+ </h1>
11
+ <span class="logo-subtitle"><%= t("pg_reports.ui.branding.subtitle") %></span>
9
12
  </div>
10
13
  </div>
11
14
 
12
15
  <div class="header-actions">
13
16
  <% if @pg_stat_status[:ready] %>
14
17
  <button class="btn btn-small btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
15
- 🗑️ Reset Statistics
18
+ <%= t("pg_reports.ui.actions.reset_statistics") %>
16
19
  </button>
17
20
  <% else %>
18
21
  <button class="btn btn-small btn-primary" onclick="enablePgStatStatements(this)" id="enable-btn">
19
- Create Extension
22
+ <%= t("pg_reports.ui.actions.create_extension") %>
20
23
  </button>
21
24
  <button class="btn-info" onclick="showPgStatInfo()">?</button>
22
25
  <% end %>
23
- <button class="btn-info" onclick="showIdeSettingsModal()" title="IDE Settings">⚙️</button>
26
+ <button class="btn-info" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">⚙️</button>
24
27
  <div class="header-badge" id="pg-stat-badge">
25
28
  <% if @pg_stat_status[:ready] %>
26
29
  <span class="badge-dot"></span>
27
- <span>pg_stat_statements ready</span>
30
+ <span><%= t("pg_reports.ui.status.pg_stat_ready") %></span>
28
31
  <% elsif @pg_stat_status[:extension_installed] %>
29
32
  <span class="badge-dot warning"></span>
30
- <span>Extension installed, not preloaded</span>
33
+ <span><%= t("pg_reports.ui.status.extension_installed") %></span>
31
34
  <% elsif @pg_stat_status[:preloaded] %>
32
35
  <span class="badge-dot warning"></span>
33
- <span>Preloaded, extension not created</span>
36
+ <span><%= t("pg_reports.ui.status.preloaded") %></span>
34
37
  <% else %>
35
38
  <span class="badge-dot error"></span>
36
- <span>Not configured</span>
39
+ <span><%= t("pg_reports.ui.status.not_configured") %></span>
37
40
  <% end %>
38
41
  </div>
39
42
  </div>
@@ -43,25 +46,25 @@
43
46
  <div id="pg-stat-modal" class="modal" style="display: none;">
44
47
  <div class="modal-content">
45
48
  <div class="modal-header">
46
- <h3>Enable pg_stat_statements</h3>
49
+ <h3><%= t("pg_reports.ui.modals.enable_pg_stat_title") %></h3>
47
50
  <button class="modal-close" onclick="closePgStatModal()">×</button>
48
51
  </div>
49
52
  <div class="modal-body">
50
- <p>To enable pg_stat_statements, follow these steps:</p>
53
+ <p><%= t("pg_reports.ui.modals.enable_pg_stat_intro") %></p>
51
54
  <ol>
52
55
  <li>
53
- <strong>Edit postgresql.conf:</strong>
56
+ <strong><%= t("pg_reports.ui.modals.edit_postgresql_conf") %></strong>
54
57
  <pre>shared_preload_libraries = 'pg_stat_statements'
55
58
  pg_stat_statements.track = all</pre>
56
59
  </li>
57
60
  <li>
58
- <strong>Restart PostgreSQL:</strong>
61
+ <strong><%= t("pg_reports.ui.modals.restart_postgresql") %></strong>
59
62
  <pre>sudo systemctl restart postgresql</pre>
60
63
  </li>
61
64
  <li>
62
- <strong>Create extension:</strong>
65
+ <strong><%= t("pg_reports.ui.modals.create_extension_step") %></strong>
63
66
  <pre>CREATE EXTENSION IF NOT EXISTS pg_stat_statements;</pre>
64
- <p>Or click "Enable" button after restart.</p>
67
+ <p><%= t("pg_reports.ui.modals.enable_button_note") %></p>
65
68
  </li>
66
69
  </ol>
67
70
  </div>
@@ -72,15 +75,15 @@ pg_stat_statements.track = all</pre>
72
75
  <div id="reset-confirm-modal" class="modal" style="display: none;">
73
76
  <div class="modal-content modal-small">
74
77
  <div class="modal-header modal-header-danger">
75
- <h3>⚠️ Reset Statistics</h3>
78
+ <h3><%= t("pg_reports.ui.modals.reset_stats_title") %></h3>
76
79
  <button class="modal-close" onclick="closeResetConfirmModal()">×</button>
77
80
  </div>
78
81
  <div class="modal-body">
79
- <p class="warning-text">Are you sure you want to reset pg_stat_statements statistics?</p>
80
- <p class="warning-subtext">This action will clear all collected query statistics and cannot be undone.</p>
82
+ <p class="warning-text"><%= t("pg_reports.ui.modals.reset_stats_confirm") %></p>
83
+ <p class="warning-subtext"><%= t("pg_reports.ui.modals.reset_stats_warning") %></p>
81
84
  <div class="modal-actions">
82
- <button class="btn btn-secondary" onclick="closeResetConfirmModal()">Cancel</button>
83
- <button class="btn btn-danger" onclick="resetStatistics()" id="confirm-reset-btn">Yes, Reset</button>
85
+ <button class="btn btn-secondary" onclick="closeResetConfirmModal()"><%= t("pg_reports.ui.actions.cancel") %></button>
86
+ <button class="btn btn-danger" onclick="resetStatistics()" id="confirm-reset-btn"><%= t("pg_reports.ui.actions.confirm_reset") %></button>
84
87
  </div>
85
88
  </div>
86
89
  </div>
@@ -90,39 +93,39 @@ pg_stat_statements.track = all</pre>
90
93
  <div id="ide-settings-modal" class="modal" style="display: none;">
91
94
  <div class="modal-content modal-small">
92
95
  <div class="modal-header">
93
- <h3>⚙️ IDE Settings</h3>
96
+ <h3><%= t("pg_reports.ui.modals.ide_settings_title") %></h3>
94
97
  <button class="modal-close" onclick="closeIdeSettingsModal()">&times;</button>
95
98
  </div>
96
99
  <div class="modal-body">
97
- <p class="settings-label">Default IDE for source links:</p>
100
+ <p class="settings-label"><%= t("pg_reports.ui.settings.default_ide_label") %></p>
98
101
  <div class="ide-options">
99
102
  <label class="ide-option">
100
103
  <input type="radio" name="default-ide" value="" onchange="setDefaultIde('')">
101
- <span>Show menu (default)</span>
104
+ <span><%= t("pg_reports.ui.settings.ide_show_menu") %></span>
102
105
  </label>
103
106
  <label class="ide-option">
104
107
  <input type="radio" name="default-ide" value="vscode-wsl" onchange="setDefaultIde('vscode-wsl')">
105
- <span>VS Code (WSL)</span>
108
+ <span><%= t("pg_reports.ui.settings.ide_vscode_wsl") %></span>
106
109
  </label>
107
110
  <label class="ide-option">
108
111
  <input type="radio" name="default-ide" value="vscode" onchange="setDefaultIde('vscode')">
109
- <span>VS Code</span>
112
+ <span><%= t("pg_reports.ui.settings.ide_vscode") %></span>
110
113
  </label>
111
114
  <label class="ide-option">
112
115
  <input type="radio" name="default-ide" value="rubymine" onchange="setDefaultIde('rubymine')">
113
- <span>RubyMine</span>
116
+ <span><%= t("pg_reports.ui.settings.ide_rubymine") %></span>
114
117
  </label>
115
118
  <label class="ide-option">
116
119
  <input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
117
- <span>IntelliJ IDEA</span>
120
+ <span><%= t("pg_reports.ui.settings.ide_intellij") %></span>
118
121
  </label>
119
122
  <label class="ide-option">
120
123
  <input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
121
- <span>Cursor (WSL)</span>
124
+ <span><%= t("pg_reports.ui.settings.ide_cursor_wsl") %></span>
122
125
  </label>
123
126
  <label class="ide-option">
124
127
  <input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
125
- <span>Cursor</span>
128
+ <span><%= t("pg_reports.ui.settings.ide_cursor") %></span>
126
129
  </label>
127
130
  </div>
128
131
  </div>
@@ -134,14 +137,14 @@ pg_stat_statements.track = all</pre>
134
137
  <div class="live-monitoring-header">
135
138
  <div class="live-monitoring-title">
136
139
  <span class="live-indicator"></span>
137
- <span>Live Monitoring</span>
140
+ <span><%= t("pg_reports.ui.monitoring.live_title") %></span>
138
141
  <span class="database-badge">
139
- Database: <strong><%= @current_database %></strong>
142
+ <strong><%= @current_database %></strong>
140
143
  </span>
141
144
  </div>
142
145
  <div class="live-monitoring-controls">
143
- <span class="live-monitoring-interval">Updates every 5s</span>
144
- <button class="btn-toggle-live" onclick="toggleLiveMonitoring()" title="Toggle live monitoring">
146
+ <span class="live-monitoring-interval"><%= t("pg_reports.ui.monitoring.update_interval") %></span>
147
+ <button class="btn-toggle-live" onclick="toggleLiveMonitoring()" title="<%= t("pg_reports.ui.monitoring.toggle_title") %>">
145
148
  <span id="toggle-icon">⏸</span>
146
149
  </button>
147
150
  </div>
@@ -152,7 +155,7 @@ pg_stat_statements.track = all</pre>
152
155
  <div class="live-metric-card" data-metric="connections">
153
156
  <div class="metric-header">
154
157
  <span class="metric-icon" style="background: rgba(107, 159, 232, 0.12); color: var(--accent-blue);">🔗</span>
155
- <span class="metric-name">Connections</span>
158
+ <span class="metric-name"><%= t("pg_reports.ui.metrics.connections_label") %></span>
156
159
  </div>
157
160
  <div class="metric-body">
158
161
  <div class="metric-value">
@@ -160,7 +163,7 @@ pg_stat_statements.track = all</pre>
160
163
  <span class="metric-unit">/ <span id="metric-connections-max">-</span></span>
161
164
  </div>
162
165
  <div class="metric-detail">
163
- <span id="metric-connections-pct">-</span>% used
166
+ <span id="metric-connections-pct">-</span><%= t("pg_reports.ui.metrics.percent_used_suffix") %>
164
167
  </div>
165
168
  <div class="metric-sparkline">
166
169
  <svg id="sparkline-connections" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
@@ -173,15 +176,15 @@ pg_stat_statements.track = all</pre>
173
176
  <div class="live-metric-card" data-metric="tps">
174
177
  <div class="metric-header">
175
178
  <span class="metric-icon" style="background: rgba(95, 184, 154, 0.12); color: var(--accent-green);">⚡</span>
176
- <span class="metric-name">TPS</span>
179
+ <span class="metric-name"><%= t("pg_reports.ui.metrics.tps_label") %></span>
177
180
  </div>
178
181
  <div class="metric-body">
179
182
  <div class="metric-value">
180
183
  <span id="metric-tps-value">-</span>
181
- <span class="metric-unit">tx/s</span>
184
+ <span class="metric-unit"><%= t("pg_reports.ui.metrics.tps_unit") %></span>
182
185
  </div>
183
186
  <div class="metric-detail">
184
- commit: <span id="metric-tps-commit">-</span> / rollback: <span id="metric-tps-rollback">-</span>
187
+ <%= t("pg_reports.ui.metrics.commit_label") %> <span id="metric-tps-commit">-</span> / <%= t("pg_reports.ui.metrics.rollback_label") %> <span id="metric-tps-rollback">-</span>
185
188
  </div>
186
189
  <div class="metric-sparkline">
187
190
  <svg id="sparkline-tps" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
@@ -194,14 +197,14 @@ pg_stat_statements.track = all</pre>
194
197
  <div class="live-metric-card" data-metric="cache">
195
198
  <div class="metric-header">
196
199
  <span class="metric-icon" style="background: rgba(157, 140, 214, 0.12); color: var(--accent-purple);">💾</span>
197
- <span class="metric-name">Cache Hit</span>
200
+ <span class="metric-name"><%= t("pg_reports.ui.metrics.cache_hit_label") %></span>
198
201
  </div>
199
202
  <div class="metric-body">
200
203
  <div class="metric-value">
201
204
  <span id="metric-cache-value">-</span>
202
205
  <span class="metric-unit">%</span>
203
206
  </div>
204
- <div class="metric-detail">heap blocks from cache</div>
207
+ <div class="metric-detail"><%= t("pg_reports.ui.metrics.cache_hit_detail") %></div>
205
208
  <div class="metric-sparkline">
206
209
  <svg id="sparkline-cache" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
207
210
  </div>
@@ -213,14 +216,14 @@ pg_stat_statements.track = all</pre>
213
216
  <div class="live-metric-card" data-metric="longrunning">
214
217
  <div class="metric-header">
215
218
  <span class="metric-icon" style="background: rgba(212, 160, 86, 0.12); color: var(--accent-amber);">🐢</span>
216
- <span class="metric-name">Long Queries</span>
219
+ <span class="metric-name"><%= t("pg_reports.ui.metrics.long_queries_label") %></span>
217
220
  </div>
218
221
  <div class="metric-body">
219
222
  <div class="metric-value">
220
223
  <span id="metric-longrunning-value">-</span>
221
- <span class="metric-unit">queries</span>
224
+ <span class="metric-unit"><%= t("pg_reports.ui.metrics.queries_unit") %></span>
222
225
  </div>
223
- <div class="metric-detail">&gt; 60s runtime</div>
226
+ <div class="metric-detail"><%= t("pg_reports.ui.metrics.long_running_threshold") %></div>
224
227
  <div class="metric-sparkline">
225
228
  <svg id="sparkline-longrunning" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
226
229
  </div>
@@ -232,14 +235,14 @@ pg_stat_statements.track = all</pre>
232
235
  <div class="live-metric-card" data-metric="blocked">
233
236
  <div class="metric-header">
234
237
  <span class="metric-icon" style="background: rgba(217, 112, 132, 0.12); color: var(--accent-rose);">🔒</span>
235
- <span class="metric-name">Blocked</span>
238
+ <span class="metric-name"><%= t("pg_reports.ui.metrics.blocked_label") %></span>
236
239
  </div>
237
240
  <div class="metric-body">
238
241
  <div class="metric-value">
239
242
  <span id="metric-blocked-value">-</span>
240
- <span class="metric-unit">processes</span>
243
+ <span class="metric-unit"><%= t("pg_reports.ui.metrics.processes_unit") %></span>
241
244
  </div>
242
- <div class="metric-detail">waiting for locks</div>
245
+ <div class="metric-detail"><%= t("pg_reports.ui.metrics.waiting_for_locks") %></div>
243
246
  <div class="metric-sparkline">
244
247
  <svg id="sparkline-blocked" viewBox="0 0 100 30" preserveAspectRatio="none"></svg>
245
248
  </div>
@@ -254,40 +257,40 @@ pg_stat_statements.track = all</pre>
254
257
  <div class="query-monitoring-header">
255
258
  <div class="query-monitoring-title">
256
259
  <span class="monitoring-indicator" id="monitor-indicator"></span>
257
- <span>SQL Query Monitor</span>
260
+ <span><%= t("pg_reports.ui.monitoring.query_monitor_title") %></span>
258
261
  <span class="session-badge" id="session-badge" style="display: none;">
259
- Session: <strong id="session-id"></strong>
262
+ <%= t("pg_reports.ui.monitoring.session_label") %> <strong id="session-id"></strong>
260
263
  </span>
261
264
  </div>
262
265
  <div class="query-monitoring-controls">
263
266
  <button class="btn btn-small btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
264
- Start Monitoring
267
+ <%= t("pg_reports.ui.actions.start_monitoring") %>
265
268
  </button>
266
269
  <button class="btn btn-small btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
267
- Stop Monitoring
270
+ <%= t("pg_reports.ui.actions.stop_monitoring") %>
268
271
  </button>
269
272
  <button class="btn btn-small btn-secondary" onclick="loadQueryHistory(this)" id="load-history-btn">
270
- 📜 Load History (50)
273
+ <%= t("pg_reports.ui.actions.load_history") %>
271
274
  </button>
272
275
  <div class="download-dropdown" id="monitor-download-dropdown" style="display: none;">
273
276
  <button class="btn btn-small btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
274
- 📥 Download
277
+ <%= t("pg_reports.ui.actions.download") %>
275
278
  </button>
276
279
  <div class="download-menu" id="monitor-download-menu" style="display: none;">
277
- <a href="#" onclick="downloadQueryMonitor('txt'); return false;">📄 Text (.txt)</a>
278
- <a href="#" onclick="downloadQueryMonitor('csv'); return false;">📊 CSV (.csv)</a>
279
- <a href="#" onclick="downloadQueryMonitor('json'); return false;">📋 JSON (.json)</a>
280
+ <a href="#" onclick="downloadQueryMonitor('txt'); return false;"><%= t("pg_reports.ui.actions.download_text") %></a>
281
+ <a href="#" onclick="downloadQueryMonitor('csv'); return false;"><%= t("pg_reports.ui.actions.download_csv") %></a>
282
+ <a href="#" onclick="downloadQueryMonitor('json'); return false;"><%= t("pg_reports.ui.actions.download_json") %></a>
280
283
  </div>
281
284
  </div>
282
285
  <span class="query-monitoring-count">
283
- Queries: <strong id="query-count">0</strong>
286
+ <%= t("pg_reports.ui.monitoring.queries_label") %> <strong id="query-count">0</strong>
284
287
  </span>
285
288
  </div>
286
289
  </div>
287
290
 
288
291
  <div class="query-feed" id="query-feed">
289
292
  <div class="query-feed-empty">
290
- Click "Start Monitoring" to begin capturing SQL queries
293
+ <%= t("pg_reports.ui.monitoring.feed_empty") %>
291
294
  </div>
292
295
  </div>
293
296
  </div>
@@ -297,7 +300,7 @@ pg_stat_statements.track = all</pre>
297
300
  <div class="category-card<%= ' disabled' if category_key == :queries && !@pg_stat_status[:ready] %>">
298
301
  <% if category_key == :queries && !@pg_stat_status[:ready] %>
299
302
  <div class="category-warning">
300
- <span>🔒</span> Requires pg_stat_statements
303
+ <%= t("pg_reports.ui.categories.requires_pg_stat") %>
301
304
  </div>
302
305
  <% end %>
303
306
  <div class="category-header">
@@ -305,7 +308,7 @@ pg_stat_statements.track = all</pre>
305
308
  <%= category[:icon] %>
306
309
  </div>
307
310
  <span class="category-title"><%= category[:name] %></span>
308
- <span class="category-count"><%= category[:reports].size %> reports</span>
311
+ <span class="category-count"><%= category[:reports].size %> <%= t("pg_reports.ui.categories.reports_count_suffix") %></span>
309
312
  </div>
310
313
 
311
314
  <div class="reports-list">
@@ -313,7 +316,10 @@ pg_stat_statements.track = all</pre>
313
316
  <% if category_key == :queries && !@pg_stat_status[:ready] %>
314
317
  <div class="report-link disabled">
315
318
  <div class="report-link-info">
316
- <span class="report-link-name"><%= report[:name] %></span>
319
+ <span class="report-link-name">
320
+ <%= report[:name] %>
321
+ <% if report[:new] %><span class="report-badge-new">NEW</span><% end %>
322
+ </span>
317
323
  <span class="report-link-desc"><%= report[:description] %></span>
318
324
  </div>
319
325
  <span class="lock">🔒</span>
@@ -321,7 +327,10 @@ pg_stat_statements.track = all</pre>
321
327
  <% else %>
322
328
  <%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
323
329
  <div class="report-link-info">
324
- <span class="report-link-name"><%= report[:name] %></span>
330
+ <span class="report-link-name">
331
+ <%= report[:name] %>
332
+ <% if report[:new] %><span class="report-badge-new">NEW</span><% end %>
333
+ </span>
325
334
  <span class="report-link-desc"><%= report[:description] %></span>
326
335
  </div>
327
336
  <span class="arrow">→</span>
@@ -404,7 +413,7 @@ pg_stat_statements.track = all</pre>
404
413
  margin: -1.5rem -1.5rem 1rem -1.5rem;
405
414
  background: rgba(245, 158, 11, 0.1);
406
415
  border-bottom: 1px solid rgba(245, 158, 11, 0.2);
407
- border-radius: 16px 16px 0 0;
416
+ border-radius: 6px 6px 0 0;
408
417
  color: var(--accent-amber);
409
418
  font-size: 0.8rem;
410
419
  font-weight: 500;
@@ -418,7 +427,7 @@ pg_stat_statements.track = all</pre>
418
427
  padding: 0.75rem 1rem;
419
428
  background: var(--bg-tertiary);
420
429
  border: 1px solid transparent;
421
- border-radius: 10px;
430
+ border-radius: 6px;
422
431
  color: var(--text-muted);
423
432
  font-size: 0.9rem;
424
433
  cursor: not-allowed;
@@ -469,7 +478,7 @@ pg_stat_statements.track = all</pre>
469
478
  .modal-content {
470
479
  background: var(--bg-card);
471
480
  border: 1px solid var(--border-color);
472
- border-radius: 16px;
481
+ border-radius: 6px;
473
482
  max-width: 600px;
474
483
  width: 90%;
475
484
  max-height: 80vh;
@@ -583,7 +592,7 @@ pg_stat_statements.track = all</pre>
583
592
  margin-bottom: 2rem;
584
593
  background: var(--bg-card);
585
594
  border: 1px solid var(--border-color);
586
- border-radius: 16px;
595
+ border-radius: 6px;
587
596
  padding: 1.25rem 1.5rem;
588
597
  }
589
598
 
@@ -691,7 +700,7 @@ pg_stat_statements.track = all</pre>
691
700
  .live-metric-card {
692
701
  background: var(--bg-tertiary);
693
702
  border: 1px solid var(--border-color);
694
- border-radius: 12px;
703
+ border-radius: 6px;
695
704
  padding: 1rem;
696
705
  position: relative;
697
706
  transition: all 0.2s;
@@ -798,7 +807,7 @@ pg_stat_statements.track = all</pre>
798
807
  padding: 1rem 1.5rem;
799
808
  background: var(--bg-card);
800
809
  border: 1px solid var(--border-color);
801
- border-radius: 12px;
810
+ border-radius: 6px;
802
811
  color: var(--text-primary);
803
812
  font-size: 0.875rem;
804
813
  font-weight: 500;
@@ -859,7 +868,7 @@ pg_stat_statements.track = all</pre>
859
868
  async function resetStatistics() {
860
869
  const button = document.getElementById('confirm-reset-btn');
861
870
  button.disabled = true;
862
- button.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px;"></span> Resetting...';
871
+ button.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px;"></span> ' + PG_REPORTS_I18N.actions.resetting;
863
872
 
864
873
  try {
865
874
  const response = await fetch(`${pgReportsRoot}/reset_statistics`, {
@@ -876,20 +885,20 @@ pg_stat_statements.track = all</pre>
876
885
  closeResetConfirmModal();
877
886
  showToast(data.message);
878
887
  } else {
879
- showToast(data.error || 'Failed to reset statistics', 'error');
888
+ showToast(data.error || PG_REPORTS_I18N.errors.reset_stats_failed, 'error');
880
889
  button.disabled = false;
881
- button.innerHTML = 'Yes, Reset';
890
+ button.innerHTML = PG_REPORTS_I18N.actions.confirm_reset;
882
891
  }
883
892
  } catch (error) {
884
- showToast('Network error: ' + error.message, 'error');
893
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
885
894
  button.disabled = false;
886
- button.innerHTML = 'Yes, Reset';
895
+ button.innerHTML = PG_REPORTS_I18N.actions.confirm_reset;
887
896
  }
888
897
  }
889
898
 
890
899
  async function enablePgStatStatements(button) {
891
900
  button.disabled = true;
892
- button.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px;"></span> Creating...';
901
+ button.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px;"></span> ' + PG_REPORTS_I18N.actions.creating;
893
902
 
894
903
  try {
895
904
  const response = await fetch(`${pgReportsRoot}/enable_pg_stat_statements`, {
@@ -912,12 +921,12 @@ pg_stat_statements.track = all</pre>
912
921
  showPgStatInfo();
913
922
  }
914
923
  button.disabled = false;
915
- button.innerHTML = '⚡ Create Extension';
924
+ button.innerHTML = PG_REPORTS_I18N.actions.create_extension;
916
925
  }
917
926
  } catch (error) {
918
- showToast('Network error: ' + error.message, 'error');
927
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
919
928
  button.disabled = false;
920
- button.innerHTML = '⚡ Create Extension';
929
+ button.innerHTML = PG_REPORTS_I18N.actions.create_extension;
921
930
  }
922
931
  }
923
932
 
@@ -985,19 +994,19 @@ pg_stat_statements.track = all</pre>
985
994
  <div class="live-monitoring-header">
986
995
  <div class="live-monitoring-title">
987
996
  <span class="badge-dot error"></span>
988
- <span>Live Monitoring Unavailable</span>
997
+ <span>${PG_REPORTS_I18N.status.monitoring_unavailable}</span>
989
998
  </div>
990
999
  </div>
991
1000
  <div style="padding: 2rem; text-align: center; color: var(--text-muted);">
992
- <p style="margin-bottom: 1rem;">Unable to fetch database statistics.</p>
993
- <p style="font-size: 0.875rem;">This may be due to:</p>
1001
+ <p style="margin-bottom: 1rem;">${PG_REPORTS_I18N.errors.unable_fetch_metrics}</p>
1002
+ <p style="font-size: 0.875rem;">${PG_REPORTS_I18N.errors.possible_causes}</p>
994
1003
  <ul style="list-style: none; padding: 0; margin: 1rem 0; font-size: 0.875rem;">
995
- <li>• Insufficient database permissions</li>
996
- <li>• Database statistics views not accessible</li>
997
- <li>• Connection issues</li>
1004
+ <li>• ${PG_REPORTS_I18N.errors.cause_permissions}</li>
1005
+ <li>• ${PG_REPORTS_I18N.errors.cause_views}</li>
1006
+ <li>• ${PG_REPORTS_I18N.errors.cause_connection}</li>
998
1007
  </ul>
999
1008
  <button class="btn btn-small btn-primary" onclick="location.reload()" style="margin-top: 1rem;">
1000
- Retry
1009
+ ${PG_REPORTS_I18N.actions.retry}
1001
1010
  </button>
1002
1011
  </div>
1003
1012
  `;
@@ -1065,7 +1074,7 @@ pg_stat_statements.track = all</pre>
1065
1074
  showMetricsUnavailableMessage();
1066
1075
  } else if (consecutiveErrors === 1) {
1067
1076
  // Show toast on first error
1068
- showToast(data.error || 'Failed to fetch live metrics', 'error');
1077
+ showToast(data.error || PG_REPORTS_I18N.errors.fetch_metrics_failed, 'error');
1069
1078
  }
1070
1079
  return false;
1071
1080
  }
@@ -1078,7 +1087,7 @@ pg_stat_statements.track = all</pre>
1078
1087
  stopPolling();
1079
1088
  showMetricsUnavailableMessage();
1080
1089
  } else if (consecutiveErrors === 1) {
1081
- showToast('Network error: ' + error.message, 'error');
1090
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1082
1091
  }
1083
1092
  return false;
1084
1093
  }
@@ -1247,37 +1256,37 @@ pg_stat_statements.track = all</pre>
1247
1256
 
1248
1257
  // VSCode (WSL Remote format)
1249
1258
  urls.push({
1250
- name: 'VS Code (WSL)',
1259
+ name: PG_REPORTS_I18N.settings.ide_vscode_wsl,
1251
1260
  url: `vscode://vscode-remote/wsl+${wslDistro}${absolutePath}:${line}`
1252
1261
  });
1253
1262
 
1254
1263
  // VSCode (direct path)
1255
1264
  urls.push({
1256
- name: 'VS Code',
1265
+ name: PG_REPORTS_I18N.settings.ide_vscode,
1257
1266
  url: `vscode://file${absolutePath}:${line}`
1258
1267
  });
1259
1268
 
1260
1269
  // RubyMine
1261
1270
  urls.push({
1262
- name: 'RubyMine',
1271
+ name: PG_REPORTS_I18N.settings.ide_rubymine,
1263
1272
  url: `rubymine://open?file=${absolutePath}&line=${line}`
1264
1273
  });
1265
1274
 
1266
1275
  // IntelliJ
1267
1276
  urls.push({
1268
- name: 'IntelliJ',
1277
+ name: PG_REPORTS_I18N.settings.ide_intellij,
1269
1278
  url: `idea://open?file=${absolutePath}&line=${line}`
1270
1279
  });
1271
1280
 
1272
1281
  // Cursor (WSL Remote format)
1273
1282
  urls.push({
1274
- name: 'Cursor (WSL)',
1283
+ name: PG_REPORTS_I18N.settings.ide_cursor_wsl,
1275
1284
  url: `cursor://vscode-remote/wsl+${wslDistro}${absolutePath}:${line}`
1276
1285
  });
1277
1286
 
1278
1287
  // Cursor (direct path)
1279
1288
  urls.push({
1280
- name: 'Cursor',
1289
+ name: PG_REPORTS_I18N.settings.ide_cursor,
1281
1290
  url: `cursor://file${absolutePath}:${line}`
1282
1291
  });
1283
1292
 
@@ -1413,7 +1422,7 @@ pg_stat_statements.track = all</pre>
1413
1422
 
1414
1423
  async function loadQueryHistory(button) {
1415
1424
  button.disabled = true;
1416
- button.innerHTML = '<span class="spinner"></span> Loading...';
1425
+ button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.loading;
1417
1426
 
1418
1427
  try {
1419
1428
  const response = await fetch(
@@ -1423,22 +1432,22 @@ pg_stat_statements.track = all</pre>
1423
1432
 
1424
1433
  if (data.success && data.queries && data.queries.length > 0) {
1425
1434
  renderQueryFeed(data.queries);
1426
- showToast(`Loaded ${data.queries.length} queries from history`);
1435
+ showToast(pgReportsFormat(PG_REPORTS_I18N.success.queries_loaded, { count: data.queries.length }));
1427
1436
  } else {
1428
- showToast('No query history found', 'error');
1437
+ showToast(PG_REPORTS_I18N.errors.no_query_history, 'error');
1429
1438
  }
1430
1439
  } catch (error) {
1431
1440
  console.error('Failed to load query history:', error);
1432
- showToast('Failed to load history: ' + error.message, 'error');
1441
+ showToast(PG_REPORTS_I18N.errors.load_history_failed + ' ' + error.message, 'error');
1433
1442
  } finally {
1434
1443
  button.disabled = false;
1435
- button.innerHTML = '📜 Load History (50)';
1444
+ button.innerHTML = PG_REPORTS_I18N.actions.load_history;
1436
1445
  }
1437
1446
  }
1438
1447
 
1439
1448
  async function startQueryMonitoring(button) {
1440
1449
  button.disabled = true;
1441
- button.innerHTML = '<span class="spinner"></span> Starting...';
1450
+ button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.starting;
1442
1451
 
1443
1452
  try {
1444
1453
  const response = await fetch(`${pgReportsRoot}/query_monitor/start`, {
@@ -1458,20 +1467,20 @@ pg_stat_statements.track = all</pre>
1458
1467
  startQueryMonitorPolling();
1459
1468
  showToast(data.message);
1460
1469
  } else {
1461
- showToast(data.message || 'Failed to start monitoring', 'error');
1470
+ showToast(data.message || PG_REPORTS_I18N.errors.start_monitoring_failed, 'error');
1462
1471
  button.disabled = false;
1463
- button.innerHTML = '▶ Start Monitoring';
1472
+ button.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
1464
1473
  }
1465
1474
  } catch (error) {
1466
- showToast('Network error: ' + error.message, 'error');
1475
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1467
1476
  button.disabled = false;
1468
- button.innerHTML = '▶ Start Monitoring';
1477
+ button.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
1469
1478
  }
1470
1479
  }
1471
1480
 
1472
1481
  async function stopQueryMonitoring(button) {
1473
1482
  button.disabled = true;
1474
- button.innerHTML = '<span class="spinner"></span> Stopping...';
1483
+ button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.stopping;
1475
1484
 
1476
1485
  try {
1477
1486
  const response = await fetch(`${pgReportsRoot}/query_monitor/stop`, {
@@ -1492,14 +1501,14 @@ pg_stat_statements.track = all</pre>
1492
1501
  updateQueryMonitorUI(false);
1493
1502
  showToast(data.message);
1494
1503
  } else {
1495
- showToast(data.message || 'Failed to stop monitoring', 'error');
1504
+ showToast(data.message || PG_REPORTS_I18N.errors.stop_monitoring_failed, 'error');
1496
1505
  button.disabled = false;
1497
- button.innerHTML = '⏹ Stop Monitoring';
1506
+ button.innerHTML = PG_REPORTS_I18N.actions.stop_monitoring;
1498
1507
  }
1499
1508
  } catch (error) {
1500
- showToast('Network error: ' + error.message, 'error');
1509
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1501
1510
  button.disabled = false;
1502
- button.innerHTML = '⏹ Stop Monitoring';
1511
+ button.innerHTML = PG_REPORTS_I18N.actions.stop_monitoring;
1503
1512
  }
1504
1513
  }
1505
1514
 
@@ -1521,7 +1530,7 @@ pg_stat_statements.track = all</pre>
1521
1530
  startBtn.style.display = 'none';
1522
1531
  stopBtn.style.display = 'inline-block';
1523
1532
  stopBtn.disabled = false;
1524
- stopBtn.innerHTML = '⏹ Stop Monitoring';
1533
+ stopBtn.innerHTML = PG_REPORTS_I18N.actions.stop_monitoring;
1525
1534
  loadHistoryBtn.style.display = 'none'; // Hide when monitoring active
1526
1535
  sessionBadge.style.display = 'inline-block';
1527
1536
  sessionIdEl.textContent = currentSessionId ? currentSessionId.substring(0, 8) : '';
@@ -1530,7 +1539,7 @@ pg_stat_statements.track = all</pre>
1530
1539
  indicator.classList.remove('active');
1531
1540
  startBtn.style.display = 'inline-block';
1532
1541
  startBtn.disabled = false;
1533
- startBtn.innerHTML = '▶ Start Monitoring';
1542
+ startBtn.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
1534
1543
  stopBtn.style.display = 'none';
1535
1544
  loadHistoryBtn.style.display = 'inline-block'; // Show when monitoring stopped
1536
1545
  sessionBadge.style.display = 'none';
@@ -1541,7 +1550,7 @@ pg_stat_statements.track = all</pre>
1541
1550
 
1542
1551
  // Only clear feed if no queries captured
1543
1552
  if (!hasQueries) {
1544
- feed.innerHTML = '<div class="query-feed-empty">Click "Start Monitoring" to begin capturing SQL queries</div>';
1553
+ feed.innerHTML = '<div class="query-feed-empty">' + PG_REPORTS_I18N.monitoring.feed_empty + '</div>';
1545
1554
  }
1546
1555
  }
1547
1556
  }
@@ -1608,7 +1617,7 @@ pg_stat_statements.track = all</pre>
1608
1617
  renderQueryFeed(data.queries);
1609
1618
  } else if (!data.success) {
1610
1619
  // Server returned an error (e.g., "Monitoring not active")
1611
- const errorMsg = data.message || data.error || 'Query monitoring error';
1620
+ const errorMsg = data.message || data.error || PG_REPORTS_I18N.errors.query_monitoring_error;
1612
1621
  showToast(errorMsg, 'error');
1613
1622
 
1614
1623
  // Stop polling and update UI since monitoring is not active
@@ -1619,7 +1628,7 @@ pg_stat_statements.track = all</pre>
1619
1628
  }
1620
1629
  } catch (error) {
1621
1630
  console.error('Failed to fetch query feed:', error);
1622
- showToast('Network error: ' + error.message, 'error');
1631
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1623
1632
  }
1624
1633
  }
1625
1634
 
@@ -1627,7 +1636,7 @@ pg_stat_statements.track = all</pre>
1627
1636
  const feed = document.getElementById('query-feed');
1628
1637
 
1629
1638
  if (queries.length === 0) {
1630
- feed.innerHTML = '<div class="query-feed-empty">No queries captured yet...</div>';
1639
+ feed.innerHTML = '<div class="query-feed-empty">' + PG_REPORTS_I18N.monitoring.feed_no_queries + '</div>';
1631
1640
  queryCount = 0;
1632
1641
  document.getElementById('query-count').textContent = '0';
1633
1642
  return;
@@ -1647,7 +1656,7 @@ pg_stat_statements.track = all</pre>
1647
1656
  const timestamp = new Date(query.timestamp).toLocaleTimeString();
1648
1657
  const queryId = `query-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1649
1658
 
1650
- let sourceInfo = 'Unknown source';
1659
+ let sourceInfo = PG_REPORTS_I18N.monitoring.unknown_source;
1651
1660
  if (query.source_location && query.source_location.file) {
1652
1661
  const file = query.source_location.file;
1653
1662
  const line = query.source_location.line;
@@ -1671,7 +1680,7 @@ pg_stat_statements.track = all</pre>
1671
1680
  <span class="query-duration ${durationClass}">${duration.toFixed(2)}ms</span>
1672
1681
  <span class="query-source">${sourceInfo}</span>
1673
1682
  </div>
1674
- <button class="query-expand-btn" onclick="toggleQueryExpand('${queryId}')" title="Expand/Collapse">
1683
+ <button class="query-expand-btn" onclick="toggleQueryExpand('${queryId}')" title="${PG_REPORTS_I18N.monitoring.expand_collapse_title}">
1675
1684
  <span id="${queryId}-icon">▼</span>
1676
1685
  </button>
1677
1686
  </div>