From 9f96150b2dba6c1ab00b826cfc95af910a3d6d63 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Wed, 9 Aug 2023 14:09:59 -0500 Subject: [PATCH 01/74] limit dl to 1 per hour --- app/site_files.rb | 8 ++++++++ migrations/118_site_dl_queued_at.rb | 9 +++++++++ 2 files changed, 17 insertions(+) create mode 100644 migrations/118_site_dl_queued_at.rb diff --git a/app/site_files.rb b/app/site_files.rb index f5694d4b..8e129579 100644 --- a/app/site_files.rb +++ b/app/site_files.rb @@ -189,9 +189,17 @@ end get '/site_files/:username.zip' do |username| require_login + if !current_site.dl_queued_at.nil? && current_site.dl_queued_at > 1.hour.ago + flash[:error] = 'Site downloads are currently limited to once per hour, please try again later.' + redirect request.referer + end + content_type 'application/zip' attachment "neocities-#{current_site.username}.zip" + current_site.dl_queued_at = Time.now + current_site.save_changes validate: false + directory_path = current_site.files_path stream do |out| diff --git a/migrations/118_site_dl_queued_at.rb b/migrations/118_site_dl_queued_at.rb new file mode 100644 index 00000000..1363d559 --- /dev/null +++ b/migrations/118_site_dl_queued_at.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_column :sites, :dl_queued_at, Time + } + + down { + DB.drop_column :sites, :dl_queued_at + } +end From 95e61a3f4521c5dfd4e55ca9fade6f3a97293945 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Wed, 9 Aug 2023 19:21:25 +0000 Subject: [PATCH 02/74] dont compress zip backups --- app/site_files.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/site_files.rb b/app/site_files.rb index 8e129579..1e94fc43 100644 --- a/app/site_files.rb +++ b/app/site_files.rb @@ -208,7 +208,7 @@ get '/site_files/:username.zip' do |username| next if File.directory?(file) zip_path = file.sub("#{directory_path}/", '') - zip.write_deflated_file(zip_path) do |file_writer| + zip.write_stored_file(zip_path) do |file_writer| File.open(file, 'rb') do |file| IO.copy_stream(file, file_writer) end From 1ecc1482261be3006c60cc7ca645bf3039d1d8d1 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Wed, 9 Aug 2023 19:58:46 +0000 Subject: [PATCH 03/74] more generic site file download path --- app/site_files.rb | 2 +- views/dashboard.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/site_files.rb b/app/site_files.rb index 1e94fc43..ef0b8bb2 100644 --- a/app/site_files.rb +++ b/app/site_files.rb @@ -186,7 +186,7 @@ post '/site_files/rename' do redirect "/dashboard#{dir_query}" end -get '/site_files/:username.zip' do |username| +get '/site_files/download' do require_login if !current_site.dl_queued_at.nil? && current_site.dl_queued_at > 1.hour.ago diff --git a/views/dashboard.erb b/views/dashboard.erb index 6d4b8a2e..068d1abb 100644 --- a/views/dashboard.erb +++ b/views/dashboard.erb @@ -218,7 +218,7 @@ <% if !current_site.plan_feature(:no_file_restrictions) %> Allowed file types | <% end %> - Download entire site | + Download entire site | <% unless is_education? %> Mount your site as a drive on your computer <% end %> From caecfa3a32ab736beca1b2a8389e72990f8a6aa9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 19 Aug 2023 01:44:22 +0000 Subject: [PATCH 04/74] Bump puma from 5.6.5 to 5.6.7 Bumps [puma](https://github.com/puma/puma) from 5.6.5 to 5.6.7. - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/master/History.md) - [Commits](https://github.com/puma/puma/compare/v5.6.5...v5.6.7) --- updated-dependencies: - dependency-name: puma dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 29be6e13..7e39792a 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ gem 'redis-namespace' gem 'bcrypt' gem 'sinatra-flash', require: 'sinatra/flash' gem 'sinatra-xsendfile', require: 'sinatra/xsendfile' -gem 'puma', '5.6.5', require: nil +gem 'puma', '5.6.7', require: nil gem 'sidekiq', '~> 7.0.8' gem 'mail' gem 'net-smtp' diff --git a/Gemfile.lock b/Gemfile.lock index 21244358..0a8a1d19 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -186,7 +186,7 @@ GEM net-smtp (0.3.3) net-protocol netrc (0.11.0) - nio4r (2.5.8) + nio4r (2.5.9) nokogiri (1.14.3-x86_64-linux) racc (~> 1.4) ox (2.14.11) @@ -197,7 +197,7 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.0) - puma (5.6.5) + puma (5.6.7) nio4r (~> 2.0) racc (1.7.1) rack (2.2.6.4) @@ -357,7 +357,7 @@ DEPENDENCIES paypal-recurring pg pry - puma (= 5.6.5) + puma (= 5.6.7) rack-cache rack-test rack_session_access From 0c8696009f8485c1b10c37611c434247f7a28d1e Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 19 Aug 2023 15:41:32 -0500 Subject: [PATCH 05/74] upgrade to latest puma --- Gemfile | 2 +- Gemfile.lock | 4 ++-- puma_config.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 7e39792a..8c43be89 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ gem 'redis-namespace' gem 'bcrypt' gem 'sinatra-flash', require: 'sinatra/flash' gem 'sinatra-xsendfile', require: 'sinatra/xsendfile' -gem 'puma', '5.6.7', require: nil +gem 'puma', '< 7', require: nil gem 'sidekiq', '~> 7.0.8' gem 'mail' gem 'net-smtp' diff --git a/Gemfile.lock b/Gemfile.lock index 0a8a1d19..a23517f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -197,7 +197,7 @@ GEM coderay (~> 1.1) method_source (~> 1.0) public_suffix (5.0.0) - puma (5.6.7) + puma (6.3.1) nio4r (~> 2.0) racc (1.7.1) rack (2.2.6.4) @@ -357,7 +357,7 @@ DEPENDENCIES paypal-recurring pg pry - puma (= 5.6.7) + puma (< 7) rack-cache rack-test rack_session_access diff --git a/puma_config.rb b/puma_config.rb index 62777654..447cb13d 100644 --- a/puma_config.rb +++ b/puma_config.rb @@ -1,7 +1,6 @@ require 'facter' -threads 1, 1 -#threads 5, 5 +#threads 1, 1 environment 'production' #daemonize pidfile '/var/run/neocities/neocities.pid' @@ -11,3 +10,4 @@ workers Facter.value('processors')['count'] preload_app! on_worker_boot { DB.disconnect } bind 'unix:/var/run/neocities/neocities.sock?backlog=2048' +supported_http_methods Puma::Const::IANA_HTTP_METHODS From 7b7b3c05e1dbad8f3136981079c8585eb54c3ac0 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Tue, 19 Sep 2023 22:07:45 +0000 Subject: [PATCH 06/74] add pls to file whitelist --- models/site.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/site.rb b/models/site.rb index a0cc7290..4a46b799 100644 --- a/models/site.rb +++ b/models/site.rb @@ -44,11 +44,11 @@ class Site < Sequel::Model } VALID_EXTENSIONS = %w{ - html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff woff2 json geojson csv tsv mf ico pdf asc key pgp xml mid midi manifest otf webapp less sass rss kml dae obj mtl scss webp avif xcf epub gltf bin webmanifest knowl atom opml rdf map gpg resolveHandle + html htm txt text css js jpg jpeg png gif svg md markdown eot ttf woff woff2 json geojson csv tsv mf ico pdf asc key pgp xml mid midi manifest otf webapp less sass rss kml dae obj mtl scss webp avif xcf epub gltf bin webmanifest knowl atom opml rdf map gpg resolveHandle pls } VALID_EDITABLE_EXTENSIONS = %w{ - html htm txt js css scss md manifest less webmanifest xml json opml rdf svg gpg pgp resolveHandle + html htm txt js css scss md manifest less webmanifest xml json opml rdf svg gpg pgp resolveHandle pls } MINIMUM_PASSWORD_LENGTH = 5 From f78775fb3fe31b464c392f3cf35dc459fd0ba45a Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 23 Sep 2023 21:28:32 -0500 Subject: [PATCH 07/74] validate all types of btc addresses with tipper --- Gemfile | 1 + Gemfile.lock | 6 +++++ ext/bitcoin_validator.rb | 49 ---------------------------------------- models/site.rb | 2 +- 4 files changed, 8 insertions(+), 50 deletions(-) delete mode 100644 ext/bitcoin_validator.rb diff --git a/Gemfile b/Gemfile index 8c43be89..db764027 100644 --- a/Gemfile +++ b/Gemfile @@ -58,6 +58,7 @@ gem 'rss' gem 'webp-ffi' gem 'rszr' gem 'zip_tricks' +gem 'adequate_crypto_address' group :development, :test do gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index a23517f0..73e62b7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -32,8 +32,12 @@ GEM tzinfo (~> 2.0) addressable (2.8.1) public_suffix (>= 2.0.2, < 6.0) + adequate_crypto_address (0.1.9) + base58 (~> 0.2) + keccak (~> 1.3) ansi (1.5.0) base32 (0.3.4) + base58 (0.2.3) bcrypt (3.1.18) builder (3.2.4) capybara (3.38.0) @@ -142,6 +146,7 @@ GEM io-extra (1.4.0) ipaddress (0.8.3) json (2.6.2) + keccak (1.3.1) llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) @@ -318,6 +323,7 @@ DEPENDENCIES acme-client (~> 2.0.0) activesupport addressable (>= 2.8.0) + adequate_crypto_address apparition! base32 bcrypt diff --git a/ext/bitcoin_validator.rb b/ext/bitcoin_validator.rb deleted file mode 100644 index 3a594334..00000000 --- a/ext/bitcoin_validator.rb +++ /dev/null @@ -1,49 +0,0 @@ -class BitcoinValidator - class << self - def address_version - "00" - end - - def p2sh_version - "05" - end - - def valid_address?(address) - hex = decode_base58(address) rescue nil - return false unless hex && hex.bytesize == 50 - return false unless [address_version, p2sh_version].include?(hex[0...2]) - base58_checksum?(address) - end - - def decode_base58(base58_val) - s = base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? '0'+s : s) - s = '' if s == '00' - leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : '').size - s = ("00"*leading_zero_bytes) + s if leading_zero_bytes > 0 - s - end - - def base58_checksum?(base58) - hex = decode_base58(base58) rescue nil - return false unless hex - checksum( hex[0...42] ) == hex[-8..-1] - end - - def checksum(hex) - b = [hex].pack("H*") # unpack hex - Digest::SHA256.hexdigest( Digest::SHA256.digest(b) )[0...8] - end - - - def base58_to_int(base58_val) - alpha = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - int_val, base = 0, alpha.size - base58_val.reverse.each_char.with_index do |char,index| - raise ArgumentError, 'Value not a valid Base58 String.' unless char_index = alpha.index(char) - int_val += char_index*(base**index) - end - int_val - end - - end -end diff --git a/models/site.rb b/models/site.rb index 4a46b799..6d886cd8 100644 --- a/models/site.rb +++ b/models/site.rb @@ -1119,7 +1119,7 @@ class Site < Sequel::Model errors.add :tipping_paypal, 'A valid PayPal tipping email address is required.' end - if !values[:tipping_bitcoin].blank? && !BitcoinValidator.valid_address?(values[:tipping_bitcoin]) + if !values[:tipping_bitcoin].blank? && !AdequateCryptoAddress.valid?(values[:tipping_bitcoin], 'BTC') errors.add :tipping_bitcoin, 'Bitcoin tipping address is not valid.' end From 5dfbc7a91aa85c3295b99d521544162bf3e80c14 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 1 Oct 2023 18:36:31 +0000 Subject: [PATCH 08/74] increase jerk block threshold --- models/site.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/site.rb b/models/site.rb index 6d886cd8..d79c943b 100644 --- a/models/site.rb +++ b/models/site.rb @@ -140,7 +140,7 @@ class Site < Sequel::Model DISPOSABLE_EMAIL_BLACKLIST_PATH = File.join(DIR_ROOT, 'files', 'disposable_email_blacklist.conf') BANNED_EMAIL_BLACKLIST_PATH = File.join(DIR_ROOT, 'files', 'banned_email_blacklist.conf') - BLOCK_JERK_THRESHOLD = 4 + BLOCK_JERK_THRESHOLD = 25 MAXIMUM_TAGS = 5 MAX_USERNAME_LENGTH = 32.freeze From 143704215faceb2e9b27abf9f15d4f4393716469 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Thu, 9 Nov 2023 14:55:48 -0600 Subject: [PATCH 09/74] first pass at phone validation --- Gemfile | 2 + Gemfile.lock | 8 ++++ app.rb | 2 + app/create.rb | 1 + app/site.rb | 58 +++++++++++++++++++++++++++ environment.rb | 2 + migrations/119_verify_phone.rb | 15 +++++++ models/site.rb | 5 +++ views/site/confirm_email.erb | 2 +- views/site/confirm_phone.erb | 73 ++++++++++++++++++++++++++++++++++ 10 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 migrations/119_verify_phone.rb create mode 100644 views/site/confirm_phone.erb diff --git a/Gemfile b/Gemfile index 8c43be89..c426ed08 100644 --- a/Gemfile +++ b/Gemfile @@ -58,6 +58,8 @@ gem 'rss' gem 'webp-ffi' gem 'rszr' gem 'zip_tricks' +gem 'twilio-ruby' +gem 'phonelib' group :development, :test do gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index a23517f0..3f0a6e1c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,6 +142,7 @@ GEM io-extra (1.4.0) ipaddress (0.8.3) json (2.6.2) + jwt (2.7.1) llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) rake (~> 13.0) @@ -192,6 +193,7 @@ GEM ox (2.14.11) paypal-recurring (1.1.0) pg (1.5.3) + phonelib (0.8.3) progress (3.6.0) pry (0.14.1) coderay (~> 1.1) @@ -286,6 +288,10 @@ GEM timeout (0.3.0) tins (1.31.1) sync + twilio-ruby (6.3.1) + faraday (>= 0.9, < 3.0) + jwt (>= 1.5, < 3.0) + nokogiri (>= 1.6, < 2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unf (0.1.4) @@ -356,6 +362,7 @@ DEPENDENCIES nokogiri paypal-recurring pg + phonelib pry puma (< 7) rack-cache @@ -385,6 +392,7 @@ DEPENDENCIES thread tilt timecop + twilio-ruby webmock webp-ffi will_paginate diff --git a/app.rb b/app.rb index f5a9d9b1..82d621f3 100644 --- a/app.rb +++ b/app.rb @@ -77,6 +77,8 @@ before do # Skips the CSRF/validation check for stripe web hooks elsif email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/signout|^\/welcome|^\/supporter/) redirect "/site/#{current_site.username}/confirm_email" + elsif current_site && current_site.phone_verification_needed? && !(request.path =~ /^\/site\/.+\/confirm_phone/) + redirect "/site/#{current_site.username}/confirm_phone" else content_type :html, 'charset' => 'utf-8' redirect '/' if request.post? && !csrf_safe? diff --git a/app/create.rb b/app/create.rb index 80c38269..5dfbb3de 100644 --- a/app/create.rb +++ b/app/create.rb @@ -98,6 +98,7 @@ post '/create' do end @site.email_confirmed = true if self.class.development? + #@site.phone_confirmed = true if self.class.development? @site.save unless education_whitelisted? diff --git a/app/site.rb b/app/site.rb index 18e2ce89..546dfd0e 100644 --- a/app/site.rb +++ b/app/site.rb @@ -295,4 +295,62 @@ get '/site/:username/unblock' do |username| current_site.unblock! site redirect request.referer +end + +get '/site/:username/confirm_phone' do + require_login + redirect '/' unless current_site.phone_verification_needed? + @title = 'Verify your Phone Number' + erb :'site/confirm_phone' +end + +post '/site/:username/confirm_phone' do + require_login + redirect '/' unless current_site.phone_verification_needed? + + if params[:phone_intl] + phone = Phonelib.parse params[:phone_intl] + + if !phone.valid? + flash[:error] = "Invalid phone number, please try again." + redirect "/site/#{current_site.username}/confirm_phone" + end + + if phone.types.include?(:premium_rate) || phone.types.include?(:shared_cost) + flash[:error] = 'Neocities does not support this type of number, please use another number.' + redirect "/site/#{current_site.username}/confirm_phone" + end + + current_site.phone_verification_sent_at = Time.now + current_site.save_changes validate: false + + verification = $twilio.verify + .v2 + .services($config['twilio_service_sid']) + .verifications + .create(to: phone.e164, channel: 'sms') + + current_site.phone_verification_sid = verification.sid + current_site.save_changes validate: false + + flash[:success] = 'Validation message sent! Check your phone and enter the code below.' + else + # Check code + vc = $twilio.verify + .v2 + .services($config['twilio_service_sid']) + .verification_checks + .create(verification_sid: current_site.phone_verification_sid, code: params[:code]) + + # puts vc.status (pending if failed, approved if it passed) + if vc.status == 'approved' + current_site.phone_verified = true + current_site.save_changes validate: false + else + flash[:error] = 'Code was not correct, please re-enter.' + end + end + + # Will redirect to / automagically if phone was verified + redirect "/site/#{current_site.username}/confirm_phone" end \ No newline at end of file diff --git a/environment.rb b/environment.rb index de6d5231..cbcba539 100644 --- a/environment.rb +++ b/environment.rb @@ -177,3 +177,5 @@ $image_optim = ImageOptim.new pngout: false, svgo: false Money.locale_backend = nil Money.default_currency = Money::Currency.new("USD") Money.rounding_mode = BigDecimal::ROUND_HALF_UP + +$twilio = Twilio::REST::Client.new $config['twilio_account_sid'], $config['twilio_auth_token'] diff --git a/migrations/119_verify_phone.rb b/migrations/119_verify_phone.rb new file mode 100644 index 00000000..206e1cae --- /dev/null +++ b/migrations/119_verify_phone.rb @@ -0,0 +1,15 @@ +Sequel.migration do + up { + DB.add_column :sites, :phone_verification_required, :boolean, default: false + DB.add_column :sites, :phone_verified, :boolean, default: false + DB.add_column :sites, :phone_verification_sid, :text + DB.add_column :sites, :phone_verification_sent_at, :time + } + + down { + DB.drop_column :sites, :phone_verification_required + DB.drop_column :sites, :phone_verified + DB.drop_column :sites, :phone_verification_sid + DB.drop_column :sites, :phone_verification_sent_at + } +end \ No newline at end of file diff --git a/models/site.rb b/models/site.rb index a0cc7290..30864524 100644 --- a/models/site.rb +++ b/models/site.rb @@ -1789,6 +1789,11 @@ class Site < Sequel::Model end end + def phone_verification_needed? + return true if phone_verification_required && !phone_verified + false + end + private def store_file(path, uploaded, opts={}) diff --git a/views/site/confirm_email.erb b/views/site/confirm_email.erb index 2a2ab660..553b928d 100644 --- a/views/site/confirm_email.erb +++ b/views/site/confirm_email.erb @@ -6,7 +6,7 @@ You're almost ready!
<% end %> - We sent an email to <%= current_site.email %> to make sure it's correct.
Please check your email, enter the confirmation code here, and you're all set. + We sent an email to <%= current_site.email %> to make sure it's correct.
Please check your email, and enter the confirmation code here.
diff --git a/views/site/confirm_phone.erb b/views/site/confirm_phone.erb new file mode 100644 index 00000000..ba0287c0 --- /dev/null +++ b/views/site/confirm_phone.erb @@ -0,0 +1,73 @@ +
+

Verify your phone number

+
+

+ You're almost ready!
+ To prevent spam and keep the searchability of your site high, we have one last step: +
please verify your mobile phone number. +

+ +
+
+ <% if flash[:success] %> +
+ <%== flash[:success] %> +
+ <% end %> + + <% if flash[:error] %> +
+ <%== flash[:error] %> +
+ <% end %> + +
+ <%== csrf_token_input_html %> + + <% if current_site.phone_verification_sid %> +
+ + +
+ + <% else %> + +
+ + + +
+ + + + + + + + <% end %> + +
+
+
+ +
From 40e848e2c0a0323bb8609acf1a8d8d068dc4e926 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Fri, 10 Nov 2023 11:53:43 -0600 Subject: [PATCH 10/74] phone validation: bugfixes, tweaks and refinements --- app/site.rb | 46 +++++++++++++++++++++-------- config.yml.ci | 5 +++- config.yml.template | 3 ++ migrations/120_fix_phone_sent_at.rb | 11 +++++++ models/site.rb | 3 ++ views/site/confirm_phone.erb | 6 ++-- 6 files changed, 58 insertions(+), 16 deletions(-) create mode 100644 migrations/120_fix_phone_sent_at.rb diff --git a/app/site.rb b/app/site.rb index 546dfd0e..412a9af6 100644 --- a/app/site.rb +++ b/app/site.rb @@ -304,6 +304,13 @@ get '/site/:username/confirm_phone' do erb :'site/confirm_phone' end +def restart_phone_verification + current_site.phone_verification_sent_at = nil + current_site.phone_verification_sid = nil + current_site.save_changes validate: false + redirect "/site/#{current_site.username}/confirm_phone" +end + post '/site/:username/confirm_phone' do require_login redirect '/' unless current_site.phone_verification_needed? @@ -335,19 +342,34 @@ post '/site/:username/confirm_phone' do flash[:success] = 'Validation message sent! Check your phone and enter the code below.' else - # Check code - vc = $twilio.verify - .v2 - .services($config['twilio_service_sid']) - .verification_checks - .create(verification_sid: current_site.phone_verification_sid, code: params[:code]) - # puts vc.status (pending if failed, approved if it passed) - if vc.status == 'approved' - current_site.phone_verified = true - current_site.save_changes validate: false - else - flash[:error] = 'Code was not correct, please re-enter.' + restart_phone_verification if current_site.phone_verification_sent_at < Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME + minutes_remaining = ((current_site.phone_verification_sent_at - (Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME))/60).round + + begin + # Check code + vc = $twilio.verify + .v2 + .services($config['twilio_service_sid']) + .verification_checks + .create(verification_sid: current_site.phone_verification_sid, code: params[:code]) + + # puts vc.status (pending if failed, approved if it passed) + if vc.status == 'approved' + current_site.phone_verified = true + current_site.save_changes validate: false + else + flash[:error] = "Code was not correct, please try again. If the phone number you entered was incorrect, you can re-enter the number after #{minutes_remaining} more minutes have passed." + end + + rescue Twilio::REST::RestError => e + if e.message =~ /60202/ + flash[:error] = "You have exhausted your check attempts. Please try again in #{minutes_remaining} minutes." + elsif e.message =~ /20404/ # Unable to create record + restart_phone_verification + else + raise e + end end end diff --git a/config.yml.ci b/config.yml.ci index 7425457d..7587484e 100644 --- a/config.yml.ci +++ b/config.yml.ci @@ -20,4 +20,7 @@ cache_control_ips: - 1.2.3.4 - 4.5.6.7 hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001" -hcaptcha_secret_key: "0x0000000000000000000000000000000000000000" \ No newline at end of file +hcaptcha_secret_key: "0x0000000000000000000000000000000000000000" +twilio_account_sid: ACEDERPDERP +twilio_auth_token: derpderpderp +twilio_service_sid: VADERPDERPDERP \ No newline at end of file diff --git a/config.yml.template b/config.yml.template index be470036..1fc968fc 100644 --- a/config.yml.template +++ b/config.yml.template @@ -55,3 +55,6 @@ test: cache_control_ips: - 1.2.3.4 - 4.5.6.7 + twilio_account_sid: ACEDERPDERP + twilio_auth_token: derpderpderp + twilio_service_sid: VADERPDERPDERP \ No newline at end of file diff --git a/migrations/120_fix_phone_sent_at.rb b/migrations/120_fix_phone_sent_at.rb new file mode 100644 index 00000000..a5d3a937 --- /dev/null +++ b/migrations/120_fix_phone_sent_at.rb @@ -0,0 +1,11 @@ +Sequel.migration do + up { + DB.drop_column :sites, :phone_verification_sent_at + DB.add_column :sites, :phone_verification_sent_at, Time + } + + down { + DB.drop_column :sites, :phone_verification_sent_at + DB.add_column :sites, :phone_verification_sent_at, :time + } +end \ No newline at end of file diff --git a/models/site.rb b/models/site.rb index 30864524..b819fc3d 100644 --- a/models/site.rb +++ b/models/site.rb @@ -167,6 +167,9 @@ class Site < Sequel::Model BLACK_BOX_WAIT_TIME = 10.seconds MAX_DISPLAY_FOLLOWS = 56*3 + PHONE_VERIFICATION_EXPIRATION_TIME = 10.minutes + PHONE_VERIFICATION_LOCKOUT_ATTEMPTS = 3 + many_to_many :tags one_to_many :profile_comments diff --git a/views/site/confirm_phone.erb b/views/site/confirm_phone.erb index ba0287c0..c29f277b 100644 --- a/views/site/confirm_phone.erb +++ b/views/site/confirm_phone.erb @@ -2,7 +2,7 @@

Verify your phone number

- You're almost ready!
+ Last thing!
To prevent spam and keep the searchability of your site high, we have one last step:
please verify your mobile phone number.

@@ -27,13 +27,13 @@ <% if current_site.phone_verification_sid %>
- +
<% else %>
- +
From 7f05c2c9dcfce96ef3f96138de4db67c596407cf Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Fri, 10 Nov 2023 13:30:05 -0600 Subject: [PATCH 11/74] code input validation, lockout after 3 attempts --- app/create.rb | 15 ++++++++++++++- app/site.rb | 7 +++++++ views/site/confirm_phone.erb | 23 ++++++++++++++++++++--- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/create.rb b/app/create.rb index 5dfbb3de..aea33991 100644 --- a/app/create.rb +++ b/app/create.rb @@ -98,7 +98,20 @@ post '/create' do end @site.email_confirmed = true if self.class.development? - #@site.phone_confirmed = true if self.class.development? + @site.phone_confirmed = true if self.class.development? + + begin + @site.phone_verification_required = true if self.class.production? && BlackBox.phone_verification_required?(site) + rescue => e + EmailWorker.perform_async({ + from: 'web@neocities.org', + to: 'errors@neocities.org', + subject: "[Neocities Error] Phone verification exception", + body: "#{e.inspect}\n#{e.backtrace}", + no_footer: true + }) + end + @site.save unless education_whitelisted? diff --git a/app/site.rb b/app/site.rb index 412a9af6..85f019de 100644 --- a/app/site.rb +++ b/app/site.rb @@ -329,6 +329,13 @@ post '/site/:username/confirm_phone' do end current_site.phone_verification_sent_at = Time.now + current_site.phone_verification_attempts += 1 + + if current_site.phone_verification_attempts > Site::PHONE_VERIFICATION_LOCKOUT_ATTEMPTS + flash[:error] = 'You have exceeded the number of phone verification attempts allowed.' + redirect "/site/#{current_site.username}/confirm_phone" + end + current_site.save_changes validate: false verification = $twilio.verify diff --git a/views/site/confirm_phone.erb b/views/site/confirm_phone.erb index c29f277b..b055b488 100644 --- a/views/site/confirm_phone.erb +++ b/views/site/confirm_phone.erb @@ -26,10 +26,27 @@ <% if current_site.phone_verification_sid %>
- - + +
- + + + + <% else %>
From 3ee578e6963b38db468187378d5e12d965865125 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Fri, 10 Nov 2023 13:30:48 -0600 Subject: [PATCH 12/74] phone verification attempts migration --- migrations/121_phone_verification_attempts.rb | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 migrations/121_phone_verification_attempts.rb diff --git a/migrations/121_phone_verification_attempts.rb b/migrations/121_phone_verification_attempts.rb new file mode 100644 index 00000000..5f8e4277 --- /dev/null +++ b/migrations/121_phone_verification_attempts.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_column :sites, :phone_verification_attempts, :integer, default: 0 + } + + down { + DB.drop_column :sites, :phone_verification_attempts + } +end \ No newline at end of file From 2e1a8af30d3320d4d6d11b7c201a8d75d60d8219 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 11 Nov 2023 10:07:01 -0600 Subject: [PATCH 13/74] fix for verify, ask to check spam folder in email confirm --- app/create.rb | 2 +- views/site/confirm_email.erb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/create.rb b/app/create.rb index aea33991..69066662 100644 --- a/app/create.rb +++ b/app/create.rb @@ -101,7 +101,7 @@ post '/create' do @site.phone_confirmed = true if self.class.development? begin - @site.phone_verification_required = true if self.class.production? && BlackBox.phone_verification_required?(site) + @site.phone_verification_required = true if self.class.production? && BlackBox.phone_verification_required?(@site) rescue => e EmailWorker.perform_async({ from: 'web@neocities.org', diff --git a/views/site/confirm_email.erb b/views/site/confirm_email.erb index 553b928d..1418649a 100644 --- a/views/site/confirm_email.erb +++ b/views/site/confirm_email.erb @@ -6,7 +6,9 @@ You're almost ready!
<% end %> - We sent an email to <%= current_site.email %> to make sure it's correct.
Please check your email, and enter the confirmation code here. + We sent an email to <%= current_site.email %> to make sure it's correct.
+ Please check your email, and enter the confirmation code here.
+ If you don't see the email in your inbox, try looking in the spam folder.
From ffaca51d3602cdaef7ee8eeab71545965a922d8a Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 11 Nov 2023 15:11:34 -0600 Subject: [PATCH 14/74] spam inbox check comment for password reset --- app/password_reset.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/password_reset.rb b/app/password_reset.rb index aa5a9550..7312a839 100644 --- a/app/password_reset.rb +++ b/app/password_reset.rb @@ -42,7 +42,7 @@ the Neocities Cat end end - flash[:success] = 'If your email was valid (and used by a site), the Neocities Cat will send an e-mail to your account with password reset instructions.' + flash[:success] = "We sent an e-mail with password reset instructions. Check your spam folder if you don't see it in your inbox." redirect '/' end From 7f354bf8f63f4ddf0f20258113d488dcc5e99666 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 11 Nov 2023 21:30:14 -0600 Subject: [PATCH 15/74] fix redirect logic --- app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.rb b/app.rb index 82d621f3..4717fcf2 100644 --- a/app.rb +++ b/app.rb @@ -77,7 +77,7 @@ before do # Skips the CSRF/validation check for stripe web hooks elsif email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/signout|^\/welcome|^\/supporter/) redirect "/site/#{current_site.username}/confirm_email" - elsif current_site && current_site.phone_verification_needed? && !(request.path =~ /^\/site\/.+\/confirm_phone/) + elsif !email_not_validated? && current_site && current_site.phone_verification_needed? && !(request.path =~ /^\/site\/.+\/confirm_phone/) redirect "/site/#{current_site.username}/confirm_phone" else content_type :html, 'charset' => 'utf-8' From 9fa4cc0e1394dfebf3be0eaf00bd4623d215593f Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Mon, 13 Nov 2023 13:18:11 -0600 Subject: [PATCH 16/74] fix signout, add press article, add dns bl client --- Gemfile | 1 + Gemfile.lock | 2 ++ app.rb | 4 ++-- views/press.erb | 14 +++++++++++--- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Gemfile b/Gemfile index 7481b8a6..6bec7d7d 100644 --- a/Gemfile +++ b/Gemfile @@ -61,6 +61,7 @@ gem 'zip_tricks' gem 'adequate_crypto_address' gem 'twilio-ruby' gem 'phonelib' +gem 'dnsbl-client' group :development, :test do gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index 8dddcfa3..d60848b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -63,6 +63,7 @@ GEM rexml crass (1.0.6) dante (0.2.0) + dnsbl-client (1.1.1) docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) @@ -337,6 +338,7 @@ DEPENDENCIES certified coveralls_reborn dav4rack! + dnsbl-client erubis fabrication facter diff --git a/app.rb b/app.rb index 4717fcf2..ffa14a55 100644 --- a/app.rb +++ b/app.rb @@ -75,9 +75,9 @@ before do content_type :json elsif request.path.match /^\/webhooks\// # Skips the CSRF/validation check for stripe web hooks - elsif email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/signout|^\/welcome|^\/supporter/) + elsif email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/signout|^\/welcome|^\/supporter|^\/signout/) redirect "/site/#{current_site.username}/confirm_email" - elsif !email_not_validated? && current_site && current_site.phone_verification_needed? && !(request.path =~ /^\/site\/.+\/confirm_phone/) + elsif !email_not_validated? && current_site && current_site.phone_verification_needed? && !(request.path =~ /^\/site\/.+\/confirm_phone|^\/signout/) redirect "/site/#{current_site.username}/confirm_phone" else content_type :html, 'charset' => 'utf-8' diff --git a/views/press.erb b/views/press.erb index d4851608..91f71e6e 100644 --- a/views/press.erb +++ b/views/press.erb @@ -13,6 +13,11 @@
+

TechSpot: Neocities is bringing the eye-bleeding "spirit" of GeoCities back to the modern web

+
+ Neocities is yet another alternative to social networking and pure nostalgia trips down memory lane. It offers a hosting space for hundreds of thousands of websites that don't need to comply with static rules or well-defined design policies to be online. Neocities introduces itself as a "social network" that brings back the "lost individual creativity of the web." +
+

Hosting Advice: How a Blank-Canvas, Static Hosting Approach Empowers Site Owners to Showcase Their Creativity Online

Neocities has acquired upward of 100,000 users in its relatively short lifespan, which is a testament to its focus on creative web design and its offer of cost-free hosting without host-branded ads — a service that’s the exception rather than the rule in the industry. By embracing ingenuity and a templateless-approach, the organization has effectively picked up where early hosts have left off — with a crucial difference. Neocities provides the modern tools, such as an in-browser HTML editor and a command line prompt, among other features, that make web development a bit more accessible to today’s crop of web visionaries. @@ -57,17 +62,20 @@ Why We All Need to Make the Internet Fun Again

Fast Company: Oh, Snap! '90s Web Design Is Hot Again

“What I'm trying to do with Neocities is re-enable that creativity, and show people that it isn’t just a nostalgia thing any more,” [Kyle Drake] says, adding that the site now hosts about 26,000 sites and has proven financially self-sustaining.
-

Wired: NeoCities Wants to Save Us From the Crushing Boredom of Social Networking

+

Wired: Neocities Wants to Save Us From the Crushing Boredom of Social Networking

There needs to be an alternative to the current pre-formatted, template-driven, standardizing platforms, which make it easy to have a web presence, but hard to make that presence your own.
-

Vice: NeoCities Is Recreating the Garish, Web 1.0 Creativity of Geocities

+

Vice: Neocities Is Recreating the Garish, Web 1.0 Creativity of Geocities

The project is a way to recreate not only the aesthetic of the early personal websites, but also the original mission of Geocities: to give anyone with internet access a free place on the web.

Ars Technica: Web host gives FCC a 28.8Kbps slow lane in net neutrality protest

-
Lots of people are angry about FCC Chairman Tom Wheeler's Internet "fast lane" proposal that would let Internet service providers charge Web services for priority access to consumers. But one Web hosting service called NeoCities isn't just writing letters to the FCC. Instead, the company found the FCC's internal IP address range and throttled all connections to 28.8Kbps speeds.
+
Lots of people are angry about FCC Chairman Tom Wheeler's Internet "fast lane" proposal that would let Internet service providers charge Web services for priority access to consumers. But one Web hosting service called Neocities isn't just writing letters to the FCC. Instead, the company found the FCC's internal IP address range and throttled all connections to 28.8Kbps speeds.
+ +

When writing about us, we prefer "Neocities" over "NeoCities". Thank you!

+

Contact Us

Contact Form

From c31b45575f53c67757ce119a9d79058210c46935 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 18 Nov 2023 10:48:12 -0600 Subject: [PATCH 17/74] ability to temp disable create --- app/index.rb | 2 ++ views/index.erb | 80 +++++++++++++++++++++++++------------------------ 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/app/index.rb b/app/index.rb index b779346b..0fd765ec 100644 --- a/app/index.rb +++ b/app/index.rb @@ -80,6 +80,8 @@ get '/?' do @blog_feed_html = SimpleCache.get :blog_feed_html end + @create_disabled = defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(request) + erb :index, layout: :index_layout end diff --git a/views/index.erb b/views/index.erb index 4adeffed..8b00ca41 100644 --- a/views/index.erb +++ b/views/index.erb @@ -96,54 +96,56 @@

Sign up for free


-
- - - - - + <% if @create_disabled %> +

Sign up is not currently available, please try again later.

+ <% else %> +
+ + + + + - - + + -
- - -
+
+ + +
-
- - -
+
+ + +
-
- - <%== hcaptcha_input %> -
+
+ + <%== hcaptcha_input %> +
-
-
- +
+
+ +
-
- + <% end %>
- <% end %>
From 0f860cfdcc5fdccd68a1a5743a97b2aa7c7044db Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 18 Nov 2023 10:53:48 -0600 Subject: [PATCH 18/74] move order of delete_all_cache on undelete to bottom --- models/site.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/site.rb b/models/site.rb index 46060a75..460b31c9 100644 --- a/models/site.rb +++ b/models/site.rb @@ -516,8 +516,8 @@ class Site < Sequel::Model save_changes } - delete_all_cache update_redis_proxy_record + delete_all_cache true end From 4c3daea4a885248e9e2e600e704b9d121a67f92a Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 18 Nov 2023 10:59:17 -0600 Subject: [PATCH 19/74] delete_all_cache for destroy and not just ban --- models/site.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/models/site.rb b/models/site.rb index 460b31c9..2b9b4db0 100644 --- a/models/site.rb +++ b/models/site.rb @@ -504,6 +504,7 @@ class Site < Sequel::Model def after_destroy update_redis_proxy_record + delete_all_cache end def undelete! @@ -543,8 +544,6 @@ class Site < Sequel::Model self.banned_at = Time.now save validate: false destroy - - delete_all_cache end def ban_all_sites_on_account! @@ -989,11 +988,6 @@ class Site < Sequel::Model parent_site_id.nil? end -# def after_destroy -# FileUtils.rm_rf files_path -# super -# end - def ssl_installed? !domain.blank? && !ssl_key.blank? && !ssl_cert.blank? end From 7b9107393b3a559fb1fc84cb46c27bd4d2edcceb Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Tue, 21 Nov 2023 16:57:49 -0600 Subject: [PATCH 20/74] use purge_all_cache instread of delete_all_cache --- app/settings.rb | 4 ++-- models/site.rb | 12 ++++-------- models/site_file.rb | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/app/settings.rb b/app/settings.rb index e343582f..4e773e00 100644 --- a/app/settings.rb +++ b/app/settings.rb @@ -92,8 +92,8 @@ post '/settings/:username/change_name' do } old_site.delete_all_thumbnails_and_screenshots - old_site.delete_all_cache - @site.delete_all_cache + old_site.purge_all_cache + @site.purge_all_cache @site.regenerate_thumbnails_and_screenshots flash[:success] = "Site/user name has been changed. You will need to use this name to login, don't forget it!" diff --git a/models/site.rb b/models/site.rb index 2b9b4db0..91d44fe5 100644 --- a/models/site.rb +++ b/models/site.rb @@ -504,7 +504,7 @@ class Site < Sequel::Model def after_destroy update_redis_proxy_record - delete_all_cache + purge_all_cache end def undelete! @@ -518,7 +518,7 @@ class Site < Sequel::Model } update_redis_proxy_record - delete_all_cache + purge_all_cache true end @@ -768,16 +768,12 @@ class Site < Sequel::Model end end - def delete_all_cache + def purge_all_cache site_files.each do |site_file| - delete_cache site_file.path + purge_cache site_file.path end end - def delete_cache(path) - purge_cache path - end - #Rye::Cmd.add_command :ipfs def add_to_ipfs diff --git a/models/site_file.rb b/models/site_file.rb index 28136ed1..8e65ba43 100644 --- a/models/site_file.rb +++ b/models/site_file.rb @@ -105,7 +105,7 @@ class SiteFile < Sequel::Model DB['update sites set space_used=space_used-? where id=?', size, site_id].first end - site.delete_cache site.files_path(path) + site.purge_cache site.files_path(path) SiteChangeFile.filter(site_id: site_id, filename: path).delete end end From 0d8684704fb3139ad96e0bd5d6aec48d5b60cc3c Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Tue, 21 Nov 2023 17:11:40 -0600 Subject: [PATCH 21/74] create disabled check to post, minfraud --- app/create.rb | 5 +++++ app/index.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/create.rb b/app/create.rb index 69066662..f55dc679 100644 --- a/app/create.rb +++ b/app/create.rb @@ -91,6 +91,11 @@ post '/create' do return {result: 'error'}.to_json end + if defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(site, request) + flash[:error] = 'Site creation is currently unavailable, please try again later.' + return {result: 'error'}.to_json + end + if !@site.valid? flash[:error] = @site.errors.first.last.first return {result: 'error'}.to_json diff --git a/app/index.rb b/app/index.rb index 0fd765ec..ceafe5d6 100644 --- a/app/index.rb +++ b/app/index.rb @@ -80,7 +80,7 @@ get '/?' do @blog_feed_html = SimpleCache.get :blog_feed_html end - @create_disabled = defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(request) + @create_disabled = false erb :index, layout: :index_layout end From 84a23e35ba5dc9c9647d3bb926dc10a7ff50ead0 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Wed, 22 Nov 2023 00:09:36 -0600 Subject: [PATCH 22/74] add minfraud --- Gemfile | 1 + Gemfile.lock | 10 ++++++++++ config.yml.ci | 4 +++- config.yml.template | 6 +++++- environment.rb | 6 ++++++ 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 6bec7d7d..ebcf6234 100644 --- a/Gemfile +++ b/Gemfile @@ -62,6 +62,7 @@ gem 'adequate_crypto_address' gem 'twilio-ruby' gem 'phonelib' gem 'dnsbl-client' +gem 'minfraud' group :development, :test do gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index d60848b8..804439d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -164,10 +164,19 @@ GEM mini_mime (>= 0.1.1) matrix (0.4.2) maxmind-db (1.1.1) + maxmind-geoip2 (1.1.0) + connection_pool (~> 2.2) + http (>= 4.3, < 6.0) + maxmind-db (~> 1.1) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) + minfraud (2.2.0) + connection_pool (~> 2.2) + http (>= 4.3, < 6.0) + maxmind-geoip2 (~> 1.1) + simpleidn (~> 0.1, >= 0.1.1) mini_mime (1.1.2) minitest (5.16.3) minitest-reporters (1.5.0) @@ -360,6 +369,7 @@ DEPENDENCIES magic mail maxmind-db + minfraud minitest minitest-reporters mocha diff --git a/config.yml.ci b/config.yml.ci index 7587484e..1df18a6f 100644 --- a/config.yml.ci +++ b/config.yml.ci @@ -23,4 +23,6 @@ hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001" hcaptcha_secret_key: "0x0000000000000000000000000000000000000000" twilio_account_sid: ACEDERPDERP twilio_auth_token: derpderpderp -twilio_service_sid: VADERPDERPDERP \ No newline at end of file +twilio_service_sid: VADERPDERPDERP +minfraud_account_id: 696969420 +minfraud_license_key: DERPDERPDERP \ No newline at end of file diff --git a/config.yml.template b/config.yml.template index 1fc968fc..260c0397 100644 --- a/config.yml.template +++ b/config.yml.template @@ -18,6 +18,8 @@ development: paypal_api_signature: tonz letsencrypt_key: ./tests/files/letsencrypt.key letsencrypt_endpoint: https://acme-staging.api.letsencrypt.org/ + minfraud_account_id: 696969420 + minfraud_license_key: DERPDERPDERP proxy_ips: - 10.0.0.1 - 10.0.0.2 @@ -57,4 +59,6 @@ test: - 4.5.6.7 twilio_account_sid: ACEDERPDERP twilio_auth_token: derpderpderp - twilio_service_sid: VADERPDERPDERP \ No newline at end of file + twilio_service_sid: VADERPDERPDERP + minfraud_account_id: 696969420 + minfraud_license_key: DERPDERPDERP \ No newline at end of file diff --git a/environment.rb b/environment.rb index cbcba539..35a4b743 100644 --- a/environment.rb +++ b/environment.rb @@ -179,3 +179,9 @@ Money.default_currency = Money::Currency.new("USD") Money.rounding_mode = BigDecimal::ROUND_HALF_UP $twilio = Twilio::REST::Client.new $config['twilio_account_sid'], $config['twilio_auth_token'] + +Minfraud.configure do |c| + c.account_id = $config['minfraud_account_id'] + c.license_key = $config['minfraud_license_key'] + c.enable_validation = true +end From 7c04f53af17abca8e0389a1c329b00deb5a528ec Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 26 Nov 2023 11:06:50 -0600 Subject: [PATCH 23/74] update copy, add faq answers, require check for faq viewing --- app/contact.rb | 4 ++ views/contact.erb | 168 ++++++++++++++++++++++++++++++++-------------- views/legal.erb | 2 +- 3 files changed, 122 insertions(+), 52 deletions(-) diff --git a/app/contact.rb b/app/contact.rb index 989ae1dd..907965f5 100644 --- a/app/contact.rb +++ b/app/contact.rb @@ -9,6 +9,10 @@ post '/contact' do @errors << 'Please fill out all fields' end + if params[:faq_check] == 'no' + @errors << 'Please check Frequently Asked Questions before sending a contact message' + end + unless hcaptcha_valid? @errors << 'Captcha was not filled out (or was filled out incorrectly)' end diff --git a/views/contact.erb b/views/contact.erb index 54628f16..bda13a7d 100644 --- a/views/contact.erb +++ b/views/contact.erb @@ -13,15 +13,43 @@
<% end %> -

Contact

-

- Please note that we can only respond to english inquiries. + Please note that we can only respond to messages in english and klingon. Thank you!

-

- Frequently asked questions: -

+

Frequently Asked Questions

+ +

Sites / Editing

+ +
+ +
+
+

+ This is almost always because you used a different email address to create the site (check all your email addresses), or because the email has somehow been put in the spam folder of your email client. +

+
+
+
+ +
+ +
+
+

+ For security reasons, we cannot reset your password if you don't have control of your email address, because there is no way to know if you're the legitimate owner or an attacker. You will have to make a new site (don’t worry, it’s free!). If you didn’t get an email from the password reset form, check your spam folder. +

+
+
+
@@ -75,15 +103,17 @@
-
+

- For security reasons, we cannot reset your password if you did not enter an email for your site. You will have to make a new site (don’t worry, it’s free!). If you didn’t get an email from the password reset form, you didn’t enter an email (or it’s in your spam folder). Again you will have to make a new site; we cannot help you for security reasons. + Hits are number of requests for any file on a site, views count for one unique IP address per hour. Views provide better insight into how many individuals (or search engines, etc) are looking at your site. There is no such thing as perfectly accurate stats, so this doesn't necessarily reflect how many actual people are viewing your site, but it does provide a rough way to see how popular your site is becoming. If you put a hit counter on your site, it may give a different number than what Neocities records and that is expected.

+ +

We don't provide a way to look at individual visitor records on Neocities, the main reason being that the information isn't very useful anymore, since most sources block HTTP Referer headers, making it difficult to determine origin.

@@ -97,7 +127,7 @@

- We don't support FTP, sorry! Aside from security problems with FTP, it isn't used by many people anymore. We need to stay focused on what we work on, so we only support ways to upload content that most people use these days. + We don't support FTP, apologies! Aside from security problems with FTP, it isn't used by many people anymore. We need to stay focused on what we work on, so we only support ways to upload content that most people use these days.

@@ -126,6 +156,73 @@

+
+ +
+
+

+ For security reasons, we can't. But you can delete your own site by logging in and going to the settings page! +

+ +

+ For safety reasons, if you have more than one site, you'll need to delete all sites before deleting the main account. Click on "Manage Site Settings" to delete each site individually. +

+
+
+
+ + +
+ +
+
+

+ Neocities does not have a policy of transferring inactive sites to different owners and can't make manual exceptions at the moment. This is a tricky subject because we don't have a way to be sure if a user has actually abandoned their site, even if it's just the default "new site" template. We may have a different policy in the future regarding this. +

+
+
+
+ +
+ +
+
+

+ We don't currently support certain types of files, particularly multimedia files and WebASM with free sites because of persistent issues with abuse (piracy, bitcoin mining, excessive bandwidth usage). In general, these files are not conducive to Neocities' mission of bringing back personal home pages vs being a piracy and webapp hosting platform. You can embed audio and videos through other providers like YouTube, which is preferred because they will automatically provide the streaming media in formats that work better for your specific users' devices (such as mobile phones). +

+
+
+
+
+ +
+
+

+ We recommend using an "old web browser" proxy server like WebOne or WRP. These translate between modern browser + standards and old browser standards, allowing for us to provide the speed and security of the new web without compromising the ability to use old browsers. +

+
+
+
+ +

Legal / Abuse

+
@@ -150,10 +247,7 @@

- Cool. -

-

- Please consult our legal guide for Neocities for more information. + Cool! Please consult our legal guide for Neocities for more information on why this is a really bad idea.

@@ -213,42 +307,6 @@
- -
- -
-
-

- For security reasons, we can't. But you can delete your own site by logging in and going to the settings page! -

- -

- For safety reasons, if you have more than one site, you'll need to delete all sites before deleting the main account. Click on "Manage Site Settings" to delete each site individually. -

-
-
-
- -
- -
-
-

- We recommend using an "old web browser" proxy server like WebOne or WRP. These translate between modern browser - standards and old browser standards, allowing for us to provide the speed and security of the new web without compromising the ability to use old browsers. -

-
-
-
-

Contact Form

@@ -265,6 +323,14 @@ + +
+ +
+ <%== hcaptcha_input %> diff --git a/views/legal.erb b/views/legal.erb index c593ca50..d4f348d4 100644 --- a/views/legal.erb +++ b/views/legal.erb @@ -98,7 +98,7 @@

Legal Threats

- Baseless legal threats are not welcome at Neocities, and will be considered an attack on our protections under the law that all online service providers have a strong interest in defending. As per our Open Company principles, we reserve the right to publish and expose any and all legal threats made against Neocities. Baseless legal threats will be considered an attempt to harm Neocities by abusing the legal system, and we will respond accordingly. We will additionally invite other major social networks and service providers to join us in defending against baseless legal claims if we conclude it is in their best interests. Think very carefully before sending Neocities legal threats. + Baseless legal threats are not welcome at Neocities, and will be considered an attack on our protections under the law that all online service providers have a strong interest in defending. We reserve the right to publish and expose any and all legal threats made against Neocities. Baseless legal threats will be considered an attempt to harm Neocities by abusing the legal system, and we will respond accordingly. We will additionally invite other major social networks and service providers to join us in defending against baseless legal claims if we conclude it is in their best interests. Think very carefully before sending Neocities legal threats.

It is strongly recommended that you consider the consequences of sending us legal threats or filing a lawsuit against Neocities before doing so. We are intimately familiar with our rights under the law, we have access to legal representation, and we can afford it. You will very likely lose your (embarassing and public) court case, and you will very likely be required to compensate us for legal fees. From 6ddef6aa5980ff1a476cc3efa39d2b7d5f65a036 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 26 Nov 2023 11:09:02 -0600 Subject: [PATCH 24/74] maxmind minfraud reporting --- app/admin.rb | 3 ++- workers/stop_forum_spam_worker.rb | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/admin.rb b/app/admin.rb index 3fd91e48..09201000 100644 --- a/app/admin.rb +++ b/app/admin.rb @@ -250,7 +250,8 @@ post '/admin/banhammer' do StopForumSpamWorker.perform_async( username: site.username, email: site.email, - ip: site.ip + ip: site.ip, + classifier: params[:classifier] ) end end diff --git a/workers/stop_forum_spam_worker.rb b/workers/stop_forum_spam_worker.rb index 90ff3465..24328aa9 100644 --- a/workers/stop_forum_spam_worker.rb +++ b/workers/stop_forum_spam_worker.rb @@ -3,8 +3,27 @@ class StopForumSpamWorker sidekiq_options queue: :stop_forum_spam, retry: 1, backtrace: true def perform(opts) - opts.merge! api_key: $config['stop_forum_spam_api_key'] - res = HTTP.post 'https://stopforumspam.com/add', form: opts + txn = Minfraud::Components::Report::Transaction.new( + ip_address: opts.ip, + tag: :spam_or_abuse, + # The following key/values are not mandatory but are encouraged + maxmind_id: $config['minfraud_account_id'], + #minfraud_id: '01c25cb0-f067-4e02-8ed0-a094c580f5e4', + #transaction_id: 'txn123' + #chargeback_code: 'BL' + notes: opts[:classifier] + ) + + reporter = Minfraud::Report.new transaction: txn + res = reporter.report_transaction + puts res.inspect + + res = HTTP.post 'https://stopforumspam.com/add', form: { + api_key: $config['stop_forum_spam_api_key'], + username: opts.username, + email: opts.email, + ip: opts.ip + } puts res.inspect end end From 6008e2be4eebc4695cd538a45cc4151db0540eff Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 26 Nov 2023 11:10:25 -0600 Subject: [PATCH 25/74] fix for site instance var --- app/create.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/create.rb b/app/create.rb index f55dc679..f4180228 100644 --- a/app/create.rb +++ b/app/create.rb @@ -91,7 +91,7 @@ post '/create' do return {result: 'error'}.to_json end - if defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(site, request) + if defined?(BlackBox.create_disabled?) && BlackBox.create_disabled?(@site, request) flash[:error] = 'Site creation is currently unavailable, please try again later.' return {result: 'error'}.to_json end From 6d237600fdf879fe4f7e4fa16d30954e3aafa0cb Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 26 Nov 2023 11:14:53 -0600 Subject: [PATCH 26/74] stopforumspamworker: hash not methods --- workers/stop_forum_spam_worker.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/workers/stop_forum_spam_worker.rb b/workers/stop_forum_spam_worker.rb index 24328aa9..aa908b6a 100644 --- a/workers/stop_forum_spam_worker.rb +++ b/workers/stop_forum_spam_worker.rb @@ -4,7 +4,7 @@ class StopForumSpamWorker def perform(opts) txn = Minfraud::Components::Report::Transaction.new( - ip_address: opts.ip, + ip_address: opts[:ip], tag: :spam_or_abuse, # The following key/values are not mandatory but are encouraged maxmind_id: $config['minfraud_account_id'], @@ -20,9 +20,9 @@ class StopForumSpamWorker res = HTTP.post 'https://stopforumspam.com/add', form: { api_key: $config['stop_forum_spam_api_key'], - username: opts.username, - email: opts.email, - ip: opts.ip + username: opts[:username], + email: opts[:email], + ip: opts[:ip] } puts res.inspect end From 7e49bd96a4488a5ae57a75247e654523d1995d9c Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 26 Nov 2023 17:27:58 +0000 Subject: [PATCH 27/74] sidekiq provides string keys not symbols --- workers/stop_forum_spam_worker.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/workers/stop_forum_spam_worker.rb b/workers/stop_forum_spam_worker.rb index aa908b6a..076c167d 100644 --- a/workers/stop_forum_spam_worker.rb +++ b/workers/stop_forum_spam_worker.rb @@ -4,26 +4,24 @@ class StopForumSpamWorker def perform(opts) txn = Minfraud::Components::Report::Transaction.new( - ip_address: opts[:ip], + ip_address: opts['ip'], tag: :spam_or_abuse, # The following key/values are not mandatory but are encouraged - maxmind_id: $config['minfraud_account_id'], + #maxmind_id: 'noideawhatthisis', #minfraud_id: '01c25cb0-f067-4e02-8ed0-a094c580f5e4', #transaction_id: 'txn123' #chargeback_code: 'BL' - notes: opts[:classifier] + notes: opts['classifier'] ) reporter = Minfraud::Report.new transaction: txn - res = reporter.report_transaction - puts res.inspect + reporter.report_transaction - res = HTTP.post 'https://stopforumspam.com/add', form: { + HTTP.post 'https://stopforumspam.com/add', form: { api_key: $config['stop_forum_spam_api_key'], - username: opts[:username], - email: opts[:email], - ip: opts[:ip] + username: opts['username'], + email: opts['email'], + ip: opts['ip'] } - puts res.inspect end end From b875fcbd2c724cd4adc5c4d7e28fd772ef60c860 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 26 Nov 2023 14:07:15 -0600 Subject: [PATCH 28/74] mention password managers in reset faq --- views/contact.erb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/views/contact.erb b/views/contact.erb index bda13a7d..36b19d93 100644 --- a/views/contact.erb +++ b/views/contact.erb @@ -30,7 +30,11 @@

- This is almost always because you used a different email address to create the site (check all your email addresses), or because the email has somehow been put in the spam folder of your email client. + Did you use a different email address to create the site? Also, check the spam folder of your email. +

+

+ This is also a great opportunity to talk about password managers. A password manager keeps a database of randomized strong passwords for all of your sites for you, so you only need to remember + one password which then controls all of the other ones. This reduces the chances of forgetting your password and having to reset it. Popular options include BitWarden and 1Password.

@@ -47,6 +51,10 @@

For security reasons, we cannot reset your password if you don't have control of your email address, because there is no way to know if you're the legitimate owner or an attacker. You will have to make a new site (don’t worry, it’s free!). If you didn’t get an email from the password reset form, check your spam folder.

+

+ This is also a great opportunity to talk about password managers. A password manager keeps a database of randomized strong passwords for all of your sites for you, so you only need to remember + one password which then controls all of the other ones. This reduces the chances of forgetting your password and having to reset it. Popular options include BitWarden and 1Password. +

From c83295b06fa67b84a05b971d4322940c7a02d069 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 26 Nov 2023 15:21:54 -0600 Subject: [PATCH 29/74] more faq copy --- views/contact.erb | 81 +++++++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/views/contact.erb b/views/contact.erb index 36b19d93..e94dee6c 100644 --- a/views/contact.erb +++ b/views/contact.erb @@ -183,6 +183,20 @@
+
+ +
+
+

+ Site downloading issues are almost always related to the internet connectivity issues. If your download is continuing to get cancelled or interrupted, check your WiFi router, or try plugging in directly to the router to see if that resolves it. You could also try downloading from a local library or coffee shop and see if you have the same issue. +

+
+
+
+ +
+ +
+
+

+ We currently have a file size limit for very large files on Neocities, including supporter sites. This is mainly because our CDN (content delivery network) is optimized for lots of small files (the kind that web sites use), and also because large files tend to be easily abused (think piracy). Files used to make web pages will not hit these limits, but things like very large zipballs are more likely to. These also tend to be the file types that lead to abuse and would make Neocities unsustainable to operate in the long run (piracy, malware, etc), another reason we're hesitant to add support for them. +

+

+ Even if we supported this, it wouldn't work very well because HTTP servers are not good at this anyways and there are almost always better alternatives for very large file distribution. For example if you need to upload a video, YouTube or Vimeo are better because they automatically format in the different file types that different browsers support, or if you are releasing a large game, it's probably better to release it through a game platform like Steam. Using BitTorrent is also a really effective way to have your site visitors help distribute your content quickly (provided it's legal!) +

+
+
+
+ +
+ +
+
+

+ Neocities does not support .htaccess files. This is an Apache HTTP server specific file type, and we don't use Apache for our backend. In addition, we generally don't add features to Neocities that make the functionality of sites depend on custom backend functionality (the sole exception being that we make sure "page not found" goes to "not_found.html" so you can create a custom 404.) +

+ +

+ Finally, we sometimes see people trying to use .htaccess for authentication (with usernames and passwords). Neocities is only for sites and information you want to make public, and it is dangerous to use it for private sites! We make backups of your site and attackers can easily find any information you're trying to hide using our archivers. Our goal is to make sure your sites stay up for a long time and persist, not to enforce secrecy zones on your site. If you want to make a controlled access site, Neocities is probably not what you're looking for. +

+
+
+
+

Legal / Abuse

@@ -290,31 +342,6 @@
- - -
- -
-
-

- No. And we don't intend to. -

- -

- The .htaccess file is an Apache specific thing, and we don't use Apache for our backend. In addition, we generally don't add features to Neocities that make the functionality of sites depend on custom backend functionality (the sole exception being that we make sure "page not found" goes to "not_found.html" so you can create a custom 404.) -

- -

- Finally, we sometimes see people trying to use .htaccess for authentication (with usernames and passwords). Neocities is only for sites and information you want to make public, and it is dangerous to use it for private sites! We make backups of your site and attackers can easily find any information you're trying to hide using our archivers. Our goal is to make sure your sites stay up for a long time and persist, not to enforce secrecy zones on your site. If you want to make a controlled access site, Neocities is not for you. -

-
-
-
-

Contact Form

@@ -331,7 +358,7 @@ - +
+
+ + + \ No newline at end of file From 36e35c913e28953677e0b7501e4c37d539433799 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Thu, 30 Nov 2023 15:32:31 -0600 Subject: [PATCH 33/74] invoicing: parent site not current site --- app/settings.rb | 2 +- views/settings/account/supporter.erb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/settings.rb b/app/settings.rb index 3ad42e83..e924011e 100644 --- a/app/settings.rb +++ b/app/settings.rb @@ -18,7 +18,7 @@ end get '/settings/invoices/?' do require_login @title = 'Invoices' - @invoices = current_site.stripe_customer_id ? Stripe::Invoice.list(customer: current_site.stripe_customer_id) : [] + @invoices = parent_site.stripe_customer_id ? Stripe::Invoice.list(customer: parent_site.stripe_customer_id) : [] erb :'settings/invoices' end diff --git a/views/settings/account/supporter.erb b/views/settings/account/supporter.erb index 3f7e8c24..1f39d0ea 100644 --- a/views/settings/account/supporter.erb +++ b/views/settings/account/supporter.erb @@ -12,7 +12,7 @@ You currently have the Free Plan (<%= current_site.maximum_space.to_space_pretty %>).
Want to get more space and help Neocities? Become a supporter!

Supporter Info - <% if current_site.stripe_customer_id || current_site.paypal_profile_id %> + <% if parent_site.stripe_customer_id || parent_site.paypal_profile_id %>
Generated Invoices <% end %> From a3832f25e1264c3c17334bee63e5c9de8af66d59 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Thu, 30 Nov 2023 21:54:10 +0000 Subject: [PATCH 34/74] invoice: more fixes --- views/settings/account/supporter.erb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/views/settings/account/supporter.erb b/views/settings/account/supporter.erb index 1f39d0ea..13eee774 100644 --- a/views/settings/account/supporter.erb +++ b/views/settings/account/supporter.erb @@ -4,20 +4,28 @@

<% if parent_site.paying_supporter? %> - Supporter Info + Supporter Info End Supporter Membership <% end %> <% else %>

You currently have the Free Plan (<%= current_site.maximum_space.to_space_pretty %>).
Want to get more space and help Neocities? Become a supporter!

- Supporter Info + Supporter Infoa + <% puts "XXXX #{parent_site.stripe_customer_id}" %> + <% puts "XXXX #{parent_site.paypal_profile_id}" %> <% if parent_site.stripe_customer_id || parent_site.paypal_profile_id %>
Generated Invoices <% end %> <% end %> + <% if parent_site.stripe_customer_id || parent_site.paypal_profile_id %> +
+ Generated Invoices + <% end %> + + - -

IPFS Archiving

- -
- - checked<% end %> - > Enable IPFS Archiving (what is this?) -
diff --git a/views/site.erb b/views/site.erb index bbbd2c35..41e8d042 100644 --- a/views/site.erb +++ b/views/site.erb @@ -19,11 +19,6 @@

<%= site.title %> <% if site.supporter? %> <% end %>

<%= site.host %>

- <% follow_count = site.follows_dataset.count %>
<%= site.views.format_large_number %> view<%= site.views == 1 ? '' : 's' %>
@@ -36,10 +31,6 @@ Edit Site <% end %> - <% if site.latest_archive && site.ipfs_archiving_enabled %> - Archives - <% end %> - <% if current_site && current_site != site %> <% is_following = current_site.is_following?(site) %> diff --git a/views/site/archives.erb b/views/site/archives.erb deleted file mode 100644 index a5f708c5..00000000 --- a/views/site/archives.erb +++ /dev/null @@ -1,33 +0,0 @@ -
-
-

IPFS Archives

-
-
- -
-
- <% if @archives.length == 0 %> - No archives yet. - <% else %> - - - - - - <% @archives.each do |archive| %> - - - - - <% end %> -
IPFS CID (what is this?)Archived Time
<%= archive.ipfs_hash %><%= archive.updated_at.ago.downcase %>
- -

- This is a preview release of a new technology. We're still figuring things out, and may stop hosting archives without notice. Learn how you can host your own copies of these archives. -

-

- Archives are captured once every <%= Archive::ARCHIVE_WAIT_TIME / 60 %> minutes, so if you don't see your latest changes, check back later. -

- <% end %> -
-
diff --git a/workers/archive_worker.rb b/workers/archive_worker.rb deleted file mode 100644 index d405ecb3..00000000 --- a/workers/archive_worker.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'sidekiq/api' - -class ArchiveWorker - include Sidekiq::Worker - sidekiq_options queue: :archive, retry: 2, backtrace: true - - def perform(site_id) - site = Site[site_id] - return if site.nil? || site.is_banned? || site.is_deleted - - if site.site_files_dataset.count > 1000 - logger.info "skipping #{site_id} (#{site.username}) due to > 1000 files" - return - end - - queue = Sidekiq::Queue.new self.class.sidekiq_options_hash['queue'] - logger.info "JOB ID: #{jid} #{site_id.inspect}" - queue.each do |job| - if job.args == [site_id] && job.jid != jid - logger.info "DELETING #{job.jid} for site_id #{site_id}" - job.delete - end - end - - scheduled_jobs = Sidekiq::ScheduledSet.new.select do |scheduled_job| - scheduled_job.klass == 'ArchiveWorker' && - scheduled_job.args[0] == site_id - end - - scheduled_jobs.each do |scheduled_job| - logger.info "DELETING scheduled job #{scheduled_job.jid} for site_id #{site_id}" - scheduled_job.delete - end - - logger.info "ARCHIVING: #{site.username}" - - site.archive! - end -end From 363453a78a55fcdf868fd388f51dde393a1404cf Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 31 Dec 2023 00:55:03 -0600 Subject: [PATCH 51/74] cleanups for copy --- views/index.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/views/index.erb b/views/index.erb index 8b00ca41..39ec78ae 100644 --- a/views/index.erb +++ b/views/index.erb @@ -220,7 +220,7 @@

- Zero advertising + Zero Advertising

Neocities will never sell your personal data or put advertising on your site. Instead, we are funded directly by people just like you with supporter accounts and donations. @@ -228,14 +228,14 @@

-

Blazing Fast Performance

-

Unlike many other web hosts, we don't skimp on infrastructure. Neocities operates our own caching CDN in 11 datacenters all over the world to quickly serve your site. We also force 100% strong SSL on all sites, and have full support for HTTP/2. Because of our commitment to quality, we routinely out-perform the pricey cloud services on reliability, speed and uptime. Whether it’s your personal home page or a busy professional site, your site loads fast.

+

Fast Site Loading

+

Neocities operates our own caching anycast CDN in over a dozen datacenters all over the world to quickly serve your site to visitors with strong SSL and support for HTTP/2. Our strict focus on static web hosting allows us to routinely out-perform the pricey cloud services on reliability, speed and uptime.

-

Developer tools

+

Developer Tools

Our fast static hosting comes with a great in-browser HTML editor, easy file uploading, a command line tool, RSS feeds for every site, APIs for building developer applications, and much more!

From 10ff8a7cc70e40bb774c9d58d3a4408312f75fb6 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 31 Dec 2023 00:55:59 -0600 Subject: [PATCH 52/74] sidekiq api available in test for screenshotworker --- workers/screenshot_worker.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/workers/screenshot_worker.rb b/workers/screenshot_worker.rb index 9eedfa70..3cf524ba 100644 --- a/workers/screenshot_worker.rb +++ b/workers/screenshot_worker.rb @@ -1,3 +1,4 @@ +require 'sidekiq/api' require 'securerandom' require 'open3' From ed02178289e0d170e8fba625fada48f29fb76916 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 31 Dec 2023 00:58:53 -0600 Subject: [PATCH 53/74] working to get the tests back online. deprecated zipruby to make selenium work, need to refactor zip code --- Gemfile | 3 +- Gemfile.lock | 33 +- models/site.rb | 27 -- tests/acceptance/admin_tests.rb | 2 + tests/acceptance/browse_tests.rb | 1 + tests/acceptance/dashboard_tests.rb | 2 +- tests/acceptance/education_tests.rb | 3 +- tests/acceptance/environment.rb | 35 +-- tests/acceptance/index_tests.rb | 6 +- tests/acceptance/password_reset_tests.rb | 3 +- tests/acceptance/settings/account_tests.rb | 9 +- tests/acceptance/settings/site_tests.rb | 350 ++++++++------------- tests/acceptance/signin_tests.rb | 1 + tests/acceptance/signup_tests.rb | 4 +- tests/acceptance/site_tests.rb | 1 + tests/acceptance/supporter_tests.rb | 3 +- 16 files changed, 174 insertions(+), 309 deletions(-) diff --git a/Gemfile b/Gemfile index 3cd828fd..e3449f7a 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,6 @@ gem 'tilt' gem 'erubis' gem 'stripe' #, source: 'https://code.stripe.com/' gem 'terrapin' -gem 'zipruby' gem 'sass', require: nil gem 'dav4rack', git: 'https://github.com/neocities/dav4rack.git', ref: '3ecde122a0b8bcc1d85581dc85ef3a7120b6a8f0' gem 'filesize' @@ -82,6 +81,7 @@ group :test do gem 'mocha', require: nil gem 'rake', '>= 12.3.3', require: nil gem 'capybara', require: nil #, '2.10.1', require: nil + gem 'selenium-webdriver' gem 'rack_session_access', require: nil gem 'webmock', require: nil gem 'stripe-ruby-mock', '~> 3.1.0.rc3', require: 'stripe_mock' @@ -89,5 +89,4 @@ group :test do gem 'mock_redis' gem 'simplecov', require: nil gem 'm' - gem 'apparition', github: 'twalpole/apparition', ref: 'ca86be4d54af835d531dbcd2b86e7b2c77f85f34' end diff --git a/Gemfile.lock b/Gemfile.lock index 42b5275b..c646c2f7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,15 +10,6 @@ GIT rack (>= 1.6) uuidtools (~> 2.1.1) -GIT - remote: https://github.com/twalpole/apparition.git - revision: ca86be4d54af835d531dbcd2b86e7b2c77f85f34 - ref: ca86be4d54af835d531dbcd2b86e7b2c77f85f34 - specs: - apparition (0.6.0) - capybara (~> 3.13, < 4) - websocket-driver (>= 0.6.5) - GEM remote: https://rubygems.org/ specs: @@ -190,7 +181,6 @@ GEM maxmind-geoip2 (~> 1.2) simpleidn (~> 0.1, >= 0.1.1) mini_mime (1.1.5) - mini_portile2 (2.8.5) minitest (5.20.0) minitest-reporters (1.6.1) ansi @@ -221,8 +211,7 @@ GEM net-protocol netrc (0.11.0) nio4r (2.7.0) - nokogiri (1.15.5) - mini_portile2 (~> 2.8.2) + nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) ox (2.14.17) paypal-recurring (1.1.0) @@ -239,7 +228,8 @@ GEM rack (2.2.8) rack-cache (1.15.0) rack (>= 0.4) - rack-protection (3.1.0) + rack-protection (3.2.0) + base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) rack-test (2.1.0) rack (>= 1.3) @@ -269,6 +259,7 @@ GEM rszr (1.3.0) ruby-progressbar (1.13.0) ruby2_keywords (0.0.5) + rubyzip (2.3.2) sanitize (6.1.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -278,6 +269,10 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) sax-machine (1.3.2) + selenium-webdriver (4.16.0) + rexml (~> 3.2, >= 3.2.5) + rubyzip (>= 1.2.2, < 3.0) + websocket (~> 1.0) sequel (5.75.0) bigdecimal sequel_pg (1.17.1) @@ -298,10 +293,10 @@ GEM simplecov_json_formatter (0.1.4) simpleidn (0.2.1) unf (~> 0.1.4) - sinatra (3.1.0) + sinatra (3.2.0) mustermann (~> 3.0) rack (~> 2.2, >= 2.2.4) - rack-protection (= 3.1.0) + rack-protection (= 3.2.0) tilt (~> 2.0) sinatra-flash (0.3.0) sinatra (>= 1.0.0) @@ -342,16 +337,13 @@ GEM ffi (>= 1.9.0) ffi-compiler (>= 0.1.2) webrick (1.8.1) - websocket-driver (0.7.6) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.5) + websocket (1.2.10) will_paginate (4.0.0) xmlrpc (0.3.3) webrick xpath (3.2.0) nokogiri (~> 1.8) zip_tricks (5.6.0) - zipruby (0.3.6) PLATFORMS x86_64-linux @@ -361,7 +353,6 @@ DEPENDENCIES activesupport addressable (>= 2.8.0) adequate_crypto_address - apparition! bcrypt capybara certified @@ -416,6 +407,7 @@ DEPENDENCIES rszr sanitize sass + selenium-webdriver sequel sequel_pg shotgun @@ -437,7 +429,6 @@ DEPENDENCIES will_paginate xmlrpc zip_tricks - zipruby BUNDLED WITH 2.3.10 diff --git a/models/site.rb b/models/site.rb index 5fbe0331..12aeedb3 100644 --- a/models/site.rb +++ b/models/site.rb @@ -813,33 +813,6 @@ class Site < Sequel::Model true end - def files_zip - zip_name = "neocities-#{username}" - - tmpfile = Tempfile.new 'neocities-site-zip' - tmpfile.close - - begin - Zip::Archive.open(tmpfile.path, Zip::CREATE) do |ar| - ar.add_dir(zip_name) - - Dir.glob("#{base_files_path}/**/*").each do |path| - relative_path = path.gsub(base_files_path+'/', '') - if File.directory?(path) - ar.add_dir(zip_name+'/'+relative_path) - else - ar.add_file(zip_name+'/'+relative_path, path) # add_file(, ) - end - end - end - rescue => e - tmpfile.unlink - raise e - end - - tmpfile.path - end - def move_files_from(oldusername) FileUtils.mkdir_p self.class.sharding_base_path(username) FileUtils.mkdir_p self.class.sharding_screenshots_path(username) diff --git a/tests/acceptance/admin_tests.rb b/tests/acceptance/admin_tests.rb index 0bba48e9..d7b92ff2 100644 --- a/tests/acceptance/admin_tests.rb +++ b/tests/acceptance/admin_tests.rb @@ -2,6 +2,8 @@ require_relative './environment.rb' describe '/admin' do include Capybara::DSL + include Capybara::Minitest::Assertions + before do Capybara.reset_sessions! diff --git a/tests/acceptance/browse_tests.rb b/tests/acceptance/browse_tests.rb index c31a3f13..31ad7cdb 100644 --- a/tests/acceptance/browse_tests.rb +++ b/tests/acceptance/browse_tests.rb @@ -2,6 +2,7 @@ require_relative './environment.rb' describe '/browse' do include Capybara::DSL + include Capybara::Minitest::Assertions =begin describe 'as admin' do diff --git a/tests/acceptance/dashboard_tests.rb b/tests/acceptance/dashboard_tests.rb index 6856523b..c3afe4dd 100644 --- a/tests/acceptance/dashboard_tests.rb +++ b/tests/acceptance/dashboard_tests.rb @@ -4,8 +4,8 @@ describe 'dashboard' do describe 'create directory' do describe 'logged in' do - include Capybara::DSL + include Capybara::Minitest::Assertions before do Capybara.reset_sessions! diff --git a/tests/acceptance/education_tests.rb b/tests/acceptance/education_tests.rb index 5d45f4ef..8a9a3f05 100644 --- a/tests/acceptance/education_tests.rb +++ b/tests/acceptance/education_tests.rb @@ -2,6 +2,7 @@ require_relative './environment.rb' describe 'signup' do include Capybara::DSL + include Capybara::Minitest::Assertions def fill_in_valid @site = Fabricate.attributes_for(:site) @@ -13,7 +14,7 @@ describe 'signup' do end before do - Capybara.default_driver = :apparition + Capybara.default_driver = :selenium_chrome_headless Capybara.reset_sessions! visit '/education' _(page).must_have_content 'Neocities' # Used to force load wait diff --git a/tests/acceptance/environment.rb b/tests/acceptance/environment.rb index 3aac7259..92a2408d 100644 --- a/tests/acceptance/environment.rb +++ b/tests/acceptance/environment.rb @@ -1,38 +1,17 @@ require_relative '../environment' +require 'capybara' require 'capybara/minitest' require 'capybara/minitest/spec' require 'rack_session_access/capybara' -require 'capybara/apparition' Capybara.app = Sinatra::Application - -include Capybara::Minitest::Assertions Capybara.default_max_wait_time = 5 -#Capybara.register_driver :apparition do |app| -# Capybara::Apparition::Driver.new(app, headless: false) -#end +Capybara.register_driver :selenium_chrome_headless_largewindow do |app| + options = ::Selenium::WebDriver::Chrome::Options.new + options.add_argument('--headless') + options.add_argument('--window-size=1280,800') # Set your desired window size -=begin -def setup - Capybara.current_driver = :apparition -end - -def teardown - Capybara.reset_sessions! - Capybara.use_default_driver -end -=end -=begin -require 'capybara' -require 'capybara/dsl' -require 'capybara/poltergeist' -require 'rack_session_access/capybara' - -Capybara.app = Sinatra::Application - -def teardown - Capybara.reset_sessions! -end -=end + Capybara::Selenium::Driver.new(app, browser: :chrome, options: options) +end \ No newline at end of file diff --git a/tests/acceptance/index_tests.rb b/tests/acceptance/index_tests.rb index 469aecae..872896be 100644 --- a/tests/acceptance/index_tests.rb +++ b/tests/acceptance/index_tests.rb @@ -2,6 +2,7 @@ require_relative './environment.rb' describe '/' do include Capybara::DSL + include Capybara::Minitest::Assertions describe 'news feed' do before do @@ -21,7 +22,8 @@ describe '/' do @another_site = Fabricate :site @followed_site.toggle_follow @another_site visit '/' - _(find('.news-item', match: :first).text).must_match /#{@followed_site.username} followed #{@another_site.username}/i + _(page).must_have_link(@followed_site.title, href: "/site/#{@followed_site.username}") + #_(find('.news-item', match: :first).text).must_match /#{@followed_site.username} followed #{@another_site.username}/i end it 'loads my activities only' do @@ -30,7 +32,7 @@ describe '/' do @another_site = Fabricate :site @followed_site.toggle_follow @another_site visit '/?activity=mine' - _(find('.news-item').text).must_match //i + _(page).must_have_link(@followed_site.title, href: "/site/#{@followed_site.username}") end it 'loads a specific event with the id' do diff --git a/tests/acceptance/password_reset_tests.rb b/tests/acceptance/password_reset_tests.rb index 9fbe5f64..b893ce37 100644 --- a/tests/acceptance/password_reset_tests.rb +++ b/tests/acceptance/password_reset_tests.rb @@ -2,6 +2,7 @@ require_relative './environment.rb' describe '/password_reset' do include Capybara::DSL + include Capybara::Minitest::Assertions before do Capybara.reset_sessions! @@ -67,7 +68,7 @@ describe '/password_reset' do fill_in 'email', with: @site.email click_button 'Send Reset Token' - _(body).must_match /send an e-mail to your account with password reset instructions/ + _(body).must_match /We sent an e-mail with password reset instructions/ _(@site.reload.password_reset_token.blank?).must_equal false _(EmailWorker.jobs.first['args'].first['body']).must_match /#{Rack::Utils.build_query(username: @site.username, token: @site.password_reset_token)}/ diff --git a/tests/acceptance/settings/account_tests.rb b/tests/acceptance/settings/account_tests.rb index 31a0ad8d..53473660 100644 --- a/tests/acceptance/settings/account_tests.rb +++ b/tests/acceptance/settings/account_tests.rb @@ -1,9 +1,10 @@ require_relative '../environment.rb' describe 'site/settings' do - describe 'email' do - include Capybara::DSL + include Capybara::DSL + include Capybara::Minitest::Assertions + describe 'email' do before do EmailWorker.jobs.clear @email = "#{SecureRandom.uuid.gsub('-', '')}@exampleedsdfdsf.com" @@ -84,8 +85,6 @@ describe 'site/settings' do end describe 'unsubscribe email' do - include Capybara::DSL - before do @email = "#{SecureRandom.uuid.gsub('-', '')}@exampleedsdfdsf.com" @site = Fabricate :site, email: @email @@ -127,8 +126,6 @@ describe 'site/settings' do end describe 'change password' do - include Capybara::DSL - before do EmailWorker.jobs.clear @site = Fabricate :site, password: 'derpie' diff --git a/tests/acceptance/settings/site_tests.rb b/tests/acceptance/settings/site_tests.rb index 387aad27..370ceee1 100644 --- a/tests/acceptance/settings/site_tests.rb +++ b/tests/acceptance/settings/site_tests.rb @@ -1,88 +1,10 @@ require_relative '../environment.rb' -def generate_ssl_certs(opts={}) - # https://github.com/kyledrake/ruby-openssl-cheat-sheet/blob/master/certificate_authority.rb - res = {} - - ca_keypair = OpenSSL::PKey::RSA.new(2048) - ca_cert = OpenSSL::X509::Certificate.new - ca_cert.not_before = Time.now - ca_cert.subject = OpenSSL::X509::Name.new([ - ["C", "US"], - ["ST", "Oregon"], - ["L", "Portland"], - ["CN", "Neocities CA"] - ]) - ca_cert.issuer = ca_cert.subject - ca_cert.not_after = Time.now + 1000000000 # 40 or so years - ca_cert.serial = 1 - ca_cert.public_key = ca_keypair.public_key - ef = OpenSSL::X509::ExtensionFactory.new - ef.subject_certificate = ca_cert - ef.issuer_certificate = ca_cert - # Read more about the various extensions here: http://www.openssl.org/docs/apps/x509v3_config.html - ca_cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true)) - ca_cert.add_extension(ef.create_extension("keyUsage","keyCertSign, cRLSign", true)) - ca_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false)) - ca_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false)) - ca_cert.sign(ca_keypair, OpenSSL::Digest::SHA256.new) - res[:ca_cert] = ca_cert - res[:ca_keypair] = ca_keypair - - ca_cert = OpenSSL::X509::Certificate.new(res[:ca_cert].to_pem) - our_cert_keypair = OpenSSL::PKey::RSA.new(2048) - our_cert_req = OpenSSL::X509::Request.new - our_cert_req.subject = OpenSSL::X509::Name.new([ - ["C", "US"], - ["ST", "Oregon"], - ["L", "Portland"], - ["O", "Neocities User"], - ["CN", "*.#{opts[:domain]}"] - ]) - our_cert_req.public_key = our_cert_keypair.public_key - our_cert_req.sign our_cert_keypair, OpenSSL::Digest::SHA1.new - our_cert = OpenSSL::X509::Certificate.new - our_cert.subject = our_cert_req.subject - our_cert.issuer = ca_cert.subject - our_cert.not_before = Time.now - if opts[:expired] - our_cert.not_after = Time.now - 100000000 - else - our_cert.not_after = Time.now + 100000000 - end - our_cert.serial = 123 # Should be an unique number, the CA probably has a database. - our_cert.public_key = our_cert_req.public_key - # To make the certificate valid for both wildcard and top level domain name, we need an extension. - ef = OpenSSL::X509::ExtensionFactory.new - ef.subject_certificate = our_cert - ef.issuer_certificate = ca_cert - our_cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{@domain}, DNS:*.#{@domain}", false)) - our_cert.sign res[:ca_keypair], OpenSSL::Digest::SHA1.new - - our_cert_tmpfile = Tempfile.new 'our_cert' - our_cert_tmpfile.write our_cert.to_pem - our_cert_tmpfile.close - res[:cert_path] = our_cert_tmpfile.path - - res[:key_path] = '/tmp/nc_test_our_cert_keypair' - File.write res[:key_path], our_cert_keypair.to_pem - - res[:cert_intermediate_path] = '/tmp/nc_test_ca_cert' - File.write res[:cert_intermediate_path], res[:ca_cert].to_pem - - res[:combined_cert_path] = '/tmp/nc_test_combined_cert' - File.write res[:combined_cert_path], "#{File.read(res[:cert_path])}\n#{File.read(res[:cert_intermediate_path])}" - - res[:bad_combined_cert_path] = '/tmp/nc_test_bad_combined_cert' - File.write res[:bad_combined_cert_path], "#{File.read(res[:cert_intermediate_path])}\n#{File.read(res[:cert_path])}" - - res -end - describe 'site/settings' do - describe 'permissions' do - include Capybara::DSL + include Capybara::DSL + include Capybara::Minitest::Assertions + describe 'permissions' do before do @parent_site = Fabricate :site @child_site = Fabricate :site, parent_site_id: @parent_site.id @@ -104,8 +26,6 @@ describe 'site/settings' do end describe 'changing username' do - include Capybara::DSL - before do Capybara.reset_sessions! @site = Fabricate :site @@ -143,142 +63,138 @@ describe 'site/settings' do _(page).must_have_content /You already have this name/ end end -end -describe 'api key' do - include Capybara::DSL + describe 'api key' do + before do + Capybara.reset_sessions! + @site = Fabricate :site + @child_site = Fabricate :site, parent_site_id: @site.id + page.set_rack_session id: @site.id + end - before do - Capybara.reset_sessions! - @site = Fabricate :site - @child_site = Fabricate :site, parent_site_id: @site.id - page.set_rack_session id: @site.id + it 'sets api key' do + visit "/settings/#{@child_site[:username]}#api_key" + _(@site.api_key).must_be_nil + _(@child_site.api_key).must_be_nil + click_button 'Generate API Key' + _(@site.reload.api_key).must_be_nil + _(@child_site.reload.api_key).wont_be_nil + _(page.body).must_match @child_site.api_key + end + + it 'regenerates api key for child site' do + visit "/settings/#{@child_site[:username]}#api_key" + @child_site.generate_api_key! + api_key = @child_site.api_key + click_button 'Generate API Key' + _(@child_site.reload.api_key).wont_equal api_key + end end - it 'sets api key' do - visit "/settings/#{@child_site[:username]}#api_key" - _(@site.api_key).must_be_nil - _(@child_site.api_key).must_be_nil - click_button 'Generate API Key' - _(@site.reload.api_key).must_be_nil - _(@child_site.reload.api_key).wont_be_nil - _(page.body).must_match @child_site.api_key + describe 'delete' do + before do + Capybara.reset_sessions! + @site = Fabricate :site + page.set_rack_session id: @site.id + visit "/settings/#{@site[:username]}#delete" + end + + it 'fails for incorrect entered username' do + fill_in 'username', with: 'NOPE' + click_button 'Delete Site' + + _(page.body).must_match /Site user name and entered user name did not match/i + _(@site.reload.is_deleted).must_equal false + end + + it 'succeeds' do + deleted_reason = 'Penelope left a hairball on my site' + + fill_in 'confirm_username', with: @site.username + fill_in 'deleted_reason', with: deleted_reason + click_button 'Delete Site' + + @site.reload + _(@site.is_deleted).must_equal true + _(@site.deleted_reason).must_equal deleted_reason + _(page.current_path).must_equal '/' + + _(File.exist?(@site.files_path('./index.html'))).must_equal false + _(Dir.exist?(@site.files_path)).must_equal false + + path = File.join Site::DELETED_SITES_ROOT, Site.sharding_dir(@site.username), @site.username + _(Dir.exist?(path)).must_equal true + _(File.exist?(File.join(path, 'index.html'))).must_equal true + + visit "/site/#{@site.username}" + _(page.status_code).must_equal 404 + end + + it 'stops charging for supporter account' do + customer = Stripe::Customer.create( + source: $stripe_helper.generate_card_token + ) + + subscription = customer.subscriptions.create plan: 'supporter' + + @site.update( + stripe_customer_id: customer.id, + stripe_subscription_id: subscription.id, + plan_type: 'supporter' + ) + + @site.plan_type = subscription.plan.id + @site.save_changes + + fill_in 'confirm_username', with: @site.username + fill_in 'deleted_reason', with: 'derp' + click_button 'Delete Site' + + _(Stripe::Customer.retrieve(@site.stripe_customer_id).subscriptions.count).must_equal 0 + @site.reload + _(@site.stripe_subscription_id).must_be_nil + _(@site.is_deleted).must_equal true + end + + it 'should fail unless owned by current user' do + someone_elses_site = Fabricate :site + page.set_rack_session id: @site.id + + page.driver.post "/settings/#{someone_elses_site.username}/delete", { + username: someone_elses_site.username, + deleted_reason: 'Dade Murphy enters Acid Burns turf' + } + + _(page.driver.status_code).must_equal 302 + _(URI.parse(page.driver.response_headers['Location']).path).must_equal '/' + someone_elses_site.reload + _(someone_elses_site.is_deleted).must_equal false + end + + it 'should not show NSFW tab for admin NSFW flag' do + owned_site = Fabricate :site, parent_site_id: @site.id, admin_nsfw: true + visit "/settings/#{owned_site.username}" + _(page.body).wont_match /18\+/ + end + + it 'should succeed if you own the site' do + owned_site = Fabricate :site, parent_site_id: @site.id + visit "/settings/#{owned_site.username}#delete" + fill_in 'confirm_username', with: owned_site.username + click_button 'Delete Site' + + @site.reload + owned_site.reload + _(owned_site.is_deleted).must_equal true + _(@site.is_deleted).must_equal false + + _(page.current_path).must_equal "/settings" + end + + it 'fails to delete parent site if children exist' do + owned_site = Fabricate :site, parent_site_id: @site.id + visit "/settings/#{@site.username}#delete" + _(page.body).must_match /You cannot delete the parent site without deleting the children sites first/i + end end - - it 'regenerates api key for child site' do - visit "/settings/#{@child_site[:username]}#api_key" - @child_site.generate_api_key! - api_key = @child_site.api_key - click_button 'Generate API Key' - _(@child_site.reload.api_key).wont_equal api_key - end -end - -describe 'delete' do - include Capybara::DSL - - before do - Capybara.reset_sessions! - @site = Fabricate :site - page.set_rack_session id: @site.id - visit "/settings/#{@site[:username]}#delete" - end - - it 'fails for incorrect entered username' do - fill_in 'username', with: 'NOPE' - click_button 'Delete Site' - - _(page.body).must_match /Site user name and entered user name did not match/i - _(@site.reload.is_deleted).must_equal false - end - - it 'succeeds' do - deleted_reason = 'Penelope left a hairball on my site' - - fill_in 'confirm_username', with: @site.username - fill_in 'deleted_reason', with: deleted_reason - click_button 'Delete Site' - - @site.reload - _(@site.is_deleted).must_equal true - _(@site.deleted_reason).must_equal deleted_reason - _(page.current_path).must_equal '/' - - _(File.exist?(@site.files_path('./index.html'))).must_equal false - _(Dir.exist?(@site.files_path)).must_equal false - - path = File.join Site::DELETED_SITES_ROOT, Site.sharding_dir(@site.username), @site.username - _(Dir.exist?(path)).must_equal true - _(File.exist?(File.join(path, 'index.html'))).must_equal true - - visit "/site/#{@site.username}" - _(page.status_code).must_equal 404 - end - - it 'stops charging for supporter account' do - customer = Stripe::Customer.create( - source: $stripe_helper.generate_card_token - ) - - subscription = customer.subscriptions.create plan: 'supporter' - - @site.update( - stripe_customer_id: customer.id, - stripe_subscription_id: subscription.id, - plan_type: 'supporter' - ) - - @site.plan_type = subscription.plan.id - @site.save_changes - - fill_in 'confirm_username', with: @site.username - fill_in 'deleted_reason', with: 'derp' - click_button 'Delete Site' - - _(Stripe::Customer.retrieve(@site.stripe_customer_id).subscriptions.count).must_equal 0 - @site.reload - _(@site.stripe_subscription_id).must_be_nil - _(@site.is_deleted).must_equal true - end - - it 'should fail unless owned by current user' do - someone_elses_site = Fabricate :site - page.set_rack_session id: @site.id - - page.driver.post "/settings/#{someone_elses_site.username}/delete", { - username: someone_elses_site.username, - deleted_reason: 'Dade Murphy enters Acid Burns turf' - } - - _(page.driver.status_code).must_equal 302 - _(URI.parse(page.driver.response_headers['Location']).path).must_equal '/' - someone_elses_site.reload - _(someone_elses_site.is_deleted).must_equal false - end - - it 'should not show NSFW tab for admin NSFW flag' do - owned_site = Fabricate :site, parent_site_id: @site.id, admin_nsfw: true - visit "/settings/#{owned_site.username}" - _(page.body).wont_match /18\+/ - end - - it 'should succeed if you own the site' do - owned_site = Fabricate :site, parent_site_id: @site.id - visit "/settings/#{owned_site.username}#delete" - fill_in 'confirm_username', with: owned_site.username - click_button 'Delete Site' - - @site.reload - owned_site.reload - _(owned_site.is_deleted).must_equal true - _(@site.is_deleted).must_equal false - - _(page.current_path).must_equal "/settings" - end - - it 'fails to delete parent site if children exist' do - owned_site = Fabricate :site, parent_site_id: @site.id - visit "/settings/#{@site.username}#delete" - _(page.body).must_match /You cannot delete the parent site without deleting the children sites first/i - end -end +end \ No newline at end of file diff --git a/tests/acceptance/signin_tests.rb b/tests/acceptance/signin_tests.rb index a263c41d..5e37440b 100644 --- a/tests/acceptance/signin_tests.rb +++ b/tests/acceptance/signin_tests.rb @@ -2,6 +2,7 @@ require_relative './environment.rb' describe 'signin' do include Capybara::DSL + include Capybara::Minitest::Assertions def fill_in_valid @site = Fabricate.attributes_for :site diff --git a/tests/acceptance/signup_tests.rb b/tests/acceptance/signup_tests.rb index 838cc860..81ea876d 100644 --- a/tests/acceptance/signup_tests.rb +++ b/tests/acceptance/signup_tests.rb @@ -2,6 +2,7 @@ require_relative './environment.rb' describe 'signup' do include Capybara::DSL + include Capybara::Minitest::Assertions def fill_in_valid @site = Fabricate.attributes_for(:site) @@ -24,7 +25,7 @@ describe 'signup' do end before do - Capybara.default_driver = :apparition + Capybara.default_driver = :selenium_chrome_headless_largewindow Capybara.reset_sessions! visit_signup end @@ -39,7 +40,6 @@ describe 'signup' do fill_in_valid click_signup_button site_created? - click_link 'Continue' _(page).must_have_content /almost ready!/ fill_in 'token', with: Site[username: @site[:username]].email_confirmation_token diff --git a/tests/acceptance/site_tests.rb b/tests/acceptance/site_tests.rb index 5ab20e76..6f547448 100644 --- a/tests/acceptance/site_tests.rb +++ b/tests/acceptance/site_tests.rb @@ -2,6 +2,7 @@ require_relative './environment.rb' describe 'site page' do include Capybara::DSL + include Capybara::Minitest::Assertions after do Capybara.default_driver = :rack_test diff --git a/tests/acceptance/supporter_tests.rb b/tests/acceptance/supporter_tests.rb index 65fb326f..1d711525 100644 --- a/tests/acceptance/supporter_tests.rb +++ b/tests/acceptance/supporter_tests.rb @@ -2,9 +2,10 @@ require_relative './environment.rb' describe '/supporter' do include Capybara::DSL + include Capybara::Minitest::Assertions before do - Capybara.default_driver = :apparition + Capybara.default_driver = :selenium_chrome_headless Capybara.reset_sessions! @site = Fabricate :site From b4000c104a6872f00466888bf955c90024e50d5d Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 31 Dec 2023 01:43:44 -0600 Subject: [PATCH 54/74] Rakefile: refactor zip code, remove some obsolete tasks --- Gemfile | 1 + Gemfile.lock | 1 + Rakefile | 126 ++++++--------------------------------------------- 3 files changed, 17 insertions(+), 111 deletions(-) diff --git a/Gemfile b/Gemfile index e3449f7a..a912a909 100644 --- a/Gemfile +++ b/Gemfile @@ -62,6 +62,7 @@ gem 'phonelib' gem 'dnsbl-client' gem 'minfraud' gem 'image_optimizer' # apt install optipng jpegoptim pngquant +gem 'rubyzip', require: 'zip' group :development, :test do gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index c646c2f7..c2293234 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -405,6 +405,7 @@ DEPENDENCIES rinku rss rszr + rubyzip sanitize sass selenium-webdriver diff --git a/Rakefile b/Rakefile index 7a0ca483..04b4d9ee 100644 --- a/Rakefile +++ b/Rakefile @@ -14,22 +14,6 @@ end task :default => :test -=begin -desc "send domain update email" -task :send_domain_update_email => [:environment] do - Site.exclude(domain: nil).exclude(domain: '').all.each do |site| - msg = <<-HERE -MESSAGE GOES HERE TEST -HERE - - site.send_email( - subject: 'SUBJECT GOES HERE', - body: msg - ) - end -end -=end - desc "prune logs" task :prune_logs => [:environment] do Stat.prune! @@ -55,29 +39,33 @@ desc 'Update banned IPs list' task :update_blocked_ips => [:environment] do filename = 'listed_ip_365_ipv46' + zip_path = "/tmp/#{filename}.zip" - File.open("/tmp/#{filename}.zip", 'wb') do |file| + File.open(zip_path, 'wb') do |file| response = HTTP.get "https://www.stopforumspam.com/downloads/#{filename}.zip" response.body.each do |chunk| file.write chunk end end - Zip::Archive.open("/tmp/#{filename}.zip") do |ar| - ar.fopen("#{filename}.txt") do |f| - ips = f.read - insert_hashes = [] - ips.each_line {|ip| insert_hashes << {ip: ip.strip, created_at: Time.now}} - ips = nil + Zip::File.open(zip_path) do |zip_file| + zip_file.each do |entry| + if entry.name == "#{filename}.txt" + ips = entry.get_input_stream.read + insert_hashes = [] + ips.each_line { |ip| insert_hashes << { ip: ip.strip, created_at: Time.now } } + ips = nil - DB.transaction do - DB[:blocked_ips].delete - DB[:blocked_ips].multi_insert insert_hashes + # Database transaction + DB.transaction do + DB[:blocked_ips].delete + DB[:blocked_ips].multi_insert insert_hashes + end end end end - FileUtils.rm "/tmp/#{filename}.zip" + FileUtils.rm zip_path end desc 'parse tor exits' @@ -91,90 +79,6 @@ task :parse_tor_exits => [:environment] do # ^^ Array of ip addresses of known exit nodes end -desc 'Compile nginx mapfiles' -task :compile_nginx_mapfiles => [:environment] do - FileUtils.mkdir_p './files/maps' - - File.open('./files/maps/domains.txt', 'w') do |file| - Site.exclude(domain: nil).exclude(domain: '').select(:username,:domain).all.each do |site| - file.write ".#{site.values[:domain]} #{site.username};\n" - end - end - - File.open('./files/maps/supporters.txt', 'w') do |file| - Site.select(:username, :domain).exclude(plan_type: 'free').exclude(plan_type: nil).all.each do |parent_site| - sites = [parent_site] + parent_site.children - sites.each do |site| - file.write "#{site.username}.neocities.org 1;\n" - unless site.host.match(/\.neocities\.org$/) - file.write ".#{site.values[:domain]} 1;\n" - end - end - end - end - - File.open('./files/maps/subdomain-to-domain.txt', 'w') do |file| - Site.select(:username, :domain).exclude(domain: nil).exclude(domain: '').all.each do |site| - file.write "#{site.username}.neocities.org #{site.values[:domain]};\n" - end - end - - File.open('./files/maps/sandboxed.txt', 'w') do |file| - usernames = DB["select username from sites where created_at > ? and parent_site_id is null and (plan_type is null or plan_type='free') and is_banned != 't' and is_deleted != 't'", 2.days.ago].all.collect {|s| s[:username]}.each {|username| file.write "#{username} 1;\n"} - end - - # Compile letsencrypt ssl keys - sites = DB[%{select username,ssl_key,ssl_cert,domain from sites where ssl_cert is not null and ssl_key is not null and (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't'}].all - - ssl_path = './files/maps/ssl' - - FileUtils.mkdir_p ssl_path - - sites.each do |site| - [site[:domain], "www.#{site[:domain]}"].each do |domain| - begin - key = OpenSSL::PKey::RSA.new site[:ssl_key] - crt = OpenSSL::X509::Certificate.new site[:ssl_cert] - rescue => e - puts "SSL ERROR: #{e.class} #{e.inspect}" - next - end - - File.open(File.join(ssl_path, "#{domain}.key"), 'wb') {|f| f.write key.to_der} - File.open(File.join(ssl_path, "#{domain}.crt"), 'wb') {|f| f.write site[:ssl_cert]} - end - end - -end - -desc 'Produce SSL config package for proxy' -task :buildssl => [:environment] do - sites = Site.select(:id, :username, :domain, :ssl_key, :ssl_cert). - exclude(domain: nil). - exclude(ssl_key: nil). - exclude(ssl_cert: nil). - all - - payload = [] - - begin - FileUtils.rm './files/sslsites.zip' - rescue Errno::ENOENT - end - - Zip::Archive.open('./files/sslsites.zip', Zip::CREATE) do |ar| - ar.add_dir 'ssl' - - sites.each do |site| - ar.add_buffer "ssl/#{site.username}.key", site.ssl_key - ar.add_buffer "ssl/#{site.username}.crt", site.ssl_cert - payload << {username: site.username, domain: site.domain} - end - - ar.add_buffer 'sslsites.json', payload.to_json - end -end - desc 'Set existing stripe customers to internal supporter plan' task :primenewstriperunonlyonce => [:environment] do # Site.exclude(stripe_customer_id: nil).all.each do |site| From 3fa6f34e54e57fddb16ac80bfe91465a8e3cb525 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 31 Dec 2023 01:55:52 -0600 Subject: [PATCH 55/74] Rakefile: further remove obsolete/unused code --- Rakefile | 263 ------------------------------------------------------- 1 file changed, 263 deletions(-) diff --git a/Rakefile b/Rakefile index 04b4d9ee..3a10e85b 100644 --- a/Rakefile +++ b/Rakefile @@ -68,89 +68,6 @@ task :update_blocked_ips => [:environment] do FileUtils.rm zip_path end -desc 'parse tor exits' -task :parse_tor_exits => [:environment] do - exit_ips = Net::HTTP.get(URI.parse('https://check.torproject.org/exit-addresses')) - - exit_ips.split("\n").collect {|line| - line.match(/ExitAddress (\d+\.\d+\.\d+\.\d+)/)&.captures&.first - }.compact - - # ^^ Array of ip addresses of known exit nodes -end - -desc 'Set existing stripe customers to internal supporter plan' -task :primenewstriperunonlyonce => [:environment] do -# Site.exclude(stripe_customer_id: nil).all.each do |site| -# site.plan_type = 'supporter' -# site.save_changes validate: false -# end - - Site.exclude(stripe_customer_id: nil).where(plan_type: nil).where(plan_ended: false).all.each do |s| - customer = Stripe::Customer.retrieve(s.stripe_customer_id) - subscription = customer.subscriptions.first - next if subscription.nil? - puts "set subscription id to #{subscription.id}" - puts "set plan type to #{subscription.plan.id}" - s.stripe_subscription_id = subscription.id - s.plan_type = subscription.plan.id - s.save_changes(validate: false) - end - -end - -desc 'dedupe tags' -task :dedupetags => [:environment] do - Tag.all.each do |tag| - begin - tag.reload - rescue Sequel::Error => e - next if e.message =~ /Record not found/ - end - - matching_tags = Tag.exclude(id: tag.id).where(name: tag.name).all - - matching_tags.each do |matching_tag| - DB[:sites_tags].where(tag_id: matching_tag.id).update(tag_id: tag.id) - matching_tag.delete - end - end -end - -desc 'Clean tags' -task :cleantags => [:environment] do - - Site.select(:id).all.each do |site| - if site.tags.length > 5 - site.tags.slice(5, site.tags.length).each {|tag| site.remove_tag tag} - end - end - - empty_tag = Tag.where(name: '').first - - if empty_tag - DB[:sites_tags].where(tag_id: empty_tag.id).delete - end - - Tag.all.each do |tag| - if tag.name.length > Tag::NAME_LENGTH_MAX || tag.name.match(/ /) - DB[:sites_tags].where(tag_id: tag.id).delete - DB[:tags].where(id: tag.id).delete - else - tag.update name: tag.name.downcase.strip - end - end - - Tag.where(name: 'porn').first.update is_nsfw: true -end - -desc 'update screenshots' -task :update_screenshots => [:environment] do - Site.select(:username).where(site_changed: true, is_banned: false, is_crashing: false).filter(~{updated_at: nil}).order(:updated_at.desc).all.each do |site| - ScreenshotWorker.perform_async site.username, 'index.html' - end -end - desc 'rebuild_thumbnails' task :rebuild_thumbnails => [:environment] do dirs = Dir[Site::SITE_FILES_ROOT+'/**/*'].collect {|s| s.sub(Site::SITE_FILES_ROOT, '')}.collect {|s| s.sub('/', '')} @@ -171,142 +88,11 @@ task :rebuild_thumbnails => [:environment] do end end -desc 'prime_space_used' -task :prime_space_used => [:environment] do - Site.select(:id,:username,:space_used).all.each do |s| - s.space_used = s.actual_space_used - s.save_changes validate: false - end -end - -desc 'prime site_updated_at' -task :prime_site_updated_at => [:environment] do - Site.select(:id,:username,:site_updated_at, :updated_at).all.each do |s| - s.site_updated_at = s.updated_at - s.save_changes validate: false - end -end - -desc 'prime_site_files' -task :prime_site_files => [:environment] do - Site.where(is_banned: false).where(is_deleted: false).select(:id, :username).all.each do |site| - Dir.glob(File.join(site.files_path, '**/*')).each do |file| - path = file.gsub(site.base_files_path, '').sub(/^\//, '') - - site_file = site.site_files_dataset[path: path] - - if site_file.nil? - mtime = File.mtime file - - site_file_opts = { - path: path, - updated_at: mtime, - created_at: mtime - } - - if File.directory? file - site_file_opts.merge! is_directory: true - else - site_file_opts.merge!( - size: File.size(file), - sha1_hash: Digest::SHA1.file(file).hexdigest - ) - end - - site.add_site_file site_file_opts - end - end - end -end - -desc 'dedupe_follows' -task :dedupe_follows => [:environment] do - follows = Follow.all - deduped_follows = Follow.all.uniq {|f| "#{f.site_id}_#{f.actioning_site_id}"} - - follows.each do |follow| - unless deduped_follows.include?(follow) - puts "deleting dedupe: #{follow.inspect}" - follow.delete - end - end -end - -desc 'flush_empty_index_sites' -task :flush_empty_index_sites => [:environment] do - sites = Site.select(:id).all - - counter = 0 - - sites.each do |site| - if site.empty_index? - counter += 1 - site.site_changed = false - site.save_changes validate: false - end - end - - puts "#{counter} sites set to not changed." -end - desc 'compute_scores' task :compute_scores => [:environment] do Site.compute_scores end -=begin -desc 'Update screenshots' -task :update_screenshots => [:environment] do - Site.select(:username).filter(is_banned: false).filter(~{updated_at: nil}).order(:updated_at.desc).all.collect {|s| - ScreenshotWorker.perform_async s.username - } -end -=end - -desc 'prime_classifier' -task :prime_classifier => [:environment] do - Site.select(:id, :username).where(is_banned: false, is_deleted: false).all.each do |site| - next if site.site_files_dataset.where(classifier: 'spam').count > 0 - html_files = site.site_files_dataset.where(path: /\.html$/).all - - html_files.each do |html_file| - print "training #{site.username}/#{html_file.path}..." - site.train html_file.path - print "done.\n" - end - end -end - -desc 'train_spam' -task :train_spam => [:environment] do - paths = File.read('./spam.txt') - - paths.split("\n").each do |path| - username, site_file_path = path.match(/^([a-zA-Z0-9_\-]+)\/(.+)$/i).captures - site = Site[username: username] - next if site.nil? - site_file = site.site_files_dataset.where(path: site_file_path).first - next if site_file.nil? - site.train site_file_path, :spam - site.ban! - puts "Deleted #{site_file_path}, banned #{site.username}" - end -end - -desc 'regenerate_ssl_certs' -task :regenerate_ssl_certs => [:environment] do - sites = DB[%{select id from sites where (domain is not null or domain != '') and is_banned != 't' and is_deleted != 't'}].all - - seconds = 2 - - sites.each do |site| - LetsEncryptWorker.perform_in seconds, site[:id] - seconds += 10 - end - - puts "#{sites.length.to_s} records are primed" -end - desc 'renew_ssl_certs' task :renew_ssl_certs => [:environment] do delay = 0 @@ -323,24 +109,6 @@ task :purge_tmp_turds => [:environment] do end end -desc 'shard_migration' -task :shard_migration => [:environment] do - #Site.exclude(is_deleted: true).exclude(is_banned: true).select(:username).each do |site| - # FileUtils.mkdir_p File.join('public', 'testsites', site.username) - #end - #exit - Dir.chdir('./public/testsites') - Dir.glob('*').each do |dir| - sharding_dir = Site.sharding_dir(dir) - FileUtils.mkdir_p File.join('..', 'newtestsites', sharding_dir) - FileUtils.mv dir, File.join('..', 'newtestsites', sharding_dir) - end - sleep 1 - FileUtils.rmdir './public/testsites' - sleep 1 - FileUtils.mv './public/newtestsites', './public/testsites' -end - desc 'compute_follow_count_scores' task :compute_follow_count_scores => [:environment] do @@ -354,37 +122,6 @@ task :compute_follow_count_scores => [:environment] do end end -desc 'prime_redis_proxy_ssl' -task :prime_redis_proxy_ssl => [:environment] do - site_ids = DB[%{ - select id from sites where domain is not null and ssl_cert is not null and ssl_key is not null - and is_deleted != ? and is_banned != ? - }, true, true].all.collect {|site_id| site_id[:id]} - - site_ids.each do |site_id| - Site[site_id].store_ssl_in_redis_proxy - end -end - -desc 'dedupe_site_blocks' -task :dedupe_site_blocks => [:environment] do - duped_blocks = [] - block_ids = Block.select(:id).all.collect {|b| b.id} - block_ids.each do |block_id| - next unless duped_blocks.select {|db| db.id == block_id}.empty? - block = Block[block_id] - if block - blocks = Block.exclude(id: block.id).where(site_id: block.site_id).where(actioning_site_id: block.actioning_site_id).all - duped_blocks << blocks - duped_blocks.flatten! - end - end - - duped_blocks.each do |duped_block| - duped_block.destroy - end -end - desc 'ml_screenshots_list_dump' task :ml_screenshots_list_dump => [:environment] do ['phishing', 'spam', 'ham', nil].each do |classifier| From 47a55089190febda49b5348853ad567469c8ddee Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Wed, 3 Jan 2024 11:33:18 -0600 Subject: [PATCH 56/74] add note about editor saving issues caused by adblockers --- views/contact.erb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/views/contact.erb b/views/contact.erb index a5596112..79ea393a 100644 --- a/views/contact.erb +++ b/views/contact.erb @@ -75,6 +75,23 @@
+
+
+ +
+
+

+ This can sometimes be caused by ad blockers or other browser extensions. Try disabling them for Neocities and try again (you won't need them here anyways since we don't do any advertising). +

+
+
+
+ +
From c7764d3ed58df60f0971bf0b91cf78309b5ea0f8 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Wed, 3 Jan 2024 17:03:27 -0600 Subject: [PATCH 57/74] unfollow sites when blocked --- models/site.rb | 13 +++++++++++++ tests/acceptance/site_tests.rb | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/models/site.rb b/models/site.rb index 12aeedb3..30971a84 100644 --- a/models/site.rb +++ b/models/site.rb @@ -627,10 +627,23 @@ class Site < Sequel::Model @blocking_site_ids ||= blockings_dataset.select(:site_id).all.collect {|s| s.site_id} end + def unfollow_blocked_sites! + blockings.each do |blocking| + follows.each do |follow| + follow.destroy if follow.actioning_site_id == blocking.site_id + end + + followings.each do |following| + following.destroy if following.site_id == blocking.site_id + end + end + end + def block!(site) block = blockings_dataset.filter(site_id: site.id).first return true if block add_blocking site: site + unfollow_blocked_sites! end def unblock!(site) diff --git a/tests/acceptance/site_tests.rb b/tests/acceptance/site_tests.rb index 6f547448..5c9d58be 100644 --- a/tests/acceptance/site_tests.rb +++ b/tests/acceptance/site_tests.rb @@ -104,6 +104,35 @@ describe 'site page' do visit "/browse?tag=#{@tag}" _(page.find('.website-Gallery .username a')['href']).must_match /\/site\/#{@blocked_site.username}/ end + + it 'removes follows/followings when blocking' do + site = Fabricate :site + not_blocked_site = Fabricate :site + blocked_site = Fabricate :site + + site.add_follow actioning_site: not_blocked_site + site.add_following site: not_blocked_site + + site.add_follow actioning_site: blocked_site + site.add_following site: blocked_site + + _(site.follows.count).must_equal 2 + _(site.followings.count).must_equal 2 + + page.set_rack_session id: site.id + + visit "/site/#{blocked_site.username}" + + click_link 'Block' + click_button 'Block Site' + + _(site.follows.count).must_equal 1 + _(site.followings.count).must_equal 1 + + _(site.follows.count {|s| s.actioning_site == blocked_site}).must_equal 0 + _(site.followings.count {|s| s.site == blocked_site}).must_equal 0 + + end end it '404s if site is banned' do From 02b54afea1312b4872fc7d8286fc5c6b8ae31b61 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Wed, 3 Jan 2024 19:18:19 -0600 Subject: [PATCH 58/74] update deps for sidekiq, redis --- Gemfile | 2 +- Gemfile.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index a912a909..a7a24939 100644 --- a/Gemfile +++ b/Gemfile @@ -8,7 +8,7 @@ gem 'bcrypt' gem 'sinatra-flash', require: 'sinatra/flash' gem 'sinatra-xsendfile', require: 'sinatra/xsendfile' gem 'puma', '< 7', require: nil -gem 'sidekiq', '~> 7.0.8' +gem 'sidekiq', '~> 7' gem 'mail' gem 'net-smtp' gem 'tilt' diff --git a/Gemfile.lock b/Gemfile.lock index c2293234..06a92c69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -280,11 +280,11 @@ GEM sequel (>= 4.38.0) shotgun (0.9.2) rack (>= 1.0) - sidekiq (7.0.9) + sidekiq (7.2.0) concurrent-ruby (< 2) connection_pool (>= 2.3.0) rack (>= 2.2.4) - redis-client (>= 0.11.0) + redis-client (>= 0.14.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -412,7 +412,7 @@ DEPENDENCIES sequel sequel_pg shotgun - sidekiq (~> 7.0.8) + sidekiq (~> 7) simplecov simpleidn sinatra From 350a7039c7f887a23ca76c69095d3e5f1b0af87b Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Thu, 4 Jan 2024 02:30:03 -0600 Subject: [PATCH 59/74] update vagrantfile to work with latest, closes #478 --- Vagrantfile | 2 +- vagrant/common.sh | 6 ++++++ vagrant/development.sh | 8 ++++---- vagrant/ruby.sh | 14 +++++++++----- vagrant/webapp.sh | 11 ++++++++--- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index 4be5831a..fa45054a 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -6,7 +6,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.network :forwarded_port, guest: 9292, host: 9292 config.vm.provider :virtualbox do |vb| - vb.customize ['modifyvm', :id, '--memory', '2048'] + vb.customize ['modifyvm', :id, '--memory', '8192'] vb.name = 'neocities' end end diff --git a/vagrant/common.sh b/vagrant/common.sh index 65153ca2..89c8e0a7 100644 --- a/vagrant/common.sh +++ b/vagrant/common.sh @@ -21,3 +21,9 @@ sed -i 's|UsePAM yes|UsePAM no|g' /etc/ssh/sshd_config #sed -i 's|[#]*PermitRootLogin yes|PermitRootLogin no|g' /etc/ssh/sshd_config service ssh restart + + +wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +sudo dpkg -i google-chrome-stable_current_amd64.deb +sudo apt-get install -f -y +rm google-chrome-stable_current_amd64.deb \ No newline at end of file diff --git a/vagrant/development.sh b/vagrant/development.sh index 02816c8a..cb55928c 100644 --- a/vagrant/development.sh +++ b/vagrant/development.sh @@ -12,10 +12,10 @@ sudo su postgres -c "createuser -d vagrant" sudo su vagrant -c "createdb neocities" sudo su vagrant -c "createdb neocities_test" -sudo sh -c 'echo "local all postgres trust" > /etc/postgresql/10/main/pg_hba.conf' -sudo sh -c 'echo "local all all trust" >> /etc/postgresql/10/main/pg_hba.conf' -sudo sh -c 'echo "host all all 127.0.0.1/32 trust" >> /etc/postgresql/10/main/pg_hba.conf' -sudo sh -c 'echo "host all all ::1/128 trust" >> /etc/postgresql/10/main/pg_hba.conf' +sudo sh -c 'echo "local all postgres trust" > /etc/postgresql/14/main/pg_hba.conf' +sudo sh -c 'echo "local all all trust" >> /etc/postgresql/14/main/pg_hba.conf' +sudo sh -c 'echo "host all all 127.0.0.1/32 trust" >> /etc/postgresql/14/main/pg_hba.conf' +sudo sh -c 'echo "host all all ::1/128 trust" >> /etc/postgresql/14/main/pg_hba.conf' sudo systemctl restart postgresql # Create empty file for disposable email accounts diff --git a/vagrant/ruby.sh b/vagrant/ruby.sh index 9c5f0c98..5bd56606 100644 --- a/vagrant/ruby.sh +++ b/vagrant/ruby.sh @@ -1,7 +1,11 @@ #!/bin/bash -apt-get -y install python-software-properties -apt-add-repository -y ppa:brightbox/ruby-ng -apt-get -y update -apt-get -y install ruby2.6 ruby2.6-dev -gem install bundler --no-document +sudo apt-get -y install autoconf patch build-essential rustc libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev uuid-dev + +wget https://cache.ruby-lang.org/pub/ruby/3.3/ruby-3.3.0.tar.gz +gzip -dc ruby-3.3.0.tar.gz | tar xf - +cd ruby-3.3.0 +./autogen.sh +./configure --enable-yjit --disable-install-doc +make -j && sudo make install +cd .. diff --git a/vagrant/webapp.sh b/vagrant/webapp.sh index fbddae89..77e64db2 100644 --- a/vagrant/webapp.sh +++ b/vagrant/webapp.sh @@ -29,10 +29,15 @@ apt-get install -y \ sed -i 's|[#]*DetectPUA false|DetectPUA true|g' /etc/clamav/clamd.conf -freshclam -service clamav-freshclam start -service clamav-daemon start +#sudo freshclam +#sudo systemctl start clamav-freshclam +# clamav download mirrors have insanely stupid limits so we just put in a github mirror for now +rm -f main.cvd daily.cld daily.cvd bytecode.cvd main.cvd.sha256 daily.cvd.sha256 bytecode.cvd.sha256 main.cvd.* daily.cvd.* && curl -LSOs https://github.com/ladar/clamav-data/raw/main/main.cvd.[01-10] -LSOs https://github.com/ladar/clamav-data/raw/main/main.cvd.sha256 -LSOs https://github.com/ladar/clamav-data/raw/main/daily.cvd.[01-10] -LSOs https://github.com/ladar/clamav-data/raw/main/daily.cvd.sha256 -LSOs https://github.com/ladar/clamav-data/raw/main/bytecode.cvd -LSOs https://github.com/ladar/clamav-data/raw/main/bytecode.cvd.sha256 && cat main.cvd.01 main.cvd.02 main.cvd.03 main.cvd.04 main.cvd.05 main.cvd.06 main.cvd.07 main.cvd.08 main.cvd.09 main.cvd.10 > main.cvd && cat daily.cvd.01 daily.cvd.02 daily.cvd.03 daily.cvd.04 daily.cvd.05 daily.cvd.06 daily.cvd.07 daily.cvd.08 daily.cvd.09 daily.cvd.10 > daily.cvd && sha256sum -c main.cvd.sha256 daily.cvd.sha256 bytecode.cvd.sha256 || { printf "ClamAV database download failed.\n" ; rm -f main.cvd daily.cvd bytecode.cvd ; } ; rm -f main.cvd.sha256 daily.cvd.sha256 bytecode.cvd.sha256 main.cvd.* daily.cvd.* && sudo mv *.cvd /var/lib/clamav/ + + +sudo systemctl enable clamav-daemon +sudo systemctl start clamav-daemon usermod -G vagrant clamav cd /vagrant From f387059c6687f21f5c4109a95d97e3a6ab016b80 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Thu, 4 Jan 2024 18:02:53 +0000 Subject: [PATCH 60/74] puma: use phased restart instead of preload --- puma_config.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/puma_config.rb b/puma_config.rb index 447cb13d..240bc22f 100644 --- a/puma_config.rb +++ b/puma_config.rb @@ -7,7 +7,8 @@ pidfile '/var/run/neocities/neocities.pid' stdout_redirect '/var/log/neocities/neocities.stdout.log', '/var/log/neocities/neocities.stderr.log', true quiet workers Facter.value('processors')['count'] -preload_app! +#preload_app! +prune_bundler on_worker_boot { DB.disconnect } bind 'unix:/var/run/neocities/neocities.sock?backlog=2048' supported_http_methods Puma::Const::IANA_HTTP_METHODS From cee8da725faf31a73b7d6ac3e3673419b8360ec5 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Thu, 4 Jan 2024 13:23:21 -0600 Subject: [PATCH 61/74] add custom domain note about removing url redirects --- views/settings/site/custom_domain.erb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/views/settings/site/custom_domain.erb b/views/settings/site/custom_domain.erb index 8308e426..89b1002c 100644 --- a/views/settings/site/custom_domain.erb +++ b/views/settings/site/custom_domain.erb @@ -36,10 +36,16 @@

Step 4

-

Wait about 5 minutes for the nameserver changes to update. Sometimes it can take a short while for your domain provider to update their records.

+

+ Remove any "URL redirects" if they are present, they are not needed and cause issues with connecting the domain. For example, Namecheap has a URL Redirect Record on new domains that needs to be deleted. +

Step 5

+

Wait about 5 minutes for the nameserver changes to update. Sometimes it can take a short while for your domain provider to update their records.

+ +

Step 6

+

Finally, add your domain name to the box below (just the yourdomain.com, don't add any subdomains), and your domain should come online within 5 minutes! We will automatically create SSL certs for your domain.

From 0ef9bdefcef98d3f350cc80873f6d50daf787dd7 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Fri, 5 Jan 2024 14:46:29 -0600 Subject: [PATCH 62/74] add CSP, remove gravicons that are now blocked by it --- app.rb | 10 +++++++--- views/_team.erb | 26 -------------------------- views/supporter/thanks.erb | 1 - 3 files changed, 7 insertions(+), 30 deletions(-) delete mode 100644 views/_team.erb diff --git a/app.rb b/app.rb index ffa14a55..16aba785 100644 --- a/app.rb +++ b/app.rb @@ -91,9 +91,13 @@ after do end end -#after do - #response.headers['Content-Security-Policy'] = %{block-all-mixed-content; default-src 'self'; connect-src 'self' https://api.stripe.com https://assets.hcaptcha.com; frame-src https://assets.hcaptcha.com https://js.stripe.com; script-src 'self' 'unsafe-inline' https://js.stripe.com https://hcaptcha.com https://assets.hcaptcha.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: } -#end +after do + response.headers['Content-Security-Policy'] = %{default-src 'self' 'unsafe-inline'; script-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' https://hcaptcha.com, https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com, https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} +end + +connect-src, https://api.stripe.com, https://maps.googleapis.com +frame-src, https://js.stripe.com, https://hooks.stripe.com +script-src, https://js.stripe.com, https://maps.googleapis.com not_found do api_not_found if @api diff --git a/views/_team.erb b/views/_team.erb deleted file mode 100644 index 741764db..00000000 --- a/views/_team.erb +++ /dev/null @@ -1,26 +0,0 @@ -
-
-

The Neocities Team

- -
-
- -
-
-

Follow us on Twitter or Facebook

-
diff --git a/views/supporter/thanks.erb b/views/supporter/thanks.erb index 43c2b30e..0e1724c1 100644 --- a/views/supporter/thanks.erb +++ b/views/supporter/thanks.erb @@ -27,5 +27,4 @@ Get Started

- <%== erb :'_team', layout: false %>
From 8c5a8b6f2281dc5cfb779113f90a32a86cc98e5a Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Fri, 5 Jan 2024 14:47:20 -0600 Subject: [PATCH 63/74] fixes for a few missing csp entries needed --- app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.rb b/app.rb index 16aba785..cbf6e267 100644 --- a/app.rb +++ b/app.rb @@ -92,7 +92,7 @@ after do end after do - response.headers['Content-Security-Policy'] = %{default-src 'self' 'unsafe-inline'; script-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' https://hcaptcha.com, https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com, https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} + response.headers['Content-Security-Policy'] = %{default-src 'self' data: blob: 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} end connect-src, https://api.stripe.com, https://maps.googleapis.com From afb1d756e00c43196c9bc377f6e347cb996a574f Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Fri, 5 Jan 2024 14:50:25 -0600 Subject: [PATCH 64/74] clean up pasted reference code --- app.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app.rb b/app.rb index cbf6e267..dce9c6da 100644 --- a/app.rb +++ b/app.rb @@ -95,10 +95,6 @@ after do response.headers['Content-Security-Policy'] = %{default-src 'self' data: blob: 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} end -connect-src, https://api.stripe.com, https://maps.googleapis.com -frame-src, https://js.stripe.com, https://hooks.stripe.com -script-src, https://js.stripe.com, https://maps.googleapis.com - not_found do api_not_found if @api redirect_to_internet_archive_for_geocities_sites From 837b0ea2d0380617918253812c4266e5ac52b84d Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Fri, 5 Jan 2024 16:37:14 -0600 Subject: [PATCH 65/74] no hyphens at start or end of username --- models/site.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/site.rb b/models/site.rb index 30971a84..80defff7 100644 --- a/models/site.rb +++ b/models/site.rb @@ -658,7 +658,7 @@ class Site < Sequel::Model end def self.valid_username?(username) - !username.empty? && username.match(/^[a-zA-Z0-9_\-]+$/i) + !username.empty? && username.match(/^[a-zA-Z0-9][a-zA-Z0-9_\-]+[a-zA-Z0-9]$/i) end def self.disposable_email_domains @@ -986,7 +986,7 @@ class Site < Sequel::Model super if !self.class.valid_username?(values[:username]) - errors.add :username, 'Usernames can only contain letters, numbers, and hyphens.' + errors.add :username, 'Usernames can only contain letters, numbers, and hyphens, and cannot start or end with a hyphen.' end if !values[:username].blank? From f9852d04fde9b908428f42b06472b31c02f50eda Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 6 Jan 2024 13:27:48 -0600 Subject: [PATCH 66/74] update to latest chartjs --- app.rb | 2 +- public/js/Chart.min.js | 11 ----- public/js/chart.js | 20 +++++++++ views/layout.erb | 2 +- views/site/stats.erb | 92 +++++++++++++++++++++++++++++------------- 5 files changed, 86 insertions(+), 41 deletions(-) delete mode 100644 public/js/Chart.min.js create mode 100644 public/js/chart.js diff --git a/app.rb b/app.rb index dce9c6da..5099557a 100644 --- a/app.rb +++ b/app.rb @@ -92,7 +92,7 @@ after do end after do - response.headers['Content-Security-Policy'] = %{default-src 'self' data: blob: 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} + response.headers['Content-Security-Policy'] = %{default-src 'self' data: blob: 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} unless self.class.development? end not_found do diff --git a/public/js/Chart.min.js b/public/js/Chart.min.js deleted file mode 100644 index 3a0a2c87..00000000 --- a/public/js/Chart.min.js +++ /dev/null @@ -1,11 +0,0 @@ -/*! - * Chart.js - * http://chartjs.org/ - * Version: 1.0.2 - * - * Copyright 2015 Nick Downie - * Released under the MIT license - * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md - */ -(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)0&&ithis.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.ythis.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file diff --git a/public/js/chart.js b/public/js/chart.js new file mode 100644 index 00000000..78c4e5d0 --- /dev/null +++ b/public/js/chart.js @@ -0,0 +1,20 @@ +/** + * Skipped minification because the original files appears to be already minified. + * Original file: /npm/chart.js@4.4.1/dist/chart.umd.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files + */ +/*! + * Chart.js v4.4.1 + * https://www.chartjs.org + * (c) 2023 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Go},get Decimation(){return Qo},get Filler(){return ma},get Legend(){return ya},get SubTitle(){return ka},get Title(){return Ma},get Tooltip(){return Ba}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;at,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)t-e)).pop(),e}function N(t){return!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;sl&&h=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return nt[s][e]et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;ss&&t[n-1]>i;)n--;return s>0||n{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const n=s.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),n}})})))}function rt(t,e){const i=t._chartjs;if(!i)return;const s=i.listeners,n=s.indexOf(e);-1!==n&&s.splice(n,1),s.length>0||(ot.forEach((e=>{delete t[e]})),delete t._chartjs)}function lt(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const ht="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function ct(t,e){let i=[],s=!1;return function(...n){i=n,s||(s=!0,ht.call(window,(()=>{s=!1,t.apply(e,i)})))}}function dt(t,e){let i;return function(...s){return e?(clearTimeout(i),i=setTimeout(t,e,s)):t.apply(this,s),e}}const ut=t=>"start"===t?"left":"end"===t?"right":"center",ft=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,gt=(t,e,i,s)=>t===(s?"left":"right")?i:"center"===t?(e+i)/2:e;function pt(t,e,i){const s=e.length;let n=0,o=s;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:h,max:c,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(n=J(Math.min(it(r,l,h).lo,i?s:it(e,l,a.getPixelForValue(h)).lo),0,s-1)),o=u?J(Math.max(it(r,a.axis,c,!0).hi+1,i?0:it(e,l,a.getPixelForValue(c),!0).hi+1),n,s)-n:s-n}return{start:n,count:o}}function mt(t){const{xScale:e,yScale:i,_scaleRanges:s}=t,n={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!s)return t._scaleRanges=n,!0;const o=s.xmin!==e.min||s.xmax!==e.max||s.ymin!==i.min||s.ymax!==i.max;return Object.assign(s,n),o}class bt{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,s){const n=e.listeners[s],o=e.duration;n.forEach((s=>s({chart:t,initial:e.initial,numSteps:o,currentStep:Math.min(i-e.start,o)})))}_refresh(){this._request||(this._running=!0,this._request=ht.call(window,(()=>{this._update(),this._request=null,this._running&&this._refresh()})))}_update(t=Date.now()){let e=0;this._charts.forEach(((i,s)=>{if(!i.running||!i.items.length)return;const n=i.items;let o,a=n.length-1,r=!1;for(;a>=0;--a)o=n[a],o._active?(o._total>i.duration&&(i.duration=o._total),o.tick(t),r=!0):(n[a]=n[n.length-1],n.pop());r&&(s.draw(),this._notify(s,i,t,"progress")),n.length||(i.running=!1,this._notify(s,i,t,"complete"),i.initial=!1),e+=n.length})),this._lastDate=t,0===e&&(this._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let s=i.length-1;for(;s>=0;--s)i[s].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var xt=new bt; +/*! + * @kurkle/color v0.3.2 + * https://github.com/kurkle/color#readme + * (c) 2023 Jukka Kurkela + * Released under the MIT License + */function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),s=Ft(s),t.r=s[0],t.g=s[1],t.b=s[2]}}function qt(t,e){return t?Object.assign(e||{},t):t}function Kt(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=Mt(t[3]))):(e=qt(t,{r:0,g:0,b:0,a:1})).a=Mt(e.a),e}function Gt(t){return"r"===t.charAt(0)?function(t){const e=$t.exec(t);let i,s,n,o=255;if(e){if(e[7]!==i){const t=+e[7];o=e[8]?vt(t):yt(255*t,0,255)}return i=+e[1],s=+e[3],n=+e[5],i=255&(e[2]?vt(i):yt(i,0,255)),s=255&(e[4]?vt(s):yt(s,0,255)),n=255&(e[6]?vt(n):yt(n,0,255)),{r:i,g:s,b:n,a:o}}}(t):Bt(t)}class Zt{constructor(t){if(t instanceof Zt)return t;const e=typeof t;let i;var s,n,o;"object"===e?i=Kt(t):"string"===e&&(o=(s=t).length,"#"===s[0]&&(4===o||5===o?n={r:255&17*St[s[1]],g:255&17*St[s[2]],b:255&17*St[s[3]],a:5===o?17*St[s[4]]:255}:7!==o&&9!==o||(n={r:St[s[1]]<<4|St[s[2]],g:St[s[3]]<<4|St[s[4]],b:St[s[5]]<<4|St[s[6]],a:9===o?St[s[7]]<<4|St[s[8]]:255})),i=n||jt(t)||Gt(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=qt(this._rgb);return t&&(t.a=wt(t.a)),t}set rgb(t){this._rgb=Kt(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${wt(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):void 0;var t}hexString(){return this._valid?At(this._rgb):void 0}hslString(){return this._valid?function(t){if(!t)return;const e=It(t),i=e[0],s=kt(e[1]),n=kt(e[2]);return t.a<255?`hsla(${i}, ${s}%, ${n}%, ${wt(t.a)})`:`hsl(${i}, ${s}%, ${n}%)`}(this._rgb):void 0}mix(t,e){if(t){const i=this.rgb,s=t.rgb;let n;const o=e===n?.5:e,a=2*o-1,r=i.a-s.a,l=((a*r==-1?a:(a+r)/(1+a*r))+1)/2;n=1-l,i.r=255&l*i.r+n*s.r+.5,i.g=255&l*i.g+n*s.g+.5,i.b=255&l*i.b+n*s.b+.5,i.a=o*i.a+(1-o)*s.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=function(t,e,i){const s=Ut(wt(t.r)),n=Ut(wt(t.g)),o=Ut(wt(t.b));return{r:Mt(Yt(s+i*(Ut(wt(e.r))-s))),g:Mt(Yt(n+i*(Ut(wt(e.g))-n))),b:Mt(Yt(o+i*(Ut(wt(e.b))-o))),a:t.a+i*(e.a-t.a)}}(this._rgb,t._rgb,e)),this}clone(){return new Zt(this.rgb)}alpha(t){return this._rgb.a=Mt(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=_t(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Xt(this._rgb,2,t),this}darken(t){return Xt(this._rgb,2,-t),this}saturate(t){return Xt(this._rgb,1,t),this}desaturate(t){return Xt(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=It(t);i[0]=Vt(i[0]+e),i=Ft(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function Jt(t){if(t&&"object"==typeof t){const e=t.toString();return"[object CanvasPattern]"===e||"[object CanvasGradient]"===e}return!1}function Qt(t){return Jt(t)?t:new Zt(t)}function te(t){return Jt(t)?t:new Zt(t).saturate(.5).darken(.1).hexString()}const ee=["x","y","borderWidth","radius","tension"],ie=["color","borderColor","backgroundColor"];const se=new Map;function ne(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let s=se.get(i);return s||(s=new Intl.NumberFormat(t,e),se.set(i,s)),s}(e,i).format(t)}const oe={values:t=>n(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const s=this.chart.options.locale;let n,o=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(n="scientific"),o=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>=1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=z(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(t,s,l)},logarithmic(t,e,i){if(0===t)return"0";const s=i[e].significand||t/Math.pow(10,Math.floor(z(t)));return[1,2,3,5,10,15].includes(s)||e>.8*i.length?oe.numeric.call(this,t,e,i):""}};var ae={formatters:oe};const re=Object.create(null),le=Object.create(null);function he(t,e){if(!e)return t;const i=e.split(".");for(let e=0,s=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>te(e.backgroundColor),this.hoverBorderColor=(t,e)=>te(e.borderColor),this.hoverColor=(t,e)=>te(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return ce(this,t,e)}get(t){return he(this,t)}describe(t,e){return ce(le,t,e)}override(t,e){return ce(re,t,e)}route(t,e,i,s){const n=he(this,t),a=he(this,i),r="_"+e;Object.defineProperties(n,{[r]:{value:n[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[r],e=a[s];return o(t)?Object.assign({},e,t):l(t,e)},set(t){this[r]=t}}})}apply(t){t.forEach((t=>t(this)))}}var ue=new de({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[function(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),t.set("animations",{colors:{type:"color",properties:ie},numbers:{type:"number",properties:ee}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}})},function(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})},function(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ae.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t&&"dash"!==t}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:t=>"backdropPadding"!==t&&"callback"!==t,_indexable:t=>"backdropPadding"!==t})}]);function fe(){return"undefined"!=typeof window&&"undefined"!=typeof document}function ge(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function pe(t,e,i){let s;return"string"==typeof t?(s=parseInt(t,10),-1!==t.indexOf("%")&&(s=s/100*e.parentNode[i])):s=t,s}const me=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function be(t,e){return me(t).getPropertyValue(e)}const xe=["top","right","bottom","left"];function _e(t,e,i){const s={};i=i?"-"+i:"";for(let n=0;n<4;n++){const o=xe[n];s[o]=parseFloat(t[e+"-"+o+i])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}const ye=(t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot);function ve(t,e){if("native"in t)return t;const{canvas:i,currentDevicePixelRatio:s}=e,n=me(i),o="border-box"===n.boxSizing,a=_e(n,"padding"),r=_e(n,"border","width"),{x:l,y:h,box:c}=function(t,e){const i=t.touches,s=i&&i.length?i[0]:t,{offsetX:n,offsetY:o}=s;let a,r,l=!1;if(ye(n,o,t.target))a=n,r=o;else{const t=e.getBoundingClientRect();a=s.clientX-t.left,r=s.clientY-t.top,l=!0}return{x:a,y:r,box:l}}(t,i),d=a.left+(c&&r.left),u=a.top+(c&&r.top);let{width:f,height:g}=e;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/s),y:Math.round((h-u)/g*i.height/s)}}const Me=t=>Math.round(10*t)/10;function we(t,e,i,s){const n=me(t),o=_e(n,"margin"),a=pe(n.maxWidth,t,"clientWidth")||T,r=pe(n.maxHeight,t,"clientHeight")||T,l=function(t,e,i){let s,n;if(void 0===e||void 0===i){const o=ge(t);if(o){const t=o.getBoundingClientRect(),a=me(o),r=_e(a,"border","width"),l=_e(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,s=pe(a.maxWidth,o,"clientWidth"),n=pe(a.maxHeight,o,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:s||T,maxHeight:n||T}}(t,e,i);let{width:h,height:c}=l;if("content-box"===n.boxSizing){const t=_e(n,"border","width"),e=_e(n,"padding");h-=e.width+t.width,c-=e.height+t.height}h=Math.max(0,h-o.width),c=Math.max(0,s?h/s:c-o.height),h=Me(Math.min(h,a,l.maxWidth)),c=Me(Math.min(c,r,l.maxHeight)),h&&!c&&(c=Me(h/2));return(void 0!==e||void 0!==i)&&s&&l.height&&c>l.height&&(c=l.height,h=Me(Math.floor(c*s))),{width:h,height:c}}function ke(t,e,i){const s=e||1,n=Math.floor(t.height*s),o=Math.floor(t.width*s);t.height=Math.floor(t.height),t.width=Math.floor(t.width);const a=t.canvas;return a.style&&(i||!a.style.height&&!a.style.width)&&(a.style.height=`${t.height}px`,a.style.width=`${t.width}px`),(t.currentDevicePixelRatio!==s||a.height!==n||a.width!==o)&&(t.currentDevicePixelRatio=s,a.height=n,a.width=o,t.ctx.setTransform(s,0,0,s,0,0),!0)}const Se=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};fe()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch(t){}return t}();function Pe(t,e){const i=be(t,e),s=i&&i.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function De(t){return!t||s(t.size)||s(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function Ce(t,e,i,s,n){let o=e[n];return o||(o=e[n]=t.measureText(n).width,i.push(n)),o>s&&(s=o),s}function Oe(t,e,i,s){let o=(s=s||{}).data=s.data||{},a=s.garbageCollect=s.garbageCollect||[];s.font!==e&&(o=s.data={},a=s.garbageCollect=[],s.font=e),t.save(),t.font=e;let r=0;const l=i.length;let h,c,d,u,f;for(h=0;hi.length){for(h=0;h0&&t.stroke()}}function Re(t,e,i){return i=i||.5,!e||t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==r.strokeColor;let c,d;for(t.save(),t.font=a.string,function(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),s(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}(t,r),c=0;ct[0])){const o=i||t;void 0===s&&(s=ti("_fallback",t));const a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:s,_getTarget:n,override:i=>je([i,...t],e,o,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,s)=>qe(i,s,(()=>function(t,e,i,s){let n;for(const o of e)if(n=ti(Ue(o,t),i),void 0!==n)return Xe(t,n)?Je(i,s,t,n):n}(s,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>ei(t).includes(e),ownKeys:t=>ei(t),set(t,e,i){const s=t._storage||(t._storage=n());return t[e]=s[e]=i,delete t._keys,!0}})}function $e(t,e,i,s){const a={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Ye(t,s),setContext:e=>$e(t,e,i,s),override:n=>$e(t.override(n),e,i,s)};return new Proxy(a,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>qe(t,e,(()=>function(t,e,i){const{_proxy:s,_context:a,_subProxy:r,_descriptors:l}=t;let h=s[e];S(h)&&l.isScriptable(e)&&(h=function(t,e,i,s){const{_proxy:n,_context:o,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+t);r.add(t);let l=e(o,a||s);r.delete(t),Xe(t,l)&&(l=Je(n._scopes,n,t,l));return l}(e,h,t,i));n(h)&&h.length&&(h=function(t,e,i,s){const{_proxy:n,_context:a,_subProxy:r,_descriptors:l}=i;if(void 0!==a.index&&s(t))return e[a.index%e.length];if(o(e[0])){const i=e,s=n._scopes.filter((t=>t!==i));e=[];for(const o of i){const i=Je(s,n,t,o);e.push($e(i,a,r&&r[t],l))}}return e}(e,h,t,l.isIndexable));Xe(e,h)&&(h=$e(h,a,r&&r[e],l));return h}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,s)=>(t[i]=s,delete e[i],!0)})}function Ye(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:s=e.indexable,_allKeys:n=e.allKeys}=t;return{allKeys:n,scriptable:i,indexable:s,isScriptable:S(i)?i:()=>i,isIndexable:S(s)?s:()=>s}}const Ue=(t,e)=>t?t+w(e):e,Xe=(t,e)=>o(e)&&"adapters"!==t&&(null===Object.getPrototypeOf(e)||e.constructor===Object);function qe(t,e,i){if(Object.prototype.hasOwnProperty.call(t,e))return t[e];const s=i();return t[e]=s,s}function Ke(t,e,i){return S(t)?t(e,i):t}const Ge=(t,e)=>!0===t?e:"string"==typeof t?M(e,t):void 0;function Ze(t,e,i,s,n){for(const o of e){const e=Ge(i,o);if(e){t.add(e);const o=Ke(e._fallback,i,n);if(void 0!==o&&o!==i&&o!==s)return o}else if(!1===e&&void 0!==s&&i!==s)return null}return!1}function Je(t,e,i,s){const a=e._rootScopes,r=Ke(e._fallback,i,s),l=[...t,...a],h=new Set;h.add(s);let c=Qe(h,l,i,r||i,s);return null!==c&&((void 0===r||r===i||(c=Qe(h,l,r,c,s),null!==c))&&je(Array.from(h),[""],a,r,(()=>function(t,e,i){const s=t._getTarget();e in s||(s[e]={});const a=s[e];if(n(a)&&o(i))return i;return a||{}}(e,i,s))))}function Qe(t,e,i,s,n){for(;i;)i=Ze(t,e,i,s,n);return i}function ti(t,e){for(const i of e){if(!i)continue;const e=i[t];if(void 0!==e)return e}}function ei(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return Array.from(e)}(t._scopes)),e}function ii(t,e,i,s){const{iScale:n}=t,{key:o="r"}=this._parsing,a=new Array(s);let r,l,h,c;for(r=0,l=s;re"x"===t?"y":"x";function ai(t,e,i,s){const n=t.skip?e:t,o=e,a=i.skip?e:i,r=q(o,n),l=q(a,o);let h=r/(r+l),c=l/(r+l);h=isNaN(h)?0:h,c=isNaN(c)?0:c;const d=s*h,u=s*c;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function ri(t,e="x"){const i=oi(e),s=t.length,n=Array(s).fill(0),o=Array(s);let a,r,l,h=ni(t,0);for(a=0;a!t.skip))),"monotone"===e.cubicInterpolationMode)ri(t,n);else{let i=s?t[t.length-1]:t[0];for(o=0,a=t.length;o0===t||1===t,di=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*O/i),ui=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*O/i)+1,fi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*E),easeOutSine:t=>Math.sin(t*E),easeInOutSine:t=>-.5*(Math.cos(C*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>ci(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ci(t)?t:di(t,.075,.3),easeOutElastic:t=>ci(t)?t:ui(t,.075,.3),easeInOutElastic(t){const e=.1125;return ci(t)?t:t<.5?.5*di(2*t,e,.45):.5+.5*ui(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-fi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*fi.easeInBounce(2*t):.5*fi.easeOutBounce(2*t-1)+.5};function gi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:t.y+i*(e.y-t.y)}}function pi(t,e,i,s){return{x:t.x+i*(e.x-t.x),y:"middle"===s?i<.5?t.y:e.y:"after"===s?i<1?t.y:e.y:i>0?e.y:t.y}}function mi(t,e,i,s){const n={x:t.cp2x,y:t.cp2y},o={x:e.cp1x,y:e.cp1y},a=gi(t,n,i),r=gi(n,o,i),l=gi(o,e,i),h=gi(a,r,i),c=gi(r,l,i);return gi(h,c,i)}const bi=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,xi=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function _i(t,e){const i=(""+t).match(bi);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}const yi=t=>+t||0;function vi(t,e){const i={},s=o(e),n=s?Object.keys(e):e,a=o(t)?s?i=>l(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of n)i[t]=yi(a(t));return i}function Mi(t){return vi(t,{top:"y",right:"x",bottom:"y",left:"x"})}function wi(t){return vi(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ki(t){const e=Mi(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Si(t,e){t=t||{},e=e||ue.font;let i=l(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let s=l(t.style,e.style);s&&!(""+s).match(xi)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);const n={family:l(t.family,e.family),lineHeight:_i(l(t.lineHeight,e.lineHeight),i),size:i,style:s,weight:l(t.weight,e.weight),string:""};return n.string=De(n),n}function Pi(t,e,i,s){let o,a,r,l=!0;for(o=0,a=t.length;oi&&0===t?0:t+e;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function Ci(t,e){return Object.assign(Object.create(t),e)}function Oi(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function Ai(t,e){let i,s;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,s=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=s)}function Ti(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function Li(t){return"angle"===t?{between:Z,compare:K,normalize:G}:{between:tt,compare:(t,e)=>t-e,normalize:t=>t}}function Ei({start:t,end:e,count:i,loop:s,style:n}){return{start:t%i,end:e%i,loop:s&&(e-t+1)%i==0,style:n}}function Ri(t,e,i){if(!i)return[t];const{property:s,start:n,end:o}=i,a=e.length,{compare:r,between:l,normalize:h}=Li(s),{start:c,end:d,loop:u,style:f}=function(t,e,i){const{property:s,start:n,end:o}=i,{between:a,normalize:r}=Li(s),l=e.length;let h,c,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,h=0,c=l;hx||l(n,b,p)&&0!==r(n,b),v=()=>!x||0===r(o,p)||l(o,b,p);for(let t=c,i=c;t<=d;++t)m=e[t%a],m.skip||(p=h(m[s]),p!==b&&(x=l(p,n,o),null===_&&y()&&(_=0===r(p,n)?t:i),null!==_&&v()&&(g.push(Ei({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,b=p));return null!==_&&g.push(Ei({start:_,end:d,loop:u,count:a,style:f})),g}function Ii(t,e){const i=[],s=t.segments;for(let n=0;nn&&t[o%e].skip;)o--;return o%=e,{start:n,end:o}}(i,n,o,s);if(!0===s)return Fi(t,[{start:a,end:r,loop:o}],i,e);return Fi(t,function(t,e,i,s){const n=t.length,o=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%n];i.skip||i.stop?l.skip||(s=!1,o.push({start:e%n,end:(a-1)%n,loop:s}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&o.push({start:e%n,end:r%n,loop:s}),o}(i,a,r{t[a](e[i],n)&&(o.push({element:t,datasetIndex:s,index:l}),r=r||t.inRange(e.x,e.y,n))})),s&&!r?[]:o}var Xi={evaluateInteractionItems:Hi,modes:{index(t,e,i,s){const n=ve(e,t),o=i.axis||"x",a=i.includeInvisible||!1,r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a),l=[];return r.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=r[0].index,i=t.data[e];i&&!i.skip&&l.push({element:i,datasetIndex:t.index,index:e})})),l):[]},dataset(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;let r=i.intersect?ji(t,n,o,s,a):Yi(t,n,o,!1,s,a);if(r.length>0){const e=r[0].datasetIndex,i=t.getDatasetMeta(e).data;r=[];for(let t=0;tji(t,ve(e,t),i.axis||"xy",s,i.includeInvisible||!1),nearest(t,e,i,s){const n=ve(e,t),o=i.axis||"xy",a=i.includeInvisible||!1;return Yi(t,n,o,i.intersect,s,a)},x:(t,e,i,s)=>Ui(t,ve(e,t),"x",i.intersect,s),y:(t,e,i,s)=>Ui(t,ve(e,t),"y",i.intersect,s)}};const qi=["left","top","right","bottom"];function Ki(t,e){return t.filter((t=>t.pos===e))}function Gi(t,e){return t.filter((t=>-1===qi.indexOf(t.pos)&&t.box.axis===e))}function Zi(t,e){return t.sort(((t,i)=>{const s=e?i:t,n=e?t:i;return s.weight===n.weight?s.index-n.index:s.weight-n.weight}))}function Ji(t,e){const i=function(t){const e={};for(const i of t){const{stack:t,pos:s,stackWeight:n}=i;if(!t||!qi.includes(s))continue;const o=e[t]||(e[t]={count:0,placed:0,weight:0,size:0});o.count++,o.weight+=n}return e}(t),{vBoxMaxWidth:s,hBoxMaxHeight:n}=e;let o,a,r;for(o=0,a=t.length;o{s[t]=Math.max(e[t],i[t])})),s}return s(t?["left","right"]:["top","bottom"])}function ss(t,e,i,s){const n=[];let o,a,r,l,h,c;for(o=0,a=t.length,h=0;ot.box.fullSize)),!0),s=Zi(Ki(e,"left"),!0),n=Zi(Ki(e,"right")),o=Zi(Ki(e,"top"),!0),a=Zi(Ki(e,"bottom")),r=Gi(e,"x"),l=Gi(e,"y");return{fullSize:i,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ki(e,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}(t.boxes),l=r.vertical,h=r.horizontal;u(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const c=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/c,hBoxMaxHeight:a/2}),f=Object.assign({},n);ts(f,ki(s));const g=Object.assign({maxPadding:f,w:o,h:a,x:n.left,y:n.top},n),p=Ji(l.concat(h),d);ss(r.fullSize,g,d,p),ss(l,g,d,p),ss(h,g,d,p)&&ss(l,g,d,p),function(t){const e=t.maxPadding;function i(i){const s=Math.max(e[i]-t[i],0);return t[i]+=s,s}t.y+=i("top"),t.x+=i("left"),i("right"),i("bottom")}(g),os(r.leftAndTop,g,d,p),g.x+=g.w,g.y+=g.h,os(r.rightAndBottom,g,d,p),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},u(r.chartArea,(e=>{const i=e.box;Object.assign(i,t.chartArea),i.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})}))}};class rs{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,s){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,s?Math.floor(e/s):i)}}isAttached(t){return!0}updateConfig(t){}}class ls extends rs{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const hs="$chartjs",cs={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ds=t=>null===t||""===t;const us=!!Se&&{passive:!0};function fs(t,e,i){t.canvas.removeEventListener(e,i,us)}function gs(t,e){for(const i of t)if(i===e||i.contains(e))return!0}function ps(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.addedNodes,s),e=e&&!gs(i.removedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}function ms(t,e,i){const s=t.canvas,n=new MutationObserver((t=>{let e=!1;for(const i of t)e=e||gs(i.removedNodes,s),e=e&&!gs(i.addedNodes,s);e&&i()}));return n.observe(document,{childList:!0,subtree:!0}),n}const bs=new Map;let xs=0;function _s(){const t=window.devicePixelRatio;t!==xs&&(xs=t,bs.forEach(((e,i)=>{i.currentDevicePixelRatio!==t&&e()})))}function ys(t,e,i){const s=t.canvas,n=s&&ge(s);if(!n)return;const o=ct(((t,e)=>{const s=n.clientWidth;i(t,e),s{const e=t[0],i=e.contentRect.width,s=e.contentRect.height;0===i&&0===s||o(i,s)}));return a.observe(n),function(t,e){bs.size||window.addEventListener("resize",_s),bs.set(t,e)}(t,o),a}function vs(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){bs.delete(t),bs.size||window.removeEventListener("resize",_s)}(t)}function Ms(t,e,i){const s=t.canvas,n=ct((e=>{null!==t.ctx&&i(function(t,e){const i=cs[t.type]||t.type,{x:s,y:n}=ve(t,e);return{type:i,chart:e,native:t,x:void 0!==s?s:null,y:void 0!==n?n:null}}(e,t))}),t);return function(t,e,i){t.addEventListener(e,i,us)}(s,e,n),n}class ws extends rs{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,s=t.getAttribute("height"),n=t.getAttribute("width");if(t[hs]={initial:{height:s,width:n,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ds(n)){const e=Pe(t,"width");void 0!==e&&(t.width=e)}if(ds(s))if(""===t.style.height)t.height=t.width/(e||2);else{const e=Pe(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e[hs])return!1;const i=e[hs].initial;["height","width"].forEach((t=>{const n=i[t];s(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e[hs],!0}addEventListener(t,e,i){this.removeEventListener(t,e);const s=t.$proxies||(t.$proxies={}),n={attach:ps,detach:ms,resize:ys}[e]||Ms;s[e]=n(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),s=i[e];if(!s)return;({attach:vs,detach:vs,resize:vs}[e]||fs)(t,e,s),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,s){return we(t,e,i,s)}isAttached(t){const e=ge(t);return!(!e||!e.isConnected)}}function ks(t){return!fe()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?ls:ws}var Ss=Object.freeze({__proto__:null,BasePlatform:rs,BasicPlatform:ls,DomPlatform:ws,_detectPlatform:ks});const Ps="transparent",Ds={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const s=Qt(t||Ps),n=s.valid&&Qt(e||Ps);return n&&n.valid?n.mix(s,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class Cs{constructor(t,e,i,s){const n=e[i];s=Pi([t.to,s,n,t.from]);const o=Pi([t.from,n,s]);this._active=!0,this._fn=t.fn||Ds[t.type||typeof o],this._easing=fi[t.easing]||fi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=o,this._to=s,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);const s=this._target[this._prop],n=i-this._start,o=this._duration-n;this._start=i,this._duration=Math.floor(Math.max(o,t.duration)),this._total+=n,this._loop=!!t.loop,this._to=Pi([t.to,e,s,t.from]),this._from=Pi([t.from,s,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const e=t-this._start,i=this._duration,s=this._prop,n=this._from,o=this._loop,a=this._to;let r;if(this._active=n!==a&&(o||e1?2-r:r,r=this._easing(Math.min(1,Math.max(0,r))),this._target[s]=this._fn(n,a,r))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t{const a=t[s];if(!o(a))return;const r={};for(const t of e)r[t]=a[t];(n(a.properties)&&a.properties||[s]).forEach((t=>{t!==s&&i.has(t)||i.set(t,r)}))}))}_animateOptions(t,e){const i=e.options,s=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!s)return[];const n=this._createAnimations(s,i);return i.$shared&&function(t,e){const i=[],s=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),n}_createAnimations(t,e){const i=this._properties,s=[],n=t.$animations||(t.$animations={}),o=Object.keys(e),a=Date.now();let r;for(r=o.length-1;r>=0;--r){const l=o[r];if("$"===l.charAt(0))continue;if("options"===l){s.push(...this._animateOptions(t,e));continue}const h=e[l];let c=n[l];const d=i.get(l);if(c){if(d&&c.active()){c.update(d,h,a);continue}c.cancel()}d&&d.duration?(n[l]=c=new Cs(d,t,l,h),s.push(c)):t[l]=h}return s}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(xt.add(this._chart,i),!0):void 0}}function As(t,e){const i=t&&t.options||{},s=i.reverse,n=void 0===i.min?e:0,o=void 0===i.max?e:0;return{start:s?o:n,end:s?n:o}}function Ts(t,e){const i=[],s=t._getSortedDatasetMetas(e);let n,o;for(n=0,o=s.length;n0||!i&&e<0)return n.index}return null}function zs(t,e){const{chart:i,_cachedMeta:s}=t,n=i._stacks||(i._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,h=a.axis,c=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(o,a,s),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Vs(t,e){const i=t.controller.index,s=t.vScale&&t.vScale.axis;if(s){e=e||t._parsed;for(const t of e){const e=t._stacks;if(!e||void 0===e[s]||void 0===e[s][i])return;delete e[s][i],void 0!==e[s]._visualValues&&void 0!==e[s]._visualValues[i]&&delete e[s]._visualValues[i]}}}const Bs=t=>"reset"===t||"none"===t,Ws=(t,e)=>e?t:Object.assign({},t);class Ns{static defaults={};static datasetElementType=null;static dataElementType=null;constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Es(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Vs(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,e=this._cachedMeta,i=this.getDataset(),s=(t,e,i,s)=>"x"===t?e:"r"===t?s:i,n=e.xAxisID=l(i.xAxisID,Fs(t,"x")),o=e.yAxisID=l(i.yAxisID,Fs(t,"y")),a=e.rAxisID=l(i.rAxisID,Fs(t,"r")),r=e.indexAxis,h=e.iAxisID=s(r,n,o,a),c=e.vAxisID=s(r,o,n,a);e.xScale=this.getScaleForId(n),e.yScale=this.getScaleForId(o),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(h),e.vScale=this.getScaleForId(c)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&rt(this._data,this),t._stacked&&Vs(t)}_dataCheck(){const t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(o(e))this._data=function(t){const e=Object.keys(t),i=new Array(e.length);let s,n,o;for(s=0,n=e.length;s0&&i._parsed[t-1];if(!1===this._parsing)i._parsed=s,i._sorted=!0,d=s;else{d=n(s[t])?this.parseArrayData(i,s,t,e):o(s[t])?this.parseObjectData(i,s,t,e):this.parsePrimitiveData(i,s,t,e);const a=()=>null===c[l]||f&&c[l]t&&!e.hidden&&e._stacked&&{keys:Ts(i,!0),values:null})(e,i,this.chart),h={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:c,max:d}=function(t){const{min:e,max:i,minDefined:s,maxDefined:n}=t.getUserBounds();return{min:s?e:Number.NEGATIVE_INFINITY,max:n?i:Number.POSITIVE_INFINITY}}(r);let u,f;function g(){f=s[u];const e=f[r.axis];return!a(f[t.axis])||c>e||d=0;--u)if(!g()){this.updateRangeFromParsed(h,t,f,l);break}return h}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let s,n,o;for(s=0,n=e.length;s=0&&tthis.getContext(i,s,e)),c);return f.$shared&&(f.$shared=r,n[o]=Object.freeze(Ws(f,r))),f}_resolveAnimations(t,e,i){const s=this.chart,n=this._cachedDataOpts,o=`animation-${e}`,a=n[o];if(a)return a;let r;if(!1!==s.options.animation){const s=this.chart.config,n=s.datasetAnimationScopeKeys(this._type,e),o=s.getOptionScopes(this.getDataset(),n);r=s.createResolver(o,this.getContext(t,i,e))}const l=new Os(s,r&&r.animations);return r&&r._cacheable&&(n[o]=Object.freeze(l)),l}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Bs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){const i=this.resolveDataElementOptions(t,e),s=this._sharedOptions,n=this.getSharedOptions(i),o=this.includeOptions(e,n)||n!==s;return this.updateSharedOptions(n,e,i),{sharedOptions:n,includeOptions:o}}updateElement(t,e,i,s){Bs(s)?Object.assign(t,i):this._resolveAnimations(e,s).update(t,i)}updateSharedOptions(t,e,i){t&&!Bs(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,s){t.active=s;const n=this.getStyle(e,s);this._resolveAnimations(e,i,s).update(t,{options:!s&&this.getSharedOptions(n)||n})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this._data,i=this._cachedMeta.data;for(const[t,e,i]of this._syncList)this[t](e,i);this._syncList=[];const s=i.length,n=e.length,o=Math.min(n,s);o&&this.parse(0,o),n>s?this._insertElements(s,n-s,t):n{for(t.length+=e,a=t.length-1;a>=o;a--)t[a]=t[a-e]};for(r(n),a=t;a{s[t]=i[t]&&i[t].active()?i[t]._to:this[t]})),s}}function js(t,e){const i=t.options.ticks,n=function(t){const e=t.options.offset,i=t._tickSize(),s=t._length/i+(e?0:1),n=t._maxLength/i;return Math.floor(Math.min(s,n))}(t),o=Math.min(i.maxTicksLimit||n,n),a=i.major.enabled?function(t){const e=[];let i,s;for(i=0,s=t.length;io)return function(t,e,i,s){let n,o=0,a=i[0];for(s=Math.ceil(s),n=0;nn)return e}return Math.max(n,1)}(a,e,o);if(r>0){let t,i;const n=r>1?Math.round((h-l)/(r-1)):null;for($s(e,c,d,s(n)?0:l-n,l),t=0,i=r-1;t"top"===e||"left"===e?t[e]+i:t[e]-i,Us=(t,e)=>Math.min(e||t,t);function Xs(t,e){const i=[],s=t.length/e,n=t.length;let o=0;for(;oa+r)))return h}function Ks(t){return t.drawTicks?t.tickLength:0}function Gs(t,e){if(!t.display)return 0;const i=Si(t.font,e),s=ki(t.padding);return(n(t.text)?t.text.length:1)*i.lineHeight+s.height}function Zs(t,e,i){let s=ut(t);return(i&&"right"!==e||!i&&"right"===e)&&(s=(t=>"left"===t?"right":"right"===t?"left":t)(s)),s}class Js extends Hs{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:s}=this;return t=r(t,Number.POSITIVE_INFINITY),e=r(e,Number.NEGATIVE_INFINITY),i=r(i,Number.POSITIVE_INFINITY),s=r(s,Number.NEGATIVE_INFINITY),{min:r(t,i),max:r(e,s),minDefined:a(t),maxDefined:a(e)}}getMinMax(t){let e,{min:i,max:s,minDefined:n,maxDefined:o}=this.getUserBounds();if(n&&o)return{min:i,max:s};const a=this.getMatchingVisibleMetas();for(let r=0,l=a.length;rs?s:i,s=n&&i>s?i:s,{min:r(i,r(s,i)),max:r(s,r(i,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){d(this.options.beforeUpdate,[this])}update(t,e,i){const{beginAtZero:s,grace:n,ticks:o}=this.options,a=o.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=Di(this,n,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const r=a=n||i<=1||!this.isHorizontal())return void(this.labelRotation=s);const h=this._getLabelSizes(),c=h.widest.width,d=h.highest.height,u=J(this.chart.width-c,0,this.maxWidth);o=t.offset?this.maxWidth/i:u/(i-1),c+6>o&&(o=u/(i-(t.offset?.5:1)),a=this.maxHeight-Ks(t.grid)-e.padding-Gs(t.title,this.chart.options.font),r=Math.sqrt(c*c+d*d),l=Y(Math.min(Math.asin(J((h.highest.height+6)/o,-1,1)),Math.asin(J(a/r,-1,1))-Math.asin(J(d/r,-1,1)))),l=Math.max(s,Math.min(n,l))),this.labelRotation=l}afterCalculateLabelRotation(){d(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){d(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:e,options:{ticks:i,title:s,grid:n}}=this,o=this._isVisible(),a=this.isHorizontal();if(o){const o=Gs(s,e.options.font);if(a?(t.width=this.maxWidth,t.height=Ks(n)+o):(t.height=this.maxHeight,t.width=Ks(n)+o),i.display&&this.ticks.length){const{first:e,last:s,widest:n,highest:o}=this._getLabelSizes(),r=2*i.padding,l=$(this.labelRotation),h=Math.cos(l),c=Math.sin(l);if(a){const e=i.mirror?0:c*n.width+h*o.height;t.height=Math.min(this.maxHeight,t.height+e+r)}else{const e=i.mirror?0:h*n.width+c*o.height;t.width=Math.min(this.maxWidth,t.width+e+r)}this._calculatePadding(e,s,c,h)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,s){const{ticks:{align:n,padding:o},position:a}=this.options,r=0!==this.labelRotation,l="top"!==a&&"x"===this.axis;if(this.isHorizontal()){const a=this.getPixelForTick(0)-this.left,h=this.right-this.getPixelForTick(this.ticks.length-1);let c=0,d=0;r?l?(c=s*t.width,d=i*e.height):(c=i*t.height,d=s*e.width):"start"===n?d=e.width:"end"===n?c=t.width:"inner"!==n&&(c=t.width/2,d=e.width/2),this.paddingLeft=Math.max((c-a+o)*this.width/(this.width-a),0),this.paddingRight=Math.max((d-h+o)*this.width/(this.width-h),0)}else{let i=e.height/2,s=t.height/2;"start"===n?(i=0,s=t.height):"end"===n&&(i=e.height,s=0),this.paddingTop=i+o,this.paddingBottom=s+o}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){d(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){let e,i;for(this.beforeTickToLabelConversion(),this.generateTickLabels(t),e=0,i=t.length;e{const i=t.gc,s=i.length/2;let n;if(s>e){for(n=0;n({width:r[t]||0,height:l[t]||0});return{first:P(0),last:P(e-1),widest:P(k),highest:P(S),widths:r,heights:l}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const e=this._startPixel+t*this._length;return Q(this._alignToPixels?Ae(this.chart,e,0):e)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this.ticks||[];if(t>=0&&ta*s?a/i:r/s:r*s0}_computeGridLineItems(t){const e=this.axis,i=this.chart,s=this.options,{grid:n,position:a,border:r}=s,h=n.offset,c=this.isHorizontal(),d=this.ticks.length+(h?1:0),u=Ks(n),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(t){return Ae(i,t,p)};let x,_,y,v,M,w,k,S,P,D,C,O;if("top"===a)x=b(this.bottom),w=this.bottom-u,S=x-m,D=b(t.top)+m,O=t.bottom;else if("bottom"===a)x=b(this.top),D=t.top,O=b(t.bottom)-m,w=x+m,S=this.top+u;else if("left"===a)x=b(this.right),M=this.right-u,k=x-m,P=b(t.left)+m,C=t.right;else if("right"===a)x=b(this.left),P=t.left,C=b(t.right)-m,M=x+m,k=this.left+u;else if("x"===e){if("center"===a)x=b((t.top+t.bottom)/2+.5);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}D=t.top,O=t.bottom,w=x+m,S=w+u}else if("y"===e){if("center"===a)x=b((t.left+t.right)/2);else if(o(a)){const t=Object.keys(a)[0],e=a[t];x=b(this.chart.scales[t].getPixelForValue(e))}M=x-m,k=M-u,P=t.left,C=t.right}const A=l(s.ticks.maxTicksLimit,d),T=Math.max(1,Math.ceil(d/A));for(_=0;_0&&(o-=s/2)}d={left:o,top:n,width:s+e.width,height:i+e.height,color:t.backdropColor}}b.push({label:v,font:P,textOffset:O,options:{rotation:m,color:i,strokeColor:o,strokeWidth:h,textAlign:f,textBaseline:A,translation:[M,w],backdrop:d}})}return b}_getXAxisLabelAlignment(){const{position:t,ticks:e}=this.options;if(-$(this.labelRotation))return"top"===t?"left":"right";let i="center";return"start"===e.align?i="left":"end"===e.align?i="right":"inner"===e.align&&(i="inner"),i}_getYAxisLabelAlignment(t){const{position:e,ticks:{crossAlign:i,mirror:s,padding:n}}=this.options,o=t+n,a=this._getLabelSizes().widest.width;let r,l;return"left"===e?s?(l=this.right+n,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l+=a)):(l=this.right-o,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l=this.left)):"right"===e?s?(l=this.left+n,"near"===i?r="right":"center"===i?(r="center",l-=a/2):(r="left",l-=a)):(l=this.left+o,"near"===i?r="left":"center"===i?(r="center",l+=a/2):(r="right",l=this.right)):r="right",{textAlign:r,x:l}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,e=this.options.position;return"left"===e||"right"===e?{top:0,left:this.left,bottom:t.height,right:this.right}:"top"===e||"bottom"===e?{top:this.top,left:0,bottom:this.bottom,right:t.width}:void 0}drawBackground(){const{ctx:t,options:{backgroundColor:e},left:i,top:s,width:n,height:o}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,s,n,o),t.restore())}getLineWidthForValue(t){const e=this.options.grid;if(!this._isVisible()||!e.display)return 0;const i=this.ticks.findIndex((e=>e.value===t));if(i>=0){return e.setContext(this.getContext(i)).lineWidth}return 0}drawGrid(t){const e=this.options.grid,i=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let n,o;const a=(t,e,s)=>{s.width&&s.color&&(i.save(),i.lineWidth=s.width,i.strokeStyle=s.color,i.setLineDash(s.borderDash||[]),i.lineDashOffset=s.borderDashOffset,i.beginPath(),i.moveTo(t.x,t.y),i.lineTo(e.x,e.y),i.stroke(),i.restore())};if(e.display)for(n=0,o=s.length;n{this.drawBackground(),this.drawGrid(t),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:e,draw:t=>{this.drawLabels(t)}}]:[{z:e,draw:t=>{this.draw(t)}}]}getMatchingVisibleMetas(t){const e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",s=[];let n,o;for(n=0,o=e.length;n{const s=i.split("."),n=s.pop(),o=[t].concat(s).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");ue.route(o,n,l,r)}))}(e,t.defaultRoutes);t.descriptors&&ue.describe(e,t.descriptors)}(t,o,i),this.override&&ue.override(t.id,t.overrides)),o}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,s=this.scope;i in e&&delete e[i],s&&i in ue[s]&&(delete ue[s][i],this.override&&delete re[i])}}class tn{constructor(){this.controllers=new Qs(Ns,"datasets",!0),this.elements=new Qs(Hs,"elements"),this.plugins=new Qs(Object,"plugins"),this.scales=new Qs(Js,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach((e=>{const s=i||this._getRegistryForType(e);i||s.isForType(e)||s===this.plugins&&e.id?this._exec(t,s,e):u(e,(e=>{const s=i||this._getRegistryForType(e);this._exec(t,s,e)}))}))}_exec(t,e,i){const s=w(t);d(i["before"+s],[],i),e[t](i),d(i["after"+s],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(s(e,i),t,"stop"),this._notify(s(i,e),t,"start")}}function nn(t,e){return e||!1!==t?!0===t?{}:t:null}function on(t,{plugin:e,local:i},s,n){const o=t.pluginScopeKeys(e),a=t.getOptionScopes(s,o);return i&&e.defaults&&a.push(e.defaults),t.createResolver(a,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function an(t,e){const i=ue.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function rn(t){if("x"===t||"y"===t||"r"===t)return t}function ln(t,...e){if(rn(t))return t;for(const s of e){const e=s.axis||("top"===(i=s.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.length>1&&rn(t[0].toLowerCase());if(e)return e}var i;throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function hn(t,e,i){if(i[e+"AxisID"]===t)return{axis:e}}function cn(t,e){const i=re[t.type]||{scales:{}},s=e.scales||{},n=an(t.type,e),a=Object.create(null);return Object.keys(s).forEach((e=>{const r=s[e];if(!o(r))return console.error(`Invalid scale configuration for scale: ${e}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${e}`);const l=ln(e,r,function(t,e){if(e.data&&e.data.datasets){const i=e.data.datasets.filter((e=>e.xAxisID===t||e.yAxisID===t));if(i.length)return hn(t,"x",i[0])||hn(t,"y",i[0])}return{}}(e,t),ue.scales[r.type]),h=function(t,e){return t===e?"_index_":"_value_"}(l,n),c=i.scales||{};a[e]=x(Object.create(null),[{axis:l},r,c[l],c[h]])})),t.data.datasets.forEach((i=>{const n=i.type||t.type,o=i.indexAxis||an(n,e),r=(re[n]||{}).scales||{};Object.keys(r).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,o),n=i[e+"AxisID"]||e;a[n]=a[n]||Object.create(null),x(a[n],[{axis:e},s[n],r[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];x(e,[ue.scales[e.type],ue.scale])})),a}function dn(t){const e=t.options||(t.options={});e.plugins=l(e.plugins,{}),e.scales=cn(t,e)}function un(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const fn=new Map,gn=new Set;function pn(t,e){let i=fn.get(t);return i||(i=e(),fn.set(t,i),gn.add(i)),i}const mn=(t,e,i)=>{const s=M(e,i);void 0!==s&&t.add(s)};class bn{constructor(t){this._config=function(t){return(t=t||{}).data=un(t.data),dn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=un(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),dn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pn(t,(()=>[[`datasets.${t}`,""]]))}datasetAnimationScopeKeys(t,e){return pn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]]))}datasetElementScopeKeys(t,e){return pn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]]))}pluginScopeKeys(t){const e=t.id;return pn(`${this.type}-plugin-${e}`,(()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let s=i.get(t);return s&&!e||(s=new Map,i.set(t,s)),s}getOptionScopes(t,e,i){const{options:s,type:n}=this,o=this._cachedScopes(t,i),a=o.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>mn(r,t,e)))),e.forEach((t=>mn(r,s,t))),e.forEach((t=>mn(r,re[n]||{},t))),e.forEach((t=>mn(r,ue,t))),e.forEach((t=>mn(r,le,t)))}));const l=Array.from(r);return 0===l.length&&l.push(Object.create(null)),gn.has(e)&&o.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,re[e]||{},ue.datasets[e]||{},{type:e},ue,le]}resolveNamedOptions(t,e,i,s=[""]){const o={$shared:!0},{resolver:a,subPrefixes:r}=xn(this._resolverCache,t,s);let l=a;if(function(t,e){const{isScriptable:i,isIndexable:s}=Ye(t);for(const o of e){const e=i(o),a=s(o),r=(a||e)&&t[o];if(e&&(S(r)||_n(r))||a&&n(r))return!0}return!1}(a,e)){o.$shared=!1;l=$e(a,i=S(i)?i():i,this.createResolver(t,i,r))}for(const t of e)o[t]=l[t];return o}createResolver(t,e,i=[""],s){const{resolver:n}=xn(this._resolverCache,t,i);return o(e)?$e(n,e,void 0,s):n}}function xn(t,e,i){let s=t.get(e);s||(s=new Map,t.set(e,s));const n=i.join();let o=s.get(n);if(!o){o={resolver:je(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},s.set(n,o)}return o}const _n=t=>o(t)&&Object.getOwnPropertyNames(t).some((e=>S(t[e])));const yn=["top","bottom","left","right","chartArea"];function vn(t,e){return"top"===t||"bottom"===t||-1===yn.indexOf(t)&&"x"===e}function Mn(t,e){return function(i,s){return i[t]===s[t]?i[e]-s[e]:i[t]-s[t]}}function wn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),d(i&&i.onComplete,[t],e)}function kn(t){const e=t.chart,i=e.options.animation;d(i&&i.onProgress,[t],e)}function Sn(t){return fe()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Pn={},Dn=t=>{const e=Sn(t);return Object.values(Pn).filter((t=>t.canvas===e)).pop()};function Cn(t,e,i){const s=Object.keys(t);for(const n of s){const s=+n;if(s>=e){const o=t[n];delete t[n],(i>0||s>e)&&(t[s+i]=o)}}}function On(t,e,i){return t.options.clip?t[i]:e[i]}class An{static defaults=ue;static instances=Pn;static overrides=re;static registry=en;static version="4.4.1";static getChart=Dn;static register(...t){en.add(...t),Tn()}static unregister(...t){en.remove(...t),Tn()}constructor(t,e){const s=this.config=new bn(e),n=Sn(t),o=Dn(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ks(n)),this.platform.updateConfig(s);const r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,h=l&&l.height,c=l&&l.width;this.id=i(),this.ctx=r,this.canvas=l,this.width=c,this.height=h,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new sn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=dt((t=>this.update(t)),a.resizeDelay||0),this._dataChanges=[],Pn[this.id]=this,r&&l?(xt.listen(this,"complete",wn),xt.listen(this,"progress",kn),this._initialize(),this.attached&&this.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return s(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return en}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ke(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Te(this.canvas,this.ctx),this}stop(){return xt.stop(this),this}resize(t,e){xt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this.options,s=this.canvas,n=i.maintainAspectRatio&&this.aspectRatio,o=this.platform.getMaximumSize(s,t,e,n),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),r=this.width?"resize":"attach";this.width=o.width,this.height=o.height,this._aspectRatio=this.aspectRatio,ke(this,a,!0)&&(this.notifyPlugins("resize",{size:o}),d(i.onResize,[this,o],this),this.attached&&this._doResize(r)&&this.render())}ensureScalesHaveIDs(){u(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this.options,e=t.scales,i=this.scales,s=Object.keys(i).reduce(((t,e)=>(t[e]=!1,t)),{});let n=[];e&&(n=n.concat(Object.keys(e).map((t=>{const i=e[t],s=ln(t,i),n="r"===s,o="x"===s;return{options:i,dposition:n?"chartArea":o?"bottom":"left",dtype:n?"radialLinear":o?"category":"linear"}})))),u(n,(e=>{const n=e.options,o=n.id,a=ln(o,n),r=l(n.type,e.dtype);void 0!==n.position&&vn(n.position,a)===vn(e.dposition)||(n.position=e.dposition),s[o]=!0;let h=null;if(o in i&&i[o].type===r)h=i[o];else{h=new(en.getScale(r))({id:o,type:r,ctx:this.ctx,chart:this}),i[h.id]=h}h.init(n,t)})),u(s,((t,e)=>{t||delete i[e]})),u(i,(t=>{as.configure(this,t,t.options),as.addBox(this,t)}))}_updateMetasets(){const t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort(((t,e)=>t.index-e.index)),i>e){for(let t=e;te.length&&delete this._stacks,t.forEach(((t,i)=>{0===e.filter((e=>e===t._dataset)).length&&this._destroyDatasetMeta(i)}))}buildOrUpdateControllers(){const t=[],e=this.data.datasets;let i,s;for(this._removeUnreferencedMetasets(),i=0,s=e.length;i{this.getDatasetMeta(e).controller.reset()}),this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this.config;e.update();const i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),!1===this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const n=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let o=0;for(let t=0,e=this.data.datasets.length;t{t.reset()})),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Mn("z","_idx"));const{_active:a,_lastEvent:r}=this;r?this._eventHandler(r,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){u(this.scales,(t=>{as.removeBox(this,t)})),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);P(e,i)&&!!this._responsiveListeners===t.responsive||(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(const{method:i,start:s,count:n}of e){Cn(t,s,"_removeElements"===i?-n:n)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const e=this.data.datasets.length,i=e=>new Set(t.filter((t=>t[0]===e)).map(((t,e)=>e+","+t.splice(1).join(",")))),s=i(0);for(let t=1;tt.split(","))).map((t=>({method:t[1],start:+t[2],count:+t[3]})))}_updateLayout(t){if(!1===this.notifyPlugins("beforeLayout",{cancelable:!0}))return;as.update(this,this.width,this.height,t);const e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],u(this.boxes,(t=>{i&&"chartArea"===t.position||(t.configure&&t.configure(),this._layers.push(...t._layers()))}),this),this._layers.forEach(((t,e)=>{t._idx=e})),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(!1!==this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let t=0,e=this.data.datasets.length;t=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this.ctx,i=t._clip,s=!i.disabled,n=function(t,e){const{xScale:i,yScale:s}=t;return i&&s?{left:On(i,e,"left"),right:On(i,e,"right"),top:On(s,e,"top"),bottom:On(s,e,"bottom")}:e}(t,this.chartArea),o={meta:t,index:t.index,cancelable:!0};!1!==this.notifyPlugins("beforeDatasetDraw",o)&&(s&&Ie(e,{left:!1===i.left?0:n.left-i.left,right:!1===i.right?this.width:n.right+i.right,top:!1===i.top?0:n.top-i.top,bottom:!1===i.bottom?this.height:n.bottom+i.bottom}),t.controller.draw(),s&&ze(e),o.cancelable=!1,this.notifyPlugins("afterDatasetDraw",o))}isPointInArea(t){return Re(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,s){const n=Xi.modes[e];return"function"==typeof n?n(this,t,i,s):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let s=i.filter((t=>t&&t._dataset===e)).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(s)),s}getContext(){return this.$context||(this.$context=Ci(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){const s=i?"show":"hide",n=this.getDatasetMeta(t),o=n.controller._resolveAnimations(void 0,s);k(e)?(n.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),o.update(n,{visible:i}),this.update((e=>e.datasetIndex===t?s:void 0)))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){const e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),xt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,i,s),t[i]=s},s=(t,e,i)=>{t.offsetX=e,t.offsetY=i,this._eventHandler(t)};u(this.options.events,(t=>i(t,s)))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,e=this.platform,i=(i,s)=>{e.addEventListener(this,i,s),t[i]=s},s=(i,s)=>{t[i]&&(e.removeEventListener(this,i,s),delete t[i])},n=(t,e)=>{this.canvas&&this.resize(t,e)};let o;const a=()=>{s("attach",a),this.attached=!0,this.resize(),i("resize",n),i("detach",o)};o=()=>{this.attached=!1,s("resize",n),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():o()}unbindEvents(){u(this._listeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._listeners={},u(this._responsiveListeners,((t,e)=>{this.platform.removeEventListener(this,e,t)})),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){const s=i?"set":"remove";let n,o,a,r;for("dataset"===e&&(n=this.getDatasetMeta(t[0].datasetIndex),n.controller["_"+s+"DatasetHoverStyle"]()),a=0,r=t.length;a{const i=this.getDatasetMeta(t);if(!i)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:i.data[e],index:e}}));!f(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return 1===this._plugins._cache.filter((e=>e.plugin.id===t)).length}_updateHoverStyles(t,e,i){const s=this.options.hover,n=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),o=n(e,t),a=i?t:n(t,e);o.length&&this.updateHoverStyle(o,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,e){const i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},s=e=>(e.options.events||this.options.events).includes(t.native.type);if(!1===this.notifyPlugins("beforeEvent",i,s))return;const n=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,s),(n||i.changed)&&this.render(),this}_handleEvent(t,e,i){const{_active:s=[],options:n}=this,o=e,a=this._getActiveElements(t,s,i,o),r=D(t),l=function(t,e,i,s){return i&&"mouseout"!==t.type?s?e:t:null}(t,this._lastEvent,i,r);i&&(this._lastEvent=null,d(n.onHover,[t,a,this],this),r&&d(n.onClick,[t,a,this],this));const h=!f(a,s);return(h||e)&&(this._active=a,this._updateHoverStyles(a,s,e)),this._lastEvent=l,h}_getActiveElements(t,e,i,s){if("mouseout"===t.type)return[];if(!i)return e;const n=this.options.hover;return this.getElementsAtEventForMode(t,n.mode,n,s)}}function Tn(){return u(An.instances,(t=>t._plugins.invalidate()))}function Ln(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class En{static override(t){Object.assign(En.prototype,t)}options;constructor(t){this.options=t||{}}init(){}formats(){return Ln()}parse(){return Ln()}format(){return Ln()}add(){return Ln()}diff(){return Ln()}startOf(){return Ln()}endOf(){return Ln()}}var Rn={_date:En};function In(t){const e=t.iScale,i=function(t,e){if(!t._cache.$bar){const i=t.getMatchingVisibleMetas(e);let s=[];for(let e=0,n=i.length;et-e)))}return t._cache.$bar}(e,t.type);let s,n,o,a,r=e._length;const l=()=>{32767!==o&&-32768!==o&&(k(a)&&(r=Math.min(r,Math.abs(o-a)||r)),a=o)};for(s=0,n=i.length;sMath.abs(r)&&(l=r,h=a),e[i.axis]=h,e._custom={barStart:l,barEnd:h,start:n,end:o,min:a,max:r}}(t,e,i,s):e[i.axis]=i.parse(t,s),e}function Fn(t,e,i,s){const n=t.iScale,o=t.vScale,a=n.getLabels(),r=n===o,l=[];let h,c,d,u;for(h=i,c=i+s;ht.x,i="left",s="right"):(e=t.base"spacing"!==t,_indexable:t=>"spacing"!==t&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")};static overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;if(e.labels.length&&e.datasets.length){const{labels:{pointStyle:i,color:s}}=t.legend.options;return e.labels.map(((e,n)=>{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}};constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,s=this._cachedMeta;if(!1===this._parsing)s._parsed=i;else{let n,a,r=t=>+i[t];if(o(i[t])){const{key:t="value"}=this._parsing;r=e=>+M(i[e],t)}for(n=t,a=t+e;nZ(t,r,l,!0)?1:Math.max(e,e*i,s,s*i),g=(t,e,s)=>Z(t,r,l,!0)?-1:Math.min(e,e*i,s,s*i),p=f(0,h,d),m=f(E,c,u),b=g(C,h,d),x=g(C+E,c,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}(u,d,r),b=(i.width-o)/f,x=(i.height-o)/g,_=Math.max(Math.min(b,x)/2,0),y=c(this.options.radius,_),v=(y-Math.max(y*r,0))/this._getVisibleDatasetWeightTotal();this.offsetX=p*y,this.offsetY=m*y,s.total=this.calculateTotal(),this.outerRadius=y-v*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-v*l,0),this.updateElements(n,0,n.length,t)}_circumference(t,e){const i=this.options,s=this._cachedMeta,n=this._getCircumference();return e&&i.animation.animateRotate||!this.chart.getDataVisibility(t)||null===s._parsed[t]||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*n/O)}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.chartArea,r=o.options.animation,l=(a.left+a.right)/2,h=(a.top+a.bottom)/2,c=n&&r.animateScale,d=c?0:this.innerRadius,u=c?0:this.outerRadius,{sharedOptions:f,includeOptions:g}=this._getSharedOptions(e,s);let p,m=this._getRotation();for(p=0;p0&&!isNaN(t)?O*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t],i.options.locale);return{label:s[t]||"",value:n}}getMaxBorderWidth(t){let e=0;const i=this.chart;let s,n,o,a,r;if(!t)for(s=0,n=i.data.datasets.length;s{const o=t.getDatasetMeta(0).controller.getStyle(n);return{text:e,fillStyle:o.backgroundColor,strokeStyle:o.borderColor,fontColor:s,lineWidth:o.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(n),index:n}}))}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,s=i.data.labels||[],n=ne(e._parsed[t].r,i.options.locale);return{label:s[t]||"",value:n}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){const t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach(((t,i)=>{const s=this.getParsed(i).r;!isNaN(s)&&this.chart.getDataVisibility(i)&&(se.max&&(e.max=s))})),e}_updateRadius(){const t=this.chart,e=t.chartArea,i=t.options,s=Math.min(e.right-e.left,e.bottom-e.top),n=Math.max(s/2,0),o=(n-Math.max(i.cutoutPercentage?n/100*i.cutoutPercentage:1,0))/t.getVisibleDatasetCount();this.outerRadius=n-o*this.index,this.innerRadius=this.outerRadius-o}updateElements(t,e,i,s){const n="reset"===s,o=this.chart,a=o.options.animation,r=this._cachedMeta.rScale,l=r.xCenter,h=r.yCenter,c=r.getIndexAngle(0)-.5*C;let d,u=c;const f=360/this.countVisibleElements();for(d=0;d{!isNaN(this.getParsed(i).r)&&this.chart.getDataVisibility(i)&&e++})),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?$(this.resolveDataElementOptions(t,e).angle||i):0}}var Yn=Object.freeze({__proto__:null,BarController:class extends Ns{static id="bar";static defaults={datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}};static overrides={scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}};parsePrimitiveData(t,e,i,s){return Fn(t,e,i,s)}parseArrayData(t,e,i,s){return Fn(t,e,i,s)}parseObjectData(t,e,i,s){const{iScale:n,vScale:o}=t,{xAxisKey:a="x",yAxisKey:r="y"}=this._parsing,l="x"===n.axis?a:r,h="x"===o.axis?a:r,c=[];let d,u,f,g;for(d=i,u=i+s;dt.controller.options.grouped)),o=i.options.stacked,a=[],r=t=>{const i=t.controller.getParsed(e),n=i&&i[t.vScale.axis];if(s(n)||isNaN(n))return!0};for(const i of n)if((void 0===e||!r(i))&&((!1===o||-1===a.indexOf(i.stack)||void 0===o&&void 0===i.stack)&&a.push(i.stack),i.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getStackIndex(t,e,i){const s=this._getStacks(t,i),n=void 0!==e?s.indexOf(e):-1;return-1===n?s.length-1:n}_getRuler(){const t=this.options,e=this._cachedMeta,i=e.iScale,s=[];let n,o;for(n=0,o=e.data.length;n=i?1:-1)}(u,e,r)*a,f===r&&(b-=u/2);const t=e.getPixelForDecimal(0),s=e.getPixelForDecimal(1),o=Math.min(t,s),h=Math.max(t,s);b=Math.max(Math.min(b,h),o),d=b+u,i&&!c&&(l._stacks[e.axis]._visualValues[n]=e.getValueForPixel(d)-e.getValueForPixel(b))}if(b===e.getPixelForValue(r)){const t=F(u)*e.getLineWidthForValue(r)/2;b+=t,u-=t}return{size:u,base:b,head:d,center:d+u/2}}_calculateBarIndexPixels(t,e){const i=e.scale,n=this.options,o=n.skipNull,a=l(n.maxBarThickness,1/0);let r,h;if(e.grouped){const i=o?this._getStackCount(t):e.stackCount,l="flex"===n.barThickness?function(t,e,i,s){const n=e.pixels,o=n[t];let a=t>0?n[t-1]:null,r=t=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:s,yScale:n}=e,o=this.getParsed(t),a=s.getLabelForValue(o.x),r=n.getLabelForValue(o.y),l=o._custom;return{label:i[t]||"",value:"("+a+", "+r+(l?", "+l:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,s){const n="reset"===s,{iScale:o,vScale:a}=this._cachedMeta,{sharedOptions:r,includeOptions:l}=this._getSharedOptions(e,s),h=o.axis,c=a.axis;for(let d=e;d0&&this.getParsed(e-1);for(let i=0;i<_;++i){const g=t[i],_=b?g:{};if(i=x){_.skip=!0;continue}const v=this.getParsed(i),M=s(v[f]),w=_[u]=a.getPixelForValue(v[u],i),k=_[f]=o||M?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,v,l):v[f],i);_.skip=isNaN(w)||isNaN(k)||M,_.stop=i>0&&Math.abs(v[u]-y[u])>m,p&&(_.parsed=v,_.raw=h.data[i]),d&&(_.options=c||this.resolveDataElementOptions(i,g.active?"active":n)),b||this.updateElement(g,i,_,n),y=v}}getMaxOverflow(){const t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,s=t.data||[];if(!s.length)return i;const n=s[0].size(this.resolveDataElementOptions(0)),o=s[s.length-1].size(this.resolveDataElementOptions(s.length-1));return Math.max(i,n,o)/2}draw(){const t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}},PieController:class extends jn{static id="pie";static defaults={cutout:0,rotation:0,circumference:360,radius:"100%"}},PolarAreaController:$n,RadarController:class extends Ns{static id="radar";static defaults={datasetElementType:"line",dataElementType:"point",indexAxis:"r",showLine:!0,elements:{line:{fill:"start"}}};static overrides={aspectRatio:1,scales:{r:{type:"radialLinear"}}};getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,s){return ii.bind(this)(t,e,i,s)}update(t){const e=this._cachedMeta,i=e.dataset,s=e.data||[],n=e.iScale.getLabels();if(i.points=s,"resize"!==t){const e=this.resolveDatasetElementOptions(t);this.options.showLine||(e.borderWidth=0);const o={_loop:!0,_fullLoop:n.length===s.length,options:e};this.updateElement(i,void 0,o,t)}this.updateElements(s,0,s.length,t)}updateElements(t,e,i,s){const n=this._cachedMeta.rScale,o="reset"===s;for(let a=e;a0&&this.getParsed(e-1);for(let c=e;c0&&Math.abs(i[f]-_[f])>b,m&&(p.parsed=i,p.raw=h.data[c]),u&&(p.options=d||this.resolveDataElementOptions(c,e.active?"active":n)),x||this.updateElement(e,c,p,n),_=i}this.updateSharedOptions(d,n,c)}getMaxOverflow(){const t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let t=0;for(let i=e.length-1;i>=0;--i)t=Math.max(t,e[i].size(this.resolveDataElementOptions(i))/2);return t>0&&t}const i=t.dataset,s=i.options&&i.options.borderWidth||0;if(!e.length)return s;const n=e[0].size(this.resolveDataElementOptions(0)),o=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(s,n,o)/2}}});function Un(t,e,i,s){const n=vi(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const o=(i-e)/2,a=Math.min(o,s*e/2),r=t=>{const e=(i-Math.min(o,t))*s/2;return J(t,0,Math.min(o,e))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:J(n.innerStart,0,a),innerEnd:J(n.innerEnd,0,a)}}function Xn(t,e,i,s){return{x:i+t*Math.cos(e),y:s+t*Math.sin(e)}}function qn(t,e,i,s,n,o){const{x:a,y:r,startAngle:l,pixelMargin:h,innerRadius:c}=e,d=Math.max(e.outerRadius+s+i-h,0),u=c>0?c+s+i+h:0;let f=0;const g=n-l;if(s){const t=((c>0?c-s:0)+(d>0?d-s:0))/2;f=(g-(0!==t?g*t/(t+s):g))/2}const p=(g-Math.max(.001,g*d-i/C)/d)/2,m=l+p+f,b=n-p-f,{outerStart:x,outerEnd:_,innerStart:y,innerEnd:v}=Un(e,u,d,b-m),M=d-x,w=d-_,k=m+x/M,S=b-_/w,P=u+y,D=u+v,O=m+y/P,A=b-v/D;if(t.beginPath(),o){const e=(k+S)/2;if(t.arc(a,r,d,k,e),t.arc(a,r,d,e,S),_>0){const e=Xn(w,S,a,r);t.arc(e.x,e.y,_,S,b+E)}const i=Xn(D,b,a,r);if(t.lineTo(i.x,i.y),v>0){const e=Xn(D,A,a,r);t.arc(e.x,e.y,v,b+E,A+Math.PI)}const s=(b-v/u+(m+y/u))/2;if(t.arc(a,r,u,b-v/u,s,!0),t.arc(a,r,u,s,m+y/u,!0),y>0){const e=Xn(P,O,a,r);t.arc(e.x,e.y,y,O+Math.PI,m-E)}const n=Xn(M,m,a,r);if(t.lineTo(n.x,n.y),x>0){const e=Xn(M,k,a,r);t.arc(e.x,e.y,x,m-E,k)}}else{t.moveTo(a,r);const e=Math.cos(k)*d+a,i=Math.sin(k)*d+r;t.lineTo(e,i);const s=Math.cos(S)*d+a,n=Math.sin(S)*d+r;t.lineTo(s,n)}t.closePath()}function Kn(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r,options:l}=e,{borderWidth:h,borderJoinStyle:c,borderDash:d,borderDashOffset:u}=l,f="inner"===l.borderAlign;if(!h)return;t.setLineDash(d||[]),t.lineDashOffset=u,f?(t.lineWidth=2*h,t.lineJoin=c||"round"):(t.lineWidth=h,t.lineJoin=c||"bevel");let g=e.endAngle;if(o){qn(t,e,i,s,g,n);for(let e=0;en?(h=n/l,t.arc(o,a,l,i+h,s-h,!0)):t.arc(o,a,n,i+E,s-E),t.closePath(),t.clip()}(t,e,g),o||(qn(t,e,i,s,g,n),t.stroke())}function Gn(t,e,i=e){t.lineCap=l(i.borderCapStyle,e.borderCapStyle),t.setLineDash(l(i.borderDash,e.borderDash)),t.lineDashOffset=l(i.borderDashOffset,e.borderDashOffset),t.lineJoin=l(i.borderJoinStyle,e.borderJoinStyle),t.lineWidth=l(i.borderWidth,e.borderWidth),t.strokeStyle=l(i.borderColor,e.borderColor)}function Zn(t,e,i){t.lineTo(i.x,i.y)}function Jn(t,e,i={}){const s=t.length,{start:n=0,end:o=s-1}=i,{start:a,end:r}=e,l=Math.max(n,a),h=Math.min(o,r),c=nr&&o>r;return{count:s,start:l,loop:e.loop,ilen:h(a+(h?r-t:t))%o,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=n[x(0)],t.moveTo(d.x,d.y)),c=0;c<=r;++c){if(d=n[x(c)],d.skip)continue;const e=d.x,i=d.y,s=0|e;s===u?(ig&&(g=i),m=(b*m+e)/++b):(_(),t.lineTo(e,i),u=s,b=0,f=g=i),p=i}_()}function eo(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?to:Qn}const io="function"==typeof Path2D;function so(t,e,i,s){io&&!e.options.segment?function(t,e,i,s){let n=e._path;n||(n=e._path=new Path2D,e.path(n,i,s)&&n.closePath()),Gn(t,e.options),t.stroke(n)}(t,e,i,s):function(t,e,i,s){const{segments:n,options:o}=e,a=eo(e);for(const r of n)Gn(t,o,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+s-1})&&t.closePath(),t.stroke()}(t,e,i,s)}class no extends Hs{static id="line";static defaults={borderCapStyle:"butt",borderDash:[],borderDashOffset:0,borderJoinStyle:"miter",borderWidth:3,capBezierPoints:!0,cubicInterpolationMode:"default",fill:!1,spanGaps:!1,stepped:!1,tension:0};static defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};static descriptors={_scriptable:!0,_indexable:t=>"borderDash"!==t&&"fill"!==t};constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){const i=this.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!this._pointsUpdated){const s=i.spanGaps?this._loop:this._fullLoop;hi(this._points,i,t,s,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=zi(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this.options,s=t[e],n=this.points,o=Ii(this,{property:e,start:s,end:s});if(!o.length)return;const a=[],r=function(t){return t.stepped?pi:t.tension||"monotone"===t.cubicInterpolationMode?mi:gi}(i);let l,h;for(l=0,h=o.length;l"borderDash"!==t};circumference;endAngle;fullCircles;innerRadius;outerRadius;pixelMargin;startAngle;constructor(t){super(),this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,t&&Object.assign(this,t)}inRange(t,e,i){const s=this.getProps(["x","y"],i),{angle:n,distance:o}=X(s,{x:t,y:e}),{startAngle:a,endAngle:r,innerRadius:h,outerRadius:c,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],i),u=(this.options.spacing+this.options.borderWidth)/2,f=l(d,r-a)>=O||Z(n,a,r),g=tt(o,h+u,c+u);return f&&g}getCenterPoint(t){const{x:e,y:i,startAngle:s,endAngle:n,innerRadius:o,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),{offset:r,spacing:l}=this.options,h=(s+n)/2,c=(o+a+l+r)/2;return{x:e+Math.cos(h)*c,y:i+Math.sin(h)*c}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const{options:e,circumference:i}=this,s=(e.offset||0)/4,n=(e.spacing||0)/2,o=e.circular;if(this.pixelMargin="inner"===e.borderAlign?.33:0,this.fullCircles=i>O?Math.floor(i/O):0,0===i||this.innerRadius<0||this.outerRadius<0)return;t.save();const a=(this.startAngle+this.endAngle)/2;t.translate(Math.cos(a)*s,Math.sin(a)*s);const r=s*(1-Math.sin(Math.min(C,i||0)));t.fillStyle=e.backgroundColor,t.strokeStyle=e.borderColor,function(t,e,i,s,n){const{fullCircles:o,startAngle:a,circumference:r}=e;let l=e.endAngle;if(o){qn(t,e,i,s,l,n);for(let e=0;e("string"==typeof e?(i=t.push(e)-1,s.unshift({index:i,label:e})):isNaN(e)&&(i=null),i))(t,e,i,s);return n!==t.lastIndexOf(e)?i:n}function po(t){const e=this.getLabels();return t>=0&&ts=e?s:t,a=t=>n=i?n:t;if(t){const t=F(s),e=F(n);t<0&&e<0?a(0):t>0&&e>0&&o(0)}if(s===n){let e=0===n?1:Math.abs(.05*n);a(n+e),t||o(s-e)}this.min=s,this.max=n}getTickLimit(){const t=this.options.ticks;let e,{maxTicksLimit:i,stepSize:s}=t;return s?(e=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,e>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${e} ticks. Limiting to 1000.`),e=1e3)):(e=this.computeTickLimit(),i=i||11),i&&(e=Math.min(i,e)),e}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,e=t.ticks;let i=this.getTickLimit();i=Math.max(2,i);const n=function(t,e){const i=[],{bounds:n,step:o,min:a,max:r,precision:l,count:h,maxTicks:c,maxDigits:d,includeBounds:u}=t,f=o||1,g=c-1,{min:p,max:m}=e,b=!s(a),x=!s(r),_=!s(h),y=(m-p)/(d+1);let v,M,w,k,S=B((m-p)/g/f)*f;if(S<1e-14&&!b&&!x)return[{value:p},{value:m}];k=Math.ceil(m/S)-Math.floor(p/S),k>g&&(S=B(k*S/g/f)*f),s(l)||(v=Math.pow(10,l),S=Math.ceil(S*v)/v),"ticks"===n?(M=Math.floor(p/S)*S,w=Math.ceil(m/S)*S):(M=p,w=m),b&&x&&o&&H((r-a)/o,S/1e3)?(k=Math.round(Math.min((r-a)/S,c)),S=(r-a)/k,M=a,w=r):_?(M=b?a:M,w=x?r:w,k=h-1,S=(w-M)/k):(k=(w-M)/S,k=V(k,Math.round(k),S/1e3)?Math.round(k):Math.ceil(k));const P=Math.max(U(S),U(M));v=Math.pow(10,s(l)?P:l),M=Math.round(M*v)/v,w=Math.round(w*v)/v;let D=0;for(b&&(u&&M!==a?(i.push({value:a}),Mr)break;i.push({value:t})}return x&&u&&w!==r?i.length&&V(i[i.length-1].value,r,mo(r,y,t))?i[i.length-1].value=r:i.push({value:r}):x&&w!==r||i.push({value:w}),i}({maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:!1!==e.includeBounds},this._range||this);return"ticks"===t.bounds&&j(n,this,"value"),t.reverse?(n.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),n}configure(){const t=this.ticks;let e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){const s=(i-e)/Math.max(t.length-1,1)/2;e-=s,i+=s}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}}class xo extends bo{static id="linear";static defaults={ticks:{callback:ae.formatters.numeric}};determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?t:0,this.max=a(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),e=t?this.width:this.height,i=$(this.options.ticks.minRotation),s=(t?Math.sin(i):Math.cos(i))||.001,n=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,n.lineHeight/s))}getPixelForValue(t){return null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}const _o=t=>Math.floor(z(t)),yo=(t,e)=>Math.pow(10,_o(t)+e);function vo(t){return 1===t/Math.pow(10,_o(t))}function Mo(t,e,i){const s=Math.pow(10,i),n=Math.floor(t/s);return Math.ceil(e/s)-n}function wo(t,{min:e,max:i}){e=r(t.min,e);const s=[],n=_o(e);let o=function(t,e){let i=_o(e-t);for(;Mo(t,e,i)>10;)i++;for(;Mo(t,e,i)<10;)i--;return Math.min(i,_o(t))}(e,i),a=o<0?Math.pow(10,Math.abs(o)):1;const l=Math.pow(10,o),h=n>o?Math.pow(10,n):0,c=Math.round((e-h)*a)/a,d=Math.floor((e-h)/l/10)*l*10;let u=Math.floor((c-d)/Math.pow(10,o)),f=r(t.min,Math.round((h+d+u*Math.pow(10,o))*a)/a);for(;f=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,a=o>=0?1:a),f=Math.round((h+d+u*Math.pow(10,o))*a)/a;const g=r(t.max,f);return s.push({value:g,major:vo(g),significand:u}),s}class ko extends Js{static id="logarithmic";static defaults={ticks:{callback:ae.formatters.logarithmic,major:{enabled:!0}}};constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){const i=bo.prototype.parse.apply(this,[t,e]);if(0!==i)return a(i)&&i>0?i:null;this._zero=!0}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!0);this.min=a(t)?Math.max(0,t):null,this.max=a(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!a(this._userMin)&&(this.min=t===yo(this.min,0)?yo(this.min,-1):yo(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let i=this.min,s=this.max;const n=e=>i=t?i:e,o=t=>s=e?s:t;i===s&&(i<=0?(n(1),o(10)):(n(yo(i,-1)),o(yo(s,1)))),i<=0&&n(yo(s,-1)),s<=0&&o(yo(i,1)),this.min=i,this.max=s}buildTicks(){const t=this.options,e=wo({min:this._userMin,max:this._userMax},this);return"ticks"===t.bounds&&j(e,this,"value"),t.reverse?(e.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),e}getLabelForValue(t){return void 0===t?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){const t=this.min;super.configure(),this._startValue=z(t),this._valueRange=z(this.max)-z(t)}getPixelForValue(t){return void 0!==t&&0!==t||(t=this.min),null===t||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(z(t)-this._startValue)/this._valueRange)}getValueForPixel(t){const e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}}function So(t){const e=t.ticks;if(e.display&&t.display){const t=ki(e.backdropPadding);return l(e.font&&e.font.size,ue.font.size)+t.height}return 0}function Po(t,e,i,s,n){return t===s||t===n?{start:e-i/2,end:e+i/2}:tn?{start:e-i,end:e}:{start:e,end:e+i}}function Do(t){const e={l:t.left+t._padding.left,r:t.right-t._padding.right,t:t.top+t._padding.top,b:t.bottom-t._padding.bottom},i=Object.assign({},e),s=[],o=[],a=t._pointLabels.length,r=t.options.pointLabels,l=r.centerPointLabels?C/a:0;for(let u=0;ue.r&&(r=(s.end-e.r)/o,t.r=Math.max(t.r,e.r+r)),n.starte.b&&(l=(n.end-e.b)/a,t.b=Math.max(t.b,e.b+l))}function Oo(t,e,i){const s=t.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=i,l=t.getPointPosition(e,s+n+a,o),h=Math.round(Y(G(l.angle+E))),c=function(t,e,i){90===i||270===i?t-=e/2:(i>270||i<90)&&(t-=e);return t}(l.y,r.h,h),d=function(t){if(0===t||180===t)return"center";if(t<180)return"left";return"right"}(h),u=function(t,e,i){"right"===i?t-=e:"center"===i&&(t-=e/2);return t}(l.x,r.w,d);return{visible:!0,x:l.x,y:c,textAlign:d,left:u,top:c,right:u+r.w,bottom:c+r.h}}function Ao(t,e){if(!e)return!0;const{left:i,top:s,right:n,bottom:o}=t;return!(Re({x:i,y:s},e)||Re({x:i,y:o},e)||Re({x:n,y:s},e)||Re({x:n,y:o},e))}function To(t,e,i){const{left:n,top:o,right:a,bottom:r}=i,{backdropColor:l}=e;if(!s(l)){const i=wi(e.borderRadius),s=ki(e.backdropPadding);t.fillStyle=l;const h=n-s.left,c=o-s.top,d=a-n+s.width,u=r-o+s.height;Object.values(i).some((t=>0!==t))?(t.beginPath(),He(t,{x:h,y:c,w:d,h:u,radius:i}),t.fill()):t.fillRect(h,c,d,u)}}function Lo(t,e,i,s){const{ctx:n}=t;if(i)n.arc(t.xCenter,t.yCenter,e,0,O);else{let i=t.getPointPosition(0,e);n.moveTo(i.x,i.y);for(let o=1;ot,padding:5,centerPointLabels:!1}};static defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"};static descriptors={angleLines:{_fallback:"grid"}};constructor(t){super(t),this.xCenter=void 0,this.yCenter=void 0,this.drawingArea=void 0,this._pointLabels=[],this._pointLabelItems=[]}setDimensions(){const t=this._padding=ki(So(this.options)/2),e=this.width=this.maxWidth-t.width,i=this.height=this.maxHeight-t.height;this.xCenter=Math.floor(this.left+e/2+t.left),this.yCenter=Math.floor(this.top+i/2+t.top),this.drawingArea=Math.floor(Math.min(e,i)/2)}determineDataLimits(){const{min:t,max:e}=this.getMinMax(!1);this.min=a(t)&&!isNaN(t)?t:0,this.max=a(e)&&!isNaN(e)?e:0,this.handleTickRangeOptions()}computeTickLimit(){return Math.ceil(this.drawingArea/So(this.options))}generateTickLabels(t){bo.prototype.generateTickLabels.call(this,t),this._pointLabels=this.getLabels().map(((t,e)=>{const i=d(this.options.pointLabels.callback,[t,e],this);return i||0===i?i:""})).filter(((t,e)=>this.chart.getDataVisibility(e)))}fit(){const t=this.options;t.display&&t.pointLabels.display?Do(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,s){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-s)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,s))}getIndexAngle(t){return G(t*(O/(this._pointLabels.length||1))+$(this.options.startAngle||0))}getDistanceFromCenterForValue(t){if(s(t))return NaN;const e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(s(t))return NaN;const e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){const e=this._pointLabels||[];if(t>=0&&t=0;n--){const e=t._pointLabelItems[n];if(!e.visible)continue;const o=s.setContext(t.getPointLabelContext(n));To(i,o,e);const a=Si(o.font),{x:r,y:l,textAlign:h}=e;Ne(i,t._pointLabels[n],r,l+a.lineHeight/2,a,{color:o.color,textAlign:h,textBaseline:"middle"})}}(this,o),s.display&&this.ticks.forEach(((t,e)=>{if(0!==e){r=this.getDistanceFromCenterForValue(t.value);const i=this.getContext(e),a=s.setContext(i),l=n.setContext(i);!function(t,e,i,s,n){const o=t.ctx,a=e.circular,{color:r,lineWidth:l}=e;!a&&!s||!r||!l||i<0||(o.save(),o.strokeStyle=r,o.lineWidth=l,o.setLineDash(n.dash),o.lineDashOffset=n.dashOffset,o.beginPath(),Lo(t,i,a,s),o.closePath(),o.stroke(),o.restore())}(this,a,r,o,l)}})),i.display){for(t.save(),a=o-1;a>=0;a--){const s=i.setContext(this.getPointLabelContext(a)),{color:n,lineWidth:o}=s;o&&n&&(t.lineWidth=o,t.strokeStyle=n,t.setLineDash(s.borderDash),t.lineDashOffset=s.borderDashOffset,r=this.getDistanceFromCenterForValue(e.ticks.reverse?this.min:this.max),l=this.getPointPosition(a,r),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(l.x,l.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){const t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;const s=this.getIndexAngle(0);let n,o;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(s),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach(((s,a)=>{if(0===a&&!e.reverse)return;const r=i.setContext(this.getContext(a)),l=Si(r.font);if(n=this.getDistanceFromCenterForValue(this.ticks[a].value),r.showLabelBackdrop){t.font=l.string,o=t.measureText(s.label).width,t.fillStyle=r.backdropColor;const e=ki(r.backdropPadding);t.fillRect(-o/2-e.left,-n-l.size/2-e.top,o+e.width,l.size+e.height)}Ne(t,s.label,0,-n,l,{color:r.color,strokeColor:r.textStrokeColor,strokeWidth:r.textStrokeWidth})})),t.restore()}drawTitle(){}}const Ro={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Io=Object.keys(Ro);function zo(t,e){return t-e}function Fo(t,e){if(s(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:r}=t._parseOpts;let l=e;return"function"==typeof n&&(l=n(l)),a(l)||(l="string"==typeof n?i.parse(l,n):i.parse(l)),null===l?null:(o&&(l="week"!==o||!N(r)&&!0!==r?i.startOf(l,o):i.startOf(l,"isoWeek",r)),+l)}function Vo(t,e,i,s){const n=Io.length;for(let o=Io.indexOf(t);o=e?i[s]:i[n]]=!0}}else t[e]=!0}function Wo(t,e,i){const s=[],n={},o=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,s,n,i):s}class No extends Js{static id="time";static defaults={bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}};constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e={}){const i=t.time||(t.time={}),s=this._adapter=new Rn._date(t.adapters.date);s.init(e),x(i.displayFormats,s.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Fo(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,e=this._adapter,i=t.time.unit||"day";let{min:s,max:n,minDefined:o,maxDefined:r}=this.getUserBounds();function l(t){o||isNaN(t.min)||(s=Math.min(s,t.min)),r||isNaN(t.max)||(n=Math.max(n,t.max))}o&&r||(l(this._getLabelBounds()),"ticks"===t.bounds&&"labels"===t.ticks.source||l(this.getMinMax(!1))),s=a(s)&&!isNaN(s)?s:+e.startOf(Date.now(),i),n=a(n)&&!isNaN(n)?n:+e.endOf(Date.now(),i)+1,this.min=Math.min(s,n-1),this.max=Math.max(s+1,n)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this.options,e=t.time,i=t.ticks,s="labels"===i.source?this.getLabelTimestamps():this._generate();"ticks"===t.bounds&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const n=this.min,o=nt(s,n,this.max);return this._unit=e.unit||(i.autoSkip?Vo(e.minUnit,this.min,this.max,this._getLabelCapacity(n)):function(t,e,i,s,n){for(let o=Io.length-1;o>=Io.indexOf(i);o--){const i=Io[o];if(Ro[i].common&&t._adapter.diff(n,s,i)>=e-1)return i}return Io[i?Io.indexOf(i):0]}(this,o.length,e.minUnit,this.min,this.max)),this._majorUnit=i.major.enabled&&"year"!==this._unit?function(t){for(let e=Io.indexOf(t)+1,i=Io.length;e+t.value)))}initOffsets(t=[]){let e,i,s=0,n=0;this.options.offset&&t.length&&(e=this.getDecimalForValue(t[0]),s=1===t.length?1-e:(this.getDecimalForValue(t[1])-e)/2,i=this.getDecimalForValue(t[t.length-1]),n=1===t.length?i:(i-this.getDecimalForValue(t[t.length-2]))/2);const o=t.length<3?.5:.25;s=J(s,0,o),n=J(n,0,o),this._offsets={start:s,end:n,factor:1/(s+1+n)}}_generate(){const t=this._adapter,e=this.min,i=this.max,s=this.options,n=s.time,o=n.unit||Vo(n.minUnit,e,i,this._getLabelCapacity(e)),a=l(s.ticks.stepSize,1),r="week"===o&&n.isoWeekday,h=N(r)||!0===r,c={};let d,u,f=e;if(h&&(f=+t.startOf(f,"isoWeek",r)),f=+t.startOf(f,h?"day":o),t.diff(i,e,o)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+o);const g="data"===s.ticks.source&&this.getDataTimestamps();for(d=f,u=0;d+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){const i=this.options.time.displayFormats,s=this._unit,n=e||i[s];return this._adapter.format(t,n)}_tickFormatFunction(t,e,i,s){const n=this.options,o=n.ticks.callback;if(o)return d(o,[t,e,i],this);const a=n.time.displayFormats,r=this._unit,l=this._majorUnit,h=r&&a[r],c=l&&a[l],u=i[e],f=l&&c&&u&&u.major;return this._adapter.format(t,s||(f?c:h))}generateTickLabels(t){let e,i,s;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t,e,i=this._cache.data||[];if(i.length)return i;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(t=0,e=s.length;t=t[r].pos&&e<=t[l].pos&&({lo:r,hi:l}=it(t,"pos",e)),({pos:s,time:o}=t[r]),({pos:n,time:a}=t[l])):(e>=t[r].time&&e<=t[l].time&&({lo:r,hi:l}=it(t,"time",e)),({time:s,pos:o}=t[r]),({time:n,pos:a}=t[l]));const h=n-s;return h?o+(a-o)*(e-s)/h:o}var jo=Object.freeze({__proto__:null,CategoryScale:class extends Js{static id="category";static defaults={ticks:{callback:po}};constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const e=this._addedLabels;if(e.length){const t=this.getLabels();for(const{index:i,label:s}of e)t[i]===s&&t.splice(i,1);this._addedLabels=[]}super.init(t)}parse(t,e){if(s(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:J(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:go(i,t,l(e,t),this._addedLabels),i.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:e}=this.getUserBounds();let{min:i,max:s}=this.getMinMax(!0);"ticks"===this.options.bounds&&(t||(i=0),e||(s=this.getLabels().length-1)),this.min=i,this.max=s}buildTicks(){const t=this.min,e=this.max,i=this.options.offset,s=[];let n=this.getLabels();n=0===t&&e===n.length-1?n:n.slice(t,e+1),this._valueRange=Math.max(n.length-(i?0:1),1),this._startValue=this.min-(i?.5:0);for(let i=t;i<=e;i++)s.push({value:i});return s}getLabelForValue(t){return po.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return"number"!=typeof t&&(t=this.parse(t)),null===t?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}},LinearScale:xo,LogarithmicScale:ko,RadialLinearScale:Eo,TimeScale:No,TimeSeriesScale:class extends No{static id="timeseries";static defaults=No.defaults;constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=Ho(e,this.min),this._tableRange=Ho(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:e,max:i}=this,s=[],n=[];let o,a,r,l,h;for(o=0,a=t.length;o=e&&l<=i&&s.push(l);if(s.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(o=0,a=s.length;ot-e))}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const e=this.getDataTimestamps(),i=this.getLabelTimestamps();return t=e.length&&i.length?this.normalize(e.concat(i)):e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(Ho(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return Ho(this._table,i*this._tableRange+this._minPos,!0)}}});const $o=["rgb(54, 162, 235)","rgb(255, 99, 132)","rgb(255, 159, 64)","rgb(255, 205, 86)","rgb(75, 192, 192)","rgb(153, 102, 255)","rgb(201, 203, 207)"],Yo=$o.map((t=>t.replace("rgb(","rgba(").replace(")",", 0.5)")));function Uo(t){return $o[t%$o.length]}function Xo(t){return Yo[t%Yo.length]}function qo(t){let e=0;return(i,s)=>{const n=t.getDatasetMeta(s).controller;n instanceof jn?e=function(t,e){return t.backgroundColor=t.data.map((()=>Uo(e++))),e}(i,e):n instanceof $n?e=function(t,e){return t.backgroundColor=t.data.map((()=>Xo(e++))),e}(i,e):n&&(e=function(t,e){return t.borderColor=Uo(e),t.backgroundColor=Xo(e),++e}(i,e))}}function Ko(t){let e;for(e in t)if(t[e].borderColor||t[e].backgroundColor)return!0;return!1}var Go={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(t,e,i){if(!i.enabled)return;const{data:{datasets:s},options:n}=t.config,{elements:o}=n;if(!i.forceOverride&&(Ko(s)||(a=n)&&(a.borderColor||a.backgroundColor)||o&&Ko(o)))return;var a;const r=qo(t);s.forEach(r)}};function Zo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{configurable:!0,enumerable:!0,writable:!0,value:e})}}function Jo(t){t.data.datasets.forEach((t=>{Zo(t)}))}var Qo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Jo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:a,indexAxis:r}=e,l=t.getDatasetMeta(o),h=a||e.data;if("y"===Pi([r,t.options.indexAxis]))return;if(!l.controller.supportsDecimation)return;const c=t.scales[l.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let{start:d,count:u}=function(t,e){const i=e.length;let s,n=0;const{iScale:o}=t,{min:a,max:r,minDefined:l,maxDefined:h}=o.getUserBounds();return l&&(n=J(it(e,o.axis,a).lo,0,i-1)),s=h?J(it(e,o.axis,r).hi+1,n,i)-n:i-n,{start:n,count:s}}(l,h);if(u<=(i.threshold||4*n))return void Zo(e);let f;switch(s(a)&&(e._data=h,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":f=function(t,e,i,s,n){const o=n.samples||s;if(o>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(o-2);let l=0;const h=e+i-1;let c,d,u,f,g,p=e;for(a[l++]=t[p],c=0;cu&&(u=f,d=t[s],g=s);a[l++]=d,p=g}return a[l++]=t[h],a}(h,d,u,n,i);break;case"min-max":f=function(t,e,i,n){let o,a,r,l,h,c,d,u,f,g,p=0,m=0;const b=[],x=e+i-1,_=t[e].x,y=t[x].x-_;for(o=e;og&&(g=l,d=o),p=(m*p+a.x)/++m;else{const i=o-1;if(!s(c)&&!s(d)){const e=Math.min(c,d),s=Math.max(c,d);e!==u&&e!==i&&b.push({...t[e],x:p}),s!==u&&s!==i&&b.push({...t[s],x:p})}o>0&&i!==u&&b.push(t[i]),b.push(a),h=e,m=0,f=g=l,c=d=u=o}}return b}(h,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=f}))},destroy(t){Jo(t)}};function ta(t,e,i,s){if(s)return;let n=e[t],o=i[t];return"angle"===t&&(n=G(n),o=G(o)),{property:t,start:n,end:o}}function ea(t,e,i){for(;e>t;e--){const t=i[e];if(!isNaN(t.x)&&!isNaN(t.y))break}return e}function ia(t,e,i,s){return t&&e?s(t[i],e[i]):t?t[i]:e?e[i]:0}function sa(t,e){let i=[],s=!1;return n(t)?(s=!0,i=t):i=function(t,e){const{x:i=null,y:s=null}=t||{},n=e.points,o=[];return e.segments.forEach((({start:t,end:e})=>{e=ea(t,e,n);const a=n[t],r=n[e];null!==s?(o.push({x:a.x,y:s}),o.push({x:r.x,y:s})):null!==i&&(o.push({x:i,y:a.y}),o.push({x:i,y:r.y}))})),o}(t,e),i.length?new no({points:i,options:{tension:0},_loop:s,_fullLoop:s}):null}function na(t){return t&&!1!==t.fill}function oa(t,e,i){let s=t[e].fill;const n=[e];let o;if(!i)return s;for(;!1!==s&&-1===n.indexOf(s);){if(!a(s))return s;if(o=t[s],!o)return!1;if(o.visible)return s;n.push(s),s=o.fill}return!1}function aa(t,e,i){const s=function(t){const e=t.options,i=e.fill;let s=l(i&&i.target,i);void 0===s&&(s=!!e.backgroundColor);if(!1===s||null===s)return!1;if(!0===s)return"origin";return s}(t);if(o(s))return!isNaN(s.value)&&s;let n=parseFloat(s);return a(n)&&Math.floor(n)===n?function(t,e,i,s){"-"!==t&&"+"!==t||(i=e+i);if(i===e||i<0||i>=s)return!1;return i}(s[0],e,n,i):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function ra(t,e,i){const s=[];for(let n=0;n=0;--e){const i=n[e].$filler;i&&(i.line.updateControlPoints(o,i.axis),s&&i.fill&&da(t.ctx,i,o))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const s=t.getSortedVisibleDatasetMetas();for(let e=s.length-1;e>=0;--e){const i=s[e].$filler;na(i)&&da(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const s=e.meta.$filler;na(s)&&"beforeDatasetDraw"===i.drawTime&&da(t.ctx,s,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const ba=(t,e)=>{let{boxHeight:i=e,boxWidth:s=e}=t;return t.usePointStyle&&(i=Math.min(i,e),s=t.pointStyleWidth||Math.min(s,e)),{boxWidth:s,boxHeight:i,itemHeight:Math.max(e,i)}};class xa extends Hs{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let e=d(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter((e=>t.filter(e,this.chart.data)))),t.sort&&(e=e.sort(((e,i)=>t.sort(e,i,this.chart.data)))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){const{options:t,ctx:e}=this;if(!t.display)return void(this.width=this.height=0);const i=t.labels,s=Si(i.font),n=s.size,o=this._computeTitleHeight(),{boxWidth:a,itemHeight:r}=ba(i,n);let l,h;e.font=s.string,this.isHorizontal()?(l=this.maxWidth,h=this._fitRows(o,n,a,r)+10):(h=this.maxHeight,l=this._fitCols(o,s,a,r)+10),this.width=Math.min(l,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,s){const{ctx:n,maxWidth:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.lineWidths=[0],h=s+a;let c=t;n.textAlign="left",n.textBaseline="middle";let d=-1,u=-h;return this.legendItems.forEach(((t,f)=>{const g=i+e/2+n.measureText(t.text).width;(0===f||l[l.length-1]+g+2*a>o)&&(c+=h,l[l.length-(f>0?0:1)]=0,u+=h,d++),r[f]={left:0,top:u,row:d,width:g,height:s},l[l.length-1]+=g+a})),c}_fitCols(t,e,i,s){const{ctx:n,maxHeight:o,options:{labels:{padding:a}}}=this,r=this.legendHitBoxes=[],l=this.columnSizes=[],h=o-t;let c=a,d=0,u=0,f=0,g=0;return this.legendItems.forEach(((t,o)=>{const{itemWidth:p,itemHeight:m}=function(t,e,i,s,n){const o=function(t,e,i,s){let n=t.text;n&&"string"!=typeof n&&(n=n.reduce(((t,e)=>t.length>e.length?t:e)));return e+i.size/2+s.measureText(n).width}(s,t,e,i),a=function(t,e,i){let s=t;"string"!=typeof e.text&&(s=_a(e,i));return s}(n,s,e.lineHeight);return{itemWidth:o,itemHeight:a}}(i,e,n,t,s);o>0&&u+m+2*a>h&&(c+=d+a,l.push({width:d,height:u}),f+=d+a,g++,d=u=0),r[o]={left:f,top:u,col:g,width:p,height:m},d=Math.max(d,p),u+=m+a})),c+=d,l.push({width:d,height:u}),c}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:s},rtl:n}}=this,o=Oi(n,this.left,this.width);if(this.isHorizontal()){let n=0,a=ft(i,this.left+s,this.right-this.lineWidths[n]);for(const r of e)n!==r.row&&(n=r.row,a=ft(i,this.left+s,this.right-this.lineWidths[n])),r.top+=this.top+t+s,r.left=o.leftForLtr(o.x(a),r.width),a+=r.width+s}else{let n=0,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height);for(const r of e)r.col!==n&&(n=r.col,a=ft(i,this.top+t+s,this.bottom-this.columnSizes[n].height)),r.top=a,r.left+=this.left+s,r.left=o.leftForLtr(o.x(r.left),r.width),a+=r.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){if(this.options.display){const t=this.ctx;Ie(t,this),this._draw(),ze(t)}}_draw(){const{options:t,columnSizes:e,lineWidths:i,ctx:s}=this,{align:n,labels:o}=t,a=ue.color,r=Oi(t.rtl,this.left,this.width),h=Si(o.font),{padding:c}=o,d=h.size,u=d/2;let f;this.drawTitle(),s.textAlign=r.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=h.string;const{boxWidth:g,boxHeight:p,itemHeight:m}=ba(o,d),b=this.isHorizontal(),x=this._computeTitleHeight();f=b?{x:ft(n,this.left+c,this.right-i[0]),y:this.top+c+x,line:0}:{x:this.left+c,y:ft(n,this.top+x+c,this.bottom-e[0].height),line:0},Ai(this.ctx,t.textDirection);const _=m+c;this.legendItems.forEach(((y,v)=>{s.strokeStyle=y.fontColor,s.fillStyle=y.fontColor;const M=s.measureText(y.text).width,w=r.textAlign(y.textAlign||(y.textAlign=o.textAlign)),k=g+u+M;let S=f.x,P=f.y;r.setWidth(this.width),b?v>0&&S+k+c>this.right&&(P=f.y+=_,f.line++,S=f.x=ft(n,this.left+c,this.right-i[f.line])):v>0&&P+_>this.bottom&&(S=f.x=S+e[f.line].width+c,f.line++,P=f.y=ft(n,this.top+x+c,this.bottom-e[f.line].height));if(function(t,e,i){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;s.save();const n=l(i.lineWidth,1);if(s.fillStyle=l(i.fillStyle,a),s.lineCap=l(i.lineCap,"butt"),s.lineDashOffset=l(i.lineDashOffset,0),s.lineJoin=l(i.lineJoin,"miter"),s.lineWidth=n,s.strokeStyle=l(i.strokeStyle,a),s.setLineDash(l(i.lineDash,[])),o.usePointStyle){const a={radius:p*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},l=r.xPlus(t,g/2);Ee(s,a,l,e+u,o.pointStyleWidth&&g)}else{const o=e+Math.max((d-p)/2,0),a=r.leftForLtr(t,g),l=wi(i.borderRadius);s.beginPath(),Object.values(l).some((t=>0!==t))?He(s,{x:a,y:o,w:g,h:p,radius:l}):s.rect(a,o,g,p),s.fill(),0!==n&&s.stroke()}s.restore()}(r.x(S),P,y),S=gt(w,S+g+u,b?S+k:this.right,t.rtl),function(t,e,i){Ne(s,i.text,t,e+m/2,h,{strikethrough:i.hidden,textAlign:r.textAlign(i.textAlign)})}(r.x(S),P,y),b)f.x+=k+c;else if("string"!=typeof y.text){const t=h.lineHeight;f.y+=_a(y,t)+c}else f.y+=_})),Ti(this.ctx,t.textDirection)}drawTitle(){const t=this.options,e=t.title,i=Si(e.font),s=ki(e.padding);if(!e.display)return;const n=Oi(t.rtl,this.left,this.width),o=this.ctx,a=e.position,r=i.size/2,l=s.top+r;let h,c=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+l,c=ft(t.align,c,this.right-d);else{const e=this.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);h=l+ft(t.align,this.top,this.bottom-e-t.labels.padding-this._computeTitleHeight())}const u=ft(a,c,c+d);o.textAlign=n.textAlign(ut(a)),o.textBaseline="middle",o.strokeStyle=e.color,o.fillStyle=e.color,o.font=i.string,Ne(o,e.text,u,h,i)}_computeTitleHeight(){const t=this.options.title,e=Si(t.font),i=ki(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,s,n;if(tt(t,this.left,this.right)&&tt(e,this.top,this.bottom))for(n=this.legendHitBoxes,i=0;it.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const l=t.controller.getStyle(i?0:void 0),h=ki(l.borderWidth);return{text:e[t.index].label,fillStyle:l.backgroundColor,fontColor:o,hidden:!t.visible,lineCap:l.borderCapStyle,lineDash:l.borderDash,lineDashOffset:l.borderDashOffset,lineJoin:l.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:l.borderColor,pointStyle:s||l.pointStyle,rotation:l.rotation,textAlign:n||l.textAlign,borderRadius:a&&(r||l.borderRadius),datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class va extends Hs{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this.options;if(this.left=0,this.top=0,!i.display)return void(this.width=this.height=this.right=this.bottom=0);this.width=this.right=t,this.height=this.bottom=e;const s=n(i.text)?i.text.length:1;this._padding=ki(i.padding);const o=s*Si(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:s,right:n,options:o}=this,a=o.align;let r,l,h,c=0;return this.isHorizontal()?(l=ft(a,i,n),h=e+t,r=n-i):("left"===o.position?(l=i+t,h=ft(a,s,e),c=-.5*C):(l=n-t,h=ft(a,e,s),c=.5*C),r=s-e),{titleX:l,titleY:h,maxWidth:r,rotation:c}}draw(){const t=this.ctx,e=this.options;if(!e.display)return;const i=Si(e.font),s=i.lineHeight/2+this._padding.top,{titleX:n,titleY:o,maxWidth:a,rotation:r}=this._drawArgs(s);Ne(t,e.text,0,0,i,{color:e.color,maxWidth:a,rotation:r,textAlign:ut(e.align),textBaseline:"middle",translation:[n,o]})}}var Ma={id:"title",_element:va,start(t,e,i){!function(t,e){const i=new va({ctx:t.ctx,options:e,chart:t});as.configure(t,i,e),as.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;as.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const s=t.titleBlock;as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const wa=new WeakMap;var ka={id:"subtitle",start(t,e,i){const s=new va({ctx:t.ctx,options:i,chart:t});as.configure(t,s,i),as.addBox(t,s),wa.set(t,s)},stop(t){as.removeBox(t,wa.get(t)),wa.delete(t)},beforeUpdate(t,e,i){const s=wa.get(t);as.configure(t,s,i),s.options=i},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Sa={average(t){if(!t.length)return!1;let e,i,s=0,n=0,o=0;for(e=0,i=t.length;e-1?t.split("\n"):t}function Ca(t,e){const{element:i,datasetIndex:s,index:n}=e,o=t.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:t,label:a,parsed:o.getParsed(n),raw:t.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:i}}function Oa(t,e){const i=t.chart.ctx,{body:s,footer:n,title:o}=t,{boxWidth:a,boxHeight:r}=e,l=Si(e.bodyFont),h=Si(e.titleFont),c=Si(e.footerFont),d=o.length,f=n.length,g=s.length,p=ki(e.padding);let m=p.height,b=0,x=s.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(m+=d*h.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){m+=g*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-g)*l.lineHeight+(x-1)*e.bodySpacing}f&&(m+=e.footerMarginTop+f*c.lineHeight+(f-1)*e.footerSpacing);let _=0;const y=function(t){b=Math.max(b,i.measureText(t).width+_)};return i.save(),i.font=h.string,u(t.title,y),i.font=l.string,u(t.beforeBody.concat(t.afterBody),y),_=e.displayColors?a+2+e.boxPadding:0,u(s,(t=>{u(t.before,y),u(t.lines,y),u(t.after,y)})),_=0,i.font=c.string,u(t.footer,y),i.restore(),b+=p.width,{width:b,height:m}}function Aa(t,e,i,s){const{x:n,width:o}=i,{width:a,chartArea:{left:r,right:l}}=t;let h="center";return"center"===s?h=n<=(r+l)/2?"left":"right":n<=o/2?h="left":n>=a-o/2&&(h="right"),function(t,e,i,s){const{x:n,width:o}=s,a=i.caretSize+i.caretPadding;return"left"===t&&n+o+a>e.width||"right"===t&&n-o-a<0||void 0}(h,t,e,i)&&(h="center"),h}function Ta(t,e,i){const s=i.yAlign||e.yAlign||function(t,e){const{y:i,height:s}=e;return it.height-s/2?"bottom":"center"}(t,i);return{xAlign:i.xAlign||e.xAlign||Aa(t,e,i,s),yAlign:s}}function La(t,e,i,s){const{caretSize:n,caretPadding:o,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,h=n+o,{topLeft:c,topRight:d,bottomLeft:u,bottomRight:f}=wi(a);let g=function(t,e){let{x:i,width:s}=t;return"right"===e?i-=s:"center"===e&&(i-=s/2),i}(e,r);const p=function(t,e,i){let{y:s,height:n}=t;return"top"===e?s+=i:s-="bottom"===e?n+i:n/2,s}(e,l,h);return"center"===l?"left"===r?g+=h:"right"===r&&(g-=h):"left"===r?g-=Math.max(c,u)+n:"right"===r&&(g+=Math.max(d,f)+n),{x:J(g,0,s.width-e.width),y:J(p,0,s.height-e.height)}}function Ea(t,e,i){const s=ki(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-s.right:t.x+s.left}function Ra(t){return Pa([],Da(t))}function Ia(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}const za={beforeTitle:e,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,s=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(s>0&&e.dataIndex{const e={before:[],lines:[],after:[]},n=Ia(i,t);Pa(e.before,Da(Fa(n,"beforeLabel",this,t))),Pa(e.lines,Fa(n,"label",this,t)),Pa(e.after,Da(Fa(n,"afterLabel",this,t))),s.push(e)})),s}getAfterBody(t,e){return Ra(Fa(e.callbacks,"afterBody",this,t))}getFooter(t,e){const{callbacks:i}=e,s=Fa(i,"beforeFooter",this,t),n=Fa(i,"footer",this,t),o=Fa(i,"afterFooter",this,t);let a=[];return a=Pa(a,Da(s)),a=Pa(a,Da(n)),a=Pa(a,Da(o)),a}_createItems(t){const e=this._active,i=this.chart.data,s=[],n=[],o=[];let a,r,l=[];for(a=0,r=e.length;at.filter(e,s,n,i)))),t.itemSort&&(l=l.sort(((e,s)=>t.itemSort(e,s,i)))),u(l,(e=>{const i=Ia(t.callbacks,e);s.push(Fa(i,"labelColor",this,e)),n.push(Fa(i,"labelPointStyle",this,e)),o.push(Fa(i,"labelTextColor",this,e))})),this.labelColors=s,this.labelPointStyles=n,this.labelTextColors=o,this.dataPoints=l,l}update(t,e){const i=this.options.setContext(this.getContext()),s=this._active;let n,o=[];if(s.length){const t=Sa[i.position].call(this,s,this._eventPosition);o=this._createItems(i),this.title=this.getTitle(o,i),this.beforeBody=this.getBeforeBody(o,i),this.body=this.getBody(o,i),this.afterBody=this.getAfterBody(o,i),this.footer=this.getFooter(o,i);const e=this._size=Oa(this,i),a=Object.assign({},t,e),r=Ta(this.chart,i,a),l=La(i,a,r,this.chart);this.xAlign=r.xAlign,this.yAlign=r.yAlign,n={opacity:1,x:l.x,y:l.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==this.opacity&&(n={opacity:0});this._tooltipItems=o,this.$context=void 0,n&&this._resolveAnimations().update(this,n),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,s){const n=this.getCaretPosition(t,i,s);e.lineTo(n.x1,n.y1),e.lineTo(n.x2,n.y2),e.lineTo(n.x3,n.y3)}getCaretPosition(t,e,i){const{xAlign:s,yAlign:n}=this,{caretSize:o,cornerRadius:a}=i,{topLeft:r,topRight:l,bottomLeft:h,bottomRight:c}=wi(a),{x:d,y:u}=t,{width:f,height:g}=e;let p,m,b,x,_,y;return"center"===n?(_=u+g/2,"left"===s?(p=d,m=p-o,x=_+o,y=_-o):(p=d+f,m=p+o,x=_-o,y=_+o),b=p):(m="left"===s?d+Math.max(r,h)+o:"right"===s?d+f-Math.max(l,c)-o:this.caretX,"top"===n?(x=u,_=x-o,p=m-o,b=m+o):(x=u+g,_=x+o,p=m+o,b=m-o),y=x),{x1:p,x2:m,x3:b,y1:x,y2:_,y3:y}}drawTitle(t,e,i){const s=this.title,n=s.length;let o,a,r;if(n){const l=Oi(i.rtl,this.x,this.width);for(t.x=Ea(this,i.titleAlign,i),e.textAlign=l.textAlign(i.titleAlign),e.textBaseline="middle",o=Si(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=o.string,r=0;r0!==t))?(t.beginPath(),t.fillStyle=n.multiKeyBackground,He(t,{x:e,y:g,w:h,h:l,radius:r}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),He(t,{x:i,y:g+1,w:h-2,h:l-2,radius:r}),t.fill()):(t.fillStyle=n.multiKeyBackground,t.fillRect(e,g,h,l),t.strokeRect(e,g,h,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,h-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){const{body:s}=this,{bodySpacing:n,bodyAlign:o,displayColors:a,boxHeight:r,boxWidth:l,boxPadding:h}=i,c=Si(i.bodyFont);let d=c.lineHeight,f=0;const g=Oi(i.rtl,this.x,this.width),p=function(i){e.fillText(i,g.x(t.x+f),t.y+d/2),t.y+=d+n},m=g.textAlign(o);let b,x,_,y,v,M,w;for(e.textAlign=o,e.textBaseline="middle",e.font=c.string,t.x=Ea(this,m,i),e.fillStyle=i.bodyColor,u(this.beforeBody,p),f=a&&"right"!==m?"center"===o?l/2+h:l+2+h:0,y=0,M=s.length;y0&&e.stroke()}_updateAnimationTarget(t){const e=this.chart,i=this.$animations,s=i&&i.x,n=i&&i.y;if(s||n){const i=Sa[t.position].call(this,this._active,this._eventPosition);if(!i)return;const o=this._size=Oa(this,t),a=Object.assign({},i,this._size),r=Ta(e,t,a),l=La(t,a,r,e);s._to===l.x&&n._to===l.y||(this.xAlign=r.xAlign,this.yAlign=r.yAlign,this.width=o.width,this.height=o.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,l))}}_willRender(){return!!this.opacity}draw(t){const e=this.options.setContext(this.getContext());let i=this.opacity;if(!i)return;this._updateAnimationTarget(e);const s={width:this.width,height:this.height},n={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;const o=ki(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(n,t,s,e),Ai(t,e.textDirection),n.y+=o.top,this.drawTitle(n,t,e),this.drawBody(n,t,e),this.drawFooter(n,t,e),Ti(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this._active,s=t.map((({datasetIndex:t,index:e})=>{const i=this.chart.getDatasetMeta(t);if(!i)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:i.data[e],index:e}})),n=!f(i,s),o=this._positionChanged(s,e);(n||o)&&(this._active=s,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,n=this._active||[],o=this._getActiveElements(t,n,e,i),a=this._positionChanged(o,t),r=e||!f(o,n)||a;return r&&(this._active=o,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),r}_getActiveElements(t,e,i,s){const n=this.options;if("mouseout"===t.type)return[];if(!s)return e.filter((t=>this.chart.data.datasets[t.datasetIndex]&&void 0!==this.chart.getDatasetMeta(t.datasetIndex).controller.getParsed(t.index)));const o=this.chart.getElementsAtEventForMode(t,n.mode,n,i);return n.reverse&&o.reverse(),o}_positionChanged(t,e){const{caretX:i,caretY:s,options:n}=this,o=Sa[n.position].call(this,t,e);return!1!==o&&(i!==o.x||s!==o.y)}}var Ba={id:"tooltip",_element:Va,positioners:Sa,afterInit(t,e,i){i&&(t.tooltip=new Va({chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const i={tooltip:e};if(!1===t.notifyPlugins("beforeTooltipDraw",{...i,cancelable:!0}))return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i)}},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:za},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};return An.register(Yn,jo,fo,t),An.helpers={...Wi},An._adapters=Rn,An.Animation=Cs,An.Animations=Os,An.animator=xt,An.controllers=en.controllers.items,An.DatasetController=Ns,An.Element=Hs,An.elements=fo,An.Interaction=Xi,An.layouts=as,An.platforms=Ss,An.Scale=Js,An.Ticks=ae,Object.assign(An,Yn,jo,fo,t,Ss),An.Chart=An,"undefined"!=typeof window&&(window.Chart=An),An})); +//# sourceMappingURL=chart.umd.js.map diff --git a/views/layout.erb b/views/layout.erb index 7ababe09..ac39ad90 100644 --- a/views/layout.erb +++ b/views/layout.erb @@ -61,7 +61,7 @@ - + + + }; + + const ctx = $("#myChart").get(0).getContext("2d"); + + const config = { + type: 'line', + data: data, + options: { + responsive: true, + scales: { + x: { + beginAtZero: true + }, + y: { + beginAtZero: true + } + }, + plugins: { + tooltip: { + mode: "index", + intersect: false, + bodyFont: { + size: 14, + }, + bodyAlign: 'right', + titleFont: { + size: 14, + }, + callbacks: { + afterTitle: function(context) { + let tooltipData = []; + if (context.length > 0) { + const index = context[0].dataIndex; + } + return tooltipData; + } + } + } + } + } + }; + + const myLineChart = new Chart(ctx, config); + }); + + From b8baad628ba1b54e3966828f021f65e4f1342287 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 6 Jan 2024 15:20:00 -0600 Subject: [PATCH 67/74] add stats link to hamburger --- views/_header.erb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/views/_header.erb b/views/_header.erb index 1e424a84..32795919 100644 --- a/views/_header.erb +++ b/views/_header.erb @@ -41,8 +41,9 @@
  • <% end %> -
  • Edit Site
  • -
  • View Site
  • +
  • Edit
  • +
  • View
  • +
  • Stats
  • Settings
  • From cb7e8661e6a63539c29f7902190cf5a90a52fcb9 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sat, 6 Jan 2024 17:19:38 -0600 Subject: [PATCH 68/74] add info about stats --- views/site/stats.erb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/views/site/stats.erb b/views/site/stats.erb index 60d53860..042a1d60 100644 --- a/views/site/stats.erb +++ b/views/site/stats.erb @@ -174,6 +174,22 @@
    <% end %> +
    +
    +

    What are these numbers?

    +

    + Hits occur each time our servers send a file. For example, if a webpage consists of an HTML file, three images, and two JavaScript files, accessing this page would result in six hits. +

    +

    + Visits are a count of unique IP addresses requesting pages from a web site per hour, regardless of how many requests that IP address makes. Visits generally give a more accurate representation of website traffic in terms of real users. +

    + +

    + Due to bots, search engine crawlers, and proxy servers these numbers should not be considered completely accurate. +

    +
    +
    + - Neocities: Create your own free website! - - - - - + <%== erb :'_meta' %> - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - + + <%== erb :'_meta' %> <% if meta_robots %> @@ -24,13 +11,6 @@ - - - - - - - <% if @dont_browser_cache %> From 0c7c2d44b12edcaabb54593c58ba175e5807c244 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 7 Jan 2024 00:00:18 -0600 Subject: [PATCH 71/74] fix test issue related to validation text --- tests/acceptance/signup_tests.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/signup_tests.rb b/tests/acceptance/signup_tests.rb index 81ea876d..8dc0ad38 100644 --- a/tests/acceptance/signup_tests.rb +++ b/tests/acceptance/signup_tests.rb @@ -109,10 +109,10 @@ describe 'signup' do _(page).must_have_content 'Usernames can only contain' fill_in 'username', with: 'nope-' click_signup_button - _(page).must_have_content 'A valid user/site name is required' + _(page).must_have_content 'Usernames can only contain' fill_in 'username', with: '-nope' click_signup_button - _(page).must_have_content 'A valid user/site name is required' + _(page).must_have_content 'Usernames can only contain' end it 'fails with username greater than 32 characters' do From 4b8c7c193393a69085b57a6fa97ee0aa4f018fb0 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 7 Jan 2024 18:23:28 +0000 Subject: [PATCH 72/74] add unsafe-eval to script-src to fix replies on comments --- app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.rb b/app.rb index 5099557a..3d3b3ea5 100644 --- a/app.rb +++ b/app.rb @@ -92,7 +92,7 @@ after do end after do - response.headers['Content-Security-Policy'] = %{default-src 'self' data: blob: 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} unless self.class.development? + response.headers['Content-Security-Policy'] = %{default-src 'self' data: blob: 'unsafe-inline'; script-src 'self' blob: 'unsafe-inline' 'unsafe-eval' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com; style-src 'self' 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; connect-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://api.stripe.com; frame-src 'self' https://hcaptcha.com https://*.hcaptcha.com https://js.stripe.com} unless self.class.development? end not_found do From 2f6ba312dc3226a0ae06c4c72a450429310d5f34 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 7 Jan 2024 12:24:44 -0600 Subject: [PATCH 73/74] add undiscovered article from polygon --- views/press.erb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/views/press.erb b/views/press.erb index 91f71e6e..c90d1322 100644 --- a/views/press.erb +++ b/views/press.erb @@ -13,6 +13,11 @@
    +

    Polygon: Neocities celebrates ‘the old internet,’ offering relief from 24/7 social feeds

    +
    + Rather than a constantly rushing river of information, Neocities sites are like homes where users fix them up, spend time on them, and invite others to visit. +
    +

    TechSpot: Neocities is bringing the eye-bleeding "spirit" of GeoCities back to the modern web

    Neocities is yet another alternative to social networking and pure nostalgia trips down memory lane. It offers a hosting space for hundreds of thousands of websites that don't need to comply with static rules or well-defined design policies to be online. Neocities introduces itself as a "social network" that brings back the "lost individual creativity of the web." @@ -30,15 +35,6 @@ It's easy to assume that those attending the Web 1.0 Conference in Portland, Oregon are caught up on an obsolete era of the internet. The conference's organizers, however, think the lowly HTML website may very well be the future of the web.
    -

    Vice: The InterPlanetary File System Wants to Create a Permanent Web

    -
    - In addition to satisfying the cravings of some for Geocities clip-art nostalgia, Drake has more serious plans up his sleeve. He wants to give people the ability to "build web sites that persist forever." -
    -
    -
    - "Building an information network that will stay up forever is as modern as it gets," he wrote. "[IPFS] will pull the internet out of the Dark Ages of fast information destruction, and move us from a short-term tech culture into a tech civilization, maintaining distributed libraries of information that could continue to persist for hundreds or even thousands of years." -
    -

    Re/code: Why We All Need to Make the Internet Fun Again

    From f261db16b0bdbc74bd60ede9ee1b518b090ed069 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Sun, 7 Jan 2024 13:35:36 -0600 Subject: [PATCH 74/74] refactor RSS from Atom to 2.0 and hopefully get it working without guids --- models/site.rb | 24 +++++++++++++++--------- views/_share.erb | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/models/site.rb b/models/site.rb index 80defff7..3dc02e69 100644 --- a/models/site.rb +++ b/models/site.rb @@ -1488,6 +1488,10 @@ class Site < Sequel::Model File.exist? File.join(base_screenshots_path, "#{path}.#{resolution}.webp") end + def sharing_screenshot_url + 'https://neocities.org'+base_screenshots_url+'/index.html.jpg' + end + def screenshot_url(path, resolution) path[0] = '' if path[0] == '/' out = '' @@ -1518,18 +1522,20 @@ class Site < Sequel::Model end def to_rss - RSS::Maker.make("atom") do |maker| - maker.channel.title = title - maker.channel.updated = (updated_at ? updated_at : created_at) - maker.channel.author = username - maker.channel.id = "#{username}.neocities.org" + RSS::Maker.make("2.0") do |m| + m.channel.title = title + m.channel.link = uri + m.channel.description = "Site feed for #{title}" + m.image.url = sharing_screenshot_url + m.image.title = title + latest_events.each do |event| if event.site_change_id - maker.items.new_item do |item| - item.link = "https://#{host}" - item.title = "#{title} has been updated" - item.updated = event.site_change.created_at + m.items.new_item do |i| + i.title = "#{title} has been updated." + i.link = "https://neocities.org/site/#{username}?event_id=#{event.id.to_s}" + i.pubDate = event.created_at end end end diff --git a/views/_share.erb b/views/_share.erb index c228c541..c1fbd67a 100644 --- a/views/_share.erb +++ b/views/_share.erb @@ -3,7 +3,7 @@ page_uri = site.uri end %> -RSS/Atom Feed +RSS Feed
    " target="_blank">Facebook