pg_reports 0.6.0 → 0.6.2

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +143 -378
  4. data/app/controllers/pg_reports/dashboard_controller.rb +21 -21
  5. data/app/views/layouts/pg_reports/application.html.erb +65 -8
  6. data/app/views/pg_reports/dashboard/_show_modals.html.erb +22 -22
  7. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +55 -57
  8. data/app/views/pg_reports/dashboard/_show_styles.html.erb +18 -0
  9. data/app/views/pg_reports/dashboard/index.html.erb +109 -106
  10. data/app/views/pg_reports/dashboard/show.html.erb +26 -26
  11. data/config/locales/en.yml +488 -0
  12. data/config/locales/ru.yml +481 -0
  13. data/config/locales/uk.yml +481 -0
  14. data/lib/pg_reports/annotation_parser.rb +13 -1
  15. data/lib/pg_reports/compatibility.rb +3 -3
  16. data/lib/pg_reports/dashboard/reports_registry.rb +83 -12
  17. data/lib/pg_reports/definitions/schema_analysis/always_null_columns.yml +31 -0
  18. data/lib/pg_reports/definitions/schema_analysis/unused_columns.yml +32 -0
  19. data/lib/pg_reports/definitions/tables/unused_tables.yml +30 -0
  20. data/lib/pg_reports/definitions/tables/update_hotspots.yml +32 -0
  21. data/lib/pg_reports/module_generator.rb +2 -1
  22. data/lib/pg_reports/modules/schema_analysis.rb +261 -2
  23. data/lib/pg_reports/modules/system.rb +3 -3
  24. data/lib/pg_reports/query_monitor.rb +2 -6
  25. data/lib/pg_reports/report_definition.rb +20 -24
  26. data/lib/pg_reports/sql/schema_analysis/always_null_columns.sql +25 -0
  27. data/lib/pg_reports/sql/schema_analysis/unused_columns.sql +36 -0
  28. data/lib/pg_reports/sql/tables/unused_tables.sql +19 -0
  29. data/lib/pg_reports/sql/tables/update_hotspots.sql +26 -0
  30. data/lib/pg_reports/version.rb +1 -1
  31. metadata +9 -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">
@@ -865,7 +868,7 @@ pg_stat_statements.track = all</pre>
865
868
  async function resetStatistics() {
866
869
  const button = document.getElementById('confirm-reset-btn');
867
870
  button.disabled = true;
868
- 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;
869
872
 
870
873
  try {
871
874
  const response = await fetch(`${pgReportsRoot}/reset_statistics`, {
@@ -882,20 +885,20 @@ pg_stat_statements.track = all</pre>
882
885
  closeResetConfirmModal();
883
886
  showToast(data.message);
884
887
  } else {
885
- showToast(data.error || 'Failed to reset statistics', 'error');
888
+ showToast(data.error || PG_REPORTS_I18N.errors.reset_stats_failed, 'error');
886
889
  button.disabled = false;
887
- button.innerHTML = 'Yes, Reset';
890
+ button.innerHTML = PG_REPORTS_I18N.actions.confirm_reset;
888
891
  }
889
892
  } catch (error) {
890
- showToast('Network error: ' + error.message, 'error');
893
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
891
894
  button.disabled = false;
892
- button.innerHTML = 'Yes, Reset';
895
+ button.innerHTML = PG_REPORTS_I18N.actions.confirm_reset;
893
896
  }
894
897
  }
895
898
 
896
899
  async function enablePgStatStatements(button) {
897
900
  button.disabled = true;
898
- 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;
899
902
 
900
903
  try {
901
904
  const response = await fetch(`${pgReportsRoot}/enable_pg_stat_statements`, {
@@ -918,12 +921,12 @@ pg_stat_statements.track = all</pre>
918
921
  showPgStatInfo();
919
922
  }
920
923
  button.disabled = false;
921
- button.innerHTML = '⚡ Create Extension';
924
+ button.innerHTML = PG_REPORTS_I18N.actions.create_extension;
922
925
  }
923
926
  } catch (error) {
924
- showToast('Network error: ' + error.message, 'error');
927
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
925
928
  button.disabled = false;
926
- button.innerHTML = '⚡ Create Extension';
929
+ button.innerHTML = PG_REPORTS_I18N.actions.create_extension;
927
930
  }
928
931
  }
929
932
 
@@ -991,19 +994,19 @@ pg_stat_statements.track = all</pre>
991
994
  <div class="live-monitoring-header">
992
995
  <div class="live-monitoring-title">
993
996
  <span class="badge-dot error"></span>
994
- <span>Live Monitoring Unavailable</span>
997
+ <span>${PG_REPORTS_I18N.status.monitoring_unavailable}</span>
995
998
  </div>
996
999
  </div>
997
1000
  <div style="padding: 2rem; text-align: center; color: var(--text-muted);">
998
- <p style="margin-bottom: 1rem;">Unable to fetch database statistics.</p>
999
- <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>
1000
1003
  <ul style="list-style: none; padding: 0; margin: 1rem 0; font-size: 0.875rem;">
1001
- <li>• Insufficient database permissions</li>
1002
- <li>• Database statistics views not accessible</li>
1003
- <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>
1004
1007
  </ul>
1005
1008
  <button class="btn btn-small btn-primary" onclick="location.reload()" style="margin-top: 1rem;">
1006
- Retry
1009
+ ${PG_REPORTS_I18N.actions.retry}
1007
1010
  </button>
1008
1011
  </div>
1009
1012
  `;
@@ -1071,7 +1074,7 @@ pg_stat_statements.track = all</pre>
1071
1074
  showMetricsUnavailableMessage();
1072
1075
  } else if (consecutiveErrors === 1) {
1073
1076
  // Show toast on first error
1074
- showToast(data.error || 'Failed to fetch live metrics', 'error');
1077
+ showToast(data.error || PG_REPORTS_I18N.errors.fetch_metrics_failed, 'error');
1075
1078
  }
1076
1079
  return false;
1077
1080
  }
@@ -1084,7 +1087,7 @@ pg_stat_statements.track = all</pre>
1084
1087
  stopPolling();
1085
1088
  showMetricsUnavailableMessage();
1086
1089
  } else if (consecutiveErrors === 1) {
1087
- showToast('Network error: ' + error.message, 'error');
1090
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1088
1091
  }
1089
1092
  return false;
1090
1093
  }
@@ -1253,37 +1256,37 @@ pg_stat_statements.track = all</pre>
1253
1256
 
1254
1257
  // VSCode (WSL Remote format)
1255
1258
  urls.push({
1256
- name: 'VS Code (WSL)',
1259
+ name: PG_REPORTS_I18N.settings.ide_vscode_wsl,
1257
1260
  url: `vscode://vscode-remote/wsl+${wslDistro}${absolutePath}:${line}`
1258
1261
  });
1259
1262
 
1260
1263
  // VSCode (direct path)
1261
1264
  urls.push({
1262
- name: 'VS Code',
1265
+ name: PG_REPORTS_I18N.settings.ide_vscode,
1263
1266
  url: `vscode://file${absolutePath}:${line}`
1264
1267
  });
1265
1268
 
1266
1269
  // RubyMine
1267
1270
  urls.push({
1268
- name: 'RubyMine',
1271
+ name: PG_REPORTS_I18N.settings.ide_rubymine,
1269
1272
  url: `rubymine://open?file=${absolutePath}&line=${line}`
1270
1273
  });
1271
1274
 
1272
1275
  // IntelliJ
1273
1276
  urls.push({
1274
- name: 'IntelliJ',
1277
+ name: PG_REPORTS_I18N.settings.ide_intellij,
1275
1278
  url: `idea://open?file=${absolutePath}&line=${line}`
1276
1279
  });
1277
1280
 
1278
1281
  // Cursor (WSL Remote format)
1279
1282
  urls.push({
1280
- name: 'Cursor (WSL)',
1283
+ name: PG_REPORTS_I18N.settings.ide_cursor_wsl,
1281
1284
  url: `cursor://vscode-remote/wsl+${wslDistro}${absolutePath}:${line}`
1282
1285
  });
1283
1286
 
1284
1287
  // Cursor (direct path)
1285
1288
  urls.push({
1286
- name: 'Cursor',
1289
+ name: PG_REPORTS_I18N.settings.ide_cursor,
1287
1290
  url: `cursor://file${absolutePath}:${line}`
1288
1291
  });
1289
1292
 
@@ -1419,7 +1422,7 @@ pg_stat_statements.track = all</pre>
1419
1422
 
1420
1423
  async function loadQueryHistory(button) {
1421
1424
  button.disabled = true;
1422
- button.innerHTML = '<span class="spinner"></span> Loading...';
1425
+ button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.loading;
1423
1426
 
1424
1427
  try {
1425
1428
  const response = await fetch(
@@ -1429,22 +1432,22 @@ pg_stat_statements.track = all</pre>
1429
1432
 
1430
1433
  if (data.success && data.queries && data.queries.length > 0) {
1431
1434
  renderQueryFeed(data.queries);
1432
- showToast(`Loaded ${data.queries.length} queries from history`);
1435
+ showToast(pgReportsFormat(PG_REPORTS_I18N.success.queries_loaded, { count: data.queries.length }));
1433
1436
  } else {
1434
- showToast('No query history found', 'error');
1437
+ showToast(PG_REPORTS_I18N.errors.no_query_history, 'error');
1435
1438
  }
1436
1439
  } catch (error) {
1437
1440
  console.error('Failed to load query history:', error);
1438
- showToast('Failed to load history: ' + error.message, 'error');
1441
+ showToast(PG_REPORTS_I18N.errors.load_history_failed + ' ' + error.message, 'error');
1439
1442
  } finally {
1440
1443
  button.disabled = false;
1441
- button.innerHTML = '📜 Load History (50)';
1444
+ button.innerHTML = PG_REPORTS_I18N.actions.load_history;
1442
1445
  }
1443
1446
  }
1444
1447
 
1445
1448
  async function startQueryMonitoring(button) {
1446
1449
  button.disabled = true;
1447
- button.innerHTML = '<span class="spinner"></span> Starting...';
1450
+ button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.starting;
1448
1451
 
1449
1452
  try {
1450
1453
  const response = await fetch(`${pgReportsRoot}/query_monitor/start`, {
@@ -1464,20 +1467,20 @@ pg_stat_statements.track = all</pre>
1464
1467
  startQueryMonitorPolling();
1465
1468
  showToast(data.message);
1466
1469
  } else {
1467
- showToast(data.message || 'Failed to start monitoring', 'error');
1470
+ showToast(data.message || PG_REPORTS_I18N.errors.start_monitoring_failed, 'error');
1468
1471
  button.disabled = false;
1469
- button.innerHTML = '▶ Start Monitoring';
1472
+ button.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
1470
1473
  }
1471
1474
  } catch (error) {
1472
- showToast('Network error: ' + error.message, 'error');
1475
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1473
1476
  button.disabled = false;
1474
- button.innerHTML = '▶ Start Monitoring';
1477
+ button.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
1475
1478
  }
1476
1479
  }
1477
1480
 
1478
1481
  async function stopQueryMonitoring(button) {
1479
1482
  button.disabled = true;
1480
- button.innerHTML = '<span class="spinner"></span> Stopping...';
1483
+ button.innerHTML = '<span class="spinner"></span> ' + PG_REPORTS_I18N.actions.stopping;
1481
1484
 
1482
1485
  try {
1483
1486
  const response = await fetch(`${pgReportsRoot}/query_monitor/stop`, {
@@ -1498,14 +1501,14 @@ pg_stat_statements.track = all</pre>
1498
1501
  updateQueryMonitorUI(false);
1499
1502
  showToast(data.message);
1500
1503
  } else {
1501
- showToast(data.message || 'Failed to stop monitoring', 'error');
1504
+ showToast(data.message || PG_REPORTS_I18N.errors.stop_monitoring_failed, 'error');
1502
1505
  button.disabled = false;
1503
- button.innerHTML = '⏹ Stop Monitoring';
1506
+ button.innerHTML = PG_REPORTS_I18N.actions.stop_monitoring;
1504
1507
  }
1505
1508
  } catch (error) {
1506
- showToast('Network error: ' + error.message, 'error');
1509
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1507
1510
  button.disabled = false;
1508
- button.innerHTML = '⏹ Stop Monitoring';
1511
+ button.innerHTML = PG_REPORTS_I18N.actions.stop_monitoring;
1509
1512
  }
1510
1513
  }
1511
1514
 
@@ -1527,7 +1530,7 @@ pg_stat_statements.track = all</pre>
1527
1530
  startBtn.style.display = 'none';
1528
1531
  stopBtn.style.display = 'inline-block';
1529
1532
  stopBtn.disabled = false;
1530
- stopBtn.innerHTML = '⏹ Stop Monitoring';
1533
+ stopBtn.innerHTML = PG_REPORTS_I18N.actions.stop_monitoring;
1531
1534
  loadHistoryBtn.style.display = 'none'; // Hide when monitoring active
1532
1535
  sessionBadge.style.display = 'inline-block';
1533
1536
  sessionIdEl.textContent = currentSessionId ? currentSessionId.substring(0, 8) : '';
@@ -1536,7 +1539,7 @@ pg_stat_statements.track = all</pre>
1536
1539
  indicator.classList.remove('active');
1537
1540
  startBtn.style.display = 'inline-block';
1538
1541
  startBtn.disabled = false;
1539
- startBtn.innerHTML = '▶ Start Monitoring';
1542
+ startBtn.innerHTML = PG_REPORTS_I18N.actions.start_monitoring;
1540
1543
  stopBtn.style.display = 'none';
1541
1544
  loadHistoryBtn.style.display = 'inline-block'; // Show when monitoring stopped
1542
1545
  sessionBadge.style.display = 'none';
@@ -1547,7 +1550,7 @@ pg_stat_statements.track = all</pre>
1547
1550
 
1548
1551
  // Only clear feed if no queries captured
1549
1552
  if (!hasQueries) {
1550
- 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>';
1551
1554
  }
1552
1555
  }
1553
1556
  }
@@ -1614,7 +1617,7 @@ pg_stat_statements.track = all</pre>
1614
1617
  renderQueryFeed(data.queries);
1615
1618
  } else if (!data.success) {
1616
1619
  // Server returned an error (e.g., "Monitoring not active")
1617
- const errorMsg = data.message || data.error || 'Query monitoring error';
1620
+ const errorMsg = data.message || data.error || PG_REPORTS_I18N.errors.query_monitoring_error;
1618
1621
  showToast(errorMsg, 'error');
1619
1622
 
1620
1623
  // Stop polling and update UI since monitoring is not active
@@ -1625,7 +1628,7 @@ pg_stat_statements.track = all</pre>
1625
1628
  }
1626
1629
  } catch (error) {
1627
1630
  console.error('Failed to fetch query feed:', error);
1628
- showToast('Network error: ' + error.message, 'error');
1631
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1629
1632
  }
1630
1633
  }
1631
1634
 
@@ -1633,7 +1636,7 @@ pg_stat_statements.track = all</pre>
1633
1636
  const feed = document.getElementById('query-feed');
1634
1637
 
1635
1638
  if (queries.length === 0) {
1636
- 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>';
1637
1640
  queryCount = 0;
1638
1641
  document.getElementById('query-count').textContent = '0';
1639
1642
  return;
@@ -1653,7 +1656,7 @@ pg_stat_statements.track = all</pre>
1653
1656
  const timestamp = new Date(query.timestamp).toLocaleTimeString();
1654
1657
  const queryId = `query-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1655
1658
 
1656
- let sourceInfo = 'Unknown source';
1659
+ let sourceInfo = PG_REPORTS_I18N.monitoring.unknown_source;
1657
1660
  if (query.source_location && query.source_location.file) {
1658
1661
  const file = query.source_location.file;
1659
1662
  const line = query.source_location.line;
@@ -1677,7 +1680,7 @@ pg_stat_statements.track = all</pre>
1677
1680
  <span class="query-duration ${durationClass}">${duration.toFixed(2)}ms</span>
1678
1681
  <span class="query-source">${sourceInfo}</span>
1679
1682
  </div>
1680
- <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}">
1681
1684
  <span id="${queryId}-icon">▼</span>
1682
1685
  </button>
1683
1686
  </div>