pg_reports 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76f8945d09a2245c88f64ac0a07634055f7fb3133b87f15d7c7ca89915ace112
4
- data.tar.gz: 28d4080e6ed159f24b3df313237b312218acdfc0c1681157db81af1aa4f09aab
3
+ metadata.gz: ebdc855958933d677530a6bee64fe5534e3b13244e4b851ee3cd06b37e1df10b
4
+ data.tar.gz: 1fae6baba2619b57b10ad86ef37678a34b279c49a7cf0f014810f4b167c2ae80
5
5
  SHA512:
6
- metadata.gz: 3591a546c84af405ffa25664aa3016e85297608ba12130d6bb0aa8f0fc2f39b5c91c8470b672b1d67a24c834638384adf658062b08a582814d1271b8c1a5d9d1
7
- data.tar.gz: 3e90622e7676573d94b2ef2c7d5e2b7070bb9a9d5fef021440aab741bfc5faa15d9b3b6a5fbcba8883c98484f1096f0d93ec048a3db3d0b728deed4d1880cb72
6
+ metadata.gz: 7b2960a2f1df123331f8cd8d41d258fcd61f45b118451bac7859e890a5fde822266e7ab77e60a5820bf1856548ebf3ec2fff77a3f7938dc8913eca53cb087582
7
+ data.tar.gz: 8bfeeff753903a9a29329221108628ae2119759f859dd01e2c1395a9ae317a956556fb115bfa28b046b859e784ea42fc0b51df7fee56b7a6a6aef1674335a96a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.2] - 2026-01-28
9
+
10
+ ### Added
11
+
12
+ - `fake_source_data` configuration option - enable via `PG_REPORTS_FAKE_SOURCE_DATA=true` env variable or in initializer
13
+ - Support for short controller#action format in source links (e.g., `posts#index` → `app/controllers/posts_controller.rb`)
14
+
15
+ ### Changed
16
+
17
+ - Fake source data moved to separate partial file for cleaner code organization
18
+ - IDE link click handling improved with event delegation in capture phase
19
+
20
+ ### Fixed
21
+
22
+ - Source badge clicks now work correctly without triggering row expansion
23
+ - Fallback fonts now use proper sans-serif system font stack when `load_external_fonts` is disabled
24
+
25
+ ## [0.2.1] - 2026-01-28
26
+
27
+ ### Added
28
+
29
+ - Cursor (WSL) IDE support
30
+
31
+ ### Fixed
32
+
33
+ - Removed test data from production code
34
+ - Copy Query button now works correctly with special characters
35
+
8
36
  ## [0.2.0] - 2026-01-28
9
37
 
10
38
  ### Added
@@ -17,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
45
  - VS Code - direct path for native Linux
18
46
  - RubyMine
19
47
  - IntelliJ IDEA
48
+ - Cursor (WSL) - for Windows Subsystem for Linux
20
49
  - Cursor
21
50
  - IDE settings modal to choose default IDE (skip menu and open directly)
22
51
  - Save records for comparison - save query results to compare before/after optimizations
data/README.md CHANGED
@@ -18,7 +18,7 @@ A comprehensive PostgreSQL monitoring and analysis library for Rails application
18
18
  - 🌐 **Web Dashboard** - Beautiful dark-themed UI with sortable tables and expandable rows
19
19
  - 📨 **Telegram Integration** - Send reports directly to Telegram
20
20
  - 📥 **Export** - Download reports in TXT, CSV, or JSON format
21
- - 🔗 **IDE Integration** - Open source locations in VS Code, RubyMine, IntelliJ, or Cursor
21
+ - 🔗 **IDE Integration** - Open source locations in VS Code, Cursor, RubyMine, or IntelliJ (with WSL support)
22
22
  - 📌 **Comparison Mode** - Save records to compare before/after optimization
23
23
  - 📊 **EXPLAIN ANALYZE** - Run query plans directly from the dashboard
24
24
  - 🗑️ **Migration Generator** - Generate Rails migrations to drop unused indexes
@@ -116,6 +116,12 @@ PgReports.configure do |config|
116
116
  end
117
117
  }
118
118
 
119
+ # External fonts (Google Fonts)
120
+ # Default: false (no external requests)
121
+ config.load_external_fonts = ENV["PG_REPORTS_LOAD_EXTERNAL_FONTS"] == "true"
122
+ # or simply:
123
+ # config.load_external_fonts = true
124
+
119
125
  end
120
126
  ```
121
127
 
@@ -280,6 +286,7 @@ Click on source locations in reports to open the file directly in your IDE. Supp
280
286
  - **VS Code** - direct path for native Linux/macOS
281
287
  - **RubyMine**
282
288
  - **IntelliJ IDEA**
289
+ - **Cursor (WSL)** - for Windows Subsystem for Linux
283
290
  - **Cursor**
284
291
 
285
292
  Use the ⚙️ button to set your default IDE and skip the selection menu.
@@ -332,6 +339,17 @@ PgReports.configure do |config|
332
339
  end
333
340
  ```
334
341
 
342
+ ### External Fonts
343
+
344
+ By default, PgReports does **not** load external fonts.
345
+
346
+ ```ruby
347
+ PgReports.configure do |config|
348
+ # Enable loading Google Fonts (optional)
349
+ config.load_external_fonts = true
350
+ end
351
+ ```
352
+
335
353
  ## Telegram Integration
336
354
 
337
355
  1. Create a bot via [@BotFather](https://t.me/BotFather)
@@ -6,9 +6,11 @@
6
6
  <meta name="pg-reports-root" content="<%= request.script_name.presence || PgReports::Engine.routes.url_helpers.root_path %>">
7
7
  <title>PgReports Dashboard</title>
8
8
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%234f46e5'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
9
- <link rel="preconnect" href="https://fonts.googleapis.com">
10
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
9
+ <% if PgReports.config.load_external_fonts %>
10
+ <link rel="preconnect" href="https://fonts.googleapis.com">
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
12
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
13
+ <% end %>
12
14
  <style>
13
15
  :root {
14
16
  --bg-primary: #0f1114;
@@ -36,7 +38,7 @@
36
38
  }
37
39
 
38
40
  body {
39
- font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
41
+ font-family: <%= PgReports.config.load_external_fonts ? "'Plus Jakarta Sans', " : "" %>-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
40
42
  background: var(--bg-primary);
41
43
  color: var(--text-primary);
42
44
  min-height: 100vh;
@@ -344,7 +346,7 @@
344
346
  .results-table {
345
347
  width: 100%;
346
348
  border-collapse: collapse;
347
- font-family: 'JetBrains Mono', monospace;
349
+ font-family: <%= PgReports.config.load_external_fonts ? "'JetBrains Mono', " : "" %>SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
348
350
  font-size: 0.8rem;
349
351
  }
350
352
 
@@ -0,0 +1,43 @@
1
+ <%# Fake source data for IDE link testing - enable via PG_REPORTS_FAKE_SOURCE_DATA=true %>
2
+ <script>
3
+ // Known controller methods with line numbers (for IDE link testing)
4
+ const knownControllerMethods = {
5
+ 'PostsController#show': { file: 'app/controllers/posts_controller.rb', line: 33 },
6
+ 'PostsController#index': { file: 'app/controllers/posts_controller.rb', line: 19 },
7
+ 'PostsController#create': { file: 'app/controllers/posts_controller.rb', line: 43 },
8
+ 'PostsController#update': { file: 'app/controllers/posts_controller.rb', line: 62 },
9
+ 'PostsController#destroy': { file: 'app/controllers/posts_controller.rb', line: 76 }
10
+ };
11
+
12
+ // Fake source values for testing IDE links
13
+ const fakeSourceValues = [
14
+ 'PostsController#show',
15
+ 'PostsController#index',
16
+ 'PostsController#create',
17
+ 'PostsController#update',
18
+ 'PostsController#destroy',
19
+ 'app/controllers/posts_controller.rb:33',
20
+ 'app/controllers/posts_controller.rb:19',
21
+ 'app/models/post.rb:45:in `find_by_slug`'
22
+ ];
23
+
24
+ // Inject fake source values into data for testing
25
+ function injectFakeSourceData(data) {
26
+ if (!data.data || data.data.length === 0) return;
27
+
28
+ // Add 'source' column if not present
29
+ if (!data.columns.includes('source')) {
30
+ data.columns.push('source');
31
+ }
32
+
33
+ // Inject fake source values
34
+ data.data.forEach((row, idx) => {
35
+ if (!row.source || row.source === '' || row.source === null) {
36
+ row.source = fakeSourceValues[idx % fakeSourceValues.length];
37
+ }
38
+ });
39
+ }
40
+
41
+ // Flag to indicate fake source data is enabled
42
+ window.PG_REPORTS_FAKE_SOURCE_DATA = true;
43
+ </script>
@@ -74,6 +74,10 @@
74
74
  <input type="radio" name="default-ide" value="intellij" onchange="setDefaultIde('intellij')">
75
75
  <span>IntelliJ IDEA</span>
76
76
  </label>
77
+ <label class="ide-option">
78
+ <input type="radio" name="default-ide" value="cursor-wsl" onchange="setDefaultIde('cursor-wsl')">
79
+ <span>Cursor (WSL)</span>
80
+ </label>
77
81
  <label class="ide-option">
78
82
  <input type="radio" name="default-ide" value="cursor" onchange="setDefaultIde('cursor')">
79
83
  <span>Cursor</span>
@@ -1212,6 +1216,10 @@
1212
1216
  }
1213
1217
  </style>
1214
1218
 
1219
+ <% if PgReports.config.fake_source_data %>
1220
+ <%= render 'pg_reports/dashboard/fake_source_data' %>
1221
+ <% end %>
1222
+
1215
1223
  <script>
1216
1224
  let currentReportData = null;
1217
1225
  const category = '<%= @category %>';
@@ -1564,44 +1572,6 @@
1564
1572
  return '';
1565
1573
  }
1566
1574
 
1567
- // TEMP: Known controller methods with line numbers (for IDE link testing)
1568
- const knownControllerMethods = {
1569
- 'PostsController#show': { file: 'app/controllers/posts_controller.rb', line: 33 },
1570
- 'PostsController#index': { file: 'app/controllers/posts_controller.rb', line: 19 },
1571
- 'PostsController#create': { file: 'app/controllers/posts_controller.rb', line: 43 },
1572
- 'PostsController#update': { file: 'app/controllers/posts_controller.rb', line: 62 },
1573
- 'PostsController#destroy': { file: 'app/controllers/posts_controller.rb', line: 76 }
1574
- };
1575
-
1576
- // TEMP: Fake source values for testing IDE links
1577
- const fakeSourceValues = [
1578
- 'PostsController#show',
1579
- 'PostsController#index',
1580
- 'PostsController#create',
1581
- 'PostsController#update',
1582
- 'PostsController#destroy',
1583
- 'app/controllers/posts_controller.rb:33',
1584
- 'app/controllers/posts_controller.rb:19',
1585
- 'app/models/post.rb:45:in `find_by_slug`'
1586
- ];
1587
-
1588
- // TEMP: Inject fake source values into data for testing
1589
- function injectFakeSourceData(data) {
1590
- if (!data.data || data.data.length === 0) return;
1591
-
1592
- // Add 'source' column if not present
1593
- if (!data.columns.includes('source')) {
1594
- data.columns.push('source');
1595
- }
1596
-
1597
- // Inject fake source values
1598
- data.data.forEach((row, idx) => {
1599
- if (!row.source || row.source === '' || row.source === null) {
1600
- row.source = fakeSourceValues[idx % fakeSourceValues.length];
1601
- }
1602
- });
1603
- }
1604
-
1605
1575
  // Parse source location and generate IDE link
1606
1576
  function parseSourceLocation(source) {
1607
1577
  if (!source || source === 'null' || source === '') return null;
@@ -1623,45 +1593,52 @@
1623
1593
  lineNumber = parseInt(fileLineMatch[3], 10);
1624
1594
  }
1625
1595
 
1626
- // Try to match Controller#action pattern
1596
+ // Try to match Controller#action pattern (e.g., PostsController#index)
1627
1597
  const controllerMatch = source.match(/^(\w+Controller)#(\w+)/);
1628
1598
  if (controllerMatch) {
1629
1599
  methodName = `${controllerMatch[1]}#${controllerMatch[2]}`;
1630
1600
 
1631
- // TEMP: Check known methods first for testing
1632
- const knownMethod = knownControllerMethods[methodName];
1633
- if (knownMethod) {
1634
- filePath = knownMethod.file;
1635
- lineNumber = knownMethod.line;
1601
+ // Check known methods first for testing (if fake data is enabled)
1602
+ if (typeof knownControllerMethods !== 'undefined' && knownControllerMethods[methodName]) {
1603
+ filePath = knownControllerMethods[methodName].file;
1604
+ lineNumber = knownControllerMethods[methodName].line;
1636
1605
  } else {
1637
- // Try to derive file path
1606
+ // Derive file path from controller name
1638
1607
  const controllerName = controllerMatch[1].replace(/Controller$/, '').toLowerCase();
1639
1608
  filePath = `app/controllers/${controllerName}_controller.rb`;
1640
1609
  }
1641
1610
  }
1642
1611
 
1612
+ // Try to match short controller#action pattern (e.g., posts#index, dashboard#show)
1613
+ if (!filePath) {
1614
+ const shortMatch = source.match(/^(\w+)#(\w+)$/);
1615
+ if (shortMatch) {
1616
+ const controllerName = shortMatch[1].toLowerCase();
1617
+ methodName = `${shortMatch[1]}#${shortMatch[2]}`;
1618
+ filePath = `app/controllers/${controllerName}_controller.rb`;
1619
+ }
1620
+ }
1621
+
1643
1622
  return { filePath, lineNumber, methodName, original: source };
1644
1623
  }
1645
1624
 
1646
- // TEMP: Base path for parent project (for IDE link testing)
1647
- const parentProjectPath = '/home/deadalice/vmist-server';
1625
+ // Rails.root path for IDE links
1626
+ const railsRootPath = '<%= Rails.root.to_s %>';
1627
+ const wslDistro = 'Ubuntu';
1648
1628
 
1649
1629
  // Generate IDE URLs
1650
1630
  function generateIdeUrls(filePath, lineNumber) {
1651
1631
  if (!filePath) return [];
1652
1632
 
1653
- // TEMP: Convert relative paths to absolute paths in parent project
1633
+ // Convert relative paths to absolute paths using Rails.root
1654
1634
  let absolutePath = filePath;
1655
1635
  if (!filePath.startsWith('/')) {
1656
- absolutePath = `${parentProjectPath}/${filePath}`;
1636
+ absolutePath = `${railsRootPath}/${filePath}`;
1657
1637
  }
1658
1638
 
1659
1639
  const line = lineNumber || 1;
1660
1640
  const urls = [];
1661
1641
 
1662
- // TEMP: WSL distro name for VS Code Remote
1663
- const wslDistro = 'Ubuntu';
1664
-
1665
1642
  // VSCode (WSL Remote format)
1666
1643
  urls.push({
1667
1644
  name: 'VS Code (WSL)',
@@ -1685,7 +1662,13 @@
1685
1662
  url: `idea://open?file=${absolutePath}&line=${line}`
1686
1663
  });
1687
1664
 
1688
- // Cursor (VSCode-based)
1665
+ // Cursor (WSL Remote format)
1666
+ urls.push({
1667
+ name: 'Cursor (WSL)',
1668
+ url: `cursor://vscode-remote/wsl+${wslDistro}${absolutePath}:${line}`
1669
+ });
1670
+
1671
+ // Cursor (direct path)
1689
1672
  urls.push({
1690
1673
  name: 'Cursor',
1691
1674
  url: `cursor://file${absolutePath}:${line}`
@@ -1700,7 +1683,8 @@
1700
1683
  'vscode': 1,
1701
1684
  'rubymine': 2,
1702
1685
  'intellij': 3,
1703
- 'cursor': 4
1686
+ 'cursor-wsl': 4,
1687
+ 'cursor': 5
1704
1688
  };
1705
1689
 
1706
1690
  // Build source badge HTML with IDE links
@@ -1731,12 +1715,12 @@
1731
1715
 
1732
1716
  let dropdownHtml = `
1733
1717
  <div class="ide-dropdown">
1734
- <span class="source-badge clickable" onclick="event.stopPropagation(); toggleIdeDropdown('${dropdownId}', this)" title="${escapeHtml(parsed.original)}">${escapeHtml(parsed.original)}</span>
1718
+ <span class="source-badge clickable" data-dropdown-id="${dropdownId}" title="${escapeHtml(parsed.original)}">${escapeHtml(parsed.original)}</span>
1735
1719
  <div class="ide-dropdown-menu" id="${dropdownId}">
1736
1720
  `;
1737
1721
 
1738
1722
  for (const ide of ideUrls) {
1739
- dropdownHtml += `<a href="${ide.url}" onclick="event.stopPropagation();">${ide.name}</a>`;
1723
+ dropdownHtml += `<a href="${ide.url}">${ide.name}</a>`;
1740
1724
  }
1741
1725
 
1742
1726
  dropdownHtml += '</div></div>';
@@ -1763,6 +1747,26 @@
1763
1747
  }
1764
1748
  }
1765
1749
 
1750
+ // Event delegation for source badge clicks
1751
+ document.addEventListener('click', function(e) {
1752
+ const badge = e.target.closest('.source-badge.clickable[data-dropdown-id]');
1753
+ if (badge) {
1754
+ e.stopPropagation();
1755
+ e.preventDefault();
1756
+ const dropdownId = badge.dataset.dropdownId;
1757
+ toggleIdeDropdown(dropdownId, badge);
1758
+ return;
1759
+ }
1760
+
1761
+ // Allow clicks on IDE dropdown menu links
1762
+ const ideLink = e.target.closest('.ide-dropdown-menu a');
1763
+ if (ideLink) {
1764
+ e.stopPropagation();
1765
+ // Let the link navigate normally
1766
+ return;
1767
+ }
1768
+ }, true); // Use capture phase to intercept before row click
1769
+
1766
1770
  function buildDetailRow(row, columns, rowIndex, thresholds, problemFields) {
1767
1771
  let html = '<div class="row-detail">';
1768
1772
  const hasQuery = columns.includes('query') && row.query;
@@ -1866,8 +1870,10 @@
1866
1870
  if (loadingEl) loadingEl.style.display = 'none';
1867
1871
 
1868
1872
  if (data.success) {
1869
- // TEMP: Inject fake source data for IDE link testing
1870
- injectFakeSourceData(data);
1873
+ // Inject fake source data for IDE link testing (if enabled)
1874
+ if (typeof injectFakeSourceData === 'function') {
1875
+ injectFakeSourceData(data);
1876
+ }
1871
1877
 
1872
1878
  currentReportData = data;
1873
1879
  const thresholds = data.thresholds || {};
@@ -27,6 +27,12 @@ module PgReports
27
27
  # Dashboard settings
28
28
  attr_accessor :dashboard_auth # Proc for dashboard authentication
29
29
 
30
+ # Assets / privacy settings
31
+ attr_accessor :load_external_fonts # When true, loads Google Fonts in the dashboard layout
32
+
33
+ # Development/testing settings
34
+ attr_accessor :fake_source_data # Inject fake source data for IDE link testing
35
+
30
36
  def initialize
31
37
  # Telegram
32
38
  @telegram_bot_token = ENV.fetch("PG_REPORTS_TELEGRAM_TOKEN", nil)
@@ -52,6 +58,12 @@ module PgReports
52
58
 
53
59
  # Dashboard
54
60
  @dashboard_auth = nil
61
+
62
+ # Assets / privacy
63
+ @load_external_fonts = ActiveModel::Type::Boolean.new.cast(ENV.fetch("PG_REPORTS_LOAD_EXTERNAL_FONTS", false))
64
+
65
+ # Development/testing
66
+ @fake_source_data = ENV.fetch("PG_REPORTS_FAKE_SOURCE_DATA", "false") == "true"
55
67
  end
56
68
 
57
69
  def connection
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgReports
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
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.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eldar Avatov
@@ -107,6 +107,7 @@ files:
107
107
  - README.md
108
108
  - app/controllers/pg_reports/dashboard_controller.rb
109
109
  - app/views/layouts/pg_reports/application.html.erb
110
+ - app/views/pg_reports/dashboard/_fake_source_data.html.erb
110
111
  - app/views/pg_reports/dashboard/index.html.erb
111
112
  - app/views/pg_reports/dashboard/show.html.erb
112
113
  - config/locales/en.yml