rails_pulse 0.3.3 → 0.4.0.pre.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/CHANGELOG.md +299 -0
- data/README.md +44 -3
- data/app/assets/stylesheets/rails_pulse/application.css +21 -32
- data/app/assets/stylesheets/rails_pulse/components/backtrace.css +238 -0
- data/app/assets/stylesheets/rails_pulse/components/badge.css +32 -0
- data/app/assets/stylesheets/rails_pulse/components/button.css +2 -36
- data/app/assets/stylesheets/rails_pulse/components/card.css +8 -11
- data/app/assets/stylesheets/rails_pulse/components/chart.css +16 -3
- data/app/assets/stylesheets/rails_pulse/components/csp_safe_positioning.css +5 -39
- data/app/assets/stylesheets/rails_pulse/components/dashboard.css +0 -52
- data/app/assets/stylesheets/rails_pulse/components/deployments.css +39 -0
- data/app/assets/stylesheets/rails_pulse/components/dialog.css +0 -5
- data/app/assets/stylesheets/rails_pulse/components/input.css +0 -14
- data/app/assets/stylesheets/rails_pulse/components/layouts.css +0 -20
- data/app/assets/stylesheets/rails_pulse/components/menu.css +0 -16
- data/app/assets/stylesheets/rails_pulse/components/sidebar_menu.css +0 -16
- data/app/assets/stylesheets/rails_pulse/components/storage.css +187 -0
- data/app/assets/stylesheets/rails_pulse/components/tags.css +0 -14
- data/app/assets/stylesheets/rails_pulse/components/time_range_selector.css +5 -5
- data/app/assets/stylesheets/rails_pulse/components/utilities.css +0 -17
- data/app/controllers/concerns/chart_table_concern.rb +15 -3
- data/app/controllers/concerns/ransack_params_concern.rb +18 -0
- data/app/controllers/concerns/response_range_concern.rb +1 -1
- data/app/controllers/concerns/time_range_concern.rb +28 -14
- data/app/controllers/rails_pulse/application_controller.rb +104 -31
- data/app/controllers/rails_pulse/dashboard_controller.rb +9 -0
- data/app/controllers/rails_pulse/deployments_controller.rb +26 -5
- data/app/controllers/rails_pulse/exception_occurrences_controller.rb +19 -0
- data/app/controllers/rails_pulse/exceptions_controller.rb +119 -0
- data/app/controllers/rails_pulse/job_runs_controller.rb +1 -1
- data/app/controllers/rails_pulse/requests_controller.rb +1 -1
- data/app/controllers/rails_pulse/routes_controller.rb +7 -1
- data/app/controllers/rails_pulse/storage_controller.rb +7 -0
- data/app/controllers/rails_pulse/tags_controller.rb +2 -2
- data/app/helpers/rails_pulse/application_helper.rb +1 -0
- data/app/helpers/rails_pulse/backtrace_helper.rb +85 -0
- data/app/helpers/rails_pulse/breadcrumbs_helper.rb +4 -1
- data/app/helpers/rails_pulse/chart_helper.rb +0 -3
- data/app/helpers/rails_pulse/route_helper.rb +8 -16
- data/app/javascript/rails_pulse/application.js +29 -8
- data/app/javascript/rails_pulse/controllers/clipboard_controller.js +58 -0
- data/app/javascript/rails_pulse/controllers/index_controller.js +16 -10
- data/app/javascript/rails_pulse/controllers/pagination_controller.js +6 -2
- data/app/javascript/rails_pulse/controllers/popover_controller.js +4 -0
- data/app/models/concerns/rails_pulse/taggable.rb +11 -6
- data/app/models/rails_pulse/cards/base.rb +53 -0
- data/app/models/rails_pulse/dashboard/health_summary.rb +52 -0
- data/app/models/rails_pulse/dashboard/needs_attention.rb +73 -1
- data/app/models/rails_pulse/dashboard/storage_status.rb +490 -0
- data/app/models/rails_pulse/deployment.rb +47 -1
- data/app/models/rails_pulse/exception_group.rb +37 -0
- data/app/models/rails_pulse/exception_occurrence.rb +27 -0
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +116 -0
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +49 -0
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +62 -0
- data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +56 -0
- data/app/models/rails_pulse/exceptions/raw_data.rb +216 -0
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -2
- data/app/models/rails_pulse/operation.rb +13 -4
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +8 -3
- data/app/models/rails_pulse/request.rb +5 -1
- data/app/models/rails_pulse/route.rb +71 -21
- data/app/models/rails_pulse/routes/cards/error_rates.rb +6 -3
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +8 -3
- data/app/models/rails_pulse/routes/tables/index.rb +8 -6
- data/app/models/rails_pulse/summary.rb +21 -10
- data/app/models/rails_pulse/tables/base.rb +4 -2
- data/app/services/rails_pulse/analysis/explain_plan_analyzer.rb +47 -12
- data/app/services/rails_pulse/exception_capture_service.rb +228 -0
- data/app/services/rails_pulse/exception_message_sanitizer.rb +152 -0
- data/app/services/rails_pulse/operations/change_point.rb +144 -0
- data/app/services/rails_pulse/operations/compare.rb +92 -0
- data/app/services/rails_pulse/operations/comparison.rb +126 -0
- data/app/services/rails_pulse/operations/metric.rb +63 -0
- data/app/services/rails_pulse/operations/series.rb +84 -0
- data/app/services/rails_pulse/operations/subject.rb +77 -0
- data/app/services/rails_pulse/route_controller_action_backfiller.rb +110 -0
- data/app/services/rails_pulse/route_merger.rb +99 -0
- data/app/services/rails_pulse/route_migrator.rb +139 -0
- data/app/services/rails_pulse/route_path_normalizer.rb +79 -0
- data/app/services/rails_pulse/route_recognizer.rb +47 -0
- data/app/services/rails_pulse/summary_service.rb +45 -0
- data/app/services/rails_pulse/tag_filter_service.rb +5 -3
- data/app/views/layouts/rails_pulse/_menu_items.html.erb +7 -0
- data/app/views/layouts/rails_pulse/_onboarding_banner.html.erb +8 -0
- data/app/views/layouts/rails_pulse/_setup_screen.html.erb +5 -5
- data/app/views/layouts/rails_pulse/application.html.erb +4 -4
- data/app/views/rails_pulse/components/_empty_state.html.erb +1 -1
- data/app/views/rails_pulse/components/_panel.html.erb +16 -6
- data/app/views/rails_pulse/components/_raw_data.html.erb +34 -0
- data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -8
- data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +64 -0
- data/app/views/rails_pulse/dashboard/_health_badge.html.erb +21 -10
- data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +67 -0
- data/app/views/rails_pulse/dashboard/index.html.erb +22 -1
- data/app/views/rails_pulse/deployments/_status_badge.html.erb +5 -0
- data/app/views/rails_pulse/deployments/_table.html.erb +29 -0
- data/app/views/rails_pulse/deployments/index.html.erb +15 -0
- data/app/views/rails_pulse/deployments/show.html.erb +62 -0
- data/app/views/rails_pulse/exception_occurrences/show.html.erb +45 -0
- data/app/views/rails_pulse/exceptions/_backtrace.html.erb +33 -0
- data/app/views/rails_pulse/exceptions/_metric_cards.html.erb +6 -0
- data/app/views/rails_pulse/exceptions/_occurrences_table.html.erb +23 -0
- data/app/views/rails_pulse/exceptions/_status_badge.html.erb +6 -0
- data/app/views/rails_pulse/exceptions/_table.html.erb +33 -0
- data/app/views/rails_pulse/exceptions/index.html.erb +66 -0
- data/app/views/rails_pulse/exceptions/show.html.erb +52 -0
- data/app/views/rails_pulse/queries/_raw_data.html.erb +1 -23
- data/app/views/rails_pulse/requests/_table.html.erb +1 -1
- data/app/views/rails_pulse/routes/_requests_table.html.erb +5 -0
- data/app/views/rails_pulse/routes/_table.html.erb +22 -8
- data/app/views/rails_pulse/routes/index.html.erb +5 -1
- data/app/views/rails_pulse/routes/show.html.erb +5 -0
- data/app/views/rails_pulse/storage/show.html.erb +123 -0
- data/config/routes.rb +13 -4
- data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +50 -0
- data/db/rails_pulse_migrate/20260507000001_add_actual_query_to_operations.rb +10 -5
- data/db/rails_pulse_migrate/20260610000001_add_controller_action_to_rails_pulse_routes.rb +8 -0
- data/db/rails_pulse_migrate/20260610000002_change_rails_pulse_routes_to_multi_verb_model.rb +185 -0
- data/db/rails_pulse_migrate/20260610000003_add_null_action_unique_index_to_routes.rb +32 -0
- data/db/rails_pulse_migrate/20260823000001_add_location_to_exception_groups.rb +12 -0
- data/db/rails_pulse_schema.rb +54 -6
- data/lib/generators/rails_pulse/base_methods.rb +2 -0
- data/lib/generators/rails_pulse/install_generator.rb +5 -0
- data/lib/generators/rails_pulse/schema_parser.rb +22 -2
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +54 -6
- data/lib/generators/rails_pulse/templates/migrations/upgrade_rails_pulse_tables.rb +1 -1
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +157 -16
- data/lib/generators/rails_pulse/upgrade_generator.rb +166 -38
- data/lib/rails_pulse/cleanup_service.rb +110 -15
- data/lib/rails_pulse/configuration.rb +125 -6
- data/lib/rails_pulse/engine.rb +32 -17
- data/lib/rails_pulse/installers/config_updater.rb +202 -0
- data/lib/rails_pulse/job_run_collector.rb +43 -16
- data/lib/rails_pulse/middleware/asset_server.rb +11 -0
- data/lib/rails_pulse/middleware/request_collector.rb +9 -4
- data/lib/rails_pulse/packaged_assets.rb +147 -0
- data/lib/rails_pulse/route_indexes.rb +66 -0
- data/lib/rails_pulse/subscribers/exception_subscriber.rb +36 -0
- data/lib/rails_pulse/tracker.rb +29 -10
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +1 -0
- data/lib/rails_pulse_server.ru +16 -47
- data/lib/tasks/rails_pulse.rake +83 -4
- data/lib/tasks/rails_pulse_assets.rake +15 -0
- data/lib/tasks/rails_pulse_tasks.rake +29 -0
- data/public/rails-pulse-assets/rails-pulse-icons.js +6 -2
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +18 -117
- metadata +78 -37
- data/Rakefile +0 -554
- data/app/javascript/rails_pulse/controllers/form_controller.js +0 -39
- data/app/javascript/rails_pulse/controllers/period_selector_controller.js +0 -19
- data/app/javascript/rails_pulse/controllers/timezone_controller.js +0 -89
- data/app/javascript/rails_pulse/theme.js +0 -416
- data/config/brakeman.ignore +0 -190
- data/config/brakeman.yml +0 -68
- data/config/initializers/rails_pulse.rb +0 -287
- data/public/rails-pulse-assets/csp-test.js +0 -120
- data/public/rails-pulse-assets/rails-pulse-icons.js.map +0 -13
- data/public/rails-pulse-assets/rails-pulse.css.map +0 -1
- data/public/rails-pulse-assets/rails-pulse.js.map +0 -7
- data/vendor/assets/javascripts/rails-pulse-icons.js +0 -93
- data/vendor/assets/javascripts/rails-pulse-icons.js.map +0 -13
- data/vendor/assets/javascripts/rails-pulse.js +0 -157
- data/vendor/assets/javascripts/rails-pulse.js.map +0 -7
- data/vendor/assets/stylesheets/rails-pulse.css +0 -1
- data/vendor/assets/stylesheets/rails-pulse.css.map +0 -1
data/Rakefile
DELETED
|
@@ -1,554 +0,0 @@
|
|
|
1
|
-
require "bundler/setup"
|
|
2
|
-
require "bundler/gem_tasks"
|
|
3
|
-
|
|
4
|
-
# Load environment variables from .env file
|
|
5
|
-
require "dotenv/load" if File.exist?(".env")
|
|
6
|
-
|
|
7
|
-
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
8
|
-
load "rails/tasks/engine.rake"
|
|
9
|
-
|
|
10
|
-
desc "Verify dummy app migrations are in sync with gem migrations"
|
|
11
|
-
task :verify_dummy_migrations do
|
|
12
|
-
# Check if db/rails_pulse_migrate directory exists (separate database setup)
|
|
13
|
-
if Dir.exist?("db/rails_pulse_migrate")
|
|
14
|
-
gem_migrations = Dir["db/rails_pulse_migrate/*.rb"].map { |f| File.basename(f) }.sort
|
|
15
|
-
|
|
16
|
-
# Get all migrations from dummy app
|
|
17
|
-
dummy_migrations = Dir["test/dummy/db/migrate/*.rb"].map { |f| File.basename(f) }
|
|
18
|
-
|
|
19
|
-
missing = gem_migrations - dummy_migrations
|
|
20
|
-
|
|
21
|
-
if missing.any?
|
|
22
|
-
puts "\n❌ Dummy app is missing Rails Pulse migrations:"
|
|
23
|
-
missing.each { |m| puts " • #{m}" }
|
|
24
|
-
puts "\nTo fix this, run:"
|
|
25
|
-
puts " cd test/dummy"
|
|
26
|
-
puts " rails generate rails_pulse:upgrade"
|
|
27
|
-
puts " rails db:migrate RAILS_ENV=test"
|
|
28
|
-
puts "\nThen commit the new migration files."
|
|
29
|
-
exit 1
|
|
30
|
-
else
|
|
31
|
-
puts "✅ Dummy app migrations are in sync with gem migrations"
|
|
32
|
-
end
|
|
33
|
-
else
|
|
34
|
-
puts "✅ Dummy app migrations check skipped (single database setup)"
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
desc "Sync Rails Pulse schema to test/dummy app"
|
|
39
|
-
task :sync_test_schema do
|
|
40
|
-
require "fileutils"
|
|
41
|
-
|
|
42
|
-
source = "db/rails_pulse_schema.rb"
|
|
43
|
-
dest = "test/dummy/db/rails_pulse_schema.rb"
|
|
44
|
-
|
|
45
|
-
if File.exist?(source)
|
|
46
|
-
FileUtils.cp(source, dest)
|
|
47
|
-
puts "✅ Synced schema: #{source} → #{dest}"
|
|
48
|
-
else
|
|
49
|
-
puts "⚠️ Source schema not found: #{source}"
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
desc "Setup database for testing"
|
|
54
|
-
task :test_setup do
|
|
55
|
-
database = ENV['DB'] || 'sqlite3'
|
|
56
|
-
|
|
57
|
-
puts "\n" + "=" * 50
|
|
58
|
-
puts "🛠️ Rails Pulse Test Setup"
|
|
59
|
-
puts "=" * 50
|
|
60
|
-
puts "Database: #{database.upcase}"
|
|
61
|
-
puts "=" * 50
|
|
62
|
-
puts
|
|
63
|
-
|
|
64
|
-
begin
|
|
65
|
-
# Sync schema file from gem to test/dummy
|
|
66
|
-
Rake::Task[:sync_test_schema].invoke
|
|
67
|
-
|
|
68
|
-
# Remove schema.rb to ensure clean migration
|
|
69
|
-
schema_file = "test/dummy/db/schema.rb"
|
|
70
|
-
if File.exist?(schema_file)
|
|
71
|
-
puts "🧹 Removing existing schema.rb file..."
|
|
72
|
-
File.delete(schema_file)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
case database.downcase
|
|
76
|
-
when 'sqlite3', 'sqlite'
|
|
77
|
-
puts "📦 Setting up SQLite database..."
|
|
78
|
-
sh "RAILS_ENV=test bin/rails db:drop db:create db:migrate"
|
|
79
|
-
|
|
80
|
-
when 'mysql2', 'mysql'
|
|
81
|
-
puts "🐬 Setting up MySQL database..."
|
|
82
|
-
mysql_password = ENV.fetch("MYSQL_PASSWORD", "")
|
|
83
|
-
sh "DB=mysql2 MYSQL_PASSWORD=#{mysql_password} RAILS_ENV=test rails db:drop db:create db:migrate"
|
|
84
|
-
|
|
85
|
-
when 'postgresql', 'postgres'
|
|
86
|
-
puts "🐘 Setting up PostgreSQL database..."
|
|
87
|
-
sh "DB=postgresql RAILS_ENV=test rails db:drop db:create db:migrate"
|
|
88
|
-
|
|
89
|
-
else
|
|
90
|
-
puts "⚠️ Unknown database: #{database}"
|
|
91
|
-
puts "Supported databases: sqlite3, mysql2, postgresql"
|
|
92
|
-
exit 1
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
puts "\n✅ Database setup complete!"
|
|
96
|
-
puts "Ready to run: rake test"
|
|
97
|
-
|
|
98
|
-
rescue => e
|
|
99
|
-
puts "\n❌ Database setup failed!"
|
|
100
|
-
puts "Error: #{e.message}"
|
|
101
|
-
puts "\nTroubleshooting:"
|
|
102
|
-
puts "• Ensure #{database} is installed and running"
|
|
103
|
-
puts "• Check database credentials in test/dummy/config/database.yml"
|
|
104
|
-
puts "• Verify RAILS_ENV=test environment is configured"
|
|
105
|
-
exit 1
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
desc "Run migration regression tests in an isolated process"
|
|
110
|
-
task :test_migrations do
|
|
111
|
-
database = ENV["DB"] || "sqlite3"
|
|
112
|
-
|
|
113
|
-
puts "\n" + "=" * 50
|
|
114
|
-
puts "🔄 Rails Pulse Migration Regression Tests"
|
|
115
|
-
puts "=" * 50
|
|
116
|
-
puts "Database: #{database.upcase}"
|
|
117
|
-
puts "=" * 50
|
|
118
|
-
puts
|
|
119
|
-
|
|
120
|
-
sh "rails test test/migrations"
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
desc "Run test suite"
|
|
124
|
-
task :test do
|
|
125
|
-
database = ENV['DB'] || 'sqlite3'
|
|
126
|
-
|
|
127
|
-
# Get Rails version from Gemfile.lock or fallback
|
|
128
|
-
rails_version = begin
|
|
129
|
-
require 'rails'
|
|
130
|
-
Rails.version
|
|
131
|
-
rescue LoadError
|
|
132
|
-
# Try to get from Gemfile.lock
|
|
133
|
-
gemfile_lock = File.read('Gemfile.lock') rescue nil
|
|
134
|
-
if gemfile_lock && gemfile_lock.match(/rails \(([^)]+)\)/)
|
|
135
|
-
$1
|
|
136
|
-
else
|
|
137
|
-
'unknown'
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
puts "\n" + "=" * 50
|
|
142
|
-
puts "💛 Rails Pulse Test Suite"
|
|
143
|
-
puts "=" * 50
|
|
144
|
-
puts "Database: #{database.upcase}"
|
|
145
|
-
puts "Rails: #{rails_version}"
|
|
146
|
-
puts "=" * 50
|
|
147
|
-
puts
|
|
148
|
-
|
|
149
|
-
sh "rails test test/controllers test/generators test/helpers test/instrumentation test/jobs test/lib test/middleware test/models test/services test/rails_pulse_test.rb test/tracker_test.rb"
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
desc "Run test suite with code coverage"
|
|
153
|
-
task :test_coverage do
|
|
154
|
-
ENV["COVERAGE"] = "true"
|
|
155
|
-
Rake::Task[:test].invoke
|
|
156
|
-
|
|
157
|
-
puts "\n" + "=" * 50
|
|
158
|
-
puts "📊 Coverage Report"
|
|
159
|
-
puts "=" * 50
|
|
160
|
-
puts "Open coverage/index.html to view detailed results"
|
|
161
|
-
puts "=" * 50
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
desc "Setup database for specific Rails version and database"
|
|
165
|
-
task :test_setup_for_version, [ :database, :rails_version ] do |t, args|
|
|
166
|
-
database = args[:database] || ENV['DB'] || 'sqlite3'
|
|
167
|
-
rails_version = args[:rails_version] || 'rails-8-0'
|
|
168
|
-
|
|
169
|
-
puts "\n" + "=" * 50
|
|
170
|
-
puts "🛠️ Rails Pulse Test Setup"
|
|
171
|
-
puts "=" * 50
|
|
172
|
-
puts "Database: #{database.upcase}"
|
|
173
|
-
puts "Rails: #{rails_version.upcase.gsub('-', ' ')}"
|
|
174
|
-
puts "=" * 50
|
|
175
|
-
puts
|
|
176
|
-
|
|
177
|
-
begin
|
|
178
|
-
# Sync schema file from gem to test/dummy
|
|
179
|
-
Rake::Task[:sync_test_schema].reenable
|
|
180
|
-
Rake::Task[:sync_test_schema].invoke
|
|
181
|
-
|
|
182
|
-
# Remove schema.rb to ensure clean migration
|
|
183
|
-
schema_file = "test/dummy/db/schema.rb"
|
|
184
|
-
if File.exist?(schema_file)
|
|
185
|
-
puts "🧹 Removing existing schema.rb file..."
|
|
186
|
-
File.delete(schema_file)
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
puts "📦 Setting up #{database.upcase} database with #{rails_version.upcase.gsub('-', ' ')}..."
|
|
190
|
-
sh "DB=#{database} bundle exec appraisal #{rails_version} rails db:drop db:create db:migrate RAILS_ENV=test"
|
|
191
|
-
|
|
192
|
-
puts "\n✅ Database setup complete for #{database.upcase} + #{rails_version.upcase.gsub('-', ' ')}!"
|
|
193
|
-
|
|
194
|
-
rescue => e
|
|
195
|
-
puts "\n❌ Database setup failed!"
|
|
196
|
-
puts "Error: #{e.message}"
|
|
197
|
-
exit 1
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
desc "Test all database and Rails version combinations"
|
|
202
|
-
task :test_matrix do
|
|
203
|
-
databases = %w[mysql2 postgresql sqlite3]
|
|
204
|
-
rails_versions = %w[rails-7-2 rails-8-0 rails-8-1]
|
|
205
|
-
|
|
206
|
-
failed_combinations = []
|
|
207
|
-
total_combinations = databases.size * rails_versions.size
|
|
208
|
-
current = 0
|
|
209
|
-
base_test_paths = "test/controllers test/generators test/helpers test/instrumentation test/jobs test/lib test/middleware test/models test/services test/rails_pulse_test.rb test/tracker_test.rb test/system"
|
|
210
|
-
|
|
211
|
-
puts "\n" + "=" * 60
|
|
212
|
-
puts "🚀 Rails Pulse Full Test Matrix"
|
|
213
|
-
puts "=" * 60
|
|
214
|
-
puts "Testing #{total_combinations} combinations..."
|
|
215
|
-
puts "=" * 60
|
|
216
|
-
|
|
217
|
-
databases.each do |database|
|
|
218
|
-
rails_versions.each do |rails_version|
|
|
219
|
-
current += 1
|
|
220
|
-
|
|
221
|
-
puts "\n[#{current}/#{total_combinations}] Testing: #{database.upcase} + #{rails_version.upcase.gsub('-', ' ')}"
|
|
222
|
-
puts "-" * 50
|
|
223
|
-
|
|
224
|
-
test_paths = base_test_paths
|
|
225
|
-
begin
|
|
226
|
-
# First setup the database for this specific combination
|
|
227
|
-
Rake::Task[:test_setup_for_version].reenable
|
|
228
|
-
Rake::Task[:test_setup_for_version].invoke(database, rails_version)
|
|
229
|
-
|
|
230
|
-
# Then run the tests
|
|
231
|
-
sh "DB=#{database} MYSQL_PASSWORD=#{ENV.fetch('MYSQL_PASSWORD', '')} bundle exec appraisal #{rails_version} rails test #{test_paths}"
|
|
232
|
-
|
|
233
|
-
puts "✅ PASSED: #{database} + #{rails_version}"
|
|
234
|
-
|
|
235
|
-
rescue => e
|
|
236
|
-
puts "❌ FAILED: #{database} + #{rails_version}"
|
|
237
|
-
puts " Error: #{e.message}"
|
|
238
|
-
failed_combinations << "#{database} + #{rails_version}"
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
puts "\n" + "=" * 60
|
|
244
|
-
puts "🏁 Test Matrix Results"
|
|
245
|
-
puts "=" * 60
|
|
246
|
-
|
|
247
|
-
if failed_combinations.empty?
|
|
248
|
-
puts "🎉 All #{total_combinations} combinations passed!"
|
|
249
|
-
else
|
|
250
|
-
puts "✅ Passed: #{total_combinations - failed_combinations.size}/#{total_combinations}"
|
|
251
|
-
puts "❌ Failed combinations:"
|
|
252
|
-
failed_combinations.each { |combo| puts " • #{combo}" }
|
|
253
|
-
exit 1
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
desc "Pre-release testing with comprehensive checks"
|
|
258
|
-
task :test_release do
|
|
259
|
-
puts "\n" + "=" * 70
|
|
260
|
-
puts "🚀 Rails Pulse Pre-Release Validation"
|
|
261
|
-
puts "=" * 70
|
|
262
|
-
puts
|
|
263
|
-
|
|
264
|
-
failed_tasks = []
|
|
265
|
-
current_step = 0
|
|
266
|
-
total_steps = 14
|
|
267
|
-
|
|
268
|
-
# Step 1: Update appraisal gemfiles
|
|
269
|
-
current_step += 1
|
|
270
|
-
begin
|
|
271
|
-
puts "\n[#{current_step}/#{total_steps}] Updating appraisal gemfiles..."
|
|
272
|
-
puts "-" * 70
|
|
273
|
-
sh "bundle exec appraisal install"
|
|
274
|
-
puts "✅ Appraisal gemfiles updated!"
|
|
275
|
-
rescue => e
|
|
276
|
-
puts "❌ Appraisal update failed!"
|
|
277
|
-
puts " Error: #{e.message}"
|
|
278
|
-
failed_tasks << "appraisal_install"
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
# Step 2: Sync test schema
|
|
282
|
-
current_step += 1
|
|
283
|
-
begin
|
|
284
|
-
puts "\n[#{current_step}/#{total_steps}] Syncing test schema..."
|
|
285
|
-
puts "-" * 70
|
|
286
|
-
Rake::Task[:sync_test_schema].invoke
|
|
287
|
-
rescue => e
|
|
288
|
-
puts "❌ Schema sync failed!"
|
|
289
|
-
puts " Error: #{e.message}"
|
|
290
|
-
failed_tasks << "sync_test_schema"
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
# Step 3: Verify dummy migrations
|
|
294
|
-
current_step += 1
|
|
295
|
-
begin
|
|
296
|
-
puts "\n[#{current_step}/#{total_steps}] Verifying dummy app migrations..."
|
|
297
|
-
puts "-" * 70
|
|
298
|
-
Rake::Task[:verify_dummy_migrations].invoke
|
|
299
|
-
rescue => e
|
|
300
|
-
puts "❌ Dummy app migration verification failed!"
|
|
301
|
-
puts " Error: #{e.message}"
|
|
302
|
-
failed_tasks << "verify_dummy_migrations"
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
# Step 4: Git status check
|
|
306
|
-
current_step += 1
|
|
307
|
-
begin
|
|
308
|
-
puts "\n[#{current_step}/#{total_steps}] Checking git status..."
|
|
309
|
-
puts "-" * 70
|
|
310
|
-
|
|
311
|
-
git_status = `git status --porcelain`.strip
|
|
312
|
-
if !git_status.empty?
|
|
313
|
-
puts "❌ Git working directory is not clean!"
|
|
314
|
-
puts "\nUncommitted changes:"
|
|
315
|
-
puts git_status
|
|
316
|
-
puts "\nPlease commit or stash your changes before running pre-release tests."
|
|
317
|
-
failed_tasks << "git_status_check"
|
|
318
|
-
else
|
|
319
|
-
puts "✅ Git working directory is clean"
|
|
320
|
-
end
|
|
321
|
-
rescue => e
|
|
322
|
-
puts "⚠️ Warning: Could not check git status (#{e.message})"
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
# Step 5: RuboCop linting
|
|
326
|
-
current_step += 1
|
|
327
|
-
begin
|
|
328
|
-
puts "\n[#{current_step}/#{total_steps}] Running RuboCop linting..."
|
|
329
|
-
puts "-" * 70
|
|
330
|
-
sh "bundle exec rubocop"
|
|
331
|
-
puts "✅ Code style checks passed!"
|
|
332
|
-
rescue => e
|
|
333
|
-
puts "❌ RuboCop linting failed!"
|
|
334
|
-
puts " Error: #{e.message}"
|
|
335
|
-
failed_tasks << "rubocop"
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
# Step 6: Brakeman security scan
|
|
339
|
-
current_step += 1
|
|
340
|
-
begin
|
|
341
|
-
puts "\n[#{current_step}/#{total_steps}] Running Brakeman security scanner..."
|
|
342
|
-
puts "-" * 70
|
|
343
|
-
Rake::Task[:brakeman].invoke
|
|
344
|
-
rescue => e
|
|
345
|
-
puts "❌ Brakeman security scan failed!"
|
|
346
|
-
puts " Error: #{e.message}"
|
|
347
|
-
failed_tasks << "brakeman"
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
# Step 7: Install Node dependencies
|
|
351
|
-
current_step += 1
|
|
352
|
-
begin
|
|
353
|
-
puts "\n[#{current_step}/#{total_steps}] Installing Node dependencies..."
|
|
354
|
-
puts "-" * 70
|
|
355
|
-
sh "npm install"
|
|
356
|
-
puts "✅ Node dependencies installed!"
|
|
357
|
-
rescue => e
|
|
358
|
-
puts "❌ npm install failed!"
|
|
359
|
-
puts " Error: #{e.message}"
|
|
360
|
-
failed_tasks << "npm_install"
|
|
361
|
-
end
|
|
362
|
-
|
|
363
|
-
# Step 8: ESLint JS linting
|
|
364
|
-
current_step += 1
|
|
365
|
-
begin
|
|
366
|
-
puts "\n[#{current_step}/#{total_steps}] Running ESLint JS linting..."
|
|
367
|
-
puts "-" * 70
|
|
368
|
-
sh "npm run lint:js"
|
|
369
|
-
puts "✅ JS linting passed!"
|
|
370
|
-
rescue => e
|
|
371
|
-
puts "❌ ESLint linting failed!"
|
|
372
|
-
puts " Error: #{e.message}"
|
|
373
|
-
failed_tasks << "eslint"
|
|
374
|
-
end
|
|
375
|
-
|
|
376
|
-
# Step 9: JavaScript unit tests
|
|
377
|
-
current_step += 1
|
|
378
|
-
begin
|
|
379
|
-
puts "\n[#{current_step}/#{total_steps}] Running JavaScript unit tests..."
|
|
380
|
-
puts "-" * 70
|
|
381
|
-
sh "npm run test:js"
|
|
382
|
-
puts "✅ JavaScript tests passed!"
|
|
383
|
-
rescue => e
|
|
384
|
-
puts "❌ JavaScript tests failed!"
|
|
385
|
-
puts " Error: #{e.message}"
|
|
386
|
-
failed_tasks << "js_tests"
|
|
387
|
-
end
|
|
388
|
-
|
|
389
|
-
# Step 10: Build and verify assets
|
|
390
|
-
current_step += 1
|
|
391
|
-
begin
|
|
392
|
-
puts "\n[#{current_step}/#{total_steps}] Building production assets..."
|
|
393
|
-
puts "-" * 70
|
|
394
|
-
sh "npm run build"
|
|
395
|
-
|
|
396
|
-
# Verify assets were built in both locations
|
|
397
|
-
public_assets = "public/rails-pulse-assets"
|
|
398
|
-
vendor_css = "vendor/assets/stylesheets"
|
|
399
|
-
vendor_js = "vendor/assets/javascripts"
|
|
400
|
-
|
|
401
|
-
all_dirs_valid = true
|
|
402
|
-
|
|
403
|
-
if Dir.exist?(public_assets) && !Dir.empty?(public_assets)
|
|
404
|
-
puts "✅ Public assets built: #{public_assets}"
|
|
405
|
-
else
|
|
406
|
-
puts "❌ Public assets directory is missing or empty!"
|
|
407
|
-
failed_tasks << "public_assets_verification"
|
|
408
|
-
all_dirs_valid = false
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
if Dir.exist?(vendor_css) && !Dir.empty?(vendor_css)
|
|
412
|
-
puts "✅ Vendor CSS built: #{vendor_css}"
|
|
413
|
-
else
|
|
414
|
-
puts "❌ Vendor CSS directory is missing or empty!"
|
|
415
|
-
failed_tasks << "vendor_css_verification"
|
|
416
|
-
all_dirs_valid = false
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
if Dir.exist?(vendor_js) && !Dir.empty?(vendor_js)
|
|
420
|
-
puts "✅ Vendor JS built: #{vendor_js}"
|
|
421
|
-
else
|
|
422
|
-
puts "❌ Vendor JS directory is missing or empty!"
|
|
423
|
-
failed_tasks << "vendor_js_verification"
|
|
424
|
-
all_dirs_valid = false
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
if all_dirs_valid
|
|
428
|
-
puts "✅ All assets built successfully!"
|
|
429
|
-
end
|
|
430
|
-
rescue => e
|
|
431
|
-
puts "❌ Asset building failed!"
|
|
432
|
-
puts " Error: #{e.message}"
|
|
433
|
-
failed_tasks << "npm_build"
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
# Step 11: Verify gem builds
|
|
437
|
-
current_step += 1
|
|
438
|
-
begin
|
|
439
|
-
puts "\n[#{current_step}/#{total_steps}] Verifying gem builds correctly..."
|
|
440
|
-
puts "-" * 70
|
|
441
|
-
sh "gem build rails_pulse.gemspec"
|
|
442
|
-
|
|
443
|
-
# Clean up the built gem
|
|
444
|
-
built_gems = Dir.glob("rails_pulse-*.gem")
|
|
445
|
-
built_gems.each { |gem_file| File.delete(gem_file) }
|
|
446
|
-
|
|
447
|
-
puts "✅ Gem builds successfully!"
|
|
448
|
-
rescue => e
|
|
449
|
-
puts "❌ Gem build failed!"
|
|
450
|
-
puts " Error: #{e.message}"
|
|
451
|
-
failed_tasks << "gem_build"
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
# Step 12: Run generator tests
|
|
455
|
-
current_step += 1
|
|
456
|
-
begin
|
|
457
|
-
puts "\n[#{current_step}/#{total_steps}] Running generator tests..."
|
|
458
|
-
puts "-" * 70
|
|
459
|
-
sh "./bin/test_generators"
|
|
460
|
-
puts "✅ Generator tests passed!"
|
|
461
|
-
rescue => e
|
|
462
|
-
puts "❌ Generator tests failed!"
|
|
463
|
-
puts " Error: #{e.message}"
|
|
464
|
-
failed_tasks << "test_generators"
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
# Step 13: Run migration regression tests
|
|
468
|
-
current_step += 1
|
|
469
|
-
begin
|
|
470
|
-
puts "\n[#{current_step}/#{total_steps}] Running migration regression tests..."
|
|
471
|
-
puts "-" * 70
|
|
472
|
-
sh "rake test_migrations"
|
|
473
|
-
puts "✅ Migration regression tests passed!"
|
|
474
|
-
rescue => e
|
|
475
|
-
puts "❌ Migration regression tests failed!"
|
|
476
|
-
puts " Error: #{e.message}"
|
|
477
|
-
failed_tasks << "test_migrations"
|
|
478
|
-
end
|
|
479
|
-
|
|
480
|
-
# Step 14: Run full test matrix
|
|
481
|
-
current_step += 1
|
|
482
|
-
begin
|
|
483
|
-
puts "\n[#{current_step}/#{total_steps}] Running full test matrix..."
|
|
484
|
-
puts "-" * 70
|
|
485
|
-
sh "rake test_matrix"
|
|
486
|
-
puts "✅ Test matrix passed!"
|
|
487
|
-
rescue => e
|
|
488
|
-
puts "❌ Test matrix failed!"
|
|
489
|
-
puts " Error: #{e.message}"
|
|
490
|
-
failed_tasks << "test_matrix"
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
# Print final results
|
|
494
|
-
puts "\n" + "=" * 70
|
|
495
|
-
puts "🏁 Pre-Release Validation Results"
|
|
496
|
-
puts "=" * 70
|
|
497
|
-
|
|
498
|
-
if failed_tasks.empty?
|
|
499
|
-
puts "🎉 All pre-release checks passed!"
|
|
500
|
-
puts "\n✅ Ready for release!"
|
|
501
|
-
puts "\nNext steps:"
|
|
502
|
-
puts " 1. Update version in lib/rails_pulse/version.rb"
|
|
503
|
-
puts " 2. Update Gemfile.lock files for all Rails versions"
|
|
504
|
-
puts " 3. Follow the release process in docs/releasing.md"
|
|
505
|
-
else
|
|
506
|
-
puts "❌ Failed checks (#{failed_tasks.size}/#{total_steps}):"
|
|
507
|
-
failed_tasks.each { |task| puts " • #{task}" }
|
|
508
|
-
puts "\n⚠️ Fix these issues before releasing."
|
|
509
|
-
exit 1
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
desc "Run Brakeman security scanner"
|
|
514
|
-
task :brakeman do
|
|
515
|
-
require "brakeman"
|
|
516
|
-
|
|
517
|
-
puts "\n" + "=" * 50
|
|
518
|
-
puts "🔒 Running Brakeman Security Scanner"
|
|
519
|
-
puts "=" * 50
|
|
520
|
-
puts
|
|
521
|
-
|
|
522
|
-
begin
|
|
523
|
-
# Run Brakeman with the configuration file
|
|
524
|
-
result = Brakeman.run(
|
|
525
|
-
app_path: ".",
|
|
526
|
-
config_file: "config/brakeman.yml",
|
|
527
|
-
print_report: true,
|
|
528
|
-
pager: false
|
|
529
|
-
)
|
|
530
|
-
|
|
531
|
-
# Check if any unignored warnings were found
|
|
532
|
-
# result.filtered_warnings only includes warnings that aren't ignored
|
|
533
|
-
unignored_warnings = result.filtered_warnings
|
|
534
|
-
total_warnings = result.warnings.count
|
|
535
|
-
ignored_count = total_warnings - unignored_warnings.count
|
|
536
|
-
|
|
537
|
-
if unignored_warnings.any? || result.errors.any?
|
|
538
|
-
puts "\n❌ Security issues found!"
|
|
539
|
-
puts " Warnings: #{unignored_warnings.count}"
|
|
540
|
-
puts " Ignored: #{ignored_count}" if ignored_count > 0
|
|
541
|
-
puts " Errors: #{result.errors.count}"
|
|
542
|
-
exit 1
|
|
543
|
-
else
|
|
544
|
-
puts "\n✅ No security issues found!"
|
|
545
|
-
puts " (#{ignored_count} warnings reviewed and ignored)" if ignored_count > 0
|
|
546
|
-
end
|
|
547
|
-
rescue => e
|
|
548
|
-
puts "\n❌ Brakeman scan failed!"
|
|
549
|
-
puts " Error: #{e.message}"
|
|
550
|
-
exit 1
|
|
551
|
-
end
|
|
552
|
-
end
|
|
553
|
-
|
|
554
|
-
task default: :test
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
|
|
3
|
-
export default class extends Controller {
|
|
4
|
-
static targets = [ "cancel" ]
|
|
5
|
-
|
|
6
|
-
initialize() {
|
|
7
|
-
// Simple debounce implementation for asset independence
|
|
8
|
-
this.search = this.debounce(this.search.bind(this), 500)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Simple debounce implementation (replaces lodash dependency)
|
|
12
|
-
debounce(func, wait) {
|
|
13
|
-
let timeout
|
|
14
|
-
return function executedFunction(...args) {
|
|
15
|
-
const later = () => {
|
|
16
|
-
clearTimeout(timeout)
|
|
17
|
-
func(...args)
|
|
18
|
-
}
|
|
19
|
-
clearTimeout(timeout)
|
|
20
|
-
timeout = setTimeout(later, wait)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
submit() {
|
|
25
|
-
this.element.requestSubmit()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
search() {
|
|
29
|
-
this.element.requestSubmit()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
cancel() {
|
|
33
|
-
this.cancelTarget?.click()
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
preventAttachment(event) {
|
|
37
|
-
event.preventDefault()
|
|
38
|
-
}
|
|
39
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
|
|
3
|
-
// Handles the 7d/14d/30d period toggle on the dashboard.
|
|
4
|
-
// Optimistically updates button active state on click.
|
|
5
|
-
export default class extends Controller {
|
|
6
|
-
static targets = ["button"]
|
|
7
|
-
|
|
8
|
-
select(event) {
|
|
9
|
-
const clickedEl = event.currentTarget
|
|
10
|
-
|
|
11
|
-
// Optimistic UI: immediately reflect the new active state
|
|
12
|
-
this.buttonTargets.forEach(btn => {
|
|
13
|
-
const isActive = btn === clickedEl
|
|
14
|
-
btn.dataset.active = isActive ? "true" : "false"
|
|
15
|
-
btn.style.background = isActive ? "var(--color-primary)" : "transparent"
|
|
16
|
-
btn.style.color = isActive ? "var(--color-text-reversed)" : "var(--color-text-subtle)"
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
|
|
3
|
-
export default class extends Controller {
|
|
4
|
-
static values = { cachedAt: String, targetFrame: String }
|
|
5
|
-
|
|
6
|
-
connect() {
|
|
7
|
-
this.updateTimestamp()
|
|
8
|
-
this.setupObserver()
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
disconnect() {
|
|
12
|
-
if (this.observer) {
|
|
13
|
-
this.observer.disconnect()
|
|
14
|
-
}
|
|
15
|
-
if (this.frameObserver) {
|
|
16
|
-
this.frameObserver.disconnect()
|
|
17
|
-
}
|
|
18
|
-
if (this.documentObserver) {
|
|
19
|
-
this.documentObserver.disconnect()
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
setupObserver() {
|
|
24
|
-
if (this.targetFrameValue) {
|
|
25
|
-
// Try immediately
|
|
26
|
-
this.updateTimestamp()
|
|
27
|
-
|
|
28
|
-
// Also observe for when the frame appears or changes
|
|
29
|
-
const observer = new MutationObserver(() => {
|
|
30
|
-
const targetFrame = document.getElementById(this.targetFrameValue)
|
|
31
|
-
if (targetFrame) {
|
|
32
|
-
this.updateTimestamp()
|
|
33
|
-
// Watch for attribute changes on the frame
|
|
34
|
-
if (this.frameObserver) {
|
|
35
|
-
this.frameObserver.disconnect()
|
|
36
|
-
}
|
|
37
|
-
this.frameObserver = new MutationObserver(() => {
|
|
38
|
-
this.updateTimestamp()
|
|
39
|
-
})
|
|
40
|
-
this.frameObserver.observe(targetFrame, {
|
|
41
|
-
attributes: true,
|
|
42
|
-
attributeFilter: ['data-cached-at'],
|
|
43
|
-
childList: true,
|
|
44
|
-
subtree: true
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
// Watch the whole document for the frame to appear
|
|
50
|
-
observer.observe(document.body, { childList: true, subtree: true })
|
|
51
|
-
this.documentObserver = observer
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
cachedAtValueChanged() {
|
|
56
|
-
this.updateTimestamp()
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
updateTimestamp() {
|
|
60
|
-
let cachedAtValue = this.cachedAtValue
|
|
61
|
-
|
|
62
|
-
// If no direct cached value but we have a target frame, try to get it from there
|
|
63
|
-
if (!cachedAtValue && this.targetFrameValue) {
|
|
64
|
-
const targetFrame = document.getElementById(this.targetFrameValue)
|
|
65
|
-
if (targetFrame) {
|
|
66
|
-
cachedAtValue = targetFrame.dataset.cachedAt
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (cachedAtValue) {
|
|
71
|
-
try {
|
|
72
|
-
const date = new Date(cachedAtValue)
|
|
73
|
-
const localTimeString = date.toLocaleString('en-US', {
|
|
74
|
-
year: 'numeric',
|
|
75
|
-
month: 'long',
|
|
76
|
-
day: 'numeric',
|
|
77
|
-
hour: 'numeric',
|
|
78
|
-
minute: '2-digit',
|
|
79
|
-
hour12: true
|
|
80
|
-
})
|
|
81
|
-
this.element.title = `Last updated: ${localTimeString}`
|
|
82
|
-
} catch {
|
|
83
|
-
this.element.title = 'Cache time unavailable'
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
this.element.title = 'Cache time unavailable'
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|