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.
- checksums.yaml +4 -4
- data/README.md +134 -16
- data/Rakefile +315 -83
- data/app/assets/images/rails_pulse/rails-pulse-logo.png +0 -0
- data/app/assets/stylesheets/rails_pulse/components/datepicker.css +191 -0
- data/app/assets/stylesheets/rails_pulse/components/switch.css +36 -0
- data/app/assets/stylesheets/rails_pulse/components/tags.css +98 -0
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +26 -0
- data/app/controllers/concerns/response_range_concern.rb +15 -2
- data/app/controllers/concerns/tag_filter_concern.rb +26 -0
- data/app/controllers/concerns/time_range_concern.rb +27 -8
- data/app/controllers/rails_pulse/application_controller.rb +73 -0
- data/app/controllers/rails_pulse/queries_controller.rb +18 -21
- data/app/controllers/rails_pulse/requests_controller.rb +80 -35
- data/app/controllers/rails_pulse/routes_controller.rb +4 -2
- data/app/controllers/rails_pulse/tags_controller.rb +51 -0
- data/app/helpers/rails_pulse/application_helper.rb +2 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +1 -1
- data/app/helpers/rails_pulse/chart_helper.rb +1 -1
- data/app/helpers/rails_pulse/form_helper.rb +75 -0
- data/app/helpers/rails_pulse/formatting_helper.rb +21 -2
- data/app/helpers/rails_pulse/tags_helper.rb +29 -0
- data/app/javascript/rails_pulse/application.js +6 -0
- data/app/javascript/rails_pulse/controllers/custom_range_controller.js +115 -0
- data/app/javascript/rails_pulse/controllers/datepicker_controller.js +48 -0
- data/app/javascript/rails_pulse/controllers/global_filters_controller.js +110 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +11 -3
- data/app/models/concerns/taggable.rb +61 -0
- data/app/models/rails_pulse/dashboard/tables/slow_queries.rb +1 -1
- data/app/models/rails_pulse/dashboard/tables/slow_routes.rb +1 -1
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +56 -17
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +1 -1
- data/app/models/rails_pulse/queries/charts/average_query_times.rb +3 -7
- data/app/models/rails_pulse/queries/tables/index.rb +10 -2
- data/app/models/rails_pulse/query.rb +2 -0
- data/app/models/rails_pulse/request.rb +10 -2
- data/app/models/rails_pulse/requests/charts/average_response_times.rb +2 -2
- data/app/models/rails_pulse/requests/tables/index.rb +77 -0
- data/app/models/rails_pulse/route.rb +2 -0
- data/app/models/rails_pulse/routes/cards/average_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/error_rate_per_route.rb +1 -1
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +1 -1
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +16 -5
- data/app/models/rails_pulse/routes/tables/index.rb +14 -4
- data/app/models/rails_pulse/summary.rb +7 -7
- data/app/services/rails_pulse/analysis/query_characteristics_analyzer.rb +11 -3
- data/app/services/rails_pulse/summary_service.rb +2 -0
- data/app/views/layouts/rails_pulse/_global_filters.html.erb +84 -0
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +5 -5
- data/app/views/layouts/rails_pulse/application.html.erb +8 -5
- data/app/views/rails_pulse/components/_metric_card.html.erb +2 -2
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- data/app/views/rails_pulse/components/_page_header.html.erb +20 -0
- data/app/views/rails_pulse/components/_sparkline_stats.html.erb +1 -1
- data/app/views/rails_pulse/dashboard/index.html.erb +1 -1
- data/app/views/rails_pulse/operations/show.html.erb +1 -1
- data/app/views/rails_pulse/queries/_analysis_results.html.erb +53 -23
- data/app/views/rails_pulse/queries/_show_table.html.erb +33 -5
- data/app/views/rails_pulse/queries/_table.html.erb +4 -6
- data/app/views/rails_pulse/queries/index.html.erb +3 -7
- data/app/views/rails_pulse/queries/show.html.erb +3 -7
- data/app/views/rails_pulse/requests/_table.html.erb +32 -19
- data/app/views/rails_pulse/requests/index.html.erb +45 -55
- data/app/views/rails_pulse/requests/show.html.erb +1 -3
- data/app/views/rails_pulse/routes/_requests_table.html.erb +41 -0
- data/app/views/rails_pulse/routes/_table.html.erb +4 -8
- data/app/views/rails_pulse/routes/index.html.erb +4 -8
- data/app/views/rails_pulse/routes/show.html.erb +6 -12
- data/app/views/rails_pulse/tags/_tag_manager.html.erb +73 -0
- data/config/initializers/rails_charts_csp_patch.rb +32 -40
- data/config/routes.rb +5 -0
- data/db/migrate/20250930105043_install_rails_pulse_tables.rb +23 -0
- data/db/rails_pulse_schema.rb +4 -1
- data/lib/generators/rails_pulse/convert_to_migrations_generator.rb +25 -9
- data/lib/generators/rails_pulse/install_generator.rb +30 -7
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +75 -2
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_tables.rb +3 -2
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +21 -0
- data/lib/generators/rails_pulse/upgrade_generator.rb +147 -30
- data/lib/rails_pulse/configuration.rb +16 -1
- data/lib/rails_pulse/engine.rb +21 -0
- data/lib/rails_pulse/version.rb +1 -1
- data/public/rails-pulse-assets/rails-pulse-icons.js +16 -15
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.css.map +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +73 -69
- data/public/rails-pulse-assets/rails-pulse.js.map +4 -4
- metadata +20 -5
- data/app/views/rails_pulse/components/_breadcrumbs.html.erb +0 -12
- data/db/migrate/20241222000001_create_rails_pulse_summaries.rb +0 -54
- 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
|
-
|
|
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[<%=
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
say "
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
127
|
+
if missing_columns.empty?
|
|
128
|
+
say "Rails Pulse is up to date! No migration needed.", :green
|
|
129
|
+
return
|
|
130
|
+
end
|
|
96
131
|
|
|
97
|
-
|
|
132
|
+
# Format missing columns by table for the template
|
|
133
|
+
missing_by_table = format_missing_columns_by_table(missing_columns)
|
|
98
134
|
|
|
99
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
137
|
+
# Set instance variables for template
|
|
138
|
+
@migration_version = ActiveRecord::Migration.current_version
|
|
139
|
+
@missing_columns = missing_by_table
|
|
107
140
|
|
|
108
|
-
|
|
141
|
+
migration_template(
|
|
142
|
+
"migrations/upgrade_rails_pulse_tables.rb",
|
|
143
|
+
"db/migrate/upgrade_rails_pulse_tables.rb"
|
|
144
|
+
)
|
|
109
145
|
|
|
110
|
-
|
|
146
|
+
say <<~MESSAGE
|
|
111
147
|
|
|
112
|
-
|
|
113
|
-
1. Run: rails db:migrate
|
|
114
|
-
2. Restart your Rails server
|
|
148
|
+
Upgrade migration created successfully!
|
|
115
149
|
|
|
116
|
-
|
|
150
|
+
Next steps:
|
|
151
|
+
1. Run: rails db:migrate
|
|
152
|
+
2. Restart your Rails server
|
|
117
153
|
|
|
118
|
-
|
|
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
|
-
#
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
[
|
data/lib/rails_pulse/engine.rb
CHANGED
|
@@ -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
|
{
|
data/lib/rails_pulse/version.rb
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
1
|
// Rails Pulse Icons Bundle - Auto-generated
|
|
2
|
-
// Contains
|
|
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": "<
|
|
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=\"
|
|
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": "<
|
|
19
|
-
"search": "<circle cx=\"11\" cy=\"11\" r=\"8\"
|
|
20
|
-
"filter": "<
|
|
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-
|
|
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\"
|
|
28
|
-
"download": "<path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" /><
|
|
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\"
|
|
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-
|
|
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=\"
|
|
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": "<
|
|
39
|
-
"trending-down": "<
|
|
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
|
|
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
|
|
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
|
}
|