rails_pulse 0.1.3 → 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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +134 -16
  3. data/Rakefile +315 -83
  4. data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
  5. data/app/assets/stylesheets/rails_pulse/components/datepicker.css +191 -0
  6. data/app/assets/stylesheets/rails_pulse/components/switch.css +36 -0
  7. data/app/assets/stylesheets/rails_pulse/components/tags.css +98 -0
  8. data/app/assets/stylesheets/rails_pulse/components/utilities.css +26 -0
  9. data/app/controllers/concerns/response_range_concern.rb +15 -2
  10. data/app/controllers/concerns/tag_filter_concern.rb +26 -0
  11. data/app/controllers/concerns/time_range_concern.rb +27 -8
  12. data/app/controllers/rails_pulse/application_controller.rb +73 -0
  13. data/app/controllers/rails_pulse/queries_controller.rb +18 -21
  14. data/app/controllers/rails_pulse/requests_controller.rb +80 -35
  15. data/app/controllers/rails_pulse/routes_controller.rb +4 -2
  16. data/app/controllers/rails_pulse/tags_controller.rb +51 -0
  17. data/app/helpers/rails_pulse/application_helper.rb +2 -0
  18. data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
  19. data/app/helpers/rails_pulse/chart_helper.rb +1 -1
  20. data/app/helpers/rails_pulse/form_helper.rb +75 -0
  21. data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
  22. data/app/helpers/rails_pulse/tags_helper.rb +29 -0
  23. data/app/javascript/rails_pulse/application.js +6 -0
  24. data/app/javascript/rails_pulse/controllers/custom_range_controller.js +115 -0
  25. data/app/javascript/rails_pulse/controllers/datepicker_controller.js +48 -0
  26. data/app/javascript/rails_pulse/controllers/global_filters_controller.js +110 -0
  27. data/app/javascript/rails_pulse/controllers/index_controller.js +11 -3
  28. data/app/models/concerns/taggable.rb +61 -0
  29. data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +1 -1
  30. data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
  31. data/app/models/rails_pulse/queries/cards/average_query_times.rb +1 -1
  32. data/app/models/rails_pulse/queries/cards/execution_rate.rb +56 -17
  33. data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
  34. data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
  35. data/app/models/rails_pulse/queries/tables/index.rb +10 -2
  36. data/app/models/rails_pulse/query.rb +2 -0
  37. data/app/models/rails_pulse/request.rb +10 -2
  38. data/app/models/rails_pulse/requests/charts/average_response_times.rb +2 -2
  39. data/app/models/rails_pulse/requests/tables/index.rb +77 -0
  40. data/app/models/rails_pulse/route.rb +2 -0
  41. data/app/models/rails_pulse/routes/cards/average_response_times.rb +1 -1
  42. data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
  43. data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
  44. data/app/models/rails_pulse/routes/cards/request_count_totals.rb +16 -5
  45. data/app/models/rails_pulse/routes/tables/index.rb +14 -4
  46. data/app/models/rails_pulse/summary.rb +7 -7
  47. data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +11 -3
  48. data/app/services/rails_pulse/summary_service.rb +2 -0
  49. data/app/views/layouts/rails_pulse/_global_filters.html.erb +84 -0
  50. data/app/views/layouts/rails_pulse/_menu_items.html.erb +5 -5
  51. data/app/views/layouts/rails_pulse/application.html.erb +8 -5
  52. data/app/views/rails_pulse/components/_metric_card.html.erb +2 -2
  53. data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
  54. data/app/views/rails_pulse/components/_page_header.html.erb +20 -0
  55. data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
  56. data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
  57. data/app/views/rails_pulse/operations/show.html.erb +1 -1
  58. data/app/views/rails_pulse/queries/_analysis_results.html.erb +53 -23
  59. data/app/views/rails_pulse/queries/_show_table.html.erb +33 -5
  60. data/app/views/rails_pulse/queries/_table.html.erb +4 -6
  61. data/app/views/rails_pulse/queries/index.html.erb +3 -7
  62. data/app/views/rails_pulse/queries/show.html.erb +3 -7
  63. data/app/views/rails_pulse/requests/_table.html.erb +32 -19
  64. data/app/views/rails_pulse/requests/index.html.erb +45 -55
  65. data/app/views/rails_pulse/requests/show.html.erb +1 -3
  66. data/app/views/rails_pulse/routes/_requests_table.html.erb +41 -0
  67. data/app/views/rails_pulse/routes/_table.html.erb +4 -8
  68. data/app/views/rails_pulse/routes/index.html.erb +4 -8
  69. data/app/views/rails_pulse/routes/show.html.erb +6 -12
  70. data/app/views/rails_pulse/tags/_tag_manager.html.erb +73 -0
  71. data/config/initializers/rails_charts_csp_patch.rb +32 -40
  72. data/config/routes.rb +5 -0
  73. data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
  74. data/db/rails_pulse_schema.rb +4 -1
  75. data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +25 -9
  76. data/lib/generators/rails_pulse/install_generator.rb +30 -7
  77. data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +75 -2
  78. data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +3 -2
  79. data/lib/generators/rails_pulse/templates/rails_pulse.rb +21 -0
  80. data/lib/generators/rails_pulse/upgrade_generator.rb +147 -30
  81. data/lib/rails_pulse/configuration.rb +16 -1
  82. data/lib/rails_pulse/engine.rb +21 -0
  83. data/lib/rails_pulse/version.rb +1 -1
  84. data/public/rails-pulse-assets/rails-pulse-icons.js +16 -15
  85. data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
  86. data/public/rails-pulse-assets/rails-pulse.css +1 -1
  87. data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
  88. data/public/rails-pulse-assets/rails-pulse.js +73 -69
  89. data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
  90. metadata +20 -5
  91. data/app/views/rails_pulse/components/_breadcrumbs.html.erb +0 -12
  92. data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
  93. data/db/migrate/20250916031656_add_analysis_to_rails_pulse_queries.rb +0 -13
@@ -3,12 +3,22 @@
3
3
  # Load with: rails db:schema:load:rails_pulse or db:prepare
4
4
 
5
5
  RailsPulse::Schema = lambda do |connection|
6
- # Skip if tables already exist to prevent conflicts
7
- return if connection.table_exists?(:rails_pulse_routes)
6
+ # Skip if all tables already exist to prevent conflicts
7
+ required_tables = [ :rails_pulse_routes, :rails_pulse_queries, :rails_pulse_requests, :rails_pulse_operations, :rails_pulse_summaries ]
8
+
9
+ if ENV["CI"] == "true"
10
+ existing_tables = required_tables.select { |table| connection.table_exists?(table) }
11
+ missing_tables = required_tables - existing_tables
12
+ puts "[RailsPulse::Schema] Existing tables: #{existing_tables.join(', ')}" if existing_tables.any?
13
+ puts "[RailsPulse::Schema] Missing tables: #{missing_tables.join(', ')}" if missing_tables.any?
14
+ end
15
+
16
+ return if required_tables.all? { |table| connection.table_exists?(table) }
8
17
 
9
18
  connection.create_table :rails_pulse_routes do |t|
10
19
  t.string :method, null: false, comment: "HTTP method (e.g., GET, POST)"
11
20
  t.string :path, null: false, comment: "Request path (e.g., /posts/index)"
21
+ t.text :tags, comment: "JSON array of tags for filtering and categorization"
12
22
  t.timestamps
13
23
  end
14
24
 
@@ -16,6 +26,16 @@ RailsPulse::Schema = lambda do |connection|
16
26
 
17
27
  connection.create_table :rails_pulse_queries do |t|
18
28
  t.string :normalized_sql, limit: 1000, null: false, comment: "Normalized SQL query string (e.g., SELECT * FROM users WHERE id = ?)"
29
+ t.datetime :analyzed_at, comment: "When query analysis was last performed"
30
+ t.text :explain_plan, comment: "EXPLAIN output from actual SQL execution"
31
+ t.text :issues, comment: "JSON array of detected performance issues"
32
+ t.text :metadata, comment: "JSON object containing query complexity metrics"
33
+ t.text :query_stats, comment: "JSON object with query characteristics analysis"
34
+ t.text :backtrace_analysis, comment: "JSON object with call chain and N+1 detection"
35
+ t.text :index_recommendations, comment: "JSON array of database index recommendations"
36
+ t.text :n_plus_one_analysis, comment: "JSON object with enhanced N+1 query detection results"
37
+ t.text :suggestions, comment: "JSON array of optimization recommendations"
38
+ t.text :tags, comment: "JSON array of tags for filtering and categorization"
19
39
  t.timestamps
20
40
  end
21
41
 
@@ -29,6 +49,7 @@ RailsPulse::Schema = lambda do |connection|
29
49
  t.string :request_uuid, null: false, comment: "Unique identifier for the request (e.g., UUID)"
30
50
  t.string :controller_action, comment: "Controller and action handling the request (e.g., PostsController#show)"
31
51
  t.timestamp :occurred_at, null: false, comment: "When the request started"
52
+ t.text :tags, comment: "JSON array of tags for filtering and categorization"
32
53
  t.timestamps
33
54
  end
34
55
 
@@ -53,6 +74,58 @@ RailsPulse::Schema = lambda do |connection|
53
74
  connection.add_index :rails_pulse_operations, [ :query_id, :occurred_at ], name: "index_rails_pulse_operations_on_query_and_time"
54
75
  connection.add_index :rails_pulse_operations, [ :query_id, :duration, :occurred_at ], name: "index_rails_pulse_operations_query_performance"
55
76
  connection.add_index :rails_pulse_operations, [ :occurred_at, :duration, :operation_type ], name: "index_rails_pulse_operations_on_time_duration_type"
77
+
78
+ connection.create_table :rails_pulse_summaries do |t|
79
+ # Time fields
80
+ t.datetime :period_start, null: false, comment: "Start of the aggregation period"
81
+ t.datetime :period_end, null: false, comment: "End of the aggregation period"
82
+ t.string :period_type, null: false, comment: "Aggregation period type: hour, day, week, month"
83
+
84
+ # Polymorphic association to handle both routes and queries
85
+ t.references :summarizable, polymorphic: true, null: false, index: true, comment: "Link to Route or Query"
86
+ # This creates summarizable_type (e.g., 'RailsPulse::Route', 'RailsPulse::Query')
87
+ # and summarizable_id (route_id or query_id)
88
+
89
+ # Universal metrics
90
+ t.integer :count, default: 0, null: false, comment: "Total number of requests/operations"
91
+ t.float :avg_duration, comment: "Average duration in milliseconds"
92
+ t.float :min_duration, comment: "Minimum duration in milliseconds"
93
+ t.float :max_duration, comment: "Maximum duration in milliseconds"
94
+ t.float :p50_duration, comment: "50th percentile duration"
95
+ t.float :p95_duration, comment: "95th percentile duration"
96
+ t.float :p99_duration, comment: "99th percentile duration"
97
+ t.float :total_duration, comment: "Total duration in milliseconds"
98
+ t.float :stddev_duration, comment: "Standard deviation of duration"
99
+
100
+ # Request/Route specific metrics
101
+ t.integer :error_count, default: 0, comment: "Number of error responses (5xx)"
102
+ t.integer :success_count, default: 0, comment: "Number of successful responses"
103
+ t.integer :status_2xx, default: 0, comment: "Number of 2xx responses"
104
+ t.integer :status_3xx, default: 0, comment: "Number of 3xx responses"
105
+ t.integer :status_4xx, default: 0, comment: "Number of 4xx responses"
106
+ t.integer :status_5xx, default: 0, comment: "Number of 5xx responses"
107
+
108
+ t.timestamps
109
+ end
110
+
111
+ # Unique constraint and indexes for summaries
112
+ connection.add_index :rails_pulse_summaries, [ :summarizable_type, :summarizable_id, :period_type, :period_start ],
113
+ unique: true,
114
+ name: "idx_pulse_summaries_unique"
115
+ connection.add_index :rails_pulse_summaries, [ :period_type, :period_start ], name: "index_rails_pulse_summaries_on_period"
116
+ connection.add_index :rails_pulse_summaries, :created_at, name: "index_rails_pulse_summaries_on_created_at"
117
+
118
+ # Add indexes to existing tables for efficient aggregation
119
+ connection.add_index :rails_pulse_requests, [ :created_at, :route_id ], name: "idx_requests_for_aggregation"
120
+ connection.add_index :rails_pulse_requests, :created_at, name: "idx_requests_created_at"
121
+
122
+ connection.add_index :rails_pulse_operations, [ :created_at, :query_id ], name: "idx_operations_for_aggregation"
123
+ connection.add_index :rails_pulse_operations, :created_at, name: "idx_operations_created_at"
124
+
125
+ if ENV["CI"] == "true"
126
+ created_tables = required_tables.select { |table| connection.table_exists?(table) }
127
+ puts "[RailsPulse::Schema] Successfully created tables: #{created_tables.join(', ')}"
128
+ end
56
129
  end
57
130
 
58
131
  if defined?(RailsPulse::ApplicationRecord)
@@ -1,5 +1,5 @@
1
1
  # Generated from Rails Pulse schema - automatically loads current schema definition
2
- class InstallRailsPulseTables < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ class InstallRailsPulseTables < ActiveRecord::Migration[<%= Rails::VERSION::MAJOR %>.<%= Rails::VERSION::MINOR %>]
3
3
  def change
4
4
  # Load and execute the Rails Pulse schema directly
5
5
  # This ensures the migration is always in sync with the schema file
@@ -15,8 +15,9 @@ class InstallRailsPulseTables < ActiveRecord::Migration[<%= ActiveRecord::Migrat
15
15
  RailsPulse::Schema.call(connection)
16
16
 
17
17
  say "Rails Pulse tables created successfully"
18
+ say "The schema file db/rails_pulse_schema.rb remains as your single source of truth"
18
19
  else
19
20
  raise "Rails Pulse schema file not found at db/rails_pulse_schema.rb"
20
21
  end
21
22
  end
22
- end
23
+ end
@@ -71,6 +71,27 @@ RailsPulse.configure do |config|
71
71
  config.ignored_requests = []
72
72
  config.ignored_queries = []
73
73
 
74
+ # ====================================================================================================
75
+ # TAGGING
76
+ # ====================================================================================================
77
+ # Define custom tags for categorizing routes, requests, and queries.
78
+ # You can add any custom tags you want for filtering and organization.
79
+ #
80
+ # Tag names should be in present tense and describe the current state or category.
81
+ # Examples of good tag names:
82
+ # - "critical" (for high-priority endpoints)
83
+ # - "experimental" (for routes under development)
84
+ # - "deprecated" (for routes being phased out)
85
+ # - "external" (for third-party API calls)
86
+ # - "background" (for async job-related operations)
87
+ # - "admin" (for administrative routes)
88
+ # - "public" (for public-facing routes)
89
+ #
90
+ # Example configuration:
91
+ # config.tags = ["ignored", "critical", "experimental", "deprecated", "external", "admin"]
92
+
93
+ config.tags = [ "ignored", "critical", "experimental" ]
94
+
74
95
  # ====================================================================================================
75
96
  # DATABASE CONFIGURATION
76
97
  # ====================================================================================================
@@ -41,17 +41,36 @@ module RailsPulse
41
41
  # Check for existing Rails Pulse tables
42
42
  tables_exist = rails_pulse_tables_exist?
43
43
 
44
- if !tables_exist && File.exist?("db/rails_pulse_schema.rb")
44
+ root_path = respond_to?(:destination_root) ? destination_root : Rails.root
45
+ schema_path = File.join(root_path, "db/rails_pulse_schema.rb")
46
+
47
+ if !tables_exist && File.exist?(schema_path)
45
48
  :schema_only
46
49
  elsif !tables_exist
47
50
  :not_installed
48
- elsif File.exist?("db/rails_pulse_migrate")
51
+ elsif has_separate_database_config?
49
52
  :separate
50
53
  else
51
54
  :single
52
55
  end
53
56
  end
54
57
 
58
+ def has_separate_database_config?
59
+ root_path = respond_to?(:destination_root) ? destination_root : Rails.root
60
+ config_path = File.join(root_path, "config/database.yml")
61
+
62
+ return false unless File.exist?(config_path)
63
+
64
+ require "yaml"
65
+ db_config = YAML.load_file(config_path)
66
+
67
+ # Check if any environment has a rails_pulse database configuration
68
+ db_config.values.any? { |env| env.is_a?(Hash) && env.key?("rails_pulse") }
69
+ rescue => e
70
+ # If we can't read the file, assume single database
71
+ false
72
+ end
73
+
55
74
  def rails_pulse_tables_exist?
56
75
  return false unless defined?(ActiveRecord::Base)
57
76
 
@@ -65,7 +84,8 @@ module RailsPulse
65
84
 
66
85
  def get_rails_pulse_table_names
67
86
  # Load the schema file to get the table names dynamically
68
- schema_file = File.join(Rails.root, "db/rails_pulse_schema.rb")
87
+ root_path = respond_to?(:destination_root) ? destination_root : Rails.root
88
+ schema_file = File.join(root_path, "db/rails_pulse_schema.rb")
69
89
 
70
90
  if File.exist?(schema_file)
71
91
  # Read the schema file and extract the required_tables array
@@ -84,44 +104,114 @@ module RailsPulse
84
104
  end
85
105
 
86
106
  def upgrade_single_database
87
- missing_columns = detect_missing_columns
107
+ # Check for new migrations in gem
108
+ gem_migrations = get_gem_migrations
109
+ existing_migrations = get_user_migrations("db/migrate")
110
+ new_migrations = gem_migrations - existing_migrations
111
+
112
+ if new_migrations.any?
113
+ say "Found #{new_migrations.size} new migration(s) to copy:", :blue
114
+ new_migrations.each do |migration|
115
+ say " - #{migration}", :blue
116
+ copy_gem_migration_to(migration, "db/migrate")
117
+ end
88
118
 
89
- if missing_columns.empty?
90
- say "Rails Pulse is up to date! No migration needed.", :green
91
- return
92
- end
119
+ say "\nMigrations copied successfully!", :green
120
+ say "\nNext steps:", :green
121
+ say "1. Run: rails db:migrate"
122
+ say "2. Restart your Rails server"
123
+ else
124
+ # Fall back to detecting missing columns
125
+ missing_columns = detect_missing_columns
93
126
 
94
- # Format missing columns by table for the template
95
- missing_by_table = format_missing_columns_by_table(missing_columns)
127
+ if missing_columns.empty?
128
+ say "Rails Pulse is up to date! No migration needed.", :green
129
+ return
130
+ end
96
131
 
97
- say "Creating upgrade migration for missing columns: #{missing_columns.keys.join(', ')}", :blue
132
+ # Format missing columns by table for the template
133
+ missing_by_table = format_missing_columns_by_table(missing_columns)
98
134
 
99
- # Set instance variables for template
100
- @migration_version = ActiveRecord::Migration.current_version
101
- @missing_columns = missing_by_table
135
+ say "Creating upgrade migration for missing columns: #{missing_columns.keys.join(', ')}", :blue
102
136
 
103
- migration_template(
104
- "migrations/upgrade_rails_pulse_tables.rb",
105
- "db/migrate/upgrade_rails_pulse_tables.rb"
106
- )
137
+ # Set instance variables for template
138
+ @migration_version = ActiveRecord::Migration.current_version
139
+ @missing_columns = missing_by_table
107
140
 
108
- say <<~MESSAGE
141
+ migration_template(
142
+ "migrations/upgrade_rails_pulse_tables.rb",
143
+ "db/migrate/upgrade_rails_pulse_tables.rb"
144
+ )
109
145
 
110
- Upgrade migration created successfully!
146
+ say <<~MESSAGE
111
147
 
112
- Next steps:
113
- 1. Run: rails db:migrate
114
- 2. Restart your Rails server
148
+ Upgrade migration created successfully!
115
149
 
116
- This migration will add: #{missing_columns.keys.join(', ')}
150
+ Next steps:
151
+ 1. Run: rails db:migrate
152
+ 2. Restart your Rails server
117
153
 
118
- MESSAGE
154
+ This migration will add: #{missing_columns.keys.join(', ')}
155
+
156
+ MESSAGE
157
+ end
119
158
  end
120
159
 
121
160
  def upgrade_separate_database
122
- # For separate database, we'd need to check the schema file and generate migrations
123
- # in db/rails_pulse_migrate/ directory
124
- say "Separate database upgrade not implemented yet. Please check db/rails_pulse_schema.rb for updates.", :yellow
161
+ # Check for new migrations in gem
162
+ gem_migrations = get_gem_migrations
163
+ existing_migrations = get_user_migrations("db/rails_pulse_migrate")
164
+ new_migrations = gem_migrations - existing_migrations
165
+
166
+ if new_migrations.any?
167
+ say "Found #{new_migrations.size} new migration(s) to copy:", :blue
168
+ new_migrations.each do |migration|
169
+ say " - #{migration}", :blue
170
+ copy_gem_migration_to(migration, "db/rails_pulse_migrate")
171
+ end
172
+
173
+ say "\nMigrations copied successfully!", :green
174
+ say "\nNext steps:", :green
175
+ say "1. Run migrations for the rails_pulse database:"
176
+ say " rails db:migrate (will run migrations for all databases)"
177
+ say " OR manually run the migration files in db/rails_pulse_migrate/"
178
+ say "2. Restart your Rails server"
179
+ else
180
+ # Fall back to detecting missing columns
181
+ missing_columns = detect_missing_columns
182
+
183
+ if missing_columns.empty?
184
+ say "Rails Pulse is up to date! No migrations needed.", :green
185
+ else
186
+ # Format missing columns by table for the template
187
+ missing_by_table = format_missing_columns_by_table(missing_columns)
188
+
189
+ say "Creating upgrade migration for missing columns: #{missing_columns.keys.join(', ')}", :blue
190
+
191
+ # Set instance variables for template
192
+ @migration_version = ActiveRecord::Migration.current_version
193
+ @missing_columns = missing_by_table
194
+
195
+ migration_template(
196
+ "migrations/upgrade_rails_pulse_tables.rb",
197
+ "db/rails_pulse_migrate/upgrade_rails_pulse_tables.rb"
198
+ )
199
+
200
+ say <<~MESSAGE
201
+
202
+ Upgrade migration created successfully!
203
+
204
+ Next steps:
205
+ 1. Run migrations for the rails_pulse database:
206
+ rails db:migrate (will run migrations for all databases)
207
+ OR manually run the migration files in db/rails_pulse_migrate/
208
+ 2. Restart your Rails server
209
+
210
+ This migration will add: #{missing_columns.keys.join(', ')}
211
+
212
+ MESSAGE
213
+ end
214
+ end
125
215
  end
126
216
 
127
217
  def offer_conversion_to_migrations
@@ -132,7 +222,8 @@ module RailsPulse
132
222
  To convert to single database setup:
133
223
  1. Run: rails generate rails_pulse:convert_to_migrations
134
224
  2. Run: rails db:migrate
135
- 3. Delete: db/rails_pulse_schema.rb
225
+
226
+ The schema file db/rails_pulse_schema.rb will remain as your single source of truth.
136
227
 
137
228
  MESSAGE
138
229
  end
@@ -164,7 +255,8 @@ module RailsPulse
164
255
  end
165
256
 
166
257
  def get_expected_schema_from_file
167
- schema_file = File.join(Rails.root, "db/rails_pulse_schema.rb")
258
+ root_path = respond_to?(:destination_root) ? destination_root : Rails.root
259
+ schema_file = File.join(root_path, "db/rails_pulse_schema.rb")
168
260
  return {} unless File.exist?(schema_file)
169
261
 
170
262
  schema_content = File.read(schema_file)
@@ -220,6 +312,31 @@ module RailsPulse
220
312
 
221
313
  missing_by_table
222
314
  end
315
+
316
+ def get_gem_migrations
317
+ gem_migrations_path = File.expand_path("../../../db/rails_pulse_migrate", __dir__)
318
+ return [] unless File.directory?(gem_migrations_path)
319
+
320
+ Dir.glob("#{gem_migrations_path}/*.rb").map { |f| File.basename(f) }
321
+ end
322
+
323
+ def get_user_migrations(directory)
324
+ # Use destination_root in tests, Rails.root in production
325
+ root_path = respond_to?(:destination_root) ? destination_root : Rails.root
326
+ full_directory = File.join(root_path, directory)
327
+
328
+ return [] unless File.directory?(full_directory)
329
+
330
+ Dir.glob("#{full_directory}/*.rb").map { |f| File.basename(f) }
331
+ end
332
+
333
+ def copy_gem_migration_to(migration_name, destination)
334
+ gem_migrations_path = File.expand_path("../../../db/rails_pulse_migrate", __dir__)
335
+ source_file = File.join(gem_migrations_path, migration_name)
336
+ destination_file = File.join(destination, migration_name)
337
+
338
+ copy_file source_file, destination_file
339
+ end
223
340
  end
224
341
  end
225
342
  end
@@ -16,7 +16,8 @@ module RailsPulse
16
16
  :connects_to,
17
17
  :authentication_enabled,
18
18
  :authentication_method,
19
- :authentication_redirect_path
19
+ :authentication_redirect_path,
20
+ :tags
20
21
 
21
22
  def initialize
22
23
  @enabled = true
@@ -41,6 +42,7 @@ module RailsPulse
41
42
  @authentication_enabled = Rails.env.production?
42
43
  @authentication_method = nil
43
44
  @authentication_redirect_path = "/"
45
+ @tags = [ "ignored", "critical", "experimental" ]
44
46
 
45
47
  validate_configuration!
46
48
  end
@@ -64,6 +66,7 @@ module RailsPulse
64
66
  validate_patterns!
65
67
  validate_database_settings!
66
68
  validate_authentication_settings!
69
+ validate_tags!
67
70
  end
68
71
 
69
72
  # Revalidate configuration after changes
@@ -135,6 +138,18 @@ module RailsPulse
135
138
  end
136
139
  end
137
140
 
141
+ def validate_tags!
142
+ unless @tags.is_a?(Array)
143
+ raise ArgumentError, "tags must be an array, got #{@tags.class}"
144
+ end
145
+
146
+ @tags.each do |tag|
147
+ unless tag.is_a?(String)
148
+ raise ArgumentError, "tags must be strings, got #{tag.class}"
149
+ end
150
+ end
151
+ end
152
+
138
153
  # Default patterns for common asset types and paths
139
154
  def default_asset_patterns
140
155
  [
@@ -66,6 +66,27 @@ module RailsPulse
66
66
  # Instead, we explicitly use time_zone: "UTC" in all groupdate calls
67
67
  end
68
68
 
69
+ initializer "rails_pulse.disable_turbo" do
70
+ # Disable Turbo navigation globally for Rails Pulse to avoid CSP issues with charts
71
+ # This ensures all navigation within Rails Pulse uses full page refreshes
72
+ ActiveSupport.on_load(:action_view) do
73
+ ActionView::Helpers::UrlHelper.module_eval do
74
+ alias_method :original_link_to, :link_to
75
+
76
+ def link_to(*args, &block)
77
+ # Only modify links within Rails Pulse namespace
78
+ if respond_to?(:controller) && controller.class.name.start_with?("RailsPulse::")
79
+ options = args.extract_options!
80
+ options[:data] ||= {}
81
+ options[:data][:turbo] = false unless options[:data].key?(:turbo)
82
+ args << options
83
+ end
84
+ original_link_to(*args, &block)
85
+ end
86
+ end
87
+ end
88
+ end
89
+
69
90
  # CSP helper methods
70
91
  def self.csp_sources
71
92
  {
@@ -1,3 +1,3 @@
1
1
  module RailsPulse
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.2"
3
3
  end
@@ -1,44 +1,45 @@
1
1
  // Rails Pulse Icons Bundle - Auto-generated
2
- // Contains 33 SVG icons for Rails Pulse
2
+ // Contains 34 SVG icons for Rails Pulse
3
3
 
4
4
  (function() {
5
5
  'use strict';
6
6
 
7
7
  // Icon data
8
8
  const icons = {
9
- "menu": "<line x1=\"4\" x2=\"20\" y1=\"12\" y2=\"12\" /><line x1=\"4\" x2=\"20\" y1=\"6\" y2=\"6\" /><line x1=\"4\" x2=\"20\" y1=\"18\" y2=\"18\" />",
9
+ "menu": "<path d=\"M4 5h16\" /><path d=\"M4 12h16\" /><path d=\"M4 19h16\" />",
10
10
  "sun": "<circle cx=\"12\" cy=\"12\" r=\"4\" /><path d=\"M12 2v2\" /><path d=\"M12 20v2\" /><path d=\"m4.93 4.93 1.41 1.41\" /><path d=\"m17.66 17.66 1.41 1.41\" /><path d=\"M2 12h2\" /><path d=\"M20 12h2\" /><path d=\"m6.34 17.66-1.41 1.41\" /><path d=\"m19.07 4.93-1.41 1.41\" />",
11
- "moon": "<path d=\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\" />",
11
+ "moon": "<path d=\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\" />",
12
12
  "chevron-right": "<path d=\"m9 18 6-6-6-6\" />",
13
13
  "chevron-left": "<path d=\"m15 18-6-6 6-6\" />",
14
14
  "chevron-down": "<path d=\"m6 9 6 6 6-6\" />",
15
15
  "chevron-up": "<path d=\"m18 15-6-6-6 6\" />",
16
16
  "chevrons-left": "<path d=\"m11 17-5-5 5-5\" /><path d=\"m18 17-5-5 5-5\" />",
17
17
  "chevrons-right": "<path d=\"m6 17 5-5-5-5\" /><path d=\"m13 17 5-5-5-5\" />",
18
- "loader-circle": "<line x1=\"12\" x2=\"12\" y1=\"2\" y2=\"6\" /><line x1=\"12\" x2=\"12\" y1=\"18\" y2=\"22\" /><line x1=\"4.93\" x2=\"7.76\" y1=\"4.93\" y2=\"7.76\" /><line x1=\"16.24\" x2=\"19.07\" y1=\"16.24\" y2=\"19.07\" /><line x1=\"2\" x2=\"6\" y1=\"12\" y2=\"12\" /><line x1=\"18\" x2=\"22\" y1=\"12\" y2=\"12\" /><line x1=\"4.93\" x2=\"7.76\" y1=\"19.07\" y2=\"16.24\" /><line x1=\"16.24\" x2=\"19.07\" y1=\"7.76\" y2=\"4.93\" />",
19
- "search": "<circle cx=\"11\" cy=\"11\" r=\"8\" /><path d=\"m21 21-4.3-4.3\" />",
20
- "filter": "<polygon points=\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\" />",
18
+ "loader-circle": "<path d=\"M12 2v4\" /><path d=\"m16.2 7.8 2.9-2.9\" /><path d=\"M18 12h4\" /><path d=\"m16.2 16.2 2.9 2.9\" /><path d=\"M12 18v4\" /><path d=\"m4.9 19.1 2.9-2.9\" /><path d=\"M2 12h4\" /><path d=\"m4.9 4.9 2.9 2.9\" />",
19
+ "search": "<path d=\"m21 21-4.34-4.34\" /><circle cx=\"11\" cy=\"11\" r=\"8\" />",
20
+ "list-filter": "<path d=\"M2 5h20\" /><path d=\"M6 12h12\" /><path d=\"M9 19h6\" />",
21
+ "list-filter-plus": "<path d=\"M12 5H2\" /><path d=\"M6 12h12\" /><path d=\"M9 19h6\" /><path d=\"M16 5h6\" /><path d=\"M19 8V2\" />",
21
22
  "x": "<path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />",
22
23
  "x-circle": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"m15 9-6 6\" /><path d=\"m9 9 6 6\" />",
23
24
  "check": "<path d=\"M20 6 9 17l-5-5\" />",
24
25
  "alert-circle": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\" /><line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\" />",
25
- "alert-triangle": "<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\" /><path d=\"M12 9v4\" /><path d=\"M12 17h.01\" />",
26
+ "alert-triangle": "<path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" /><path d=\"M12 9v4\" /><path d=\"M12 17h.01\" />",
26
27
  "info": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><path d=\"M12 16v-4\" /><path d=\"M12 8h.01\" />",
27
- "external-link": "<path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" /><polyline points=\"15 3 21 3 21 9\" /><line x1=\"10\" x2=\"21\" y1=\"14\" y2=\"3\" />",
28
- "download": "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" /><polyline points=\"7 10 12 15 17 10\" /><line x1=\"12\" x2=\"12\" y1=\"15\" y2=\"3\" />",
28
+ "external-link": "<path d=\"M15 3h6v6\" /><path d=\"M10 14 21 3\" /><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\" />",
29
+ "download": "<path d=\"M12 15V3\" /><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" /><path d=\"m7 10 5 5 5-5\" />",
29
30
  "refresh-cw": "<path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\" /><path d=\"M21 3v5h-5\" /><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\" /><path d=\"M8 16H3v5\" />",
30
- "clock": "<circle cx=\"12\" cy=\"12\" r=\"10\" /><polyline points=\"12 6 12 12 16 14\" />",
31
+ "clock": "<path d=\"M12 6v6l4 2\" /><circle cx=\"12\" cy=\"12\" r=\"10\" />",
31
32
  "database": "<ellipse cx=\"12\" cy=\"5\" rx=\"9\" ry=\"3\" /><path d=\"M3 5V19A9 3 0 0 0 21 19V5\" /><path d=\"M3 12A9 3 0 0 0 21 12\" />",
32
33
  "server": "<rect width=\"20\" height=\"8\" x=\"2\" y=\"2\" rx=\"2\" ry=\"2\" /><rect width=\"20\" height=\"8\" x=\"2\" y=\"14\" rx=\"2\" ry=\"2\" /><line x1=\"6\" x2=\"6.01\" y1=\"6\" y2=\"6\" /><line x1=\"6\" x2=\"6.01\" y1=\"18\" y2=\"18\" />",
33
- "activity": "<path d=\"M22 12h-4l-3 9L9 3l-3 9H2\" />",
34
+ "activity": "<path d=\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\" />",
34
35
  "layout-dashboard": "<rect width=\"7\" height=\"9\" x=\"3\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"5\" x=\"14\" y=\"3\" rx=\"1\" /><rect width=\"7\" height=\"9\" x=\"14\" y=\"12\" rx=\"1\" /><rect width=\"7\" height=\"5\" x=\"3\" y=\"16\" rx=\"1\" />",
35
36
  "audio-lines": "<path d=\"M2 10v3\" /><path d=\"M6 6v11\" /><path d=\"M10 3v18\" /><path d=\"M14 8v7\" /><path d=\"M18 5v13\" /><path d=\"M22 10v3\" />",
36
- "message-circle-question": "<path d=\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\" /><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\" /><path d=\"M12 17h.01\" />",
37
+ "message-circle-question": "<path d=\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\" /><path d=\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\" /><path d=\"M12 17h.01\" />",
37
38
  "route": "<circle cx=\"6\" cy=\"19\" r=\"3\" /><path d=\"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\" /><circle cx=\"18\" cy=\"5\" r=\"3\" />",
38
- "trending-up": "<polyline points=\"22 7 13.5 15.5 8.5 10.5 2 17\" /><polyline points=\"16 7 22 7 22 13\" />",
39
- "trending-down": "<polyline points=\"22 17 13.5 8.5 8.5 13.5 2 7\" /><polyline points=\"16 17 22 17 22 11\" />",
39
+ "trending-up": "<path d=\"M16 7h6v6\" /><path d=\"m22 7-8.5 8.5-5-5L2 17\" />",
40
+ "trending-down": "<path d=\"M16 17h6v-6\" /><path d=\"m22 17-8.5-8.5-5 5L2 7\" />",
40
41
  "move-right": "<path d=\"M18 8L22 12L18 16\" /><path d=\"M2 12H22\" />",
41
- "eye": "<path d=\"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />"
42
+ "eye": "<path d=\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\" /><circle cx=\"12\" cy=\"12\" r=\"3\" />"
42
43
  };
43
44
 
44
45
  // Global icon registry
@@ -8,6 +8,6 @@
8
8
  "names": [],
9
9
  "mappings": "",
10
10
  "sourcesContent": [
11
- "// Rails Pulse Icons Bundle - Auto-generated\n// Contains 33 SVG icons for Rails Pulse\n\n(function() {\n 'use strict';\n\n // Icon data\n const icons = {\n \"menu\": \"<line x1=\\\"4\\\" x2=\\\"20\\\" y1=\\\"12\\\" y2=\\\"12\\\" /><line x1=\\\"4\\\" x2=\\\"20\\\" y1=\\\"6\\\" y2=\\\"6\\\" /><line x1=\\\"4\\\" x2=\\\"20\\\" y1=\\\"18\\\" y2=\\\"18\\\" />\",\n \"sun\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"4\\\" /><path d=\\\"M12 2v2\\\" /><path d=\\\"M12 20v2\\\" /><path d=\\\"m4.93 4.93 1.41 1.41\\\" /><path d=\\\"m17.66 17.66 1.41 1.41\\\" /><path d=\\\"M2 12h2\\\" /><path d=\\\"M20 12h2\\\" /><path d=\\\"m6.34 17.66-1.41 1.41\\\" /><path d=\\\"m19.07 4.93-1.41 1.41\\\" />\",\n \"moon\": \"<path d=\\\"M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z\\\" />\",\n \"chevron-right\": \"<path d=\\\"m9 18 6-6-6-6\\\" />\",\n \"chevron-left\": \"<path d=\\\"m15 18-6-6 6-6\\\" />\",\n \"chevron-down\": \"<path d=\\\"m6 9 6 6 6-6\\\" />\",\n \"chevron-up\": \"<path d=\\\"m18 15-6-6-6 6\\\" />\",\n \"chevrons-left\": \"<path d=\\\"m11 17-5-5 5-5\\\" /><path d=\\\"m18 17-5-5 5-5\\\" />\",\n \"chevrons-right\": \"<path d=\\\"m6 17 5-5-5-5\\\" /><path d=\\\"m13 17 5-5-5-5\\\" />\",\n \"loader-circle\": \"<line x1=\\\"12\\\" x2=\\\"12\\\" y1=\\\"2\\\" y2=\\\"6\\\" /><line x1=\\\"12\\\" x2=\\\"12\\\" y1=\\\"18\\\" y2=\\\"22\\\" /><line x1=\\\"4.93\\\" x2=\\\"7.76\\\" y1=\\\"4.93\\\" y2=\\\"7.76\\\" /><line x1=\\\"16.24\\\" x2=\\\"19.07\\\" y1=\\\"16.24\\\" y2=\\\"19.07\\\" /><line x1=\\\"2\\\" x2=\\\"6\\\" y1=\\\"12\\\" y2=\\\"12\\\" /><line x1=\\\"18\\\" x2=\\\"22\\\" y1=\\\"12\\\" y2=\\\"12\\\" /><line x1=\\\"4.93\\\" x2=\\\"7.76\\\" y1=\\\"19.07\\\" y2=\\\"16.24\\\" /><line x1=\\\"16.24\\\" x2=\\\"19.07\\\" y1=\\\"7.76\\\" y2=\\\"4.93\\\" />\",\n \"search\": \"<circle cx=\\\"11\\\" cy=\\\"11\\\" r=\\\"8\\\" /><path d=\\\"m21 21-4.3-4.3\\\" />\",\n \"filter\": \"<polygon points=\\\"22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3\\\" />\",\n \"x\": \"<path d=\\\"M18 6 6 18\\\" /><path d=\\\"m6 6 12 12\\\" />\",\n \"x-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"m15 9-6 6\\\" /><path d=\\\"m9 9 6 6\\\" />\",\n \"check\": \"<path d=\\\"M20 6 9 17l-5-5\\\" />\",\n \"alert-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><line x1=\\\"12\\\" x2=\\\"12\\\" y1=\\\"8\\\" y2=\\\"12\\\" /><line x1=\\\"12\\\" x2=\\\"12.01\\\" y1=\\\"16\\\" y2=\\\"16\\\" />\",\n \"alert-triangle\": \"<path d=\\\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z\\\" /><path d=\\\"M12 9v4\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"info\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"M12 16v-4\\\" /><path d=\\\"M12 8h.01\\\" />\",\n \"external-link\": \"<path d=\\\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\\\" /><polyline points=\\\"15 3 21 3 21 9\\\" /><line x1=\\\"10\\\" x2=\\\"21\\\" y1=\\\"14\\\" y2=\\\"3\\\" />\",\n \"download\": \"<path d=\\\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\\\" /><polyline points=\\\"7 10 12 15 17 10\\\" /><line x1=\\\"12\\\" x2=\\\"12\\\" y1=\\\"15\\\" y2=\\\"3\\\" />\",\n \"refresh-cw\": \"<path d=\\\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\\\" /><path d=\\\"M21 3v5h-5\\\" /><path d=\\\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\\\" /><path d=\\\"M8 16H3v5\\\" />\",\n \"clock\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><polyline points=\\\"12 6 12 12 16 14\\\" />\",\n \"database\": \"<ellipse cx=\\\"12\\\" cy=\\\"5\\\" rx=\\\"9\\\" ry=\\\"3\\\" /><path d=\\\"M3 5V19A9 3 0 0 0 21 19V5\\\" /><path d=\\\"M3 12A9 3 0 0 0 21 12\\\" />\",\n \"server\": \"<rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"2\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"14\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"6\\\" y2=\\\"6\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"18\\\" y2=\\\"18\\\" />\",\n \"activity\": \"<path d=\\\"M22 12h-4l-3 9L9 3l-3 9H2\\\" />\",\n \"layout-dashboard\": \"<rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"3\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"14\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"14\\\" y=\\\"12\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"3\\\" y=\\\"16\\\" rx=\\\"1\\\" />\",\n \"audio-lines\": \"<path d=\\\"M2 10v3\\\" /><path d=\\\"M6 6v11\\\" /><path d=\\\"M10 3v18\\\" /><path d=\\\"M14 8v7\\\" /><path d=\\\"M18 5v13\\\" /><path d=\\\"M22 10v3\\\" />\",\n \"message-circle-question\": \"<path d=\\\"M7.9 20A9 9 0 1 0 4 16.1L2 22Z\\\" /><path d=\\\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"route\": \"<circle cx=\\\"6\\\" cy=\\\"19\\\" r=\\\"3\\\" /><path d=\\\"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\\\" /><circle cx=\\\"18\\\" cy=\\\"5\\\" r=\\\"3\\\" />\",\n \"trending-up\": \"<polyline points=\\\"22 7 13.5 15.5 8.5 10.5 2 17\\\" /><polyline points=\\\"16 7 22 7 22 13\\\" />\",\n \"trending-down\": \"<polyline points=\\\"22 17 13.5 8.5 8.5 13.5 2 7\\\" /><polyline points=\\\"16 17 22 17 22 11\\\" />\",\n \"move-right\": \"<path d=\\\"M18 8L22 12L18 16\\\" /><path d=\\\"M2 12H22\\\" />\",\n \"eye\": \"<path d=\\\"M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"3\\\" />\"\n};\n\n // Global icon registry\n window.RailsPulseIcons = {\n icons: icons,\n\n // Get icon SVG content\n get: function(name) {\n return icons[name] || null;\n },\n\n // Check if icon exists\n has: function(name) {\n return name in icons;\n },\n\n // Get all icon names\n list: function() {\n return Object.keys(icons);\n },\n\n // Render icon to element (CSP-safe)\n render: function(name, element, options = {}) {\n const svgContent = this.get(name);\n if (!svgContent || !element) return false;\n\n // Create SVG element\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', options.width || '24');\n svg.setAttribute('height', options.height || '24');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n svg.setAttribute('stroke-width', '2');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n\n // Add icon content\n svg.innerHTML = svgContent;\n\n // Replace element content\n element.innerHTML = '';\n element.appendChild(svg);\n\n return true;\n }\n };\n})();\n"
11
+ "// Rails Pulse Icons Bundle - Auto-generated\n// Contains 34 SVG icons for Rails Pulse\n\n(function() {\n 'use strict';\n\n // Icon data\n const icons = {\n \"menu\": \"<path d=\\\"M4 5h16\\\" /><path d=\\\"M4 12h16\\\" /><path d=\\\"M4 19h16\\\" />\",\n \"sun\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"4\\\" /><path d=\\\"M12 2v2\\\" /><path d=\\\"M12 20v2\\\" /><path d=\\\"m4.93 4.93 1.41 1.41\\\" /><path d=\\\"m17.66 17.66 1.41 1.41\\\" /><path d=\\\"M2 12h2\\\" /><path d=\\\"M20 12h2\\\" /><path d=\\\"m6.34 17.66-1.41 1.41\\\" /><path d=\\\"m19.07 4.93-1.41 1.41\\\" />\",\n \"moon\": \"<path d=\\\"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\\\" />\",\n \"chevron-right\": \"<path d=\\\"m9 18 6-6-6-6\\\" />\",\n \"chevron-left\": \"<path d=\\\"m15 18-6-6 6-6\\\" />\",\n \"chevron-down\": \"<path d=\\\"m6 9 6 6 6-6\\\" />\",\n \"chevron-up\": \"<path d=\\\"m18 15-6-6-6 6\\\" />\",\n \"chevrons-left\": \"<path d=\\\"m11 17-5-5 5-5\\\" /><path d=\\\"m18 17-5-5 5-5\\\" />\",\n \"chevrons-right\": \"<path d=\\\"m6 17 5-5-5-5\\\" /><path d=\\\"m13 17 5-5-5-5\\\" />\",\n \"loader-circle\": \"<path d=\\\"M12 2v4\\\" /><path d=\\\"m16.2 7.8 2.9-2.9\\\" /><path d=\\\"M18 12h4\\\" /><path d=\\\"m16.2 16.2 2.9 2.9\\\" /><path d=\\\"M12 18v4\\\" /><path d=\\\"m4.9 19.1 2.9-2.9\\\" /><path d=\\\"M2 12h4\\\" /><path d=\\\"m4.9 4.9 2.9 2.9\\\" />\",\n \"search\": \"<path d=\\\"m21 21-4.34-4.34\\\" /><circle cx=\\\"11\\\" cy=\\\"11\\\" r=\\\"8\\\" />\",\n \"list-filter\": \"<path d=\\\"M2 5h20\\\" /><path d=\\\"M6 12h12\\\" /><path d=\\\"M9 19h6\\\" />\",\n \"list-filter-plus\": \"<path d=\\\"M12 5H2\\\" /><path d=\\\"M6 12h12\\\" /><path d=\\\"M9 19h6\\\" /><path d=\\\"M16 5h6\\\" /><path d=\\\"M19 8V2\\\" />\",\n \"x\": \"<path d=\\\"M18 6 6 18\\\" /><path d=\\\"m6 6 12 12\\\" />\",\n \"x-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"m15 9-6 6\\\" /><path d=\\\"m9 9 6 6\\\" />\",\n \"check\": \"<path d=\\\"M20 6 9 17l-5-5\\\" />\",\n \"alert-circle\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><line x1=\\\"12\\\" x2=\\\"12\\\" y1=\\\"8\\\" y2=\\\"12\\\" /><line x1=\\\"12\\\" x2=\\\"12.01\\\" y1=\\\"16\\\" y2=\\\"16\\\" />\",\n \"alert-triangle\": \"<path d=\\\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\\\" /><path d=\\\"M12 9v4\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"info\": \"<circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" /><path d=\\\"M12 16v-4\\\" /><path d=\\\"M12 8h.01\\\" />\",\n \"external-link\": \"<path d=\\\"M15 3h6v6\\\" /><path d=\\\"M10 14 21 3\\\" /><path d=\\\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6\\\" />\",\n \"download\": \"<path d=\\\"M12 15V3\\\" /><path d=\\\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\\\" /><path d=\\\"m7 10 5 5 5-5\\\" />\",\n \"refresh-cw\": \"<path d=\\\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\\\" /><path d=\\\"M21 3v5h-5\\\" /><path d=\\\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\\\" /><path d=\\\"M8 16H3v5\\\" />\",\n \"clock\": \"<path d=\\\"M12 6v6l4 2\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"10\\\" />\",\n \"database\": \"<ellipse cx=\\\"12\\\" cy=\\\"5\\\" rx=\\\"9\\\" ry=\\\"3\\\" /><path d=\\\"M3 5V19A9 3 0 0 0 21 19V5\\\" /><path d=\\\"M3 12A9 3 0 0 0 21 12\\\" />\",\n \"server\": \"<rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"2\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><rect width=\\\"20\\\" height=\\\"8\\\" x=\\\"2\\\" y=\\\"14\\\" rx=\\\"2\\\" ry=\\\"2\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"6\\\" y2=\\\"6\\\" /><line x1=\\\"6\\\" x2=\\\"6.01\\\" y1=\\\"18\\\" y2=\\\"18\\\" />\",\n \"activity\": \"<path d=\\\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\\\" />\",\n \"layout-dashboard\": \"<rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"3\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"14\\\" y=\\\"3\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"9\\\" x=\\\"14\\\" y=\\\"12\\\" rx=\\\"1\\\" /><rect width=\\\"7\\\" height=\\\"5\\\" x=\\\"3\\\" y=\\\"16\\\" rx=\\\"1\\\" />\",\n \"audio-lines\": \"<path d=\\\"M2 10v3\\\" /><path d=\\\"M6 6v11\\\" /><path d=\\\"M10 3v18\\\" /><path d=\\\"M14 8v7\\\" /><path d=\\\"M18 5v13\\\" /><path d=\\\"M22 10v3\\\" />\",\n \"message-circle-question\": \"<path d=\\\"M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719\\\" /><path d=\\\"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3\\\" /><path d=\\\"M12 17h.01\\\" />\",\n \"route\": \"<circle cx=\\\"6\\\" cy=\\\"19\\\" r=\\\"3\\\" /><path d=\\\"M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15\\\" /><circle cx=\\\"18\\\" cy=\\\"5\\\" r=\\\"3\\\" />\",\n \"trending-up\": \"<path d=\\\"M16 7h6v6\\\" /><path d=\\\"m22 7-8.5 8.5-5-5L2 17\\\" />\",\n \"trending-down\": \"<path d=\\\"M16 17h6v-6\\\" /><path d=\\\"m22 17-8.5-8.5-5 5L2 7\\\" />\",\n \"move-right\": \"<path d=\\\"M18 8L22 12L18 16\\\" /><path d=\\\"M2 12H22\\\" />\",\n \"eye\": \"<path d=\\\"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0\\\" /><circle cx=\\\"12\\\" cy=\\\"12\\\" r=\\\"3\\\" />\"\n};\n\n // Global icon registry\n window.RailsPulseIcons = {\n icons: icons,\n\n // Get icon SVG content\n get: function(name) {\n return icons[name] || null;\n },\n\n // Check if icon exists\n has: function(name) {\n return name in icons;\n },\n\n // Get all icon names\n list: function() {\n return Object.keys(icons);\n },\n\n // Render icon to element (CSP-safe)\n render: function(name, element, options = {}) {\n const svgContent = this.get(name);\n if (!svgContent || !element) return false;\n\n // Create SVG element\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', options.width || '24');\n svg.setAttribute('height', options.height || '24');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n svg.setAttribute('stroke-width', '2');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n\n // Add icon content\n svg.innerHTML = svgContent;\n\n // Replace element content\n element.innerHTML = '';\n element.appendChild(svg);\n\n return true;\n }\n };\n})();\n"
12
12
  ]
13
13
  }