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.
@@ -2,11 +2,14 @@
2
2
 
3
3
  <div class="report-page">
4
4
  <nav class="breadcrumb">
5
- <%= link_to t("pg_reports.ui.navigation.dashboard"), root_path %>
6
- <span>/</span>
5
+ <%= link_to root_path, class: "breadcrumb-back", title: t("pg_reports.ui.navigation.back") do %>
6
+ <svg class="icon" aria-hidden="true"><use href="#i-arrow-left"></use></svg>
7
+ <span><%= t("pg_reports.ui.navigation.dashboard") %></span>
8
+ <% end %>
9
+ <span class="breadcrumb-sep">/</span>
7
10
  <span><%= @categories[@category][:name] %></span>
8
- <span>/</span>
9
- <span><%= @report_info[:name] %></span>
11
+ <span class="breadcrumb-sep">/</span>
12
+ <span class="breadcrumb-current"><%= @report_info[:name] %></span>
10
13
 
11
14
  <span class="breadcrumb-spacer"></span>
12
15
  <%= render "target_selector" %>
@@ -35,7 +38,7 @@
35
38
  <a href="#" onclick="downloadReport('json'); return false;"><%= t("pg_reports.ui.actions.download_json") %></a>
36
39
  <% if @documentation && @documentation[:ai_prompt].present? %>
37
40
  <div class="dropdown-divider"></div>
38
- <a href="#" id="ai-prompt-btn" onclick="copyAiPrompt(this); return false;"><span class="ai-icon">AI</span> <%= t("pg_reports.ui.actions.copy_ai_prompt") %></a>
41
+ <a href="#" id="ai-prompt-btn" onclick="copyAiPrompt(this); return false;"><span class="ai-icon">AI</span><%= t("pg_reports.ui.actions.copy_ai_prompt") %></a>
39
42
  <% end %>
40
43
  </div>
41
44
  </div>
@@ -45,10 +48,9 @@
45
48
  <%= t("pg_reports.ui.actions.send_telegram") %>
46
49
  </button>
47
50
  <% end %>
48
- <button class="btn btn-icon" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">
49
- ⚙️
51
+ <button type="button" class="btn-icon" onclick="showIdeSettingsModal()" title="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>" aria-label="<%= t("pg_reports.ui.actions.ide_settings_button_title") %>">
52
+ <svg class="icon icon-lg"><use href="#i-settings"></use></svg>
50
53
  </button>
51
- <%= link_to t("pg_reports.ui.navigation.back"), root_path, class: "btn btn-secondary" %>
52
54
  </div>
53
55
  </div>
54
56
 
@@ -63,7 +65,7 @@
63
65
  <% if @documentation && @documentation[:what].present? %>
64
66
  <details class="documentation-section">
65
67
  <summary class="documentation-toggle">
66
- <span class="toggle-icon">▶</span>
68
+ <svg class="icon toggle-icon" aria-hidden="true"><use href="#i-chevron-right"></use></svg>
67
69
  <span><%= t("pg_reports.ui.documentation.toggle_title") %></span>
68
70
  </summary>
69
71
  <div class="documentation-content">
@@ -118,7 +120,7 @@
118
120
  <div class="filter-section">
119
121
  <details class="filter-details">
120
122
  <summary class="filter-toggle">
121
- <span class="toggle-icon">▶</span>
123
+ <svg class="icon toggle-icon" aria-hidden="true"><use href="#i-chevron-right"></use></svg>
122
124
  <span><%= t("pg_reports.ui.filters.title") %></span>
123
125
  </summary>
124
126
  <div class="filter-content">
@@ -176,7 +178,7 @@
176
178
  </div>
177
179
 
178
180
  <div id="empty-state" class="empty-state" style="display: none;">
179
- <div class="empty-state-icon">✓</div>
181
+ <svg class="icon empty-state-icon" aria-hidden="true"><use href="#i-check"></use></svg>
180
182
  <p><%= t("pg_reports.ui.results.empty_message") %></p>
181
183
  </div>
182
184
 
data/bin/pg_reports CHANGED
@@ -23,7 +23,16 @@ options = {
23
23
  host: "127.0.0.1",
24
24
  mount: "/",
25
25
  database_url: nil,
26
- server: nil
26
+ server: nil,
27
+ config_file: nil
28
+ }
29
+
30
+ # Per-setting overrides from flags. nil means "unset" — the config file / ENV
31
+ # value is kept. Populated below and forwarded to Standalone.run(overrides:).
32
+ overrides = {
33
+ allow_raw_query_execution: nil,
34
+ allow_migration_creation: nil,
35
+ load_external_fonts: nil
27
36
  }
28
37
 
29
38
  banner = <<~BANNER
@@ -35,6 +44,12 @@ banner = <<~BANNER
35
44
  Connection is resolved from --database-url, else DATABASE_URL, else the
36
45
  standard libpq env vars (PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE).
37
46
 
47
+ Configuration is layered, lowest to highest precedence:
48
+ PG_REPORTS_* env vars < config file < the flags below.
49
+ A config file (--config, else ./pg_reports.rb or config/pg_reports.rb) is
50
+ plain Ruby calling `PgReports.configure` — the way to set every option that
51
+ has no flag (thresholds, Telegram, Grafana, dashboard_auth, …).
52
+
38
53
  Options:
39
54
  BANNER
40
55
 
@@ -45,6 +60,10 @@ parser = OptionParser.new do |o|
45
60
  o.on("-m", "--mount PATH", "Path to mount the dashboard at (default: /)") { |v| options[:mount] = v }
46
61
  o.on("-d", "--database-url URL", "PostgreSQL connection URL") { |v| options[:database_url] = v }
47
62
  o.on("-s", "--server NAME", "Web server to use (e.g. puma, webrick)") { |v| options[:server] = v }
63
+ o.on("-c", "--config PATH", "Ruby config file to load (default: ./pg_reports.rb or config/pg_reports.rb)") { |v| options[:config_file] = v }
64
+ o.on("--[no-]allow-raw-query-execution", "Allow running raw SQL / EXPLAIN ANALYZE from the dashboard") { |v| overrides[:allow_raw_query_execution] = v }
65
+ o.on("--[no-]allow-migration-creation", "Allow the dashboard to write migration files to db/migrate/") { |v| overrides[:allow_migration_creation] = v }
66
+ o.on("--[no-]external-fonts", "Load Google Fonts in the dashboard (off by default)") { |v| overrides[:load_external_fonts] = v }
48
67
  o.on("-h", "--help", "Show this help") do
49
68
  puts o
50
69
  exit
@@ -70,11 +89,18 @@ when "server"
70
89
  host: options[:host],
71
90
  mount_path: options[:mount],
72
91
  database_url: options[:database_url],
73
- server: options[:server]
92
+ server: options[:server],
93
+ config_file: options[:config_file],
94
+ overrides: overrides
74
95
  )
75
96
  rescue PgReports::Standalone::ServerUnavailable => e
76
97
  warn e.message
77
98
  exit 1
99
+ rescue PgReports::Error => e
100
+ # Config-file and connection problems surface as PgReports::Error with a
101
+ # human-readable message — print it, not a backtrace.
102
+ warn "pg_reports: #{e.message}"
103
+ exit 1
78
104
  rescue Interrupt
79
105
  warn "\npg_reports: stopped"
80
106
  end
@@ -0,0 +1,97 @@
1
+ {
2
+ "ignored_warnings": [
3
+ {
4
+ "warning_type": "SQL Injection",
5
+ "warning_code": 0,
6
+ "fingerprint": "5209397462e689c349f77e534f897c12a8550b9a013bd535153a929c12b944e2",
7
+ "check_name": "SQL",
8
+ "message": "Possible SQL injection",
9
+ "file": "app/controllers/pg_reports/dashboard_controller.rb",
10
+ "line": 357,
11
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
12
+ "code": "ActiveRecord::Base.connection.execute(\"SELECT COUNT(*) FROM (#{substitute_params(retrieve_query_by_hash(params[:query_hash]), (params[:params] or {}))}) AS count_query\")",
13
+ "render_path": null,
14
+ "location": {
15
+ "type": "method",
16
+ "class": "PgReports::DashboardController",
17
+ "method": "execute_query"
18
+ },
19
+ "user_input": "params[:params]",
20
+ "confidence": "High",
21
+ "cwe_id": [
22
+ 89
23
+ ],
24
+ "note": "Reviewed & accepted: user-query execution feature (execute_query count). Gated by config.allow_raw_query_execution; the query is fetched by hash from an internal registry (retrieve_query_by_hash), parameters are validated/substituted, and SecurityError checks reject dangerous input. Not arbitrary attacker-controlled SQL."
25
+ },
26
+ {
27
+ "warning_type": "SQL Injection",
28
+ "warning_code": 0,
29
+ "fingerprint": "52294539b2a9666f9a79f7ad52cdd1dc531a3b71e487e5134174029cd5138571",
30
+ "check_name": "SQL",
31
+ "message": "Possible SQL injection",
32
+ "file": "app/controllers/pg_reports/dashboard_controller.rb",
33
+ "line": 440,
34
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
35
+ "code": "ActiveRecord::Base.connection.execute(\"SELECT COUNT(*) FROM (#{params[:query].to_s}) AS count_query\")",
36
+ "render_path": null,
37
+ "location": {
38
+ "type": "method",
39
+ "class": "PgReports::DashboardController",
40
+ "method": "run_query"
41
+ },
42
+ "user_input": "params[:query]",
43
+ "confidence": "High",
44
+ "cwe_id": [
45
+ 89
46
+ ],
47
+ "note": "Reviewed & accepted: SQL Console (run_query count). Gated by config.allow_raw_query_execution; the client-typed query passes enforce_select_only! (SELECT-only, no semicolons, keyword denylist) and runs under a bounded statement_timeout. Intentional DBA tooling; same risk profile as execute_query/explain_analyze."
48
+ },
49
+ {
50
+ "warning_type": "Cross-Site Request Forgery",
51
+ "warning_code": 7,
52
+ "fingerprint": "b9682cc0e0a1cd86fddef96834d4f71c3558989c2136fe58b5ff51c75b83c8b4",
53
+ "check_name": "ForgerySetting",
54
+ "message": "`protect_from_forgery` should be called in `PgReports::MetricsController`",
55
+ "file": "app/controllers/pg_reports/metrics_controller.rb",
56
+ "line": 6,
57
+ "link": "https://brakemanscanner.org/docs/warning_types/cross-site_request_forgery/",
58
+ "code": null,
59
+ "render_path": null,
60
+ "location": {
61
+ "type": "controller",
62
+ "controller": "PgReports::MetricsController"
63
+ },
64
+ "user_input": null,
65
+ "confidence": "High",
66
+ "cwe_id": [
67
+ 352
68
+ ],
69
+ "note": "Reviewed & accepted: read-only Prometheus /metrics endpoint. Uses bearer-token auth (config.grafana_metrics_token) with constant-time compare; CSRF protection is not applicable to a GET scrape endpoint."
70
+ },
71
+ {
72
+ "warning_type": "SQL Injection",
73
+ "warning_code": 0,
74
+ "fingerprint": "f133378f2542f3c09b527aebcc0939a1d227c46b0ab124be613e1e5aac829c79",
75
+ "check_name": "SQL",
76
+ "message": "Possible SQL injection",
77
+ "file": "app/controllers/pg_reports/dashboard_controller.rb",
78
+ "line": 278,
79
+ "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
80
+ "code": "ActiveRecord::Base.connection.execute(\"EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{substitute_params(retrieve_query_by_hash(params[:query_hash]), (params[:params] or {}))}\")",
81
+ "render_path": null,
82
+ "location": {
83
+ "type": "method",
84
+ "class": "PgReports::DashboardController",
85
+ "method": "explain_analyze"
86
+ },
87
+ "user_input": "params[:params]",
88
+ "confidence": "High",
89
+ "cwe_id": [
90
+ 89
91
+ ],
92
+ "note": "Reviewed & accepted: EXPLAIN ANALYZE feature. Same safeguards as execute_query - query resolved by hash from internal registry, params validated, SecurityError guards. Intentional DBA tooling."
93
+ }
94
+ ],
95
+ "updated": "2026-07-10 19:15:00 +0300",
96
+ "brakeman_version": "8.0.5"
97
+ }
@@ -591,7 +591,7 @@ en:
591
591
  page_title: "PgReports Dashboard"
592
592
  navigation:
593
593
  dashboard: "Dashboard"
594
- back: "Back"
594
+ back: "Back"
595
595
  database_selector:
596
596
  label: "Database"
597
597
  target_selector:
@@ -599,39 +599,41 @@ en:
599
599
  actions:
600
600
  cancel: "Cancel"
601
601
  retry: "Retry"
602
- copy: "📋 Copy"
603
- copy_query: "📋 Copy Query"
604
- copy_code: "📋 Copy Code"
602
+ copy: "Copy"
603
+ copy_query: "Copy Query"
604
+ copy_code: "Copy Code"
605
605
  copy_to_clipboard_title: "Copy to clipboard"
606
- copied_feedback: "Copied!"
606
+ copied_feedback: "Copied!"
607
607
  clear_all: "Clear All"
608
- run_report: "Run Report"
609
- export: "Export"
610
- download_text: "📄 Text (.txt)"
611
- download_csv: "📊 CSV (.csv)"
612
- download_json: "📋 JSON (.json)"
613
- download: "📥 Download"
608
+ run_report: "Run Report"
609
+ export: "Export"
610
+ download_text: "Text (.txt)"
611
+ download_csv: "CSV (.csv)"
612
+ download_json: "JSON (.json)"
613
+ download: "Download"
614
614
  copy_ai_prompt: "Copy Prompt"
615
- send_telegram: "📨 Telegram"
615
+ send_telegram: "Telegram"
616
616
  sending: "Sending..."
617
- reset_statistics: "🗑️ Reset Statistics"
617
+ reset_statistics: "Reset Statistics"
618
618
  resetting: "Resetting..."
619
619
  confirm_reset: "Yes, Reset"
620
- create_extension: "Create Extension"
620
+ create_extension: "Create Extension"
621
621
  creating: "Creating..."
622
622
  ide_settings_button_title: "IDE Settings"
623
- explain_analyze: "📊 EXPLAIN ANALYZE"
624
- execute_query: "Execute Query"
625
- create_migration_file: "📁 Create File & Open in IDE"
626
- start_monitoring: " Start Monitoring"
627
- stop_monitoring: " Stop Monitoring"
623
+ explain_analyze: "EXPLAIN ANALYZE"
624
+ execute_query: "Execute Query"
625
+ run_query: "SQL Console"
626
+ create_migration_file: "Create File & Open in IDE"
627
+ start_monitoring: "Start Monitoring"
628
+ stop_monitoring: "Stop Monitoring"
628
629
  starting: "Starting..."
629
630
  stopping: "Stopping..."
630
- load_history: "📜 Load History (50)"
631
+ load_history: "Load History"
632
+ load_history_unavailable_title: "No stored history — set config.query_monitor_log_file to keep captured queries across restarts"
631
633
  loading: "Loading..."
632
634
  running: "Running..."
633
- save_for_comparison: "📌 Save for Comparison"
634
- saved_marker: "📌 Saved"
635
+ save_for_comparison: "Save for Comparison"
636
+ saved_marker: "Saved"
635
637
  status:
636
638
  pg_stat_ready: "Active"
637
639
  not_preloaded: "Preload required"
@@ -649,20 +651,24 @@ en:
649
651
  create_extension_title: "Create pg_stat_statements extension"
650
652
  create_extension_intro: "The pg_stat_statements extension isn't created in this database yet. Click below to create it:"
651
653
  create_extension_note: "If the library isn't preloaded, you'll also need to add it to shared_preload_libraries and restart PostgreSQL."
652
- reset_stats_title: "⚠️ Reset Statistics"
654
+ reset_stats_title: "Reset Statistics"
653
655
  reset_stats_confirm: "Are you sure you want to reset pg_stat_statements statistics?"
654
656
  reset_stats_warning: "This action will clear all collected query statistics and cannot be undone."
655
- ide_settings_title: "⚙️ IDE Settings"
656
- problem_detected_title: "⚠️ Problem Detected"
657
- query_analyzer_title: "📊 Query Analyzer"
657
+ ide_settings_title: "IDE Settings"
658
+ problem_detected_title: "Problem Detected"
659
+ query_analyzer_title: "Query Analyzer"
658
660
  query_label: "Query:"
659
661
  parameters_label: "Parameters:"
660
- migration_title: "🗑️ Drop Index Migration"
662
+ migration_title: "Drop Index Migration"
661
663
  migration_subtitle: "Generated migration to remove the index:"
662
664
  migration_warning: "Creating a migration will generate a migration file in your project. Running this migration will drop the index from the database, which may significantly impact application performance."
663
665
  migration_warning_dev_only: "This operation should only be performed in a local development environment."
664
- query_execution_disabled_title: "⚠️ Query execution is disabled"
666
+ query_execution_disabled_title: "Query execution is disabled"
667
+ migration_disabled_title: "Migration creation is disabled"
665
668
  query_execution_disabled_intro: "To enable this feature, add to your configuration:"
669
+ query_execution_disabled_env_note: "...or set the following environment variable instead:"
670
+ run_query_title: "SQL Console"
671
+ run_query_placeholder: "SELECT * FROM users LIMIT 10;"
666
672
  settings:
667
673
  default_ide_label: "Default IDE for source links:"
668
674
  ide_show_menu: "Show menu (default)"
@@ -701,24 +707,27 @@ en:
701
707
  waiting_for_locks: "waiting for locks"
702
708
  percent_used_suffix: "% used"
703
709
  categories:
704
- requires_pg_stat: "🔒 Requires pg_stat_statements"
710
+ requires_pg_stat: "Requires pg_stat_statements"
711
+ pg_stat_missing_reason: "Requires pg_stat_statements, which is not installed on \"%{database}\". Create the extension there, or switch to a database that has it."
712
+ pg_stat_not_preloaded_reason: "Requires pg_stat_statements. The extension exists on \"%{database}\" but is not in shared_preload_libraries, so it returns no data until PostgreSQL is restarted with it preloaded."
705
713
  reports_count_suffix: "reports"
706
- primary_only_reason: "🔒 This category only runs on the primary database — it inspects the host application's models. Switch back to the default database to use it."
714
+ primary_only_reason: "This category only runs on the primary database — it inspects the host application's models. Switch back to the default database to use it."
715
+ standalone_no_host_app_reason: "Not available in standalone mode — this category inspects the host application's models, and there is no host app when running the dashboard on its own."
707
716
  documentation:
708
- toggle_title: "📖 What does this report show?"
709
- what_section: "📋 What"
710
- why_section: "Why It Matters"
711
- nuances_section: "⚠️ Nuances"
712
- thresholds_section: "📊 Thresholds"
713
- threshold_warning_label: "⚠️ Warning:"
714
- threshold_critical_label: "🔴 Critical:"
717
+ toggle_title: "What does this report show?"
718
+ what_section: "What"
719
+ why_section: "Why It Matters"
720
+ nuances_section: "Nuances"
721
+ thresholds_section: "Thresholds"
722
+ threshold_warning_label: "Warning:"
723
+ threshold_critical_label: "Critical:"
715
724
  threshold_inverted_note: "(lower is worse)"
716
725
  filters:
717
- title: "🔍 Filter Parameters"
726
+ title: "Filter Parameters"
718
727
  current_value: "current"
719
728
  saved:
720
- title: "📌 Saved for Comparison"
721
- saved_at_prefix: "Saved:"
729
+ title: "Saved for Comparison"
730
+ saved_at_prefix: "Saved:"
722
731
  click_to_expand: "Click to expand"
723
732
  confirm_clear_all: "Remove all saved records for this report?"
724
733
  remove_title: "Remove"
@@ -732,9 +741,9 @@ en:
732
741
  execution_time_label: "Execution time:"
733
742
  null_placeholder: "<null>"
734
743
  sections:
735
- recommendation: "💡 Recommendation"
736
- detected_issues: "⚠️ Detected Issues"
737
- execution_plan: "📊 Execution Plan"
744
+ recommendation: "Recommendation"
745
+ detected_issues: "Detected Issues"
746
+ execution_plan: "Execution Plan"
738
747
  line_label: "Line"
739
748
  current_label: "Current:"
740
749
  threshold_label: "Threshold:"
@@ -742,8 +751,8 @@ en:
742
751
  warning_eq: "warning"
743
752
  critical_eq: "critical"
744
753
  levels:
745
- critical: "🔴 Critical"
746
- warning: "⚠️ Warning"
754
+ critical: "Critical"
755
+ warning: "Warning"
747
756
  errors:
748
757
  error_prefix: "Error:"
749
758
  unable_fetch_metrics: "Unable to fetch database statistics."
@@ -768,13 +777,19 @@ en:
768
777
  decode_query_failed: "Failed to decode query"
769
778
  explain_analyze_failed: "Failed to run EXPLAIN ANALYZE"
770
779
  execute_query_failed: "Failed to execute query"
780
+ run_query_failed: "Failed to run query"
781
+ query_required: "Please enter a query"
771
782
  create_migration_failed: "Failed to create migration"
772
- explain_disabled_toast: "⚠️ EXPLAIN ANALYZE is disabled. Enable in configuration: config.allow_raw_query_execution = true"
773
- execute_disabled_toast: "⚠️ Query execution is disabled. Enable in configuration: config.allow_raw_query_execution = true"
783
+ explain_disabled_toast: "EXPLAIN ANALYZE is disabled. Enable in configuration: config.allow_raw_query_execution = true"
784
+ execute_disabled_toast: "Query execution is disabled. Enable in configuration: config.allow_raw_query_execution = true"
785
+ migration_disabled_toast: "Migration creation is disabled. Enable in configuration: config.allow_raw_query_execution = true"
774
786
  query_monitoring_error: "Query monitoring error"
775
787
  query_hash_required: "Query hash is required"
776
788
  query_execution_disabled: "Query execution from dashboard is disabled. Enable it in configuration with 'config.allow_raw_query_execution = true'"
777
789
  query_not_found_expired: "Query not found or expired. Please refresh the page."
790
+ query_monitor_unavailable_standalone: "SQL Query Monitor isn't available in standalone mode — there is no host application process to observe."
791
+ query_timed_out: "Query timed out after %{ms}ms (statement_timeout). Try narrowing the query or increase config.raw_query_statement_timeout_ms."
792
+ rate_limit_exceeded: "Too many requests. Please wait a bit before trying again."
778
793
  security_violation_prefix: "Security violation:"
779
794
  trigger_variables_not_allowed: "Cannot EXPLAIN ANALYZE queries with trigger variables (NEW, OLD). These are only available within trigger functions."
780
795
  missing_parameter_values: "Please provide values for all parameter placeholders ($1, $2, etc.)"
@@ -556,7 +556,7 @@ ru:
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 @@ ru:
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 @@ ru:
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 @@ ru:
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 @@ ru:
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 @@ ru:
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 @@ ru:
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 и т.д.)"