pg_reports 0.8.1 → 0.9.0

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.
@@ -556,7 +556,7 @@ uk:
556
556
  page_title: "PgReports — Дашборд"
557
557
  navigation:
558
558
  dashboard: "Дашборд"
559
- back: "Назад"
559
+ back: "Назад"
560
560
  database_selector:
561
561
  label: "База даних"
562
562
  target_selector:
@@ -564,39 +564,41 @@ uk:
564
564
  actions:
565
565
  cancel: "Скасувати"
566
566
  retry: "Повторити"
567
- copy: "📋 Копіювати"
568
- copy_query: "📋 Копіювати запит"
569
- copy_code: "📋 Копіювати код"
567
+ copy: "Копіювати"
568
+ copy_query: "Копіювати запит"
569
+ copy_code: "Копіювати код"
570
570
  copy_to_clipboard_title: "Скопіювати в буфер обміну"
571
- copied_feedback: "Скопійовано!"
571
+ copied_feedback: "Скопійовано!"
572
572
  clear_all: "Очистити все"
573
- run_report: "Запустити звіт"
574
- export: "Експорт"
575
- download_text: "📄 Текст (.txt)"
576
- download_csv: "📊 CSV (.csv)"
577
- download_json: "📋 JSON (.json)"
578
- download: "📥 Завантажити"
573
+ run_report: "Запустити звіт"
574
+ export: "Експорт"
575
+ download_text: "Текст (.txt)"
576
+ download_csv: "CSV (.csv)"
577
+ download_json: "JSON (.json)"
578
+ download: "Завантажити"
579
579
  copy_ai_prompt: "Копіювати промпт"
580
- send_telegram: "📨 Telegram"
580
+ send_telegram: "Telegram"
581
581
  sending: "Надсилання..."
582
- reset_statistics: "🗑️ Скинути статистику"
582
+ reset_statistics: "Скинути статистику"
583
583
  resetting: "Скидання..."
584
584
  confirm_reset: "Так, скинути"
585
- create_extension: "Створити розширення"
585
+ create_extension: "Створити розширення"
586
586
  creating: "Створення..."
587
587
  ide_settings_button_title: "Налаштування IDE"
588
- explain_analyze: "📊 EXPLAIN ANALYZE"
589
- execute_query: "Виконати запит"
590
- create_migration_file: "📁 Створити файл і відкрити в IDE"
591
- start_monitoring: " Запустити моніторинг"
592
- stop_monitoring: " Зупинити моніторинг"
588
+ explain_analyze: "EXPLAIN ANALYZE"
589
+ execute_query: "Виконати запит"
590
+ run_query: "SQL-консоль"
591
+ create_migration_file: "Створити файл і відкрити в IDE"
592
+ start_monitoring: "Запустити моніторинг"
593
+ stop_monitoring: "Зупинити моніторинг"
593
594
  starting: "Запуск..."
594
595
  stopping: "Зупинка..."
595
- load_history: "📜 Завантажити історію (50)"
596
+ load_history: "Завантажити історію"
597
+ load_history_unavailable_title: "Збереженої історії немає — задайте config.query_monitor_log_file, щоб запити зберігалися між перезапусками"
596
598
  loading: "Завантаження..."
597
599
  running: "Виконання..."
598
- save_for_comparison: "📌 Зберегти для порівняння"
599
- saved_marker: "📌 Збережено"
600
+ save_for_comparison: "Зберегти для порівняння"
601
+ saved_marker: "Збережено"
600
602
  status:
601
603
  pg_stat_ready: "Активно"
602
604
  not_preloaded: "Потрібне передзавантаження"
@@ -614,20 +616,24 @@ uk:
614
616
  create_extension_title: "Створення розширення pg_stat_statements"
615
617
  create_extension_intro: "Розширення pg_stat_statements ще не створено в цій базі даних. Натисніть, щоб створити його:"
616
618
  create_extension_note: "Якщо бібліотеку не передзавантажено, її також потрібно додати до shared_preload_libraries та перезапустити PostgreSQL."
617
- reset_stats_title: "⚠️ Скидання статистики"
619
+ reset_stats_title: "Скидання статистики"
618
620
  reset_stats_confirm: "Ви впевнені, що хочете скинути статистику pg_stat_statements?"
619
621
  reset_stats_warning: "Ця дія очистить усю зібрану статистику запитів і не може бути скасована."
620
- ide_settings_title: "⚙️ Налаштування IDE"
621
- problem_detected_title: "⚠️ Виявлено проблему"
622
- query_analyzer_title: "📊 Аналізатор запиту"
622
+ ide_settings_title: "Налаштування IDE"
623
+ problem_detected_title: "Виявлено проблему"
624
+ query_analyzer_title: "Аналізатор запиту"
623
625
  query_label: "Запит:"
624
626
  parameters_label: "Параметри:"
625
- migration_title: "🗑️ Міграція видалення індексу"
627
+ migration_title: "Міграція видалення індексу"
626
628
  migration_subtitle: "Згенерована міграція для видалення індексу:"
627
629
  migration_warning: "Створення міграції згенерує файл міграції у вашому проєкті. Запуск цієї міграції видалить індекс із БД, що може значно вплинути на продуктивність застосунку."
628
630
  migration_warning_dev_only: "Цю операцію слід виконувати лише в локальному dev-середовищі."
629
- query_execution_disabled_title: "⚠️ Виконання запитів вимкнено"
631
+ query_execution_disabled_title: "Виконання запитів вимкнено"
632
+ migration_disabled_title: "Створення міграцій вимкнено"
630
633
  query_execution_disabled_intro: "Щоб увімкнути цю функцію, додайте до конфігурації:"
634
+ query_execution_disabled_env_note: "...або замість цього встановіть змінну середовища:"
635
+ run_query_title: "SQL-консоль"
636
+ run_query_placeholder: "SELECT * FROM users LIMIT 10;"
631
637
  settings:
632
638
  default_ide_label: "IDE за замовчуванням для посилань на джерела:"
633
639
  ide_show_menu: "Показувати меню (за замовчуванням)"
@@ -666,24 +672,27 @@ uk:
666
672
  waiting_for_locks: "чекають блокувань"
667
673
  percent_used_suffix: "% використано"
668
674
  categories:
669
- requires_pg_stat: "🔒 Потрібен pg_stat_statements"
675
+ requires_pg_stat: "Потрібен pg_stat_statements"
676
+ pg_stat_missing_reason: "Потрібен pg_stat_statements, але розширення не встановлено в базі «%{database}». Створіть його там або перемкніться на базу, де воно є."
677
+ pg_stat_not_preloaded_reason: "Потрібен pg_stat_statements. Розширення є в базі «%{database}», але відсутнє в shared_preload_libraries — даних не буде, доки PostgreSQL не перезапустять із ним."
670
678
  reports_count_suffix: "звітів"
671
- primary_only_reason: "🔒 Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
679
+ primary_only_reason: "Ця категорія працює лише на первинній базі — вона перевіряє моделі хост-застосунку. Щоб користуватися, перемкніться на базу за замовчуванням."
680
+ standalone_no_host_app_reason: "Недоступно в автономному режимі — ця категорія перевіряє моделі хост-застосунку, а при самостійному запуску дашборда хост-застосунку немає."
672
681
  documentation:
673
- toggle_title: "📖 Що показує цей звіт?"
674
- what_section: "📋 Що"
675
- why_section: "Чому це важливо"
676
- nuances_section: "⚠️ Нюанси"
677
- thresholds_section: "📊 Пороги"
678
- threshold_warning_label: "⚠️ Warning:"
679
- threshold_critical_label: "🔴 Critical:"
682
+ toggle_title: "Що показує цей звіт?"
683
+ what_section: "Що"
684
+ why_section: "Чому це важливо"
685
+ nuances_section: "Нюанси"
686
+ thresholds_section: "Пороги"
687
+ threshold_warning_label: "Warning:"
688
+ threshold_critical_label: "Critical:"
680
689
  threshold_inverted_note: "(менше — гірше)"
681
690
  filters:
682
- title: "🔍 Параметри фільтрації"
691
+ title: "Параметри фільтрації"
683
692
  current_value: "зараз"
684
693
  saved:
685
- title: "📌 Збережено для порівняння"
686
- saved_at_prefix: "Збережено:"
694
+ title: "Збережено для порівняння"
695
+ saved_at_prefix: "Збережено:"
687
696
  click_to_expand: "Натисніть, щоб розгорнути"
688
697
  confirm_clear_all: "Видалити всі збережені записи для цього звіту?"
689
698
  remove_title: "Видалити"
@@ -697,9 +706,9 @@ uk:
697
706
  execution_time_label: "Час виконання:"
698
707
  null_placeholder: "<null>"
699
708
  sections:
700
- recommendation: "💡 Рекомендація"
701
- detected_issues: "⚠️ Виявлені проблеми"
702
- execution_plan: "📊 План виконання"
709
+ recommendation: "Рекомендація"
710
+ detected_issues: "Виявлені проблеми"
711
+ execution_plan: "План виконання"
703
712
  line_label: "Рядок"
704
713
  current_label: "Поточне:"
705
714
  threshold_label: "Поріг:"
@@ -707,8 +716,8 @@ uk:
707
716
  warning_eq: "warning"
708
717
  critical_eq: "critical"
709
718
  levels:
710
- critical: "🔴 Критично"
711
- warning: "⚠️ Увага"
719
+ critical: "Критично"
720
+ warning: "Увага"
712
721
  errors:
713
722
  error_prefix: "Помилка:"
714
723
  unable_fetch_metrics: "Не вдалося отримати статистику БД."
@@ -733,13 +742,19 @@ uk:
733
742
  decode_query_failed: "Не вдалося декодувати запит"
734
743
  explain_analyze_failed: "Не вдалося виконати EXPLAIN ANALYZE"
735
744
  execute_query_failed: "Не вдалося виконати запит"
745
+ run_query_failed: "Не вдалося виконати запит"
746
+ query_required: "Введіть запит"
736
747
  create_migration_failed: "Не вдалося створити міграцію"
737
- explain_disabled_toast: "⚠️ EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
738
- execute_disabled_toast: "⚠️ Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
748
+ explain_disabled_toast: "EXPLAIN ANALYZE вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
749
+ execute_disabled_toast: "Виконання запитів вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
750
+ migration_disabled_toast: "Створення міграцій вимкнено. Увімкніть у конфігурації: config.allow_raw_query_execution = true"
739
751
  query_monitoring_error: "Помилка моніторингу запитів"
740
752
  query_hash_required: "Потрібен хеш запиту"
741
753
  query_execution_disabled: "Виконання запитів із дашборду вимкнено. Увімкніть у конфігурації: 'config.allow_raw_query_execution = true'"
742
754
  query_not_found_expired: "Запит не знайдено або термін дії минув. Оновіть сторінку."
755
+ query_monitor_unavailable_standalone: "SQL Query Monitor недоступний в автономному режимі — немає процесу основного застосунку для спостереження."
756
+ query_timed_out: "Перевищено час очікування запиту (%{ms} мс, statement_timeout). Звузьте запит або збільшіть config.raw_query_statement_timeout_ms."
757
+ rate_limit_exceeded: "Забагато запитів. Зачекайте трохи і спробуйте знову."
743
758
  security_violation_prefix: "Порушення безпеки:"
744
759
  trigger_variables_not_allowed: "Не можна виконати EXPLAIN ANALYZE для запитів із тригерними змінними (NEW, OLD). Вони доступні лише в контексті тригерних функцій."
745
760
  missing_parameter_values: "Вкажіть значення для всіх плейсхолдерів параметрів ($1, $2 тощо)"
data/config/routes.rb CHANGED
@@ -14,6 +14,7 @@ PgReports::Engine.routes.draw do
14
14
  post "reset_statistics", to: "dashboard#reset_statistics", as: :reset_statistics
15
15
  post "explain_analyze", to: "dashboard#explain_analyze", as: :explain_analyze
16
16
  post "execute_query", to: "dashboard#execute_query", as: :execute_query
17
+ post "run_query", to: "dashboard#run_query", as: :run_query
17
18
  post "create_migration", to: "dashboard#create_migration", as: :create_migration
18
19
 
19
20
  # Query monitoring
@@ -27,6 +27,7 @@ module PgReports
27
27
 
28
28
  # Dashboard settings
29
29
  attr_accessor :dashboard_auth # Proc for dashboard authentication
30
+ attr_accessor :standalone # True when running via PgReports::Standalone (no host app)
30
31
 
31
32
  # Assets / privacy settings
32
33
  attr_accessor :load_external_fonts # When true, loads Google Fonts in the dashboard layout
@@ -42,6 +43,9 @@ module PgReports
42
43
  # Security settings
43
44
  attr_accessor :allow_raw_query_execution # Allow execute_query and explain_analyze from dashboard
44
45
  attr_accessor :allow_migration_creation # Allow dashboard's "Generate Migration" button to write files into db/migrate/
46
+ attr_accessor :raw_query_statement_timeout_ms # Postgres statement_timeout (ms) applied to Execute Query / EXPLAIN ANALYZE / SQL Console. 0 disables it.
47
+ attr_accessor :raw_query_rate_limit # Max privileged requests (raw query execution / Generate Migration) per client IP per window. nil disables rate limiting.
48
+ attr_accessor :raw_query_rate_limit_window_seconds # Window size (seconds) for raw_query_rate_limit
45
49
 
46
50
  # Grafana / Prometheus exporter settings
47
51
  attr_accessor :grafana_favorites # Reports exposed at /metrics (Array of keys or Hash with per-report opts)
@@ -74,6 +78,7 @@ module PgReports
74
78
 
75
79
  # Dashboard
76
80
  @dashboard_auth = nil
81
+ @standalone = false
77
82
 
78
83
  # Assets / privacy
79
84
  @load_external_fonts = ActiveModel::Type::Boolean.new.cast(ENV.fetch("PG_REPORTS_LOAD_EXTERNAL_FONTS", false))
@@ -101,6 +106,9 @@ module PgReports
101
106
  else
102
107
  ActiveModel::Type::Boolean.new.cast(env_override)
103
108
  end
109
+ @raw_query_statement_timeout_ms = ENV.fetch("PG_REPORTS_RAW_QUERY_STATEMENT_TIMEOUT_MS", 5_000).to_i
110
+ @raw_query_rate_limit = ENV.fetch("PG_REPORTS_RAW_QUERY_RATE_LIMIT", 30).to_i
111
+ @raw_query_rate_limit_window_seconds = ENV.fetch("PG_REPORTS_RAW_QUERY_RATE_LIMIT_WINDOW_SECONDS", 60).to_i
104
112
 
105
113
  # Grafana / Prometheus exporter
106
114
  @grafana_favorites = []
@@ -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
@@ -105,7 +105,11 @@ module PgReports
105
105
  mod = module_for(key) or raise ArgumentError, "Unknown report: #{key}"
106
106
 
107
107
  started = @clock.now
108
- report = mod.public_send(key, **report_args(opts))
108
+ args = report_args(opts)
109
+ # Call with no arguments when there are no kwargs to forward. On Ruby 2.7
110
+ # `public_send(key, **{})` does not reliably elide to a no-arg call, which
111
+ # breaks report methods (and `have_received(...).with(no_args)` matchers).
112
+ report = args.empty? ? mod.public_send(key) : mod.public_send(key, **args)
109
113
  finished = @clock.now
110
114
 
111
115
  {
@@ -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
@@ -24,6 +24,10 @@ module PgReports
24
24
  # Rack handlers tried, in order, when none is named explicitly.
25
25
  CANDIDATE_SERVERS = %w[puma webrick].freeze
26
26
 
27
+ # Config files auto-loaded (first that exists), relative to the working
28
+ # directory, when no explicit --config path is given.
29
+ DEFAULT_CONFIG_FILES = %w[pg_reports.rb config/pg_reports.rb].freeze
30
+
27
31
  class ServerUnavailable < PgReports::Error; end
28
32
 
29
33
  # Boot the app and start a (blocking) web server.
@@ -34,14 +38,27 @@ module PgReports
34
38
  # @param database_url [String, nil] explicit connection URL; otherwise resolved
35
39
  # from DATABASE_URL or libpq-style PG* env vars
36
40
  # @param server [String, nil] Rack handler name to force (e.g. "puma")
41
+ # @param config_file [String, nil] path to a Ruby config file that calls
42
+ # `PgReports.configure`; auto-detected from DEFAULT_CONFIG_FILES otherwise
43
+ # @param overrides [Hash] per-setting overrides applied last (from CLI flags);
44
+ # nil values are ignored. Keys are Configuration attribute names.
37
45
  def run(port: DEFAULT_PORT, host: DEFAULT_HOST, mount_path: DEFAULT_MOUNT,
38
- database_url: nil, server: nil)
46
+ database_url: nil, server: nil, config_file: nil, overrides: {})
39
47
  # Rails' ActiveRecord railtie reads the connection from DATABASE_URL when no
40
48
  # config/database.yml exists — so we route our resolved connection through
41
49
  # it. The connection registry then auto-registers it as the :primary target,
42
50
  # and database switching / multi-cluster all work unchanged.
43
51
  ENV["DATABASE_URL"] = connection_url(database_url)
44
52
 
53
+ # Mark this process as standalone so the dashboard can hide reports that
54
+ # only make sense with a host app (e.g. Schema Analysis, which introspects
55
+ # the host application's ActiveRecord models — there are none here).
56
+ PgReports.config.standalone = true
57
+
58
+ # Layer settings on top of the ENV-derived defaults: config file first, then
59
+ # explicit CLI overrides win.
60
+ apply_configuration(config_file: config_file, overrides: overrides)
61
+
45
62
  app = build_application(mount_path)
46
63
  app.initialize!
47
64
  verify_connection!
@@ -76,6 +93,47 @@ module PgReports
76
93
 
77
94
  private
78
95
 
96
+ # Apply configuration in increasing order of precedence:
97
+ # ENV vars (already read when Configuration was built)
98
+ # < config file (full Ruby, calls PgReports.configure)
99
+ # < CLI overrides (individual flags)
100
+ #
101
+ # The config file is the escape hatch for every setting that has no dedicated
102
+ # flag — thresholds, Telegram, Grafana favorites, even the dashboard_auth
103
+ # proc. CLI flags cover only the handful of common security toggles.
104
+ def apply_configuration(config_file:, overrides:)
105
+ path = config_file || detect_config_file
106
+ load_config_file(path) if path
107
+
108
+ overrides.each do |key, value|
109
+ next if value.nil? # unset flag — leave the file/ENV value in place
110
+ PgReports.config.public_send(:"#{key}=", value)
111
+ end
112
+ end
113
+
114
+ # First existing DEFAULT_CONFIG_FILES entry (relative to the working dir), or
115
+ # nil when the user keeps no config file.
116
+ def detect_config_file
117
+ DEFAULT_CONFIG_FILES
118
+ .map { |f| File.expand_path(f, Dir.pwd) }
119
+ .find { |f| File.file?(f) }
120
+ end
121
+
122
+ # Evaluate a Ruby config file. A missing explicit path is an error (the user
123
+ # asked for it); a broken file is reported with context rather than a raw
124
+ # backtrace.
125
+ def load_config_file(path)
126
+ full = File.expand_path(path)
127
+ raise PgReports::Error, "Config file not found: #{path}" unless File.file?(full)
128
+
129
+ warn "pg_reports: loading config from #{full}"
130
+ load full
131
+ rescue PgReports::Error
132
+ raise
133
+ rescue => e
134
+ raise PgReports::Error, "Failed to load config file #{full}: #{e.message}"
135
+ end
136
+
79
137
  # Build (and register as Rails.application) a minimal Rails app that mounts
80
138
  # the engine. Kept intentionally small: no asset pipeline (views are inline),
81
139
  # cookie sessions for the dashboard's database selector + CSRF.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- VERSION = "0.8.1"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/pg_reports.rb CHANGED
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # ActiveSupport < 7.1 relies on Ruby's `logger` being loaded implicitly, which
4
+ # concurrent-ruby dropped in 1.3.5. Require it up front so pg_reports loads on
5
+ # Rails 6.1/7.0 (and any bundle with a modern concurrent-ruby). Harmless on
6
+ # newer Rails, which require it themselves.
7
+ require "logger"
3
8
  require "active_support"
4
9
  require "active_support/core_ext"
5
10
  require "active_record"
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.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eldar Avatov
@@ -160,6 +160,7 @@ files:
160
160
  - app/views/pg_reports/dashboard/index.html.erb
161
161
  - app/views/pg_reports/dashboard/show.html.erb
162
162
  - bin/pg_reports
163
+ - config/brakeman.ignore
163
164
  - config/locales/en.yml
164
165
  - config/locales/ru.yml
165
166
  - config/locales/uk.yml