rails_pulse 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -20
  3. data/Rakefile +169 -86
  4. data/app/assets/images/rails_pulse/dashboard.png +0 -0
  5. data/app/assets/images/rails_pulse/request.png +0 -0
  6. data/app/assets/stylesheets/rails_pulse/application.css +28 -5
  7. data/app/assets/stylesheets/rails_pulse/components/badge.css +13 -0
  8. data/app/assets/stylesheets/rails_pulse/components/base.css +12 -2
  9. data/app/assets/stylesheets/rails_pulse/components/collapsible.css +30 -0
  10. data/app/assets/stylesheets/rails_pulse/components/popover.css +0 -1
  11. data/app/assets/stylesheets/rails_pulse/components/row.css +55 -3
  12. data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +23 -0
  13. data/app/controllers/concerns/zoom_range_concern.rb +31 -0
  14. data/app/controllers/rails_pulse/application_controller.rb +5 -1
  15. data/app/controllers/rails_pulse/queries_controller.rb +49 -10
  16. data/app/controllers/rails_pulse/requests_controller.rb +46 -20
  17. data/app/controllers/rails_pulse/routes_controller.rb +40 -1
  18. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
  19. data/app/helpers/rails_pulse/chart_helper.rb +16 -8
  20. data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
  21. data/app/javascript/rails_pulse/application.js +34 -3
  22. data/app/javascript/rails_pulse/controllers/collapsible_controller.js +32 -0
  23. data/app/javascript/rails_pulse/controllers/color_scheme_controller.js +2 -1
  24. data/app/javascript/rails_pulse/controllers/expandable_rows_controller.js +58 -0
  25. data/app/javascript/rails_pulse/controllers/index_controller.js +249 -11
  26. data/app/javascript/rails_pulse/controllers/popover_controller.js +28 -4
  27. data/app/javascript/rails_pulse/controllers/table_sort_controller.js +14 -0
  28. data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +1 -1
  29. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
  30. data/app/models/rails_pulse/queries/cards/average_query_times.rb +20 -20
  31. data/app/models/rails_pulse/queries/cards/execution_rate.rb +58 -14
  32. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +14 -9
  33. data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
  34. data/app/models/rails_pulse/query.rb +46 -0
  35. data/app/models/rails_pulse/request.rb +1 -1
  36. data/app/models/rails_pulse/requests/charts/average_response_times.rb +2 -2
  37. data/app/models/rails_pulse/requests/tables/index.rb +77 -0
  38. data/app/models/rails_pulse/routes/cards/average_response_times.rb +18 -20
  39. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +14 -9
  40. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +14 -9
  41. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +29 -13
  42. data/app/models/rails_pulse/routes/tables/index.rb +4 -2
  43. data/app/models/rails_pulse/summary.rb +7 -7
  44. data/app/services/rails_pulse/analysis/backtrace_analyzer.rb +256 -0
  45. data/app/services/rails_pulse/analysis/base_analyzer.rb +67 -0
  46. data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +206 -0
  47. data/app/services/rails_pulse/analysis/index_recommendation_engine.rb +326 -0
  48. data/app/services/rails_pulse/analysis/n_plus_one_detector.rb +241 -0
  49. data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +154 -0
  50. data/app/services/rails_pulse/analysis/suggestion_generator.rb +217 -0
  51. data/app/services/rails_pulse/query_analysis_service.rb +125 -0
  52. data/app/views/layouts/rails_pulse/_sidebar_menu.html.erb +0 -1
  53. data/app/views/layouts/rails_pulse/application.html.erb +0 -2
  54. data/app/views/rails_pulse/components/_breadcrumbs.html.erb +1 -1
  55. data/app/views/rails_pulse/components/_code_panel.html.erb +17 -3
  56. data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
  57. data/app/views/rails_pulse/components/_metric_card.html.erb +28 -5
  58. data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
  59. data/app/views/rails_pulse/components/_panel.html.erb +1 -1
  60. data/app/views/rails_pulse/components/_sparkline_stats.html.erb +5 -7
  61. data/app/views/rails_pulse/components/_table_head.html.erb +6 -1
  62. data/app/views/rails_pulse/dashboard/index.html.erb +2 -2
  63. data/app/views/rails_pulse/operations/show.html.erb +17 -15
  64. data/app/views/rails_pulse/queries/_analysis_error.html.erb +15 -0
  65. data/app/views/rails_pulse/queries/_analysis_prompt.html.erb +27 -0
  66. data/app/views/rails_pulse/queries/_analysis_results.html.erb +117 -0
  67. data/app/views/rails_pulse/queries/_analysis_section.html.erb +39 -0
  68. data/app/views/rails_pulse/queries/_show_table.html.erb +34 -6
  69. data/app/views/rails_pulse/queries/_table.html.erb +4 -8
  70. data/app/views/rails_pulse/queries/index.html.erb +48 -51
  71. data/app/views/rails_pulse/queries/show.html.erb +56 -52
  72. data/app/views/rails_pulse/requests/_operations.html.erb +30 -43
  73. data/app/views/rails_pulse/requests/_table.html.erb +31 -18
  74. data/app/views/rails_pulse/requests/index.html.erb +55 -50
  75. data/app/views/rails_pulse/requests/show.html.erb +0 -2
  76. data/app/views/rails_pulse/routes/_requests_table.html.erb +39 -0
  77. data/app/views/rails_pulse/routes/_table.html.erb +4 -10
  78. data/app/views/rails_pulse/routes/index.html.erb +49 -52
  79. data/app/views/rails_pulse/routes/show.html.erb +6 -8
  80. data/config/initializers/rails_charts_csp_patch.rb +32 -40
  81. data/config/routes.rb +5 -1
  82. data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
  83. data/db/rails_pulse_schema.rb +10 -1
  84. data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +81 -0
  85. data/lib/generators/rails_pulse/install_generator.rb +75 -18
  86. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +72 -2
  87. data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +23 -0
  88. data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +19 -0
  89. data/lib/generators/rails_pulse/upgrade_generator.rb +226 -0
  90. data/lib/rails_pulse/engine.rb +21 -0
  91. data/lib/rails_pulse/version.rb +1 -1
  92. data/lib/tasks/rails_pulse.rake +27 -8
  93. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  94. data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
  95. data/public/rails-pulse-assets/rails-pulse.js +53 -53
  96. data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
  97. metadata +25 -6
  98. data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
  99. data/app/assets/images/rails_pulse/routes.png +0 -0
  100. data/app/javascript/rails_pulse/controllers/expandable_row_controller.js +0 -67
  101. data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c6b5219e5da388e0ec200a6382527d227ab0e2ee20faa11e3ce8e49992ce6b8
4
- data.tar.gz: 1a713357c2ab7eb454b269f358c9b9fb1939eef9e3d17c8cb7cf9a12cab8206c
3
+ metadata.gz: 4d5eb9055acbfc951ef6874c484b10f0c4c9a02f4886148fd917ec1f83cdc386
4
+ data.tar.gz: fe1db6e87b0788868ab99a4c5ee2c818fb2720b8aaa88a59d86023a8a4bc0f92
5
5
  SHA512:
6
- metadata.gz: cf37a0c4d1ac19450ae702ee22b4f460d8b16528a00603d1cbb0fbad6ff5d6b8caf1dca90ff9ec0834683f56651af76ec0d14465c244d3e95482c76a8fcf9ccc
7
- data.tar.gz: f150bd31c92462f5c1594368816aaaa60fe6e17ec4e4e0cb67d8b0ff7c0732be9d544df040548c6c80050fea6e66d6f21cf960dcec1b4af6bec4cbb4c318bb24
6
+ metadata.gz: 17141eaefe36c4ee6e857fe36ae3ee7c9e500cb6b3dc07ba79b2ba20248cb1a01920d5f0441c09279af0ab04efa020ffe144aeef1f4fb00036094378c1e2fc65
7
+ data.tar.gz: 8bf37d00fc3b161eb84587eef5562cebe493adb6a0729897f00aed9d38a69b1847ba82781473a8d2b3deb4ad1acea764b014460a758bc12ef686028096974355
data/README.md CHANGED
@@ -60,7 +60,13 @@ Rails Pulse is a comprehensive performance monitoring and debugging gem that pro
60
60
 
61
61
  ## Screenshots
62
62
 
63
- <img src="app/assets/images/rails_pulse/dashboard.png" alt="Rails Pulse" />
63
+ <table>
64
+ <tr>
65
+ <td><img src="app/assets/images/rails_pulse/dashboard.png" alt="Rails Pulse Dashboard" width="400" /></td>
66
+ <td><img src="app/assets/images/rails_pulse/request.png" alt="Rails Pulse Requests" width="400" /></td>
67
+ </tr>
68
+ </table>
69
+
64
70
 
65
71
  ## Getting Started
66
72
 
@@ -81,15 +87,22 @@ bundle install
81
87
  Generate the installation files:
82
88
 
83
89
  ```bash
90
+ # Install with single database setup (default - recommended)
84
91
  rails generate rails_pulse:install
85
- ```
86
92
 
87
- Load the database schema:
93
+ # Or install with separate database setup
94
+ rails generate rails_pulse:install --database=separate
95
+ ```
88
96
 
97
+ **For single database setup (default):**
89
98
  ```bash
90
- rails db:prepare
99
+ rails db:migrate # Creates Rails Pulse tables in your main database
91
100
  ```
92
101
 
102
+ **For separate database setup:**
103
+ 1. Configure `config/database.yml` with your Rails Pulse database settings
104
+ 2. Run: `rails db:prepare` to create and load the schema
105
+
93
106
  Add the Rails Pulse route to your application:
94
107
 
95
108
  ```ruby
@@ -101,10 +114,10 @@ end
101
114
  Schedule background jobs:
102
115
 
103
116
  ```ruby
104
- # Schedule to run 5 minutes past every hour
117
+ # Schedule to run 5 minutes past every hour. cron: 5 * * * *
105
118
  RailsPulse::SummaryJob.perform_later
106
119
 
107
- # Schedule to run daily
120
+ # Schedule to run daily. cron: 0 1 * * *
108
121
  RailsPulse::CleanupJob.perform_later
109
122
  ```
110
123
 
@@ -116,6 +129,10 @@ Rails Pulse automatically starts collecting performance data once installed. Acc
116
129
  http://localhost:3000/rails_pulse
117
130
  ```
118
131
 
132
+ **Database Setup:**
133
+ - **Single Database (default)**: Rails Pulse tables are created in your main database - no additional configuration needed
134
+ - **Separate Database**: See the [Separate Database Support](#separate-database-support) section for setup instructions
135
+
119
136
  ### Basic Configuration
120
137
 
121
138
  Customize Rails Pulse in `config/initializers/rails_pulse.rb`:
@@ -281,20 +298,36 @@ RailsPulse::CleanupJob.perform_later
281
298
 
282
299
  ## Separate Database Support
283
300
 
284
- Rails Pulse supports storing performance monitoring data in a **separate database**. By default, Rails Pulse stores data in your main application database alongside your existing tables.
301
+ Rails Pulse offers two database setup options to fit your application's needs:
302
+
303
+ ### Option 1: Single Database (Default - Recommended)
304
+
305
+ Stores Rails Pulse data in your main application database alongside your existing tables. This is the simplest setup and works great for most applications.
306
+
307
+ **Advantages:**
308
+ - Zero additional configuration required
309
+ - Simpler backup and deployment strategies
310
+ - Works with any database (SQLite, PostgreSQL, MySQL)
311
+
312
+ **Installation:**
313
+ ```bash
314
+ rails generate rails_pulse:install
315
+ rails db:migrate
316
+ ```
285
317
 
286
- Use a separate database when you want:
318
+ ### Option 2: Separate Database
287
319
 
320
+ Stores Rails Pulse data in a dedicated database, completely isolated from your main application.
321
+
322
+ **Use a separate database when you want:**
288
323
  - **Isolating monitoring data** from your main application database
289
324
  - **Using different database engines** optimized for time-series data
290
325
  - **Scaling monitoring independently** from your application
291
326
  - **Simplified backup strategies** with separate retention policies
292
327
 
293
- **For shared database setup (default)**, no database configuration is needed - simply run `rails db:prepare` after installation.
294
-
295
328
  ### Configuration
296
329
 
297
- To use a separate database, configure the `connects_to` option in your Rails Pulse initializer:
330
+ To use a separate database, install with the `--database=separate` flag, then configure the `connects_to` option in your Rails Pulse initializer:
298
331
 
299
332
  ```ruby
300
333
  RailsPulse.configure do |config|
@@ -350,17 +383,30 @@ production:
350
383
  pool: 5
351
384
  ```
352
385
 
353
- ### Schema Loading
386
+ ### Installation Steps
354
387
 
355
- After installation, load the Rails Pulse database schema:
388
+ **For separate database setup:**
356
389
 
357
- ```bash
358
- rails db:prepare
359
- ```
390
+ 1. **Generate installation files:**
391
+ ```bash
392
+ rails generate rails_pulse:install --database=separate
393
+ ```
394
+
395
+ 2. **Configure `config/database.yml`** (see examples above)
396
+
397
+ 3. **Create and load the schema:**
398
+ ```bash
399
+ rails db:prepare
400
+ ```
401
+ This automatically creates the database and loads the Rails Pulse schema.
402
+
403
+ **Schema Management:**
360
404
 
361
- This command works for both:
362
- - Shared database setup (default): Loads tables into your main application database
363
- - Separate database setup: Automatically loads tables into your configured Rails Pulse database
405
+ The schema file `db/rails_pulse_schema.rb` serves as your single source of truth for the database structure. It:
406
+ - Defines all Rails Pulse tables in one place
407
+ - Is loaded by the installation migration
408
+ - Should not be deleted or modified
409
+ - Future updates will provide migrations in `db/rails_pulse_migrate/`
364
410
 
365
411
  ## Testing
366
412
 
@@ -447,7 +493,7 @@ Test individual databases locally:
447
493
  # Test with SQLite (default)
448
494
  rails test:all
449
495
 
450
- # Test with PostgreSQL
496
+ # Test with PostgreSQL
451
497
  DB=postgresql FORCE_DB_CONFIG=true rails test:all
452
498
 
453
499
  # Test with MySQL (local only)
data/Rakefile CHANGED
@@ -1,109 +1,192 @@
1
1
  require "bundler/setup"
2
+ require "bundler/gem_tasks"
2
3
 
3
4
  # Load environment variables from .env file
4
5
  require "dotenv/load" if File.exist?(".env")
5
6
 
6
7
  APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
7
8
  load "rails/tasks/engine.rake"
8
- load "rails/tasks/statistics.rake"
9
9
 
10
- require "bundler/gem_tasks"
10
+ desc "Setup database for testing"
11
+ task :test_setup do
12
+ database = ENV['DB'] || 'sqlite3'
13
+
14
+ puts "\n" + "=" * 50
15
+ puts "🛠️ Rails Pulse Test Setup"
16
+ puts "=" * 50
17
+ puts "Database: #{database.upcase}"
18
+ puts "=" * 50
19
+ puts
20
+
21
+ begin
22
+ # Remove schema.rb to ensure clean migration
23
+ schema_file = "test/dummy/db/schema.rb"
24
+ if File.exist?(schema_file)
25
+ puts "🧹 Removing existing schema.rb file..."
26
+ File.delete(schema_file)
27
+ end
11
28
 
12
- # Test tasks
13
- namespace :test do
14
- desc "Run unit tests (models, helpers, services, instrumentation)"
15
- task :unit do
16
- sh "rails test test/models test/helpers test/services test/support test/instrumentation"
17
- end
29
+ case database.downcase
30
+ when 'sqlite3', 'sqlite'
31
+ puts "📦 Setting up SQLite database..."
32
+ sh "RAILS_ENV=test bin/rails db:drop db:create db:migrate"
33
+
34
+ when 'mysql2', 'mysql'
35
+ puts "🐬 Setting up MySQL database..."
36
+ sh "DB=mysql2 RAILS_ENV=test rails db:drop db:create db:migrate"
37
+
38
+ when 'postgresql', 'postgres'
39
+ puts "🐘 Setting up PostgreSQL database..."
40
+ sh "DB=postgresql RAILS_ENV=test rails db:drop db:create db:migrate"
41
+
42
+ else
43
+ puts "⚠️ Unknown database: #{database}"
44
+ puts "Supported databases: sqlite3, mysql2, postgresql"
45
+ exit 1
46
+ end
18
47
 
19
- desc "Run functional tests (controllers)"
20
- task :functional do
21
- sh "rails test test/controllers"
48
+ puts "\n✅ Database setup complete!"
49
+ puts "Ready to run: rake test"
50
+
51
+ rescue => e
52
+ puts "\n❌ Database setup failed!"
53
+ puts "Error: #{e.message}"
54
+ puts "\nTroubleshooting:"
55
+ puts "• Ensure #{database} is installed and running"
56
+ puts "• Check database credentials in test/dummy/config/database.yml"
57
+ puts "• Verify RAILS_ENV=test environment is configured"
58
+ exit 1
22
59
  end
60
+ end
23
61
 
24
- desc "Run integration tests"
25
- task :integration do
26
- sh "rails test test/integration test/system"
62
+ desc "Run test suite"
63
+ task :test do
64
+ database = ENV['DB'] || 'sqlite3'
65
+
66
+ # Get Rails version from Gemfile.lock or fallback
67
+ rails_version = begin
68
+ require 'rails'
69
+ Rails.version
70
+ rescue LoadError
71
+ # Try to get from Gemfile.lock
72
+ gemfile_lock = File.read('Gemfile.lock') rescue nil
73
+ if gemfile_lock && gemfile_lock.match(/rails \(([^)]+)\)/)
74
+ $1
75
+ else
76
+ 'unknown'
77
+ end
27
78
  end
28
79
 
29
- desc "Run all tests"
30
- task :all do
31
- sh "rails test"
80
+ puts "\n" + "=" * 50
81
+ puts "💛 Rails Pulse Test Suite"
82
+ puts "=" * 50
83
+ puts "Database: #{database.upcase}"
84
+ puts "Rails: #{rails_version}"
85
+ puts "=" * 50
86
+ puts
87
+
88
+ sh "rails test test/controllers test/helpers test/instrumentation test/jobs test/models test/services test/system"
89
+ end
90
+
91
+ desc "Setup database for specific Rails version and database"
92
+ task :test_setup_for_version, [ :database, :rails_version ] do |t, args|
93
+ database = args[:database] || ENV['DB'] || 'sqlite3'
94
+ rails_version = args[:rails_version] || 'rails-8-0'
95
+
96
+ puts "\n" + "=" * 50
97
+ puts "🛠️ Rails Pulse Test Setup"
98
+ puts "=" * 50
99
+ puts "Database: #{database.upcase}"
100
+ puts "Rails: #{rails_version.upcase.gsub('-', ' ')}"
101
+ puts "=" * 50
102
+ puts
103
+
104
+ begin
105
+ # Remove schema.rb to ensure clean migration
106
+ schema_file = "test/dummy/db/schema.rb"
107
+ if File.exist?(schema_file)
108
+ puts "🧹 Removing existing schema.rb file..."
109
+ File.delete(schema_file)
110
+ end
111
+
112
+ if rails_version == "rails-8-0" && database == "sqlite3"
113
+ # Use current default setup
114
+ puts "📦 Setting up #{database.upcase} database with Rails 8.0..."
115
+ sh "RAILS_ENV=test bin/rails db:drop db:create db:migrate"
116
+ else
117
+ # Use appraisal with specific database and Rails version
118
+ puts "📦 Setting up #{database.upcase} database with #{rails_version.upcase.gsub('-', ' ')}..."
119
+ sh "DB=#{database} bundle exec appraisal #{rails_version} rails db:drop db:create db:migrate RAILS_ENV=test"
120
+ end
121
+
122
+ puts "\n✅ Database setup complete for #{database.upcase} + #{rails_version.upcase.gsub('-', ' ')}!"
123
+
124
+ rescue => e
125
+ puts "\n❌ Database setup failed!"
126
+ puts "Error: #{e.message}"
127
+ exit 1
32
128
  end
129
+ end
33
130
 
34
- desc "Run tests across all database and Rails version combinations (local only - CI uses sqlite3 + postgresql)"
35
- task :matrix do
36
- databases = [ "sqlite3", "postgresql", "mysql2" ]
37
- rails_versions = [ "rails-7-2", "rails-8-0" ]
38
-
39
- failed_combinations = []
40
-
41
- databases.each do |database|
42
- rails_versions.each do |rails_version|
43
- puts "\n" + "=" * 80
44
- puts "🧪 Local Testing: #{database.upcase} + #{rails_version.upcase}"
45
- puts "(CI only tests SQLite3 + PostgreSQL for reliability)"
46
- puts "=" * 80
47
-
48
- begin
49
- gemfile = "gemfiles/#{rails_version.gsub('-', '_')}.gemfile"
50
-
51
- # Set environment variables
52
- env_vars = {
53
- "DB" => database,
54
- "BUNDLE_GEMFILE" => gemfile,
55
- "FORCE_DB_CONFIG" => "true"
56
- }
57
-
58
- # Add database-specific environment variables
59
- case database
60
- when "postgresql"
61
- env_vars.merge!({
62
- "POSTGRES_USERNAME" => ENV.fetch("POSTGRES_USERNAME", "postgres"),
63
- "POSTGRES_PASSWORD" => ENV.fetch("POSTGRES_PASSWORD", ""),
64
- "POSTGRES_HOST" => ENV.fetch("POSTGRES_HOST", "localhost"),
65
- "POSTGRES_PORT" => ENV.fetch("POSTGRES_PORT", "5432")
66
- })
67
- when "mysql2"
68
- env_vars.merge!({
69
- "MYSQL_USERNAME" => ENV.fetch("MYSQL_USERNAME", "root"),
70
- "MYSQL_PASSWORD" => ENV.fetch("MYSQL_PASSWORD", "password"),
71
- "MYSQL_HOST" => ENV.fetch("MYSQL_HOST", "localhost"),
72
- "MYSQL_PORT" => ENV.fetch("MYSQL_PORT", "3306")
73
- })
74
- end
75
-
76
- # Build environment string
77
- env_string = env_vars.map { |k, v| "#{k}=#{v}" }.join(" ")
78
-
79
- # Run the test command
80
- sh "#{env_string} bundle exec rails test:all"
81
-
82
- puts "✅ PASSED: #{database} + #{rails_version}"
83
-
84
- rescue => e
85
- puts "❌ FAILED: #{database} + #{rails_version}"
86
- puts "Error: #{e.message}"
87
- failed_combinations << "#{database} + #{rails_version}"
131
+ desc "Test all database and Rails version combinations"
132
+ task :test_matrix do
133
+ databases = %w[sqlite3 postgresql mysql2]
134
+ rails_versions = %w[rails-7-2 rails-8-0]
135
+
136
+ failed_combinations = []
137
+ total_combinations = databases.size * rails_versions.size
138
+ current = 0
139
+
140
+ puts "\n" + "=" * 60
141
+ puts "🚀 Rails Pulse Full Test Matrix"
142
+ puts "=" * 60
143
+ puts "Testing #{total_combinations} combinations..."
144
+ puts "=" * 60
145
+
146
+ databases.each do |database|
147
+ rails_versions.each do |rails_version|
148
+ current += 1
149
+
150
+ puts "\n[#{current}/#{total_combinations}] Testing: #{database.upcase} + #{rails_version.upcase.gsub('-', ' ')}"
151
+ puts "-" * 50
152
+
153
+ begin
154
+ # First setup the database for this specific combination
155
+ Rake::Task[:test_setup_for_version].reenable
156
+ Rake::Task[:test_setup_for_version].invoke(database, rails_version)
157
+
158
+ # Then run the tests
159
+ if rails_version == "rails-8-0" && database == "sqlite3"
160
+ # Current default setup
161
+ sh "bundle exec rake test"
162
+ else
163
+ # Use appraisal with specific database
164
+ sh "DB=#{database} bundle exec appraisal #{rails_version} rake test"
88
165
  end
89
- end
90
- end
91
166
 
92
- puts "\n" + "=" * 80
93
- puts "🏁 Local Test Matrix Results"
94
- puts "(CI automatically tests SQLite3 + PostgreSQL only)"
95
- puts "=" * 80
167
+ puts " PASSED: #{database} + #{rails_version}"
96
168
 
97
- if failed_combinations.empty?
98
- puts " All combinations passed!"
99
- else
100
- puts " Failed combinations:"
101
- failed_combinations.each { |combo| puts " - #{combo}" }
102
- exit 1
169
+ rescue => e
170
+ puts " FAILED: #{database} + #{rails_version}"
171
+ puts " Error: #{e.message}"
172
+ failed_combinations << "#{database} + #{rails_version}"
173
+ end
103
174
  end
104
175
  end
176
+
177
+ puts "\n" + "=" * 60
178
+ puts "🏁 Test Matrix Results"
179
+ puts "=" * 60
180
+
181
+ if failed_combinations.empty?
182
+ puts "🎉 All #{total_combinations} combinations passed!"
183
+ else
184
+ puts "✅ Passed: #{total_combinations - failed_combinations.size}/#{total_combinations}"
185
+ puts "❌ Failed combinations:"
186
+ failed_combinations.each { |combo| puts " • #{combo}" }
187
+ exit 1
188
+ end
105
189
  end
106
190
 
107
- # Override default test task
108
- desc "Run all tests"
109
- task test: "test:all"
191
+
192
+ task default: :test
@@ -4,25 +4,48 @@
4
4
 
5
5
  a {
6
6
  text-decoration: underline;
7
- color: #0048b5;
7
+ color: var(--color-link);
8
8
  }
9
9
 
10
10
  #header {
11
- background-color: #ffc91f;
11
+ background-color: var(--header-bg);
12
12
  }
13
13
 
14
14
  #header a {
15
- color: black
15
+ color: var(--header-link);
16
+ text-decoration: none;
16
17
  }
17
18
 
18
19
  #header a:hover {
19
- background-color: #ffe284;
20
+ background-color: transparent;
21
+ text-decoration: underline;
20
22
  }
21
23
 
22
24
  a:hover {
23
25
  cursor: pointer;
24
26
  }
25
27
 
28
+ /* Dark mode */
29
+
30
+ /* Dark scheme tweaks via component variables */
31
+ html[data-color-scheme="dark"] .card {
32
+ /* Scope card surfaces slightly darker than page */
33
+ --color-bg: rgb(47, 47, 47);
34
+ --color-border: rgb(64, 64, 64);
35
+ }
36
+
37
+ /* Header colors are handled by --header-* tokens in base.css */
38
+
39
+ html[data-color-scheme="dark"] .badge--positive-inverse,
40
+ html[data-color-scheme="dark"] .badge--negative-inverse {
41
+ --badge-background: rgb(47, 47, 47);
42
+ }
43
+
44
+ html[data-color-scheme="dark"] .input {
45
+ --input-background: #535252;
46
+ --input-border-color: #7e7d7d;
47
+ }
48
+
26
49
  .hidden {
27
50
  display: none;
28
51
  }
@@ -66,7 +89,7 @@ a:hover {
66
89
  height: 16px;
67
90
  padding: 2px;
68
91
  position: absolute;
69
- top: 11px;
92
+ top: 20px;
70
93
  }
71
94
 
72
95
  /* REQUEST OPERATIONS BAR */
@@ -56,3 +56,16 @@
56
56
  --badge-border-color: transparent;
57
57
  --badge-color: var(--color-negative);
58
58
  }
59
+
60
+ /* Trend badge icon lightening (dark mode only) */
61
+ .badge--trend rails-pulse-icon { color: var(--badge-color, currentColor); }
62
+ html[data-color-scheme="dark"] .badge--trend rails-pulse-icon {
63
+ /* Lighten icon relative to badge text color for contrast */
64
+ color: color-mix(in srgb, var(--badge-color) 55%, white 45%);
65
+ }
66
+
67
+ /* Trend amount lightening (dark mode only) */
68
+ .badge--trend .badge__trend-amount { color: var(--badge-color, currentColor); }
69
+ html[data-color-scheme="dark"] .badge--trend .badge__trend-amount {
70
+ color: color-mix(in srgb, var(--badge-color) 55%, white 45%);
71
+ }
@@ -5,6 +5,10 @@
5
5
  --color-text-reversed: white;
6
6
  --color-text-subtle: var(--zinc-500);
7
7
  --color-link: var(--blue-700);
8
+ /* Header tokens */
9
+ --header-bg: #ffc91f;
10
+ --header-link: black;
11
+ --header-link-hover-bg: #ffe284;
8
12
  --color-border-light: var(--zinc-100);
9
13
  --color-border: var(--zinc-200);
10
14
  --color-border-dark: var(--zinc-400);
@@ -30,8 +34,9 @@ html[data-color-scheme="dark"] {
30
34
  --color-bg: var(--zinc-800);
31
35
  --color-text: white;
32
36
  --color-text-reversed: black;
33
- --color-text-subtle: var(--zinc-400);
34
- --color-link: var(--blue-400);
37
+ --color-text-subtle: var(--zinc-300);
38
+ /* Use brand yellow for links in dark mode */
39
+ --color-link: #ffc91f;
35
40
  --color-border-light: var(--zinc-900);
36
41
  --color-border: var(--zinc-800);
37
42
  --color-border-dark: var(--zinc-600);
@@ -39,6 +44,11 @@ html[data-color-scheme="dark"] {
39
44
  --color-selected-dark: var(--blue-800);
40
45
  --color-highlight: var(--yellow-900);
41
46
 
47
+ /* Header tokens */
48
+ --header-bg: rgb(32, 32, 32);
49
+ --header-link: #ffc91f;
50
+ --header-link-hover-bg: #ffe284; /* keep existing hover color */
51
+
42
52
  /* Accent colors */
43
53
  --color-primary: var(--zinc-50);
44
54
  --color-secondary: var(--zinc-800);
@@ -0,0 +1,30 @@
1
+ .collapsible-code.collapsed pre {
2
+ max-height: 4.5em;
3
+ overflow: hidden;
4
+ position: relative;
5
+ }
6
+
7
+ .collapsible-code.collapsed pre::after {
8
+ content: '';
9
+ position: absolute;
10
+ bottom: 0;
11
+ left: 0;
12
+ right: 0;
13
+ height: 1em;
14
+ background: linear-gradient(transparent, var(--color-border-light));
15
+ pointer-events: none;
16
+ }
17
+
18
+ .collapsible-toggle {
19
+ margin-top: 0.5rem;
20
+ font-size: 0.875rem;
21
+ color: var(--color-link);
22
+ text-decoration: underline;
23
+ transform: lowercase;
24
+ cursor: pointer;
25
+ border: none;
26
+ background: none;
27
+ padding: 0;
28
+ font-weight: normal;
29
+ margin-left: 10px;
30
+ }
@@ -31,6 +31,5 @@
31
31
  left: var(--popover-x, 0) !important;
32
32
  top: var(--popover-y, 0) !important;
33
33
  margin: 0 !important;
34
- inset: unset !important;
35
34
  }
36
35
  }
@@ -3,22 +3,74 @@
3
3
  justify-content: space-between;
4
4
  width: 100%;
5
5
  gap: var(--column-gap, 0.5rem);
6
+ align-items: stretch;
6
7
  }
7
8
 
8
9
  .row > * {
9
10
  flex: 1;
10
11
  min-width: 0;
12
+ display: flex;
13
+ flex-direction: column;
11
14
  }
12
15
 
13
- /* Stack items on smaller screens */
16
+ /* Ensure metric cards and their panels stretch to full height */
17
+ .row > .grid-item {
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+
22
+ .row > .grid-item > * {
23
+ flex: 1;
24
+ }
25
+
26
+ /* Responsive layout for screens smaller than 768px */
14
27
  @media (max-width: 768px) {
15
28
  .row {
16
- flex-direction: column;
29
+ display: flex;
30
+ flex-wrap: wrap;
31
+ justify-content: space-between;
17
32
  gap: 0.5rem;
33
+ align-items: flex-start;
18
34
  }
19
35
 
20
36
  .row > * {
37
+ flex: 0 0 calc(50% - 0.25rem);
38
+ min-width: 0;
39
+ height: auto;
40
+ }
41
+
42
+ .row > .grid-item {
43
+ height: auto;
44
+ }
45
+
46
+ .row > .grid-item > * {
21
47
  flex: none;
22
- width: 100%;
48
+ }
49
+
50
+ /* Tables should stack in single column on mobile */
51
+ .row:has(.table-container) > * {
52
+ flex: 0 0 100%;
53
+ }
54
+
55
+ /* Single column for very small screens */
56
+ @media (max-width: 480px) {
57
+ .row > * {
58
+ flex: 0 0 100%;
59
+ }
60
+
61
+ .row > .grid-item {
62
+ min-height: auto;
63
+ }
64
+
65
+ /* Make metric cards more compact on mobile */
66
+ .row > .grid-item .card {
67
+ padding: var(--size-3);
68
+ }
69
+
70
+ /* Make charts smaller on mobile */
71
+ .row > .grid-item .chart-container {
72
+ height: 60px !important;
73
+ max-height: 60px;
74
+ }
23
75
  }
24
76
  }