pg_reports 0.8.2 → 0.9.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.
@@ -554,9 +554,12 @@ uk:
554
554
  title: "PgReports"
555
555
  subtitle: "Дашборд аналізу PostgreSQL"
556
556
  page_title: "PgReports — Дашборд"
557
+ language:
558
+ label: "Мова"
559
+ name: "Українська"
557
560
  navigation:
558
561
  dashboard: "Дашборд"
559
- back: "Назад"
562
+ back: "Назад"
560
563
  database_selector:
561
564
  label: "База даних"
562
565
  target_selector:
@@ -564,40 +567,41 @@ uk:
564
567
  actions:
565
568
  cancel: "Скасувати"
566
569
  retry: "Повторити"
567
- copy: "📋 Копіювати"
568
- copy_query: "📋 Копіювати запит"
569
- copy_code: "📋 Копіювати код"
570
+ copy: "Копіювати"
571
+ copy_query: "Копіювати запит"
572
+ copy_code: "Копіювати код"
570
573
  copy_to_clipboard_title: "Скопіювати в буфер обміну"
571
- copied_feedback: "Скопійовано!"
574
+ copied_feedback: "Скопійовано!"
572
575
  clear_all: "Очистити все"
573
- run_report: "Запустити звіт"
574
- export: "Експорт"
575
- download_text: "📄 Текст (.txt)"
576
- download_csv: "📊 CSV (.csv)"
577
- download_json: "📋 JSON (.json)"
578
- download: "📥 Завантажити"
576
+ run_report: "Запустити звіт"
577
+ export: "Експорт"
578
+ download_text: "Текст (.txt)"
579
+ download_csv: "CSV (.csv)"
580
+ download_json: "JSON (.json)"
581
+ download: "Завантажити"
579
582
  copy_ai_prompt: "Копіювати промпт"
580
- send_telegram: "📨 Telegram"
583
+ send_telegram: "Telegram"
581
584
  sending: "Надсилання..."
582
- reset_statistics: "🗑️ Скинути статистику"
585
+ reset_statistics: "Скинути статистику"
583
586
  resetting: "Скидання..."
584
587
  confirm_reset: "Так, скинути"
585
- create_extension: "Створити розширення"
588
+ create_extension: "Створити розширення"
586
589
  creating: "Створення..."
587
- ide_settings_button_title: "Налаштування IDE"
588
- explain_analyze: "📊 EXPLAIN ANALYZE"
589
- execute_query: "Виконати запит"
590
+ settings_button_title: "Налаштування"
591
+ explain_analyze: "EXPLAIN ANALYZE"
592
+ execute_query: "Виконати запит"
590
593
  run_query: "SQL-консоль"
591
- create_migration_file: "📁 Створити файл і відкрити в IDE"
592
- start_monitoring: "Запустити моніторинг"
593
- stop_monitoring: "Зупинити моніторинг"
594
+ create_migration_file: "Створити файл і відкрити в IDE"
595
+ start_monitoring: "Запустити моніторинг"
596
+ stop_monitoring: "Зупинити моніторинг"
594
597
  starting: "Запуск..."
595
598
  stopping: "Зупинка..."
596
- load_history: "📜 Завантажити історію (50)"
599
+ load_history: "Завантажити історію"
600
+ load_history_unavailable_title: "Збереженої історії немає — задайте config.query_monitor_log_file, щоб запити зберігалися між перезапусками"
597
601
  loading: "Завантаження..."
598
602
  running: "Виконання..."
599
- save_for_comparison: "📌 Зберегти для порівняння"
600
- saved_marker: "📌 Збережено"
603
+ save_for_comparison: "Зберегти для порівняння"
604
+ saved_marker: "Збережено"
601
605
  status:
602
606
  pg_stat_ready: "Активно"
603
607
  not_preloaded: "Потрібне передзавантаження"
@@ -615,25 +619,26 @@ uk:
615
619
  create_extension_title: "Створення розширення pg_stat_statements"
616
620
  create_extension_intro: "Розширення pg_stat_statements ще не створено в цій базі даних. Натисніть, щоб створити його:"
617
621
  create_extension_note: "Якщо бібліотеку не передзавантажено, її також потрібно додати до shared_preload_libraries та перезапустити PostgreSQL."
618
- reset_stats_title: "⚠️ Скидання статистики"
622
+ reset_stats_title: "Скидання статистики"
619
623
  reset_stats_confirm: "Ви впевнені, що хочете скинути статистику pg_stat_statements?"
620
624
  reset_stats_warning: "Ця дія очистить усю зібрану статистику запитів і не може бути скасована."
621
- ide_settings_title: "⚙️ Налаштування IDE"
622
- problem_detected_title: "⚠️ Виявлено проблему"
623
- query_analyzer_title: "📊 Аналізатор запиту"
625
+ settings_title: "Налаштування"
626
+ problem_detected_title: "Виявлено проблему"
627
+ query_analyzer_title: "Аналізатор запиту"
624
628
  query_label: "Запит:"
625
629
  parameters_label: "Параметри:"
626
- migration_title: "🗑️ Міграція видалення індексу"
630
+ migration_title: "Міграція видалення індексу"
627
631
  migration_subtitle: "Згенерована міграція для видалення індексу:"
628
632
  migration_warning: "Створення міграції згенерує файл міграції у вашому проєкті. Запуск цієї міграції видалить індекс із БД, що може значно вплинути на продуктивність застосунку."
629
633
  migration_warning_dev_only: "Цю операцію слід виконувати лише в локальному dev-середовищі."
630
- query_execution_disabled_title: "⚠️ Виконання запитів вимкнено"
634
+ query_execution_disabled_title: "Виконання запитів вимкнено"
635
+ migration_disabled_title: "Створення міграцій вимкнено"
631
636
  query_execution_disabled_intro: "Щоб увімкнути цю функцію, додайте до конфігурації:"
632
637
  query_execution_disabled_env_note: "...або замість цього встановіть змінну середовища:"
633
638
  run_query_title: "SQL-консоль"
634
639
  run_query_placeholder: "SELECT * FROM users LIMIT 10;"
635
640
  settings:
636
- default_ide_label: "IDE за замовчуванням для посилань на джерела:"
641
+ default_ide_label: "IDE за замовчуванням для посилань на джерела"
637
642
  ide_show_menu: "Показувати меню (за замовчуванням)"
638
643
  ide_vscode_wsl: "VS Code (WSL)"
639
644
  ide_vscode: "VS Code"
@@ -670,25 +675,27 @@ uk:
670
675
  waiting_for_locks: "чекають блокувань"
671
676
  percent_used_suffix: "% використано"
672
677
  categories:
673
- requires_pg_stat: "🔒 Потрібен pg_stat_statements"
678
+ requires_pg_stat: "Потрібен pg_stat_statements"
679
+ pg_stat_missing_reason: "Потрібен pg_stat_statements, але розширення не встановлено в базі «%{database}». Створіть його там або перемкніться на базу, де воно є."
680
+ pg_stat_not_preloaded_reason: "Потрібен pg_stat_statements. Розширення є в базі «%{database}», але відсутнє в shared_preload_libraries — даних не буде, доки PostgreSQL не перезапустять із ним."
674
681
  reports_count_suffix: "звітів"
675
- primary_only_reason: "🔒 Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
676
- standalone_no_host_app_reason: "🔒 Недоступно в автономному режимі — ця категорія перевіряє моделі хост-застосунку, а при самостійному запуску дашборда хост-застосунку немає."
682
+ primary_only_reason: "Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
683
+ standalone_no_host_app_reason: "Недоступно в автономному режимі — ця категорія перевіряє моделі хост-застосунку, а при самостійному запуску дашборда хост-застосунку немає."
677
684
  documentation:
678
- toggle_title: "📖 Що показує цей звіт?"
679
- what_section: "📋 Що"
680
- why_section: "Чому це важливо"
681
- nuances_section: "⚠️ Нюанси"
682
- thresholds_section: "📊 Пороги"
683
- threshold_warning_label: "⚠️ Warning:"
684
- threshold_critical_label: "🔴 Critical:"
685
+ toggle_title: "Що показує цей звіт?"
686
+ what_section: "Що"
687
+ why_section: "Чому це важливо"
688
+ nuances_section: "Нюанси"
689
+ thresholds_section: "Пороги"
690
+ threshold_warning_label: "Warning:"
691
+ threshold_critical_label: "Critical:"
685
692
  threshold_inverted_note: "(менше — гірше)"
686
693
  filters:
687
- title: "🔍 Параметри фільтрації"
694
+ title: "Параметри фільтрації"
688
695
  current_value: "зараз"
689
696
  saved:
690
- title: "📌 Збережено для порівняння"
691
- saved_at_prefix: "Збережено:"
697
+ title: "Збережено для порівняння"
698
+ saved_at_prefix: "Збережено:"
692
699
  click_to_expand: "Натисніть, щоб розгорнути"
693
700
  confirm_clear_all: "Видалити всі збережені записи для цього звіту?"
694
701
  remove_title: "Видалити"
@@ -702,9 +709,9 @@ uk:
702
709
  execution_time_label: "Час виконання:"
703
710
  null_placeholder: "<null>"
704
711
  sections:
705
- recommendation: "💡 Рекомендація"
706
- detected_issues: "⚠️ Виявлені проблеми"
707
- execution_plan: "📊 План виконання"
712
+ recommendation: "Рекомендація"
713
+ detected_issues: "Виявлені проблеми"
714
+ execution_plan: "План виконання"
708
715
  line_label: "Рядок"
709
716
  current_label: "Поточне:"
710
717
  threshold_label: "Поріг:"
@@ -712,8 +719,8 @@ uk:
712
719
  warning_eq: "warning"
713
720
  critical_eq: "critical"
714
721
  levels:
715
- critical: "🔴 Критично"
716
- warning: "⚠️ Увага"
722
+ critical: "Критично"
723
+ warning: "Увага"
717
724
  errors:
718
725
  error_prefix: "Помилка:"
719
726
  unable_fetch_metrics: "Не вдалося отримати статистику БД."
@@ -741,8 +748,9 @@ uk:
741
748
  run_query_failed: "Не вдалося виконати запит"
742
749
  query_required: "Введіть запит"
743
750
  create_migration_failed: "Не вдалося створити міграцію"
744
- explain_disabled_toast: "⚠️ EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
745
- execute_disabled_toast: "⚠️ Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
751
+ explain_disabled_toast: "EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
752
+ execute_disabled_toast: "Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
753
+ migration_disabled_toast: "Створення міграцій вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
746
754
  query_monitoring_error: "Помилка моніторингу запитів"
747
755
  query_hash_required: "Потрібен хеш запиту"
748
756
  query_execution_disabled: "Виконання запитів із дашборду вимкнено. Увімкніть у конфігурації: 'config.allow_raw_query_execution = true'"
data/config/routes.rb CHANGED
@@ -9,6 +9,7 @@ PgReports::Engine.routes.draw do
9
9
 
10
10
  post "switch_database", to: "dashboard#switch_database", as: :switch_database
11
11
  post "switch_target", to: "dashboard#switch_target", as: :switch_target
12
+ post "switch_locale", to: "dashboard#switch_locale", as: :switch_locale
12
13
 
13
14
  post "enable_pg_stat_statements", to: "dashboard#enable_pg_stat_statements", as: :enable_pg_stat_statements
14
15
  post "reset_statistics", to: "dashboard#reset_statistics", as: :reset_statistics
@@ -9,6 +9,16 @@ module PgReports
9
9
  # PgReports::Connection::ErrorTranslator.translate(error)
10
10
  # # => { title: "...", detail: "...", hint: "GRANT ...", code: "42501" }
11
11
  module ErrorTranslator
12
+ # Relations the reports read that are supplied by an extension rather than
13
+ # by core PostgreSQL. Used to turn "relation X does not exist" into the
14
+ # actionable "extension Y isn't installed here".
15
+ EXTENSION_RELATIONS = {
16
+ "pg_stat_statements" => "pg_stat_statements",
17
+ "pg_stat_statements_info" => "pg_stat_statements",
18
+ "pgstattuple" => "pgstattuple",
19
+ "pg_buffercache" => "pg_buffercache"
20
+ }.freeze
21
+
12
22
  module_function
13
23
 
14
24
  # Returns a Hash with :title, :detail, :hint, :code, :raw_message.
@@ -23,6 +33,8 @@ module PgReports
23
33
  when "28000", "28P01" then auth_failed(message)
24
34
  when "08001", "08006", "08000", "08003", "08004" then connection_refused(message)
25
35
  when "53300" then too_many_connections(message)
36
+ when "42P01" then undefined_table(message)
37
+ when "42883" then undefined_function(message)
26
38
  else generic(error)
27
39
  end
28
40
 
@@ -92,6 +104,37 @@ module PgReports
92
104
  }
93
105
  end
94
106
 
107
+ # A report asked for a relation this database doesn't have. Almost always an
108
+ # extension that exists on one database in the cluster but not the one
109
+ # currently selected — extensions are per-database, the dashboard is not.
110
+ def undefined_table(message)
111
+ target = extract_object(message, /relation "([^"]+)" does not exist/)
112
+
113
+ if target && EXTENSION_RELATIONS.key?(target)
114
+ extension = EXTENSION_RELATIONS.fetch(target)
115
+ return {
116
+ title: "#{extension} not available",
117
+ detail: "This report reads \"#{target}\", which does not exist on the selected database.",
118
+ hint: "CREATE EXTENSION IF NOT EXISTS #{extension};"
119
+ }
120
+ end
121
+
122
+ {
123
+ title: "Relation not found",
124
+ detail: target ? "The selected database has no relation named \"#{target}\"." : "The report referenced a relation that does not exist on the selected database.",
125
+ hint: nil
126
+ }
127
+ end
128
+
129
+ def undefined_function(message)
130
+ target = extract_object(message, /function ([\w.]+)\(/)
131
+ {
132
+ title: "Function not found",
133
+ detail: target ? "The selected database has no function named \"#{target}\"." : "The report called a function that does not exist on the selected database.",
134
+ hint: "It is usually provided by an extension that is not installed on this database."
135
+ }
136
+ end
137
+
95
138
  def generic(error)
96
139
  {
97
140
  title: error.class.name.split("::").last,
@@ -237,6 +237,10 @@ module PgReports
237
237
  name: "Queries",
238
238
  icon: "⚡",
239
239
  color: "#6366f1",
240
+ # Every report here reads the pg_stat_statements view. The extension is
241
+ # created per-database, so availability has to be re-checked against
242
+ # whichever database the dashboard is pointed at — not once at boot.
243
+ requires: :pg_stat_statements,
240
244
  reports: {
241
245
  slow_queries: {name: "Slow Queries", description: "Queries with high mean execution time"},
242
246
  heavy_queries: {name: "Heavy Queries", description: "Most frequently called queries"},
@@ -412,6 +416,12 @@ module PgReports
412
416
  def self.target_constraint(category)
413
417
  REPORTS.dig(category.to_sym, :target_constraint)
414
418
  end
419
+
420
+ # A database-level dependency the whole category needs, or nil. Currently
421
+ # only :pg_stat_statements — the caller resolves what "available" means.
422
+ def self.requires(category)
423
+ REPORTS.dig(category.to_sym, :requires)
424
+ end
415
425
  end
416
426
  end
417
427
  end
@@ -313,15 +313,12 @@ module PgReports
313
313
  if @summary[:critical_problems] > 0
314
314
  @summary[:status] = "critical"
315
315
  @summary[:status_text] = "Critical issues detected"
316
- @summary[:status_icon] = "🔴"
317
316
  elsif @summary[:warnings] > 0
318
317
  @summary[:status] = "warning"
319
318
  @summary[:status_text] = "Potential issues detected"
320
- @summary[:status_icon] = "🟡"
321
319
  else
322
320
  @summary[:status] = "good"
323
321
  @summary[:status_text] = "No issues detected"
324
- @summary[:status_icon] = "🟢"
325
322
  end
326
323
 
327
324
  # Group problems by type for summary
@@ -106,10 +106,18 @@ module PgReports
106
106
  {
107
107
  enabled: enabled,
108
108
  session_id: session_id,
109
- query_count: @queries.size
109
+ query_count: @queries.size,
110
+ history_available: history_available?
110
111
  }
111
112
  end
112
113
 
114
+ # Whether #load_from_log has anything to return. History is persisted only
115
+ # when a log file is configured, and the file exists only once something has
116
+ # been written to it — without both, "Load history" can only ever no-op.
117
+ def history_available?
118
+ log_file_enabled? && File.exist?(log_file_path)
119
+ end
120
+
113
121
  def queries(limit: nil, session_id: nil)
114
122
  result = @queries.dup
115
123
 
@@ -348,6 +356,10 @@ module PgReports
348
356
  end
349
357
  end
350
358
 
359
+ # JSON.generate, not #to_json: ActiveSupport overrides #to_json and (up to
360
+ # Rails 7.0) passes json's removed `quirks_mode` option, so on json >= 3 it
361
+ # raises ArgumentError. The rescue below would swallow that and leave the
362
+ # log silently empty. json's own generator takes no such option.
351
363
  def write_session_marker(marker_type, sid = @session_id)
352
364
  return unless log_file_enabled?
353
365
 
@@ -358,7 +370,7 @@ module PgReports
358
370
  }
359
371
 
360
372
  File.open(log_file_path, "a") do |f|
361
- f.puts marker.to_json
373
+ f.puts JSON.generate(marker)
362
374
  end
363
375
  rescue => e
364
376
  # Silently fail - don't break monitoring if file write fails
@@ -371,7 +383,7 @@ module PgReports
371
383
 
372
384
  File.open(log_file_path, "a") do |f|
373
385
  @queries.each do |query|
374
- f.puts query.to_json
386
+ f.puts JSON.generate(query)
375
387
  end
376
388
  end
377
389
  rescue => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- VERSION = "0.8.2"
4
+ VERSION = "0.9.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_reports
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eldar Avatov
@@ -153,6 +153,7 @@ files:
153
153
  - app/views/layouts/pg_reports/application.html.erb
154
154
  - app/views/pg_reports/dashboard/_database_selector.html.erb
155
155
  - app/views/pg_reports/dashboard/_fake_source_data.html.erb
156
+ - app/views/pg_reports/dashboard/_settings_modal.html.erb
156
157
  - app/views/pg_reports/dashboard/_show_modals.html.erb
157
158
  - app/views/pg_reports/dashboard/_show_scripts.html.erb
158
159
  - app/views/pg_reports/dashboard/_show_styles.html.erb