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
@@ -233,7 +233,7 @@
233
233
  function copyToClipboard(text, btn) {
234
234
  navigator.clipboard.writeText(text).then(() => {
235
235
  const originalText = btn.textContent;
236
- btn.textContent = '✓ Copied!';
236
+ btn.textContent = PG_REPORTS_I18N.actions.copied_feedback;
237
237
  btn.style.background = 'var(--accent-green)';
238
238
  btn.style.borderColor = 'var(--accent-green)';
239
239
  btn.style.color = 'white';
@@ -244,7 +244,59 @@
244
244
  btn.style.color = '';
245
245
  }, 1500);
246
246
  }).catch(() => {
247
- showToast('Failed to copy', 'error');
247
+ showToast(PG_REPORTS_I18N.errors.copy_failed, 'error');
248
+ });
249
+ }
250
+
251
+ // AI Prompt builder — assembles a prompt from documentation + report data
252
+ const aiPromptInstruction = <%== @documentation[:ai_prompt].to_json %>;
253
+ const aiPromptWhat = <%== @documentation[:what].to_json %>;
254
+ const aiPromptHow = <%== @documentation[:how].to_json %>;
255
+ const aiPromptNuances = <%== (@documentation[:nuances] || []).to_json %>;
256
+
257
+ function buildAiPrompt() {
258
+ if (!currentReportData || !currentReportData.data) return null;
259
+
260
+ const rows = currentReportData.data.slice(0, 15);
261
+ const cols = currentReportData.columns || [];
262
+
263
+ // Build markdown table from report data
264
+ let table = '| ' + cols.join(' | ') + ' |\n';
265
+ table += '| ' + cols.map(() => '---').join(' | ') + ' |\n';
266
+ rows.forEach(row => {
267
+ const values = cols.map(c => {
268
+ const v = row[c];
269
+ return v == null ? '' : String(v).replace(/\|/g, '\\|').substring(0, 120);
270
+ });
271
+ table += '| ' + values.join(' | ') + ' |\n';
272
+ });
273
+ if (currentReportData.data.length > 15) {
274
+ table += `\n(${currentReportData.data.length - 15} more rows omitted)\n`;
275
+ }
276
+
277
+ // Assemble the prompt
278
+ let prompt = `## Problem\n\n${aiPromptWhat}\n\n${aiPromptHow}\n`;
279
+ prompt += `\n## What needs to be done\n\n${aiPromptInstruction}\n`;
280
+ prompt += `\n## Detected issues\n\n${table}`;
281
+ if (aiPromptNuances.length > 0) {
282
+ prompt += `\n## Important context\n\n`;
283
+ aiPromptNuances.forEach(n => { prompt += `- ${n}\n`; });
284
+ }
285
+ return prompt;
286
+ }
287
+
288
+ function copyAiPrompt(el) {
289
+ const prompt = buildAiPrompt();
290
+ if (!prompt) {
291
+ showToast(PG_REPORTS_I18N.errors.run_report_first, 'error');
292
+ return;
293
+ }
294
+
295
+ document.getElementById('dropdown-menu').classList.remove('show');
296
+ navigator.clipboard.writeText(prompt).then(() => {
297
+ showToast(PG_REPORTS_I18N.success.ai_prompt_copied);
298
+ }).catch(() => {
299
+ showToast(PG_REPORTS_I18N.errors.copy_failed, 'error');
248
300
  });
249
301
  }
250
302
 
@@ -299,15 +351,15 @@
299
351
 
300
352
  for (const problem of problems) {
301
353
  const levelClass = problem.level === 'critical' ? 'critical' : 'warning';
302
- const levelText = problem.level === 'critical' ? '🔴 Critical' : '⚠️ Warning';
354
+ const levelText = problem.level === 'critical' ? PG_REPORTS_I18N.levels.critical : PG_REPORTS_I18N.levels.warning;
303
355
 
304
356
  html += `
305
357
  <div class="problem-field">
306
358
  <span class="problem-field-label">${escapeHtml(problem.field)} (${levelText})</span>
307
359
  <div class="problem-field-value ${levelClass}">
308
- Current: ${escapeHtml(String(problem.value))}
309
- <br>Threshold: warning=${problem.threshold.warning}, critical=${problem.threshold.critical}
310
- ${problem.threshold.inverted ? '<br><em>(inverted: lower values are worse)</em>' : ''}
360
+ ${PG_REPORTS_I18N.sections.current_label} ${escapeHtml(String(problem.value))}
361
+ <br>${PG_REPORTS_I18N.sections.threshold_label} ${PG_REPORTS_I18N.sections.warning_eq}=${problem.threshold.warning}, ${PG_REPORTS_I18N.sections.critical_eq}=${problem.threshold.critical}
362
+ ${problem.threshold.inverted ? '<br><em>' + PG_REPORTS_I18N.sections.threshold_inverted_long + '</em>' : ''}
311
363
  </div>
312
364
  </div>
313
365
  `;
@@ -320,7 +372,7 @@
320
372
  if (explanation) {
321
373
  html += `
322
374
  <div class="problem-explanation">
323
- <h4>💡 Recommendation</h4>
375
+ <h4>${PG_REPORTS_I18N.sections.recommendation}</h4>
324
376
  <p>${escapeHtml(explanation)}</p>
325
377
  </div>
326
378
  `;
@@ -580,7 +632,7 @@
580
632
  <div class="row-detail-item${isLongText ? ' full-width' : ''}">
581
633
  <span class="row-detail-label">${escapeHtml(col)}</span>
582
634
  <div class="row-detail-value ${valueClass}">${escapeHtml(strValue)}</div>
583
- ${isQuery ? `<button class="copy-btn" data-query="${escapeHtmlAttr(strValue)}" onclick="event.stopPropagation(); copyQueryFromButton(this)">📋 Copy Query</button>` : ''}
635
+ ${isQuery ? `<button class="copy-btn" data-query="${escapeHtmlAttr(strValue)}" onclick="event.stopPropagation(); copyQueryFromButton(this)">${PG_REPORTS_I18N.actions.copy_query}</button>` : ''}
584
636
  </div>
585
637
  `;
586
638
  });
@@ -632,7 +684,7 @@
632
684
 
633
685
  if (button) {
634
686
  button.disabled = true;
635
- button.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;vertical-align:middle;"></span> Running...';
687
+ button.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;vertical-align:middle;"></span> ' + PG_REPORTS_I18N.actions.running;
636
688
  }
637
689
 
638
690
  if (loadingEl) loadingEl.style.display = 'flex';
@@ -688,7 +740,7 @@
688
740
  `;
689
741
  }
690
742
 
691
- // Show download and telegram buttons
743
+ // Show export and telegram buttons
692
744
  if (downloadDropdown) downloadDropdown.style.display = 'inline-block';
693
745
  if (telegramBtn) telegramBtn.style.display = 'inline-flex';
694
746
 
@@ -709,18 +761,18 @@
709
761
  renderTableBody(data.data, data.columns, thresholds, problemFields);
710
762
  }
711
763
 
712
- showToast('Report generated successfully');
764
+ showToast(PG_REPORTS_I18N.success.report_generated);
713
765
  } else {
714
- showToast(data.error || 'Failed to run report', 'error');
766
+ showToast(data.error || PG_REPORTS_I18N.errors.run_report_failed, 'error');
715
767
  }
716
768
  } catch (error) {
717
769
  if (loadingEl) loadingEl.style.display = 'none';
718
- showToast('Network error: ' + error.message, 'error');
770
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
719
771
  }
720
772
 
721
773
  if (button) {
722
774
  button.disabled = false;
723
- button.innerHTML = '▶ Run Report';
775
+ button.innerHTML = PG_REPORTS_I18N.actions.run_report;
724
776
  }
725
777
  }
726
778
 
@@ -833,8 +885,8 @@
833
885
  // Remove
834
886
  saved.splice(existingIdx, 1);
835
887
  btn.classList.remove('saved');
836
- btn.textContent = '📌 Save for Comparison';
837
- showToast('Record removed from saved');
888
+ btn.textContent = PG_REPORTS_I18N.actions.save_for_comparison;
889
+ showToast(PG_REPORTS_I18N.success.record_removed_saved);
838
890
  } else {
839
891
  // Add
840
892
  saved.unshift({
@@ -843,8 +895,8 @@
843
895
  data: row
844
896
  });
845
897
  btn.classList.add('saved');
846
- btn.textContent = '📌 Saved';
847
- showToast('Record saved for comparison');
898
+ btn.textContent = PG_REPORTS_I18N.actions.saved_marker;
899
+ showToast(PG_REPORTS_I18N.success.record_saved);
848
900
  }
849
901
 
850
902
  saveSavedRecords(saved);
@@ -861,24 +913,24 @@
861
913
  const btn = document.querySelector(`.btn-save[onclick*="'${rowId}'"]`);
862
914
  if (btn) {
863
915
  btn.classList.remove('saved');
864
- btn.textContent = '📌 Save for Comparison';
916
+ btn.textContent = PG_REPORTS_I18N.actions.save_for_comparison;
865
917
  }
866
918
 
867
- showToast('Record removed');
919
+ showToast(PG_REPORTS_I18N.success.record_removed);
868
920
  }
869
921
 
870
922
  function clearAllSavedRecords() {
871
- if (!confirm('Remove all saved records for this report?')) return;
923
+ if (!confirm(PG_REPORTS_I18N.saved.confirm_clear_all)) return;
872
924
  saveSavedRecords([]);
873
925
  renderSavedRecords();
874
926
 
875
927
  // Update all buttons in table
876
928
  document.querySelectorAll('.btn-save.saved').forEach(btn => {
877
929
  btn.classList.remove('saved');
878
- btn.textContent = '📌 Save for Comparison';
930
+ btn.textContent = PG_REPORTS_I18N.actions.save_for_comparison;
879
931
  });
880
932
 
881
- showToast('All saved records cleared');
933
+ showToast(PG_REPORTS_I18N.success.all_saved_cleared);
882
934
  }
883
935
 
884
936
  function renderSavedRecords() {
@@ -907,9 +959,9 @@
907
959
  html += `
908
960
  <div class="saved-record-card" id="saved-card-${idx}" onclick="toggleSavedRecordDetail(${idx}, event)">
909
961
  <div class="saved-record-header">
910
- <span class="saved-record-time">▸ Saved: ${savedTime}</span>
911
- <span class="saved-record-expand-hint">Click to expand</span>
912
- <button class="saved-record-remove" onclick="event.stopPropagation(); removeSavedRecord('${record.id}')" title="Remove">×</button>
962
+ <span class="saved-record-time">${PG_REPORTS_I18N.saved.saved_at_prefix} ${savedTime}</span>
963
+ <span class="saved-record-expand-hint">${PG_REPORTS_I18N.saved.click_to_expand}</span>
964
+ <button class="saved-record-remove" onclick="event.stopPropagation(); removeSavedRecord('${record.id}')" title="${PG_REPORTS_I18N.saved.remove_title}">×</button>
913
965
  </div>
914
966
  <div class="saved-record-data">
915
967
  `;
@@ -1034,7 +1086,7 @@
1034
1086
  const query = decodeURIComponent(escape(atob(queryBase64)));
1035
1087
  runExplainAnalyze(query, queryHash);
1036
1088
  } catch (e) {
1037
- showToast('Failed to decode query', 'error');
1089
+ showToast(PG_REPORTS_I18N.errors.decode_query_failed, 'error');
1038
1090
  }
1039
1091
  }
1040
1092
 
@@ -1155,7 +1207,7 @@
1155
1207
  // Show problems list
1156
1208
  if (data.problems && data.problems.length > 0) {
1157
1209
  html += '<div class="explain-problems">';
1158
- html += '<div class="explain-problems-header">⚠️ Detected Issues</div>';
1210
+ html += '<div class="explain-problems-header">' + PG_REPORTS_I18N.sections.detected_issues + '</div>';
1159
1211
  html += '<div class="explain-problems-list">';
1160
1212
 
1161
1213
  data.problems.forEach(problem => {
@@ -1167,7 +1219,7 @@
1167
1219
  html += '<span class="explain-problem-icon">' + severityIcon + '</span>';
1168
1220
  html += '<span class="explain-problem-message">' + escapeHtml(problem.message) + '</span>';
1169
1221
  if (problem.line_number) {
1170
- html += '<span class="explain-problem-line">Line ' + problem.line_number + '</span>';
1222
+ html += '<span class="explain-problem-line">' + PG_REPORTS_I18N.sections.line_label + ' ' + problem.line_number + '</span>';
1171
1223
  }
1172
1224
  html += '</div>';
1173
1225
  if (problem.details) {
@@ -1187,8 +1239,8 @@
1187
1239
  if (data.annotated_lines && data.annotated_lines.length > 0) {
1188
1240
  html += '<div class="explain-output">';
1189
1241
  html += '<div class="explain-output-header">';
1190
- html += '📊 Execution Plan';
1191
- html += '<button class="btn-copy-small" onclick="copyExplainOutput()" title="Copy to clipboard">📋 Copy</button>';
1242
+ html += PG_REPORTS_I18N.sections.execution_plan;
1243
+ html += '<button class="btn-copy-small" onclick="copyExplainOutput()" title="' + PG_REPORTS_I18N.actions.copy_to_clipboard_title + '">' + PG_REPORTS_I18N.actions.copy + '</button>';
1192
1244
  html += '</div>';
1193
1245
  html += '<div class="explain-lines">';
1194
1246
 
@@ -1272,9 +1324,9 @@
1272
1324
  const text = Array.from(lines).map(line => line.textContent).join('\n');
1273
1325
 
1274
1326
  navigator.clipboard.writeText(text).then(() => {
1275
- showToast('EXPLAIN output copied to clipboard');
1327
+ showToast(PG_REPORTS_I18N.success.explain_copied);
1276
1328
  }).catch(() => {
1277
- showToast('Failed to copy', 'error');
1329
+ showToast(PG_REPORTS_I18N.errors.copy_failed, 'error');
1278
1330
  });
1279
1331
  }
1280
1332
 
@@ -1285,11 +1337,10 @@
1285
1337
 
1286
1338
  // Security check
1287
1339
  if (!allowRawQueryExecution) {
1288
- const message = '⚠️ EXPLAIN ANALYZE отключен. Включите в конфигурации: config.allow_raw_query_execution = true';
1289
- showToast(message, 'error');
1340
+ showToast(PG_REPORTS_I18N.errors.explain_disabled_toast, 'error');
1290
1341
  content.innerHTML = `<div class="error-message">
1291
- <strong>⚠️ Query execution is disabled</strong><br><br>
1292
- To enable this feature, add to your configuration:<br>
1342
+ <strong>${PG_REPORTS_I18N.modals.query_execution_disabled_title}</strong><br><br>
1343
+ ${PG_REPORTS_I18N.modals.query_execution_disabled_intro}<br>
1293
1344
  <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
1294
1345
  PgReports.configure do |config|<br>
1295
1346
  &nbsp;&nbsp;config.allow_raw_query_execution = true<br>
@@ -1320,11 +1371,11 @@ end
1320
1371
  if (data.success) {
1321
1372
  content.innerHTML = renderExplainAnalysis(data);
1322
1373
  } else {
1323
- content.innerHTML = `<div class="error-message">${escapeHtml(data.error || 'Failed to run EXPLAIN ANALYZE')}</div>`;
1374
+ content.innerHTML = `<div class="error-message">${escapeHtml(data.error || PG_REPORTS_I18N.errors.explain_analyze_failed)}</div>`;
1324
1375
  }
1325
1376
  } catch (error) {
1326
1377
  loading.style.display = 'none';
1327
- content.innerHTML = `<div class="error-message">Network error: ${escapeHtml(error.message)}</div>`;
1378
+ content.innerHTML = `<div class="error-message">${PG_REPORTS_I18N.errors.network_error_prefix} ${escapeHtml(error.message)}</div>`;
1328
1379
  }
1329
1380
  }
1330
1381
 
@@ -1335,11 +1386,10 @@ end
1335
1386
 
1336
1387
  // Security check
1337
1388
  if (!allowRawQueryExecution) {
1338
- const message = '⚠️ Выполнение запросов отключено. Включите в конфигурации: config.allow_raw_query_execution = true';
1339
- showToast(message, 'error');
1389
+ showToast(PG_REPORTS_I18N.errors.execute_disabled_toast, 'error');
1340
1390
  content.innerHTML = `<div class="error-message">
1341
- <strong>⚠️ Query execution is disabled</strong><br><br>
1342
- To enable this feature, add to your configuration:<br>
1391
+ <strong>${PG_REPORTS_I18N.modals.query_execution_disabled_title}</strong><br><br>
1392
+ ${PG_REPORTS_I18N.modals.query_execution_disabled_intro}<br>
1343
1393
  <code style="display: block; margin-top: 0.5rem; padding: 0.5rem; background: rgba(0,0,0,0.2); border-radius: 4px;">
1344
1394
  PgReports.configure do |config|<br>
1345
1395
  &nbsp;&nbsp;config.allow_raw_query_execution = true<br>
@@ -1372,8 +1422,8 @@ end
1372
1422
 
1373
1423
  // Show info
1374
1424
  html += `<div class="query-results-info">
1375
- <span>Rows: <span class="count">${data.count}</span></span>
1376
- <span>Execution time: <span class="time">${data.execution_time} ms</span></span>
1425
+ <span>${PG_REPORTS_I18N.results.rows_label} <span class="count">${data.count}</span></span>
1426
+ <span>${PG_REPORTS_I18N.results.execution_time_label} <span class="time">${data.execution_time} ms</span></span>
1377
1427
  </div>`;
1378
1428
 
1379
1429
  if (data.rows && data.rows.length > 0) {
@@ -1393,7 +1443,7 @@ end
1393
1443
  html += '<tr>';
1394
1444
  data.columns.forEach(col => {
1395
1445
  const value = row[col];
1396
- const displayValue = value === null ? '<null>' : String(value);
1446
+ const displayValue = value === null ? PG_REPORTS_I18N.results.null_placeholder : String(value);
1397
1447
  html += `<td title="${escapeHtmlAttr(displayValue)}">${escapeHtml(displayValue)}</td>`;
1398
1448
  });
1399
1449
  html += '</tr>';
@@ -1404,19 +1454,19 @@ end
1404
1454
  html += '</div>';
1405
1455
 
1406
1456
  if (data.truncated) {
1407
- html += `<p style="margin-top: 0.5rem; color: var(--text-muted); font-size: 0.75rem;">Showing first ${data.rows.length} of ${data.total_count} rows</p>`;
1457
+ html += `<p style="margin-top: 0.5rem; color: var(--text-muted); font-size: 0.75rem;">${pgReportsFormat(PG_REPORTS_I18N.results.showing_first_of_total, { count: data.rows.length, total: data.total_count })}</p>`;
1408
1458
  }
1409
1459
  } else {
1410
- html += '<p style="margin-top: 1rem; color: var(--text-muted);">No rows returned</p>';
1460
+ html += '<p style="margin-top: 1rem; color: var(--text-muted);">' + PG_REPORTS_I18N.results.no_rows_returned + '</p>';
1411
1461
  }
1412
1462
 
1413
1463
  content.innerHTML = html;
1414
1464
  } else {
1415
- content.innerHTML = `<div class="error-message">${escapeHtml(data.error || 'Failed to execute query')}</div>`;
1465
+ content.innerHTML = `<div class="error-message">${escapeHtml(data.error || PG_REPORTS_I18N.errors.execute_query_failed)}</div>`;
1416
1466
  }
1417
1467
  } catch (error) {
1418
1468
  loading.style.display = 'none';
1419
- content.innerHTML = `<div class="error-message">Network error: ${escapeHtml(error.message)}</div>`;
1469
+ content.innerHTML = `<div class="error-message">${PG_REPORTS_I18N.errors.network_error_prefix} ${escapeHtml(error.message)}</div>`;
1420
1470
  }
1421
1471
  }
1422
1472
 
@@ -1491,9 +1541,9 @@ end
1491
1541
  function copyMigrationCode() {
1492
1542
  if (!currentMigrationData) return;
1493
1543
  navigator.clipboard.writeText(currentMigrationData.code).then(() => {
1494
- showToast('Migration code copied to clipboard');
1544
+ showToast(PG_REPORTS_I18N.success.migration_copied);
1495
1545
  }).catch(() => {
1496
- showToast('Failed to copy', 'error');
1546
+ showToast(PG_REPORTS_I18N.errors.copy_failed, 'error');
1497
1547
  });
1498
1548
  }
1499
1549
 
@@ -1540,7 +1590,7 @@ end
1540
1590
  const data = await response.json();
1541
1591
 
1542
1592
  if (data.success) {
1543
- showToast('Migration file created');
1593
+ showToast(PG_REPORTS_I18N.success.migration_created);
1544
1594
  closeMigrationModal();
1545
1595
 
1546
1596
  // Open in IDE if path provided
@@ -1555,10 +1605,10 @@ end
1555
1605
  }
1556
1606
  }
1557
1607
  } else {
1558
- showToast(data.error || 'Failed to create migration', 'error');
1608
+ showToast(data.error || PG_REPORTS_I18N.errors.create_migration_failed, 'error');
1559
1609
  }
1560
1610
  } catch (error) {
1561
- showToast('Network error: ' + error.message, 'error');
1611
+ showToast(PG_REPORTS_I18N.errors.network_error_prefix + ' ' + error.message, 'error');
1562
1612
  }
1563
1613
  }
1564
1614
  </script>
@@ -3,7 +3,7 @@
3
3
  .documentation-section {
4
4
  background: var(--bg-card);
5
5
  border: 1px solid var(--border-color);
6
- border-radius: 12px;
6
+ border-radius: 6px;
7
7
  overflow: hidden;
8
8
  }
9
9
 
@@ -138,9 +138,9 @@
138
138
  margin-top: 4px;
139
139
  background: var(--bg-card);
140
140
  border: 1px solid var(--border-color);
141
- border-radius: 10px;
141
+ border-radius: 6px;
142
142
  min-width: 160px;
143
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
143
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
144
144
  z-index: 100;
145
145
  overflow: hidden;
146
146
  }
@@ -167,6 +167,26 @@
167
167
  border-bottom: 1px solid var(--border-color);
168
168
  }
169
169
 
170
+ .dropdown-divider {
171
+ height: 0;
172
+ margin: 0;
173
+ border-top: 1px solid var(--border-color);
174
+ }
175
+
176
+ .dropdown-menu .ai-icon {
177
+ display: inline-block;
178
+ font-size: 0.6rem;
179
+ font-weight: 800;
180
+ background: #8b5cf6;
181
+ color: #fff;
182
+ border-radius: 3px;
183
+ padding: 0.1rem 0.3rem;
184
+ letter-spacing: 0.03em;
185
+ line-height: 1.2;
186
+ vertical-align: middle;
187
+ margin-right: 0.2rem;
188
+ }
189
+
170
190
  /* Clickable rows */
171
191
  .results-table tbody tr.data-row {
172
192
  cursor: pointer;
@@ -260,11 +280,29 @@
260
280
  .problem-modal-content {
261
281
  background: var(--bg-card);
262
282
  border: 1px solid var(--border-color);
263
- border-radius: 16px;
283
+ border-radius: 6px;
264
284
  max-width: 600px;
265
285
  width: 90%;
266
286
  max-height: 80vh;
267
287
  overflow: auto;
288
+ scrollbar-width: thin;
289
+ scrollbar-color: var(--border-color) var(--bg-secondary);
290
+ }
291
+
292
+ .problem-modal-content::-webkit-scrollbar {
293
+ width: 8px;
294
+ height: 8px;
295
+ }
296
+ .problem-modal-content::-webkit-scrollbar-track {
297
+ background: var(--bg-secondary);
298
+ border-radius: 4px;
299
+ }
300
+ .problem-modal-content::-webkit-scrollbar-thumb {
301
+ background: var(--border-color);
302
+ border-radius: 4px;
303
+ }
304
+ .problem-modal-content::-webkit-scrollbar-thumb:hover {
305
+ background: var(--text-muted);
268
306
  }
269
307
 
270
308
  .problem-modal-header {
@@ -649,7 +687,7 @@
649
687
  .saved-records-section {
650
688
  background: var(--bg-card);
651
689
  border: 1px solid var(--accent-purple);
652
- border-radius: 12px;
690
+ border-radius: 6px;
653
691
  margin-bottom: 1rem;
654
692
  overflow: hidden;
655
693
  }
@@ -1161,7 +1199,7 @@
1161
1199
  .filter-section {
1162
1200
  background: var(--bg-card);
1163
1201
  border: 1px solid var(--border-color);
1164
- border-radius: 12px;
1202
+ border-radius: 6px;
1165
1203
  overflow: hidden;
1166
1204
  }
1167
1205
 
@@ -1272,7 +1310,7 @@
1272
1310
  /* Summary card at top */
1273
1311
  .explain-summary {
1274
1312
  background: var(--bg-card);
1275
- border-radius: 12px;
1313
+ border-radius: 6px;
1276
1314
  padding: 1.25rem;
1277
1315
  margin-bottom: 1.5rem;
1278
1316
  border-left: 4px solid var(--accent-blue);
@@ -1344,7 +1382,7 @@
1344
1382
  .explain-stat {
1345
1383
  background: var(--bg-card);
1346
1384
  border: 1px solid var(--border-color);
1347
- border-radius: 10px;
1385
+ border-radius: 6px;
1348
1386
  padding: 1rem;
1349
1387
  display: flex;
1350
1388
  flex-direction: column;
@@ -1387,7 +1425,7 @@
1387
1425
  .explain-problems {
1388
1426
  background: var(--bg-card);
1389
1427
  border: 1px solid var(--border-color);
1390
- border-radius: 12px;
1428
+ border-radius: 6px;
1391
1429
  padding: 1.25rem;
1392
1430
  margin-bottom: 1.5rem;
1393
1431
  }
@@ -1482,7 +1520,7 @@
1482
1520
  .explain-output {
1483
1521
  background: var(--bg-card);
1484
1522
  border: 1px solid var(--border-color);
1485
- border-radius: 12px;
1523
+ border-radius: 6px;
1486
1524
  overflow: hidden;
1487
1525
  }
1488
1526
 
@@ -1633,7 +1671,7 @@
1633
1671
  overflow-x: auto;
1634
1672
  background: var(--bg-card);
1635
1673
  border: 1px solid var(--border-color);
1636
- border-radius: 12px;
1674
+ border-radius: 6px;
1637
1675
  padding: 1.25rem;
1638
1676
  color: var(--text-secondary);
1639
1677
  }