diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml index aee38e13..54f61e76 100644 --- a/.github/workflows/brakeman.yml +++ b/.github/workflows/brakeman.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.3.4' + ruby-version: '4.0.6' bundler-cache: true - name: Run Brakeman diff --git a/.gitignore b/.gitignore index d2a365de..a94c835c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ # Ignore bundler config. /.bundle +/vendor/bundle # Ignore all logfiles and tempfiles. /log/* diff --git a/.ruby-version b/.ruby-version index 6d5369b9..1cf76f52 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-3.3.4 +ruby-4.0.6 diff --git a/.tool-versions b/.tool-versions index 276edf42..c99a8bd2 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -ruby 3.3.4 +ruby 4.0.6 nodejs 18.18.2 bundler 2.5.9 yarn 1.22.22 diff --git a/Capfile b/Capfile index cf18f3ec..189af7ac 100644 --- a/Capfile +++ b/Capfile @@ -9,13 +9,9 @@ require "capistrano/scm/git" install_plugin Capistrano::SCM::Git # Include tasks from other gems included in your Gemfile - require 'capistrano/rails' require 'capistrano/bundler' require 'capistrano/asdf' -# require 'capistrano/sidekiq' -# install_plugin Capistrano::Sidekiq -# install_plugin Capistrano::Sidekiq::Systemd # Load custom tasks from `lib/capistrano/tasks` if you have any defined Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r } diff --git a/Gemfile b/Gemfile index 96338b45..a45d7626 100644 --- a/Gemfile +++ b/Gemfile @@ -3,33 +3,40 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -gem 'rails', '~> 7.2' -ruby '3.3.4' +gem 'rails', '~> 8.1' +ruby '4.0.6' gem 'actiontext' gem 'appsignal' gem 'bootsnap', require: false +gem 'benchmark' +gem 'cgi' gem 'country_select' gem 'cssbundling-rails' gem 'csv', '~> 3.2' -gem 'devise', '~> 4.9' +gem 'logger' +gem 'tsort' +gem 'devise', '~> 5.0' gem 'google-cloud-storage', '~> 1.52' gem 'image_processing', '~> 1.2' gem 'jbuilder' gem 'jsbundling-rails' gem 'lsa_tdx_feedback' gem 'mysql2', '~> 0.5.3' +gem 'net-imap', '>= 0.5' gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-saml', '~> 2.1' -gem 'pagy', '~> 6.4' +gem 'pagy', '~> 43.0' +gem 'propshaft' gem 'puma' gem 'pundit' -gem 'redis', '~> 5.0' gem 'sentry-ruby' gem 'sentry-rails' gem 'stackprof' -gem 'sidekiq', '~> 7.3' -gem 'sassc-rails' +gem 'solid_cable' +gem 'solid_cache' +gem 'solid_queue' +gem 'mission_control-jobs' gem 'simple_form', '~> 5.3' gem 'stimulus-rails' gem 'skylight' @@ -38,14 +45,12 @@ gem 'turnout2024', require: 'turnout' gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] group :development do - gem 'annotate' - gem 'better_errors' - gem 'binding_of_caller' + gem 'better_errors', '>= 2.10' + gem 'binding_of_caller', '>= 2.0' gem 'brakeman' gem 'capistrano', '~> 3.17', require: false gem 'capistrano-rails', '~> 1.6', '>= 1.6.1', require: false gem 'capistrano-asdf', require: false - gem 'capistrano-sidekiq', '~> 2.0', require: false gem 'web-console' end @@ -55,7 +60,6 @@ group :test do gem 'rails-controller-testing' gem 'selenium-webdriver' gem 'simplecov', require: false - gem 'webdrivers' end group :development, :staging do @@ -65,7 +69,6 @@ end group :development, :test do gem 'capybara' gem 'debug', platforms: %i[mri mingw x64_mingw] - gem 'pry-byebug' gem 'pry-rails' gem 'pundit-matchers' gem 'rspec-rails' diff --git a/Gemfile.lock b/Gemfile.lock index d3fea7d0..1cb5c9ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,105 +1,104 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + action_text-trix (2.1.19) + railties + actioncable (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailbox (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) - actionmailer (7.2.2.2) - actionpack (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailer (8.1.3) + actionpack (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activesupport (= 8.1.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.2.2) - actionview (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionpack (8.1.3) + actionview (= 8.1.3) + activesupport (= 8.1.3) nokogiri (>= 1.8.5) - racc - rack (>= 2.2.4, < 3.2) + rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.2.2) - actionpack (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actiontext (8.1.3) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.2.2) - activesupport (= 7.2.2.2) + actionview (8.1.3) + activesupport (= 8.1.3) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.2.2) - activesupport (= 7.2.2.2) + activejob (8.1.3) + activesupport (= 8.1.3) globalid (>= 0.3.6) - activemodel (7.2.2.2) - activesupport (= 7.2.2.2) - activerecord (7.2.2.2) - activemodel (= 7.2.2.2) - activesupport (= 7.2.2.2) + activemodel (8.1.3) + activesupport (= 8.1.3) + activerecord (8.1.3) + activemodel (= 8.1.3) + activesupport (= 8.1.3) timeout (>= 0.4.0) - activestorage (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activesupport (= 7.2.2.2) + activestorage (8.1.3) + actionpack (= 8.1.3) + activejob (= 8.1.3) + activerecord (= 8.1.3) + activesupport (= 8.1.3) marcel (~> 1.0) - activesupport (7.2.2.2) + activesupport (8.1.3) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + uri (>= 0.13.1) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) airbrussh (1.6.0) sshkit (>= 1.6.1, != 1.7.0) - annotate (3.2.0) - activerecord (>= 3.2, < 8.0) - rake (>= 10.4, < 14.0) appsignal (4.8.5) logger rack (>= 2.0.0) ast (2.4.2) base64 (0.3.0) - bcrypt (3.1.20) - benchmark (0.4.1) + bcrypt (3.1.22) + benchmark (0.5.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) rouge (>= 1.0.0) - bigdecimal (3.2.2) + bigdecimal (4.1.2) bindex (0.8.1) - binding_of_caller (1.0.1) + binding_of_caller (2.0.0) debug_inspector (>= 1.2.0) - bootsnap (1.18.4) + bootsnap (1.24.6) msgpack (~> 1.2) brakeman (7.0.2) racc builder (3.3.0) - byebug (11.1.3) capistrano (3.20.0) airbrussh (>= 1.0.0) i18n @@ -113,10 +112,6 @@ GEM capistrano-rails (1.6.3) capistrano (~> 3.1) capistrano-bundler (>= 1.1, < 3) - capistrano-sidekiq (2.3.1) - capistrano (>= 3.9.0) - capistrano-bundler - sidekiq (>= 6.0) capybara (3.40.0) addressable matrix @@ -126,16 +121,17 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) + cgi (0.5.2) childprocess (5.1.0) logger (~> 1.5) coderay (1.1.3) - concurrent-ruby (1.3.6) - connection_pool (2.5.3) + concurrent-ruby (1.3.7) + connection_pool (3.0.2) countries (6.0.1) unaccent (~> 0.3) country_select (9.0.0) countries (> 5.0, < 7.0) - crass (1.0.6) + crass (1.0.7) cssbundling-rails (1.4.1) railties (>= 6.0.0) csv (3.3.3) @@ -143,16 +139,16 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.4.1) + date (3.5.1) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) debug_inspector (1.2.0) declarative (0.0.20) - devise (4.9.4) + devise (5.0.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0) + railties (>= 7.0) responders warden (~> 1.2.3) diff-lcs (1.5.1) @@ -160,7 +156,10 @@ GEM rake (>= 12.0.0, < 14.0.0) docile (1.4.1) drb (2.2.3) - erubi (1.13.0) + erb (6.0.4) + erubi (1.13.1) + et-orbi (1.4.0) + tzinfo factory_bot (6.4.6) activesupport (>= 5.0.0) factory_bot_rails (6.4.3) @@ -168,14 +167,16 @@ GEM railties (>= 5.0.0) faker (3.4.2) i18n (>= 1.8.11, < 2) - faraday (2.12.0) - faraday-net_http (>= 2.0, < 3.4) + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) json logger - faraday-net_http (3.3.0) - net-http - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86_64-linux-gnu) + faraday-net_http (3.4.4) + net-http (~> 0.5) + ffi (1.17.0) + fugit (1.13.0) + et-orbi (~> 1.4) + raabro (~> 1.4) globalid (1.2.1) activesupport (>= 6.1) google-apis-core (0.15.1) @@ -218,13 +219,19 @@ GEM mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.14.8) + i18n (1.15.2) concurrent-ruby (~> 1.0) image_processing (1.13.0) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) - io-console (0.7.2) - irb (1.14.0) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jbuilder (2.12.0) @@ -232,8 +239,8 @@ GEM activesupport (>= 5.0.0) jsbundling-rails (1.3.1) railties (>= 6.0.0) - json (2.7.2) - jwt (2.9.3) + json (2.21.1) + jwt (2.10.3) base64 language_server-protocol (3.17.0.3) launchy (3.0.1) @@ -247,7 +254,7 @@ GEM railties (>= 6.1) rexml logger (1.7.0) - loofah (2.23.1) + loofah (2.25.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) lsa_tdx_feedback (1.0.3) @@ -264,16 +271,28 @@ GEM method_source (1.1.0) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.5) - msgpack (1.7.2) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) + mission_control-jobs (1.1.0) + actioncable (>= 7.1) + actionpack (>= 7.1) + activejob (>= 7.1) + activerecord (>= 7.1) + importmap-rails (>= 1.2.1) + irb (~> 1.13) + railties (>= 7.1) + stimulus-rails + turbo-rails + msgpack (1.8.3) multi_json (1.15.0) multi_xml (0.8.1) bigdecimal (>= 3.1, < 5) mutex_m (0.2.0) mysql2 (0.5.6) - net-http (0.4.1) - uri - net-imap (0.4.20) + net-http (0.9.1) + uri (>= 0.11.1) + net-imap (0.6.4.1) date net-protocol net-pop (0.1.2) @@ -287,10 +306,10 @@ GEM net-smtp (0.5.0) net-protocol net-ssh (7.3.0) - nio4r (2.7.3) - nokogiri (1.18.9-arm64-darwin) + nio4r (2.7.5) + nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux-gnu) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) omniauth (2.1.3) hashie (>= 3.4.6) @@ -305,23 +324,29 @@ GEM orm_adapter (0.5.0) os (1.1.4) ostruct (0.6.3) - pagy (6.5.0) + pagy (43.6.0) + json + uri + yaml parallel (1.26.3) parser (3.3.5.0) ast (~> 2.4.1) racc + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + propshaft (1.3.2) + actionpack (>= 7.0.0) + activesupport (>= 7.0.0) + rack pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry-byebug (3.10.1) - byebug (~> 11.0) - pry (>= 0.13, < 0.15) pry-rails (0.3.11) pry (>= 0.13.0) - psych (5.1.2) - stringio - public_suffix (6.0.1) - puma (6.4.3) + public_suffix (7.0.5) + puma (7.2.1) nio4r (~> 2.0) pundit (2.4.0) activesupport (>= 3.0.0) @@ -330,76 +355,85 @@ GEM rspec-expectations (~> 3.12) rspec-mocks (~> 3.12) rspec-support (~> 3.12) + raabro (1.4.0) racc (1.8.1) - rack (3.1.18) + rack (3.2.6) rack-accept (0.4.5) rack (>= 0.4) rack-protection (4.1.1) base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) - rack-session (2.1.1) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) - rackup (2.1.0) + rackup (2.3.1) rack (>= 3) - webrick (~> 1.8) - rails (7.2.2.2) - actioncable (= 7.2.2.2) - actionmailbox (= 7.2.2.2) - actionmailer (= 7.2.2.2) - actionpack (= 7.2.2.2) - actiontext (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activemodel (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + rails (8.1.3) + actioncable (= 8.1.3) + actionmailbox (= 8.1.3) + actionmailer (= 8.1.3) + actionpack (= 8.1.3) + actiontext (= 8.1.3) + actionview (= 8.1.3) + activejob (= 8.1.3) + activemodel (= 8.1.3) + activerecord (= 8.1.3) + activestorage (= 8.1.3) + activesupport (= 8.1.3) bundler (>= 1.15.0) - railties (= 7.2.2.2) + railties (= 8.1.3) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) activesupport (>= 5.0.1.rc1) - rails-dom-testing (2.2.0) + rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.1) - loofah (~> 2.21) + rails-html-sanitizer (1.7.0) + loofah (~> 2.25) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + railties (8.1.3) + actionpack (= 8.1.3) + activesupport (= 8.1.3) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.1) - rdoc (6.7.0) - psych (>= 4.0.0) + rake (13.4.2) + rbs (4.0.3) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort redis (5.3.0) redis-client (>= 0.22.0) redis-client (0.24.0) connection_pool regexp_parser (2.9.2) - reline (0.5.9) + reline (0.6.3) io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) retriable (3.1.2) - rexml (3.4.2) - rouge (4.3.0) + rexml (3.4.4) + rouge (5.0.0) + strscan (~> 3.1) rspec-core (3.13.1) rspec-support (~> 3.13.0) rspec-expectations (3.13.2) @@ -461,19 +495,13 @@ GEM ruby-vips (2.2.2) ffi (~> 1.12) logger - rubyzip (2.3.2) - sassc (2.4.0) - ffi (~> 1.9) - sassc-rails (2.1.2) - railties (>= 4.0.0) - sassc (>= 2.0) - sprockets (> 3.0) - sprockets-rails - tilt + rubyzip (3.4.1) securerandom (0.4.1) - selenium-webdriver (4.10.0) + selenium-webdriver (4.46.0) + base64 (~> 0.2) + logger (~> 1.4) rexml (~> 3.2, >= 3.2.5) - rubyzip (>= 1.2.2, < 3.0) + rubyzip (>= 1.2.2, < 4.0) websocket (~> 1.0) sentry-rails (5.23.0) railties (>= 5.0) @@ -481,12 +509,6 @@ GEM sentry-ruby (5.23.0) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sidekiq (7.3.9) - base64 - connection_pool (>= 2.3.0) - logger - rack (>= 2.2.4) - redis-client (>= 0.22.2) signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -503,13 +525,22 @@ GEM simplecov_json_formatter (0.1.4) skylight (6.0.4) activesupport (>= 5.2.0) - sprockets (4.2.1) - concurrent-ruby (~> 1.0) - rack (>= 2.2.4, < 4) - sprockets-rails (3.5.2) - actionpack (>= 6.1) - activesupport (>= 6.1) - sprockets (>= 3.0.0) + solid_cable (4.0.0) + actioncable (>= 7.2) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_cache (1.0.10) + activejob (>= 7.2) + activerecord (>= 7.2) + railties (>= 7.2) + solid_queue (1.4.0) + activejob (>= 7.1) + activerecord (>= 7.1) + concurrent-ruby (>= 1.3.1) + fugit (~> 1.11) + railties (>= 7.1) + thor (>= 1.3.1) sshkit (1.25.0) base64 logger @@ -520,11 +551,12 @@ GEM stackprof (0.2.27) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.1) - thor (1.4.0) + strscan (3.1.8) + thor (1.5.0) tilt (2.4.0) - timeout (0.4.3) + timeout (0.6.1) trailblazer-option (0.1.2) + tsort (0.2.0) turbo-rails (2.0.6) actionpack (>= 6.0.0) activejob (>= 6.0.0) @@ -539,7 +571,7 @@ GEM uber (0.1.0) unaccent (0.4.0) unicode-display_width (2.5.0) - uri (0.13.3) + uri (1.1.1) useragent (0.16.11) warden (1.2.9) rack (>= 2.0.9) @@ -548,42 +580,39 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webdrivers (5.3.1) - nokogiri (~> 1.6) - rubyzip (>= 1.3.0) - selenium-webdriver (~> 4.0, < 4.11) - webrick (1.8.2) websocket (1.2.11) - websocket-driver (0.7.6) + websocket-driver (0.8.2) + base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.18) + yaml (0.4.0) + zeitwerk (2.8.2) PLATFORMS - arm64-darwin-23 + arm64-darwin-25 x86_64-linux DEPENDENCIES actiontext - annotate appsignal - better_errors - binding_of_caller + benchmark + better_errors (>= 2.10) + binding_of_caller (>= 2.0) bootsnap brakeman capistrano (~> 3.17) capistrano-asdf capistrano-rails (~> 1.6, >= 1.6.1) - capistrano-sidekiq (~> 2.0) capybara + cgi country_select cssbundling-rails csv (~> 3.2) database_cleaner-active_record (~> 2.0) debug - devise (~> 4.9) + devise (~> 5.0) factory_bot_rails faker google-cloud-storage (~> 1.52) @@ -591,19 +620,21 @@ DEPENDENCIES jbuilder jsbundling-rails letter_opener_web + logger lsa_tdx_feedback + mission_control-jobs mysql2 (~> 0.5.3) + net-imap (>= 0.5) omniauth-rails_csrf_protection (~> 1.0) omniauth-saml (~> 2.1) - pagy (~> 6.4) - pry-byebug + pagy (~> 43.0) + propshaft pry-rails puma pundit pundit-matchers - rails (~> 7.2) + rails (~> 8.1) rails-controller-testing - redis (~> 5.0) rspec-rails rubocop rubocop-capybara @@ -612,24 +643,25 @@ DEPENDENCIES rubocop-rails-omakase rubocop-rspec rubocop-rspec_rails - sassc-rails selenium-webdriver sentry-rails sentry-ruby - sidekiq (~> 7.3) simple_form (~> 5.3) simplecov skylight + solid_cable + solid_cache + solid_queue stackprof stimulus-rails + tsort turbo-rails turnout2024 tzinfo-data web-console - webdrivers RUBY VERSION - ruby 3.3.4p94 + ruby 4.0.6p0 BUNDLED WITH 2.5.9 diff --git a/Procfile b/Procfile index 6d19a6cf..c2c566e8 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1 @@ web: bundle exec puma -C config/puma.rb -worker: bundle exec sidekiq -e ${RAILS_ENV:-development} -C config/sidekiq.yml diff --git a/README.md b/README.md index f924b2b2..7b195291 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # LSA Evaluate -[![Ruby on Rails](https://img.shields.io/badge/Ruby%20on%20Rails-7.2-red)](https://rubyonrails.org/) -[![Ruby](https://img.shields.io/badge/Ruby-3.3.4-blue)](https://www.ruby-lang.org/en/) +[![Ruby on Rails](https://img.shields.io/badge/Ruby%20on%20Rails-8.1-red)](https://rubyonrails.org/) +[![Ruby](https://img.shields.io/badge/Ruby-4.0.6-blue)](https://www.ruby-lang.org/en/) [![MySQL](https://img.shields.io/badge/Database-MySQL%208-green)](https://www.mysql.com/) LSA Evaluate is a Ruby on Rails application designed to facilitate a comprehensive submission and evaluation process. The application allows entrants to complete a profile, submit a body of work electronically, and have that work evaluated. Key features include the maintenance of entry forms, management of evaluations, and visual cues to entrants regarding the status of their submissions. @@ -14,9 +14,14 @@ In its initial implementation, LSA Evaluate will serve the University of Michiga ## Technical Specifications -- **Ruby on Rails:** 7.2 -- **Ruby Version:** 3.3.4 -- **Database:** MySQL 8 +- **Ruby on Rails:** 8.1 +- **Ruby Version:** 4.0.6 +- **Database:** MySQL 8 (primary + Solid Queue / Cache / Cable databases) +- **Assets:** Propshaft + cssbundling-rails / jsbundling-rails (esbuild + Dart Sass) +- **Jobs:** Solid Queue (via Puma plugin in production/staging) +- **Cache / Cable:** Solid Cache and Solid Cable (no Redis) + +> Note: `lsa_tdx_feedback` still declares a Redis gem dependency, so `redis` may appear transitively in `Gemfile.lock`, but the application no longer uses Redis for jobs, cache, or Action Cable. ## Installation @@ -37,14 +42,16 @@ In its initial implementation, LSA Evaluate will serve the University of Michiga 3. **Set up the database:** ```bash - rails db:create db:migrate - + rails db:create db:prepare ``` -4. **Run the server:** + This creates the primary database plus queue, cache, and cable databases and loads their schemas. + +4. **Build assets and run the server:** ```bash - rails server + yarn build && yarn build:css + bin/dev ``` ## Email Configuration with SendGrid @@ -59,9 +66,17 @@ This application uses SendGrid for email delivery in the production environment. SENDGRID_API_KEY=your_sendgrid_api_key_here DOMAIN_NAME=yourdomain.com ``` -4. Ensure Sidekiq is set up and running to process emails asynchronously +4. Solid Queue runs inside Puma in production/staging and processes `deliver_later` mailers asynchronously. Monitor jobs at `/jobs` (axis_mundi users). + +## Production / Staging Ops Notes + +Before deploying: -Emails are automatically configured to be sent asynchronously through Sidekiq background jobs. +1. Install Ruby 4.0.6 via asdf on the host. +2. Create MySQL databases: `evaluate_db_prod_queue`, `evaluate_db_prod_cache`, `evaluate_db_prod_cable` (and staging equivalents / `QUEUE_DATABASE_URL`, `CACHE_DATABASE_URL`, `CABLE_DATABASE_URL` if separate). +3. Run `rails db:prepare` (or load `db/queue_schema.rb`, `db/cache_schema.rb`, `db/cable_schema.rb`) on those databases. +4. Stop and remove Sidekiq/Redis once in-flight Sidekiq jobs are drained. +5. Restart Puma after deploy — Solid Queue is enabled via the Puma plugin. ## Protected Branches and Pre-Push Hook diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js deleted file mode 100644 index 6d8bb9fb..00000000 --- a/app/assets/config/manifest.js +++ /dev/null @@ -1,4 +0,0 @@ -//= link_tree ../images -//= link_tree ../builds -//= link_tree ../stylesheets -//= link judge_dashboard.css diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8b9a45a9..582b7917 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,7 +2,7 @@ class ApplicationController < ActionController::Base include Pundit::Authorization include ApplicationHelper - include Pagy::Backend + include Pagy::Method before_action :authenticate_user! before_action :set_sentry_context @@ -68,11 +68,14 @@ def log_exception(exception) end def set_sentry_context - Sentry.set_user(id: current_user.id, email: current_user.email) if current_user + if current_user + Sentry.set_user(id: current_user.id, email: current_user.email) + end + Sentry.set_context('request', { controller: controller_name, action: action_name, - params: params.except(:controller, :action) + params: request.filtered_parameters.except('controller', 'action') }) end end diff --git a/app/controllers/users_dashboard_controller.rb b/app/controllers/users_dashboard_controller.rb index 0b4a1ddf..2889e3f0 100644 --- a/app/controllers/users_dashboard_controller.rb +++ b/app/controllers/users_dashboard_controller.rb @@ -16,12 +16,17 @@ def index end @pagy, @users = pagy( + :offset, users.order(sort_column => sort_direction), - items: 20, # Explicitly set number of items per page - params: { - principal_name_filter: params[:principal_name_filter], - email_filter: params[:email_filter] - }.compact # Pass filters to pagination links + limit: 20, + querify: lambda { |p| + p.merge!( + { + 'principal_name_filter' => params[:principal_name_filter], + 'email_filter' => params[:email_filter] + }.compact + ) + } ) end diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb index 7aa73495..9c620dc9 100644 --- a/app/helpers/sentry_helper.rb +++ b/app/helpers/sentry_helper.rb @@ -1,4 +1,20 @@ +# frozen_string_literal: true + module SentryHelper + def sentry_meta_tags + dsn = Rails.application.credentials.dig(:sentry, :dsn) + return if dsn.blank? + return unless Sentry.configuration.enabled_environments.include?(Rails.env) + + safe_join( + [ + tag.meta(name: 'sentry-dsn', content: dsn), + tag.meta(name: 'sentry-environment', content: Rails.env), + tag.meta(name: 'sentry-release', content: SentryRelease.current.to_s) + ] + ) + end + def set_sentry_user(user) Sentry.set_user(id: user.id, email: user.email) end diff --git a/app/helpers/users_dashboard_helper.rb b/app/helpers/users_dashboard_helper.rb index 980213c8..78618034 100644 --- a/app/helpers/users_dashboard_helper.rb +++ b/app/helpers/users_dashboard_helper.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true module UsersDashboardHelper - include Pagy::Frontend - def sort_link(column, title = nil) title ||= column.titleize diff --git a/app/javascript/application.js b/app/javascript/application.js index dc744f82..94b59ef5 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -1,4 +1,5 @@ // Entry point for the build script in your package.json +import * as Sentry from "@sentry/browser" import "@hotwired/turbo-rails" import "./controllers" import * as bootstrap from "bootstrap" @@ -7,4 +8,39 @@ import Sortable from "sortablejs" import "trix" import "@rails/actiontext" +const sentryDsn = document.querySelector('meta[name="sentry-dsn"]')?.content + +if (sentryDsn) { + const environment = + document.querySelector('meta[name="sentry-environment"]')?.content || "production" + const release = document.querySelector('meta[name="sentry-release"]')?.content + const isProduction = environment === "production" + + Sentry.init({ + dsn: sentryDsn, + environment, + release: release || undefined, + integrations: [Sentry.browserTracingIntegration()], + // Match server-side sampling; keep all browser errors + tracesSampleRate: isProduction ? 0.1 : 1.0, + // Ignore common extension / noise errors + ignoreErrors: [ + "ResizeObserver loop limit exceeded", + "ResizeObserver loop completed with undelivered notifications", + "Non-Error promise rejection captured", + /^Loading CSS chunk/, + /^Loading chunk/ + ], + beforeSend(event) { + // Drop events from browser extensions + if (event.exception?.values?.some((ex) => + /chrome-extension:|moz-extension:|safari-extension:/i.test(ex.stacktrace?.frames?.[0]?.filename || "") + )) { + return null + } + return event + } + }) +} + window.Sortable = Sortable; diff --git a/app/views/judge_dashboard/index.html.erb b/app/views/judge_dashboard/index.html.erb index b610b0d9..bd84f036 100644 --- a/app/views/judge_dashboard/index.html.erb +++ b/app/views/judge_dashboard/index.html.erb @@ -1,7 +1,3 @@ -<% content_for :head do %> - <%= stylesheet_link_tag "judge_dashboard", "data-turbo-track": "reload" %> -<% end %> -

Judge Dashboard

diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 28cc247e..74187792 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,7 +6,8 @@ <%= lsa_tdx_feedback_css %> <%= csrf_meta_tags %> <%= csp_meta_tag %> - <%= stylesheet_link_tag "application.bootstrap", "data-turbo-track": "reload" %> + <%= sentry_meta_tags %> + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> <%= yield(:head) %> <%= Sentry.get_trace_propagation_meta.html_safe %> diff --git a/app/views/users_dashboard/index.html.erb b/app/views/users_dashboard/index.html.erb index 3e15f096..4d7f201c 100644 --- a/app/views/users_dashboard/index.html.erb +++ b/app/views/users_dashboard/index.html.erb @@ -77,7 +77,7 @@
- <%== pagy_bootstrap_nav(@pagy) %> + <%== @pagy.series_nav(:bootstrap) %>
diff --git a/app/views/users_dashboard/show.html.erb b/app/views/users_dashboard/show.html.erb index 42162a92..dd565e95 100644 --- a/app/views/users_dashboard/show.html.erb +++ b/app/views/users_dashboard/show.html.erb @@ -28,6 +28,17 @@
Uniqname
<%= @user.uniqname %>
+ +
Affiliations
+
+ <% if @user.affiliations.any? %> + <% @user.affiliations.each do |affiliation| %> + <%= affiliation.name %> + <% end %> + <% else %> + None + <% end %> +
diff --git a/bin/jobs b/bin/jobs new file mode 100755 index 00000000..dcf59f30 --- /dev/null +++ b/bin/jobs @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby + +require_relative "../config/environment" +require "solid_queue/cli" + +SolidQueue::Cli.start(ARGV) diff --git a/config/application.rb b/config/application.rb index c8ac06d7..dd748614 100644 --- a/config/application.rb +++ b/config/application.rb @@ -25,9 +25,11 @@ module LsaEvaluate class Application < Rails::Application # rubocop:disable Style/Documentation # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.0 - # Add lib to the eager load paths - config.eager_load_paths << Rails.root.join('lib') + config.load_defaults 8.1 + + # Autoload lib/ (ignore non-Ruby dirs). Rack::Defense is also required above for boot. + config.autoload_lib(ignore: %w[assets tasks capistrano]) + config.time_zone = 'Eastern Time (US & Canada)' config.active_record.default_timezone = :utc config.exceptions_app = self.routes @@ -40,21 +42,9 @@ class Application < Rails::Application # rubocop:disable Style/Documentation # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. - # - # config.time_zone = "Central Time (US & Canada)" - # config.eager_load_paths << Rails.root.join("extras") - # Don't generate system test files. config.generators.system_tests = nil # Add security middleware config.middleware.use Rack::Defense - - # Configure rate limiting - config.action_dispatch.rate_limiter = { - limit: 300, - period: 5.minutes, - store: :redis, - key: ->(request) { request.ip } - } end end diff --git a/config/cable.yml b/config/cable.yml index 8acd62fe..8fffefde 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -1,11 +1,25 @@ +# Async adapter only works within the same process, so for manually triggering cable updates from a console, +# and seeing results in the browser, you must do so from the web console (running inside the dev process), +# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view +# to make the web console appear. development: - adapter: redis - url: redis://:<%= Rails.application.credentials.redis_password %>@localhost:6379/1 + adapter: async test: adapter: test +staging: + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day + production: - adapter: redis - url: <%= ENV.fetch("REDIS_URL") { "redis://:#{Rails.application.credentials.redis_password}@localhost:6379/1" } %> - channel_prefix: lsa_evaluate_production + adapter: solid_cable + connects_to: + database: + writing: cable + polling_interval: 0.1.seconds + message_retention: 1.day diff --git a/config/cache.yml b/config/cache.yml new file mode 100644 index 00000000..344c2ccf --- /dev/null +++ b/config/cache.yml @@ -0,0 +1,20 @@ +default: &default + store_options: + # Cap age of oldest cache entry to fulfill retention policies + # max_age: <%= 60.days.to_i %> + max_size: <%= 256.megabytes %> + namespace: <%= Rails.env %> + +development: + <<: *default + +test: + <<: *default + +production: + database: cache + <<: *default + +staging: + database: cache + <<: *default diff --git a/config/database.yml b/config/database.yml index f82e2e44..f2391174 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,4 +1,3 @@ - default: &default adapter: mysql2 encoding: utf8mb4 @@ -9,26 +8,102 @@ default: &default port: 3306 development: - <<: *default - database: lsa_evaluate_development + primary: + <<: *default + database: lsa_evaluate_development + queue: + <<: *default + database: lsa_evaluate_development_queue + migrations_paths: db/queue_migrate + cache: + <<: *default + database: lsa_evaluate_development_cache + migrations_paths: db/cache_migrate + cable: + <<: *default + database: lsa_evaluate_development_cable + migrations_paths: db/cable_migrate development_feature: - <<: *default - database: lsa_evaluate_development_feature + primary: + <<: *default + database: lsa_evaluate_development_feature + queue: + <<: *default + database: lsa_evaluate_development_feature_queue + migrations_paths: db/queue_migrate + cache: + <<: *default + database: lsa_evaluate_development_feature_cache + migrations_paths: db/cache_migrate + cable: + <<: *default + database: lsa_evaluate_development_feature_cable + migrations_paths: db/cable_migrate test: - <<: *default - database: lsa_evaluate_test + primary: + <<: *default + database: lsa_evaluate_test + queue: + <<: *default + database: lsa_evaluate_test_queue + migrations_paths: db/queue_migrate + cache: + <<: *default + database: lsa_evaluate_test_cache + migrations_paths: db/cache_migrate + cable: + <<: *default + database: lsa_evaluate_test_cable + migrations_paths: db/cable_migrate staging: - <<: *default - url: <%= ENV["DATABASE_URL"] %> + primary: + <<: *default + url: <%= ENV["DATABASE_URL"] %> + queue: + <<: *default + url: <%= ENV.fetch("QUEUE_DATABASE_URL") { ENV["DATABASE_URL"] } %> + migrations_paths: db/queue_migrate + cache: + <<: *default + url: <%= ENV.fetch("CACHE_DATABASE_URL") { ENV["DATABASE_URL"] } %> + migrations_paths: db/cache_migrate + cable: + <<: *default + url: <%= ENV.fetch("CABLE_DATABASE_URL") { ENV["DATABASE_URL"] } %> + migrations_paths: db/cable_migrate production: - <<: *default - # url: <%= ENV["DATABASE_URL"] %> - database: evaluate_db_prod - username: <%= Rails.application.credentials.mysql[:prod_user] %> - password: <%= Rails.application.credentials.mysql[:prod_password] %> - host: <%= Rails.application.credentials.mysql[:prod_servername] %> - sslca: <%= Rails.application.credentials.mysql[:prod_sslca] %> + primary: + <<: *default + database: evaluate_db_prod + username: <%= Rails.application.credentials.dig(:mysql, :prod_user) %> + password: <%= Rails.application.credentials.dig(:mysql, :prod_password) %> + host: <%= Rails.application.credentials.dig(:mysql, :prod_servername) %> + sslca: <%= Rails.application.credentials.dig(:mysql, :prod_sslca) %> + queue: + <<: *default + database: evaluate_db_prod_queue + username: <%= Rails.application.credentials.dig(:mysql, :prod_user) %> + password: <%= Rails.application.credentials.dig(:mysql, :prod_password) %> + host: <%= Rails.application.credentials.dig(:mysql, :prod_servername) %> + sslca: <%= Rails.application.credentials.dig(:mysql, :prod_sslca) %> + migrations_paths: db/queue_migrate + cache: + <<: *default + database: evaluate_db_prod_cache + username: <%= Rails.application.credentials.dig(:mysql, :prod_user) %> + password: <%= Rails.application.credentials.dig(:mysql, :prod_password) %> + host: <%= Rails.application.credentials.dig(:mysql, :prod_servername) %> + sslca: <%= Rails.application.credentials.dig(:mysql, :prod_sslca) %> + migrations_paths: db/cache_migrate + cable: + <<: *default + database: evaluate_db_prod_cable + username: <%= Rails.application.credentials.dig(:mysql, :prod_user) %> + password: <%= Rails.application.credentials.dig(:mysql, :prod_password) %> + host: <%= Rails.application.credentials.dig(:mysql, :prod_servername) %> + sslca: <%= Rails.application.credentials.dig(:mysql, :prod_sslca) %> + migrations_paths: db/cable_migrate diff --git a/config/deploy.rb b/config/deploy.rb index e17536a5..4fb00258 100644 --- a/config/deploy.rb +++ b/config/deploy.rb @@ -2,7 +2,6 @@ lock '~> 3.20.0' set :default_env, { - 'NODE_OPTIONS' => '--openssl-legacy-provider', 'PATH' => '$HOME/.asdf/shims:$HOME/.asdf/bin:$PATH', 'SKIP_ECHO_ENV' => 'true', 'SKIP_SEEDS' => 'true' @@ -48,7 +47,7 @@ desc 'Start the PUMA service' task :start do on roles(:app) do - puts 'You must intially start the puma service using sudo on the server' + puts 'You must initially start the puma service using sudo on the server' end end end @@ -78,7 +77,6 @@ before 'bundler:install', 'debug:print_ruby_version' before :starting, :check_revision after :finishing, 'puma:restart' - after :finishing, 'sidekiq:manual_restart' end namespace :debug do @@ -95,13 +93,11 @@ on roles(:app) do within current_path do with rails_env: fetch(:rails_env) do - # Capture the output of each command ruby_version = capture(:ruby, '-v') which_ruby = capture(:which, 'ruby') asdf_ruby_list = capture(:asdf, 'list ruby') rails_version = capture(:bundle, 'exec rails -v') - # Log the captured outputs info "Ruby Version: #{ruby_version.strip}" info "Ruby Path: #{which_ruby.strip}" info "asdf Ruby Versions: #{asdf_ruby_list.strip}" @@ -112,19 +108,6 @@ end end -namespace :sidekiq do - desc 'Manually restart Sidekiq service' - task :manual_restart do - on roles(:app) do - puts "\n\n==========================================================" - puts "IMPORTANT: Sidekiq needs to be restarted manually!" - puts "Please run the following command on the server as a user with sudo privileges:" - puts "sudo systemctl restart sidekiq" - puts "==========================================================\n\n" - end - end -end - namespace :maintenance do desc 'Maintenance start (edit config/maintenance_template.yml to provide parameters)' task :start do diff --git a/config/deploy/templates/sidekiq.service.erb b/config/deploy/templates/sidekiq.service.erb deleted file mode 100644 index cacc0102..00000000 --- a/config/deploy/templates/sidekiq.service.erb +++ /dev/null @@ -1,22 +0,0 @@ -[Unit] -Description=sidekiq -After=syslog.target network.target - -[Service] -Type=simple -WorkingDirectory=<%= current_path %> -ExecStart=/bin/bash -lc 'bundle exec sidekiq -e <%= fetch(:rails_env) %> -C <%= current_path %>/config/sidekiq.yml' -User=<%= fetch(:user) %> -Group=<%= fetch(:user) %> -UMask=0002 -RestartSec=1 -Restart=on-failure -StandardOutput=append:<%= shared_path %>/log/sidekiq.log -StandardError=append:<%= shared_path %>/log/sidekiq.error.log - -# Greatly reduce Ruby memory fragmentation and heap usage -# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/ -Environment=MALLOC_ARENA_MAX=2 - -[Install] -WantedBy=multi-user.target diff --git a/config/environments/development.rb b/config/environments/development.rb index 7715d246..62db8f8e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -8,7 +8,7 @@ # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. - config.cache_classes = false + config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false @@ -70,9 +70,6 @@ # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true - # Suppress logger output for asset requests. - config.assets.quiet = true - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true diff --git a/config/environments/production.rb b/config/environments/production.rb index 92c4f0f3..e69144f1 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -6,7 +6,7 @@ # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. - config.cache_classes = true + config.enable_reloading = false # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers @@ -30,7 +30,7 @@ # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false + # config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" @@ -62,10 +62,11 @@ config.log_tags = [ :request_id ] # Use a different cache store in production. - # config.cache_store = :mem_cache_store + config.cache_store = :solid_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). - config.active_job.queue_adapter = :sidekiq + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } config.active_job.queue_name_prefix = 'lsa_evaluate_production' config.action_mailer.perform_caching = false diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 51ad6747..c210ef76 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -6,7 +6,7 @@ # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. - config.cache_classes = true + config.enable_reloading = false # Eager load code on boot. config.eager_load = true @@ -26,7 +26,7 @@ # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. - config.assets.compile = false + # config.assets.compile = false # Store uploaded files on the local file system. config.active_storage.service = :local @@ -47,9 +47,6 @@ # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = Logger::Formatter.new - # Caching - # config.cache_store = :mem_cache_store - # Action Mailer host = 'https://evaluate-staging.lsa.umich.edu/' config.action_mailer.default_url_options = { host: host } @@ -58,7 +55,7 @@ config.action_mailer.delivery_method = :letter_opener_web config.action_mailer.perform_deliveries = true -# Enable mailer previews (protected by authorization in config/initializers/mailer_previews.rb) + # Enable mailer previews (protected by authorization in config/initializers/mailer_previews.rb) config.action_mailer.show_previews = true # I18n @@ -67,8 +64,10 @@ # Deprecations config.active_support.report_deprecations = false - # Active Job - config.active_job.queue_adapter = :sidekiq + # Use Solid Cache and Solid Queue (no Redis / Sidekiq) + config.cache_store = :solid_cache_store + config.active_job.queue_adapter = :solid_queue + config.solid_queue.connects_to = { database: { writing: :queue } } config.active_job.queue_name_prefix = 'lsa_evaluate_staging' # Do not dump schema after migrations. diff --git a/config/environments/test.rb b/config/environments/test.rb index 56275067..761a60d1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -11,7 +11,7 @@ # Settings specified here will take precedence over those in config/application.rb. # Turn false under Spring and add config.action_view.cache_template_loading = true. - config.cache_classes = true + config.enable_reloading = false # Eager loading loads your whole application. When running a single test locally, # this probably isn't necessary. It's a good idea to do in a continuous integration diff --git a/config/initializers/action_mailer.rb b/config/initializers/action_mailer.rb index 77ee2e8c..e5b37775 100644 --- a/config/initializers/action_mailer.rb +++ b/config/initializers/action_mailer.rb @@ -1,32 +1,19 @@ -# frozen_string_literal: true - -# Configure ActionMailer to deliver emails asynchronously using Sidekiq +# Configure ActionMailer to deliver emails asynchronously via Active Job / Solid Queue Rails.application.config.action_mailer.deliver_later_queue_name = :default # Extend ActionMailer to use mail delivery logging -ActionMailer::Base.class_eval do - # Add delivery tracking method - def notify_delivered - if self.class.delivery_notification_method - send(self.class.delivery_notification_method, message) - end - end - - # Hook into the delivery process - # Remove problematic observer and interceptor - # self.register_observer(-> (message) { message.delivery_method.settings[:return_response] = true }) - # self.register_interceptor(-> (message) { message }) - # Remove problematic observer that causes recursion - # self.register_observer(-> (message) { - # message.deliver_later unless Rails.env.test? - # }) - +ActiveSupport.on_load(:action_mailer) do class << self attr_accessor :delivery_notification_method - # Method to set notification callback def notify_on_delivery(method_name) self.delivery_notification_method = method_name end end + + def notify_delivered + return unless self.class.delivery_notification_method + + send(self.class.delivery_notification_method, message) + end end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 3dc3e275..6e95be16 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,21 +1,4 @@ # frozen_string_literal: true -# Be sure to restart your server when you modify this file. - -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '1.0' - -# Add node_modules to the asset load path for Sprockets -Rails.application.config.assets.paths << Rails.root.join('node_modules') - -# Add additional assets to the asset load path. -Rails.application.config.assets.paths << Rails.root.join('node_modules/bootstrap-icons/font') - -# Adjust precompilation settings -Rails.application.config.assets.precompile -= %w[ actiontext.css actiontext.scss ] -Rails.application.config.assets.precompile += %w[ application.bootstrap.css ] - -# Precompile additional assets. -# application.js, application.bootstrap.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w( admin.js admin.css ) +# Propshaft serves digests from app/assets and app/assets/builds. +# CSS/JS are built by cssbundling-rails and jsbundling-rails into app/assets/builds. diff --git a/config/initializers/letter_opener_sidekiq.rb b/config/initializers/letter_opener_sidekiq.rb deleted file mode 100644 index bc987821..00000000 --- a/config/initializers/letter_opener_sidekiq.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -# Special configuration for letter_opener with Sidekiq in staging -if Rails.env.staging? - require 'letter_opener_web' - - # Ensure delivery_method settings are accessible in Sidekiq threads - Sidekiq.configure_server do |config| - config.client_middleware do |chain| - chain.add(Class.new do - def call(worker_class, job, queue, redis_pool) - if worker_class == 'ActionMailer::MailDeliveryJob' && Rails.application.config.action_mailer.delivery_method == :letter_opener_web - # Ensure ActionMailer's settings are correctly set in the Sidekiq context - ActionMailer::Base.delivery_method = :letter_opener_web - end - yield - end - end) - end - end -end diff --git a/config/initializers/mission_control_jobs.rb b/config/initializers/mission_control_jobs.rb new file mode 100644 index 00000000..ebcb3910 --- /dev/null +++ b/config/initializers/mission_control_jobs.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Devise already protects the /jobs mount; disable Mission Control HTTP basic auth. +MissionControl::Jobs.http_basic_auth_enabled = false diff --git a/config/initializers/pagy.rb b/config/initializers/pagy.rb index 96edce74..16e75dfd 100644 --- a/config/initializers/pagy.rb +++ b/config/initializers/pagy.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true -# Pagy initializer file -# See https://ddnexus.github.io/pagy/api/pagy#configuration +# Pagy 43+ configuration +# See https://ddnexus.github.io/pagy/guides/upgrade-guide/ -require 'pagy/extras/bootstrap' # Add bootstrap extra for styled pagination -require 'pagy/extras/items' # Allow users to change the number of items per page - -Pagy::DEFAULT[:items] = 20 # Default items per page -Pagy::DEFAULT[:max_items] = 100 # Max items per page to prevent abuse +Pagy::OPTIONS[:limit] = 20 +Pagy::OPTIONS[:max_limit] = 100 diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 7e93053b..bb70110a 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -1,121 +1,71 @@ # frozen_string_literal: true +require_relative '../../lib/sentry_release' + Sentry.init do |config| - # Use credentials instead of ENV or hardcoded value config.dsn = Rails.application.credentials.dig(:sentry, :dsn) # Only enable in production and staging environments config.enabled_environments = %w[production staging] - # Environment-specific configuration config.environment = Rails.env - config.release = Rails.application.class.module_parent_name.underscore.dasherize + config.release = SentryRelease.current + config.server_name = Socket.gethostname - # Logging configuration - config.breadcrumbs_logger = [ :active_support_logger, :http_logger ] + config.breadcrumbs_logger = %i[active_support_logger http_logger] - # Enable logging integration (this is the correct way for sentry-rails) - # The sentry-rails gem automatically integrates with Rails logger - # To send logs to Sentry, use: Sentry.capture_message("Your log message", level: :info) + # Keep PII off by default; user id/email are set explicitly in ApplicationController + config.send_default_pii = false - # Add user context data (PII) - be careful with this in production - config.send_default_pii = Rails.env.development? || Rails.env.test? + # Capture all errors — sample only performance data (high-traffic apps still need full error coverage) + config.sample_rate = 1.0 - # Server name for better debugging - config.server_name = Socket.gethostname + config.max_breadcrumbs = 50 - # Performance monitoring - # In production, you might want to lower this to something like 0.1 (10%) - # depending on your traffic volume - config.traces_sample_rate = Rails.env.production? ? 0.1 : 1.0 + # Dynamic trace sampling (takes precedence over traces_sample_rate) + # sentry-rails already excludes RoutingError / UnknownFormat / etc. from error reporting + config.traces_sampler = lambda do |context| + transaction_name = context.dig(:transaction_context, :name).to_s - # Profile sampling - adjust based on your needs - config.profiles_sample_rate = Rails.env.production? ? 0.1 : 1.0 + return 0.0 if transaction_name.include?('health_check') + return 0.0 if transaction_name.match?(/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$/i) - # Custom sampling logic if needed - config.traces_sampler = lambda do |context| - # Don't sample health check endpoints - if context[:transaction_context][:name]&.include?('health_check') - 0.0 - else - # Sample based on environment - Rails.env.production? ? 0.1 : 1.0 - end + # Respect parent sampling decision for distributed traces (browser → Rails) + return context[:parent_sampled] unless context[:parent_sampled].nil? + + Rails.env.production? ? 0.1 : 1.0 end - # Add additional context to errors + # Relative to sampled traces; requires the stackprof gem (already in Gemfile) + config.profiles_sample_rate = Rails.env.production? ? 0.1 : 1.0 + config.before_send = lambda do |event, _hint| - # Add request context - if event.request - event.request.data = { - user_agent: event.request.headers['User-Agent'], - referer: event.request.headers['Referer'], - remote_addr: event.request.headers['X-Forwarded-For'] || event.request.headers['Remote-Addr'] - } - end - - # Add user context if available - if defined?(Current) && Current.user - event.user = { - id: Current.user.id, - email: Current.user.email, - username: Current.user.try(:username) - } - end - - # Add custom tags - event.tags = event.tags.merge( + event.tags = (event.tags || {}).merge( environment: Rails.env, - version: Rails.application.class.module_parent_name.underscore.dasherize + app: 'lsa-evaluate' ) - # Filter out sensitive data - if event.exception - event.exception.values.each do |exception| - next unless exception.value - # Remove potential passwords, tokens, etc. - exception.value.gsub!(/password[=:]\s*[^\s&]+/i, 'password=[FILTERED]') - exception.value.gsub!(/token[=:]\s*[^\s&]+/i, 'token=[FILTERED]') - exception.value.gsub!(/secret[=:]\s*[^\s&]+/i, 'secret=[FILTERED]') - end - end - event end - # Configure backtrace cleanup - config.backtrace_cleanup_callback = lambda do |backtrace| - Rails.backtrace_cleaner.clean(backtrace) - end - - # Configure error filtering config.before_send_transaction = lambda do |event, _hint| - # Filter out health check transactions return nil if event.transaction&.include?('health_check') - - # Filter out static asset requests - return nil if event.transaction&.match?(/\.(css|js|png|jpg|jpeg|gif|ico|svg)$/) + return nil if event.transaction&.match?(/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$/i) event end - # Configure breadcrumb filtering config.before_breadcrumb = lambda do |breadcrumb, _hint| - # Filter out sensitive breadcrumbs return nil if breadcrumb.message&.match?(/password|token|secret/i) - - # Filter out noisy breadcrumbs return nil if breadcrumb.message&.match?(/SELECT.*FROM.*users/i) breadcrumb end - # Configure sampling for different types of events - config.sample_rate = Rails.env.production? ? 0.1 : 1.0 - - # Configure max breadcrumbs - config.max_breadcrumbs = 50 + config.backtrace_cleanup_callback = lambda do |backtrace| + Rails.backtrace_cleaner.clean(backtrace) + end - # Configure debug mode (only in development) - config.debug = Rails.env.development? + # Useful on staging when diagnosing SDK issues (Sentry is disabled in development) + config.debug = Rails.env.staging? end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb deleted file mode 100644 index f1b4755b..00000000 --- a/config/initializers/sidekiq.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'sidekiq' - -# Configure Redis connection -redis_config = { - url: ENV.fetch('REDIS_URL') { "redis://:#{Rails.application.credentials.redis_password}@localhost:6379/1" }, - ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } # Only if using Redis over SSL -} - -Sidekiq.configure_server do |config| - config.redis = redis_config - - # Set default retry count for Sidekiq 7.3.x - # The retry option is now set in the sidekiq.yml file -end - -Sidekiq.configure_client do |config| - config.redis = redis_config -end - -# Set Sidekiq as the ActiveJob queue adapter -Rails.application.config.active_job.queue_adapter = :sidekiq diff --git a/config/puma.rb b/config/puma.rb index 1713441e..d3a2dfe9 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -43,3 +43,7 @@ # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart + +# Run Solid Queue inside Puma for single-host Capistrano deploys. +# Avoid ActiveSupport (e.g. present?) here — Puma loads this file before Rails boots. +plugin :solid_queue if ENV['SOLID_QUEUE_IN_PUMA'] || %w[production staging].include?(ENV.fetch('RAILS_ENV', 'development')) diff --git a/config/queue.yml b/config/queue.yml new file mode 100644 index 00000000..9eace59c --- /dev/null +++ b/config/queue.yml @@ -0,0 +1,18 @@ +default: &default + dispatchers: + - polling_interval: 1 + batch_size: 500 + workers: + - queues: "*" + threads: 3 + processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %> + polling_interval: 0.1 + +development: + <<: *default + +test: + <<: *default + +production: + <<: *default diff --git a/config/recurring.yml b/config/recurring.yml new file mode 100644 index 00000000..a54c4bef --- /dev/null +++ b/config/recurring.yml @@ -0,0 +1,20 @@ +# examples: +# periodic_cleanup: +# class: CleanSoftDeletedRecordsJob +# queue: background +# args: [ 1000, { batch_size: 500 } ] +# schedule: every hour +# periodic_cleanup_with_command: +# command: "SoftDeletedRecord.due.delete_all" +# priority: 2 +# schedule: at 5am every day + +production: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 + +staging: + clear_solid_queue_finished_jobs: + command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)" + schedule: every hour at minute 12 diff --git a/config/routes.rb b/config/routes.rb index db7af590..647d1c10 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,10 +6,9 @@ get 'bulk_contest_instances/create' root 'static_pages#home' - # Sidekiq Web UI - require 'sidekiq/web' + # Mission Control Jobs UI (Solid Queue) authenticate :user, ->(user) { user.axis_mundi? } do - mount Sidekiq::Web => '/sidekiq' + mount MissionControl::Jobs::Engine, at: '/jobs' end resources :entries do diff --git a/config/sidekiq.yml b/config/sidekiq.yml deleted file mode 100644 index 3c3ec642..00000000 --- a/config/sidekiq.yml +++ /dev/null @@ -1,29 +0,0 @@ ---- -# Default configuration -:concurrency: 5 -:retry: 3 - -# Sidekiq will run this file through ERB when loading it -:queues: - - [critical, 3] - - [default, 2] - - [<%= ENV['RAILS_ENV'] || 'development' %>, 2] - - [<%= ENV['RAILS_ENV'] || 'development' %>_mailers, 2] - - [lsa_evaluate_<%= ENV['RAILS_ENV'] || 'development' %>_default, 2] - - [lsa_evaluate_<%= ENV['RAILS_ENV'] || 'development' %>_mailers, 2] - - [low, 1] - -# Environment-specific configurations -development: - :concurrency: 5 - -test: - :concurrency: 1 - -staging: - :concurrency: 10 - :retry: 5 - -production: - :concurrency: 20 - :retry: 10 diff --git a/db/cable_schema.rb b/db/cable_schema.rb new file mode 100644 index 00000000..145052dd --- /dev/null +++ b/db/cable_schema.rb @@ -0,0 +1,23 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 1) do + create_table "solid_cable_messages", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.binary "channel", limit: 1024, null: false + t.bigint "channel_hash", null: false + t.datetime "created_at", null: false + t.binary "payload", size: :long, null: false + t.index ["channel"], name: "index_solid_cable_messages_on_channel" + t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" + t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" + end +end diff --git a/db/cache_schema.rb b/db/cache_schema.rb new file mode 100644 index 00000000..c3f885ac --- /dev/null +++ b/db/cache_schema.rb @@ -0,0 +1,24 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 1) do + create_table "solid_cache_entries", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.integer "byte_size", null: false + t.datetime "created_at", null: false + t.binary "key", limit: 1024, null: false + t.bigint "key_hash", null: false + t.binary "value", size: :long, null: false + t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size" + t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size" + t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true + end +end diff --git a/db/queue_schema.rb b/db/queue_schema.rb new file mode 100644 index 00000000..73eef229 --- /dev/null +++ b/db/queue_schema.rb @@ -0,0 +1,138 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 1) do + create_table "solid_queue_blocked_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.string "concurrency_key", null: false + t.datetime "created_at", null: false + t.datetime "expires_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release" + t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.bigint "process_id" + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "error" + t.bigint "job_id", null: false + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.string "active_job_id" + t.text "arguments" + t.string "class_name", null: false + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "finished_at" + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.datetime "scheduled_at" + t.datetime "updated_at", null: false + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering" + t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "queue_name", null: false + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "hostname" + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.text "metadata" + t.string "name", null: false + t.integer "pid", null: false + t.bigint "supervisor_id" + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index ["priority", "job_id"], name: "index_solid_queue_poll_all" + t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.datetime "run_at", null: false + t.string "task_key", null: false + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.text "arguments" + t.string "class_name" + t.string "command", limit: 2048 + t.datetime "created_at", null: false + t.text "description" + t.string "key", null: false + t.integer "priority", default: 0 + t.string "queue_name" + t.string "schedule", null: false + t.boolean "static", default: true, null: false + t.datetime "updated_at", null: false + t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.datetime "scheduled_at", null: false + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "expires_at", null: false + t.string "key", null: false + t.datetime "updated_at", null: false + t.integer "value", default: 1, null: false + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true + end + + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade +end diff --git a/db/schema.rb b/db/schema.rb index 7b6ddcba..de7e0f1f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,36 +10,36 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_01_13_165213) do +ActiveRecord::Schema[8.1].define(version: 2026_01_13_165213) do create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "name", null: false t.text "body", size: :long - t.string "record_type", null: false - t.bigint "record_id", null: false t.datetime "created_at", null: false + t.string "name", null: false + t.bigint "record_id", null: false + t.string "record_type", null: false t.datetime "updated_at", null: false t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true end create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "name", null: false - t.string "record_type", null: false - t.bigint "record_id", null: false t.bigint "blob_id", null: false t.datetime "created_at", null: false + t.string "name", null: false + t.bigint "record_id", null: false + t.string "record_type", null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end create_table "active_storage_blobs", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "key", null: false - t.string "filename", null: false - t.string "content_type" - t.text "metadata" - t.string "service_name", null: false t.bigint "byte_size", null: false t.string "checksum" + t.string "content_type" t.datetime "created_at", null: false + t.string "filename", null: false + t.string "key", null: false + t.text "metadata" + t.string "service_name", null: false t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end @@ -50,19 +50,19 @@ end create_table "affiliations", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "name" - t.bigint "user_id", null: false t.datetime "created_at", null: false + t.string "name" t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["user_id"], name: "index_affiliations_on_user_id" end create_table "assignments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.bigint "user_id", null: false t.bigint "container_id", null: false - t.bigint "role_id", null: false t.datetime "created_at", null: false + t.bigint "role_id", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["container_id"], name: "index_assignments_on_container_id" t.index ["role_id", "user_id", "container_id"], name: "index_assignments_on_role_user_container", unique: true t.index ["role_id"], name: "index_assignments_on_role_id" @@ -71,8 +71,8 @@ end create_table "campuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "campus_descr", null: false t.integer "campus_cd", null: false + t.string "campus_descr", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["campus_cd"], name: "campus_cd_unq_idx", unique: true @@ -82,9 +82,9 @@ end create_table "categories", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "kind" - t.text "description" t.datetime "created_at", null: false + t.text "description" + t.string "kind" t.datetime "updated_at", null: false t.index ["kind"], name: "index_categories_on_kind", unique: true end @@ -100,8 +100,8 @@ end create_table "class_level_requirements", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.bigint "contest_instance_id", null: false t.bigint "class_level_id", null: false + t.bigint "contest_instance_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["class_level_id"], name: "index_class_level_requirements_on_class_level_id" @@ -110,89 +110,89 @@ end create_table "class_levels", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "name", null: false - t.text "description", null: false t.datetime "created_at", null: false + t.text "description", null: false + t.string "name", null: false t.datetime "updated_at", null: false end create_table "containers", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.string "contact_email" + t.datetime "created_at", null: false + t.bigint "department_id", null: false t.string "name" t.text "notes" - t.bigint "department_id", null: false - t.bigint "visibility_id", null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "contact_email" + t.bigint "visibility_id", null: false t.index ["department_id"], name: "index_containers_on_department_id" t.index ["visibility_id"], name: "index_containers_on_visibility_id" end create_table "contest_descriptions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.boolean "active", default: false, null: false + t.boolean "archived", default: false, null: false t.bigint "container_id", null: false + t.datetime "created_at", null: false + t.string "created_by", null: false t.string "name", null: false t.string "short_name" - t.string "created_by", null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.boolean "active", default: false, null: false - t.boolean "archived", default: false, null: false t.index ["container_id"], name: "index_contest_descriptions_on_container_id" end create_table "contest_instances", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.boolean "active", default: false, null: false + t.boolean "archived", default: false, null: false t.bigint "contest_description_id", null: false - t.datetime "date_open", null: false + t.text "course_requirement_description" + t.datetime "created_at", null: false + t.string "created_by" t.datetime "date_closed", null: false - t.text "notes" + t.datetime "date_open", null: false t.boolean "has_course_requirement", default: false, null: false - t.text "course_requirement_description" + t.integer "maximum_number_entries_per_applicant", default: 1, null: false + t.text "notes" t.boolean "recletter_required", default: false, null: false + t.boolean "require_campus_employment_info", default: false, null: false + t.boolean "require_finaid_info", default: false, null: false + t.boolean "require_pen_name", default: false, null: false t.boolean "transcript_required", default: false, null: false - t.integer "maximum_number_entries_per_applicant", default: 1, null: false - t.string "created_by" - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.boolean "active", default: false, null: false - t.boolean "archived", default: false, null: false - t.boolean "require_pen_name", default: false, null: false - t.boolean "require_finaid_info", default: false, null: false - t.boolean "require_campus_employment_info", default: false, null: false t.index ["contest_description_id"], name: "contest_description_id_idx" t.index ["contest_description_id"], name: "index_contest_instances_on_contest_description_id" t.index ["id"], name: "id_unq_idx", unique: true end create_table "departments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.integer "dept_id", null: false - t.text "dept_description" t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.text "dept_description" + t.integer "dept_id", null: false t.string "name" + t.datetime "updated_at", null: false end create_table "editable_contents", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false t.string "page", null: false t.string "section", null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["page", "section"], name: "index_editable_contents_on_page_and_section", unique: true end create_table "entries", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "title", null: false - t.bigint "contest_instance_id", null: false - t.bigint "profile_id", null: false + t.boolean "accepted_financial_aid_notice", default: false, null: false + t.boolean "campus_employee", default: false, null: false t.bigint "category_id", null: false + t.bigint "contest_instance_id", null: false t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "disqualified", default: false, null: false t.boolean "deleted", default: false, null: false + t.boolean "disqualified", default: false, null: false + t.text "financial_aid_description" t.string "pen_name" - t.boolean "campus_employee", default: false, null: false - t.boolean "accepted_financial_aid_notice", default: false, null: false + t.bigint "profile_id", null: false t.boolean "receiving_financial_aid", default: false, null: false - t.text "financial_aid_description" + t.string "title", null: false + t.datetime "updated_at", null: false t.index ["category_id"], name: "category_id_idx" t.index ["category_id"], name: "index_entries_on_category_id" t.index ["contest_instance_id"], name: "contest_instance_id_idx" @@ -203,17 +203,17 @@ end create_table "entry_rankings", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false t.bigint "entry_id", null: false + t.text "external_comments" + t.boolean "finalized", default: false, null: false + t.text "internal_comments" t.bigint "judging_round_id", null: false - t.bigint "user_id", null: false - t.integer "rank" t.text "notes" + t.integer "rank" t.boolean "selected_for_next_round", default: false, null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.text "internal_comments" - t.text "external_comments" - t.boolean "finalized", default: false, null: false + t.bigint "user_id", null: false t.index ["entry_id", "judging_round_id", "user_id"], name: "index_entry_rankings_uniqueness", unique: true t.index ["entry_id"], name: "index_entry_rankings_on_entry_id" t.index ["judging_round_id"], name: "index_entry_rankings_on_judging_round_id" @@ -221,58 +221,58 @@ end create_table "judging_assignments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.bigint "user_id", null: false - t.bigint "contest_instance_id", null: false t.boolean "active", default: true, null: false + t.bigint "contest_instance_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["contest_instance_id"], name: "index_judging_assignments_on_contest_instance_id" t.index ["user_id", "contest_instance_id"], name: "index_judging_assignments_on_user_id_and_contest_instance_id", unique: true t.index ["user_id"], name: "index_judging_assignments_on_user_id" end create_table "judging_rounds", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.bigint "contest_instance_id", null: false - t.integer "round_number", null: false - t.datetime "start_date" - t.datetime "end_date" t.boolean "active", default: false, null: false t.boolean "completed", default: false, null: false + t.bigint "contest_instance_id", null: false t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "require_internal_comments", default: false, null: false - t.boolean "require_external_comments", default: false, null: false - t.integer "min_internal_comment_words", default: 0, null: false - t.integer "min_external_comment_words", default: 0, null: false - t.text "special_instructions" - t.integer "required_entries_count", default: 0, null: false t.integer "emails_sent_count", default: 0, null: false - t.boolean "include_average_ranking", default: false + t.datetime "end_date" t.boolean "include_advancement_status", default: false + t.boolean "include_average_ranking", default: false + t.integer "min_external_comment_words", default: 0, null: false + t.integer "min_internal_comment_words", default: 0, null: false + t.boolean "require_external_comments", default: false, null: false + t.boolean "require_internal_comments", default: false, null: false + t.integer "required_entries_count", default: 0, null: false + t.integer "round_number", null: false + t.text "special_instructions" + t.datetime "start_date" + t.datetime "updated_at", null: false t.index ["contest_instance_id", "round_number"], name: "index_judging_rounds_on_contest_instance_id_and_round_number", unique: true t.index ["contest_instance_id"], name: "index_judging_rounds_on_contest_instance_id" end create_table "profiles", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.bigint "user_id", null: false - t.string "preferred_first_name", default: "", null: false - t.string "preferred_last_name", default: "", null: false - t.bigint "class_level_id" - t.bigint "school_id" + t.boolean "accepted_financial_aid_notice", default: false, null: false + t.boolean "campus_employee", default: false, null: false t.bigint "campus_id" - t.string "major" - t.date "grad_date", null: false + t.bigint "class_level_id" + t.datetime "created_at", null: false t.string "degree", null: false - t.boolean "receiving_financial_aid", default: false, null: false - t.boolean "accepted_financial_aid_notice", default: false, null: false + t.string "department" t.text "financial_aid_description" + t.date "grad_date", null: false t.string "hometown_publication" + t.string "major" t.string "pen_name" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "campus_employee", default: false, null: false - t.string "department" + t.string "preferred_first_name", default: "", null: false + t.string "preferred_last_name", default: "", null: false + t.boolean "receiving_financial_aid", default: false, null: false + t.bigint "school_id" t.string "umid" + t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["campus_id"], name: "campus_id_idx" t.index ["campus_id"], name: "index_profiles_on_campus_id" t.index ["class_level_id"], name: "class_level_id_idx" @@ -286,73 +286,73 @@ end create_table "roles", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "kind" - t.text "description" t.datetime "created_at", null: false + t.text "description" + t.string "kind" t.datetime "updated_at", null: false end create_table "round_judge_assignments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.bigint "judging_round_id", null: false - t.bigint "user_id", null: false t.boolean "active", default: true t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.datetime "instructions_sent_at" + t.bigint "judging_round_id", null: false + t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["judging_round_id", "user_id"], name: "index_round_judge_assignments_on_judging_round_id_and_user_id", unique: true t.index ["judging_round_id"], name: "index_round_judge_assignments_on_judging_round_id" t.index ["user_id"], name: "index_round_judge_assignments_on_user_id" end create_table "schools", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "name" t.datetime "created_at", null: false + t.string "name" t.datetime "updated_at", null: false t.index ["id"], name: "id_unq_idx", unique: true end create_table "user_roles", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.bigint "user_id", null: false - t.bigint "role_id", null: false t.datetime "created_at", null: false + t.bigint "role_id", null: false t.datetime "updated_at", null: false + t.bigint "user_id", null: false t.index ["role_id"], name: "index_user_roles_on_role_id" t.index ["user_id", "role_id"], name: "index_user_roles_on_user_id_and_role_id", unique: true t.index ["user_id"], name: "index_user_roles_on_user_id" end create_table "users", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "current_sign_in_at" + t.string "current_sign_in_ip" + t.string "display_name" t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false - t.datetime "current_sign_in_at" + t.integer "failed_attempts", default: 0, null: false + t.string "first_name", default: "", null: false + t.string "last_name", default: "", null: false t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" t.string "last_sign_in_ip" - t.integer "failed_attempts", default: 0, null: false - t.string "unlock_token" t.datetime "locked_at" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.string "uniqname" + t.string "principal_name" t.string "provider" + t.datetime "remember_created_at" + t.datetime "reset_password_sent_at" + t.string "reset_password_token" + t.integer "sign_in_count", default: 0, null: false t.string "uid" - t.string "principal_name" - t.string "display_name" - t.string "first_name", default: "", null: false - t.string "last_name", default: "", null: false + t.string "uniqname" + t.string "unlock_token" + t.datetime "updated_at", null: false t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true end create_table "visibilities", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t| - t.string "kind", null: false - t.text "description" t.datetime "created_at", null: false + t.text "description" + t.string "kind", null: false t.datetime "updated_at", null: false end diff --git a/lib/capistrano/tasks/redis.rake b/lib/capistrano/tasks/redis.rake deleted file mode 100644 index 76f6d15e..00000000 --- a/lib/capistrano/tasks/redis.rake +++ /dev/null @@ -1,24 +0,0 @@ -namespace :redis do - desc 'Check if Redis is installed' - task :check do - on roles(:app) do - unless test('which redis-server') - error 'Redis is not installed. Please install Redis before deploying.' - exit 1 - end - end - end - - desc 'Install Redis' - task :install do - on roles(:app) do - execute :sudo, 'apt-get update' - execute :sudo, 'apt-get install -y redis-server' - execute :sudo, 'systemctl enable redis-server' - execute :sudo, 'systemctl start redis-server' - end - end -end - -# Hook into the deployment flow -before 'deploy:check', 'redis:check' diff --git a/lib/capistrano/tasks/sentry.rake b/lib/capistrano/tasks/sentry.rake new file mode 100644 index 00000000..f7591143 --- /dev/null +++ b/lib/capistrano/tasks/sentry.rake @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +namespace :sentry do + desc 'Write REVISION file so Rails can report a deploy-specific Sentry release' + task :write_revision do + on roles(:app) do + revision = fetch(:current_revision) + upload! StringIO.new(revision), "#{release_path}/REVISION" + end + end + + desc 'Notify Sentry of a new deploy (requires SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT)' + task :notify_deploy do + auth_token = ENV['SENTRY_AUTH_TOKEN'] + org = ENV['SENTRY_ORG'] + project = ENV['SENTRY_PROJECT'] + next unless auth_token.present? && org.present? && project.present? + + revision = fetch(:current_revision) + environment = fetch(:rails_env, 'production') + + run_locally do + execute :curl, '-sS', '-X', 'POST', + "https://sentry.io/api/0/organizations/#{org}/releases/", + '-H', "Authorization: Bearer #{auth_token}", + '-H', 'Content-Type: application/json', + '-d', "'{\"version\":\"#{revision}\",\"projects\":[\"#{project}\"]}'" + + execute :curl, '-sS', '-X', 'POST', + "https://sentry.io/api/0/organizations/#{org}/releases/#{revision}/deploys/", + '-H', "Authorization: Bearer #{auth_token}", + '-H', 'Content-Type: application/json', + '-d', "'{\"environment\":\"#{environment}\"}'" + end + end +end + +after 'deploy:updated', 'sentry:write_revision' +after 'deploy:finished', 'sentry:notify_deploy' diff --git a/lib/sentry_release.rb b/lib/sentry_release.rb new file mode 100644 index 00000000..84a4ee0d --- /dev/null +++ b/lib/sentry_release.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Resolves a deploy-specific release id for Sentry (git SHA preferred). +module SentryRelease + module_function + + def current + ENV['SENTRY_RELEASE'].presence || + ENV['HATCHBOX_REVISION'].presence || + revision_file.presence || + git_sha.presence + end + + def revision_file + path = Rails.root.join('REVISION') + return unless path.file? + + path.read.strip.presence + end + private_class_method :revision_file + + def git_sha + sha = `git rev-parse HEAD 2>/dev/null`.strip + sha.presence + rescue StandardError + nil + end + private_class_method :git_sha +end diff --git a/lib/sentry_test_helper.rb b/lib/sentry_test_helper.rb index f98df42b..0fdae0d0 100644 --- a/lib/sentry_test_helper.rb +++ b/lib/sentry_test_helper.rb @@ -6,7 +6,6 @@ # SentryTestHelper.verify_config # SentryTestHelper.test_message # SentryTestHelper.test_exception -# SentryTestHelper.test_with_sampling # SentryTestHelper.full_test module SentryTestHelper class << self @@ -18,82 +17,42 @@ def verify_config puts "Is enabled: #{Sentry.configuration.enabled_environments.include?(Rails.env)}" puts "DSN configured: #{Sentry.configuration.dsn.present?}" puts "DSN (masked): #{mask_dsn(Sentry.configuration.dsn)}" if Sentry.configuration.dsn - puts "Sample rate: #{Sentry.configuration.sample_rate}" - puts "Traces sample rate: #{Sentry.configuration.traces_sample_rate}" + puts "Error sample rate: #{Sentry.configuration.sample_rate}" + puts "Traces sampler configured: #{!Sentry.configuration.traces_sampler.nil?}" + puts "Profiles sample rate: #{Sentry.configuration.profiles_sample_rate}" puts "Release: #{Sentry.configuration.release}" puts "Environment: #{Sentry.configuration.environment}" puts "==========================\n" end - # Send a test message that bypasses sampling by sending to a transaction - # This increases the likelihood of delivery + # Send a test message def test_message(message = "Test message from #{Rails.env} at #{Time.current}") puts "\n=== Testing Sentry Message ===" puts "Sending message: #{message}" - # Temporarily override sample rate - original_sample_rate = Sentry.configuration.sample_rate - Sentry.configuration.sample_rate = 1.0 - - begin - event_id = Sentry.capture_message(message, level: :info) - puts "Event ID: #{event_id}" - puts "Status: #{event_id ? 'Sent successfully!' : 'Failed to send (returned nil)'}" - puts "Note: Check Sentry dashboard in a few moments" - ensure - # Restore original sample rate - Sentry.configuration.sample_rate = original_sample_rate - end - + event_id = Sentry.capture_message(message, level: :info) + puts "Event ID: #{event_id}" + puts "Status: #{event_id ? 'Sent successfully!' : 'Failed to send (returned nil)'}" + puts "Note: Check Sentry dashboard in a few moments" puts "===========================\n" event_id end - # Send a test exception that bypasses sampling + # Send a test exception def test_exception puts "\n=== Testing Sentry Exception ===" - # Temporarily override sample rate - original_sample_rate = Sentry.configuration.sample_rate - Sentry.configuration.sample_rate = 1.0 - begin - begin - raise StandardError, "Test exception from #{Rails.env} at #{Time.current}" - rescue StandardError => e - event_id = Sentry.capture_exception(e) - puts "Exception raised and captured" - puts "Event ID: #{event_id}" - puts "Status: #{event_id ? 'Sent successfully!' : 'Failed to send (returned nil)'}" - puts "Note: Check Sentry dashboard in a few moments" - event_id - end - ensure - # Restore original sample rate - Sentry.configuration.sample_rate = original_sample_rate - end - - puts "=============================\n" - end - - # Send multiple messages to test sampling (some should get through) - def test_with_sampling(count = 20) - puts "\n=== Testing with Normal Sampling ===" - puts "Sending #{count} messages with #{(Sentry.configuration.sample_rate * 100).to_i}% sample rate" - puts "Expected to see approximately #{(count * Sentry.configuration.sample_rate).round} events" - - sent_ids = [] - count.times do |i| - event_id = Sentry.capture_message("Sampled test message #{i + 1}/#{count} at #{Time.current}") - sent_ids << event_id if event_id - print "." + raise StandardError, "Test exception from #{Rails.env} at #{Time.current}" + rescue StandardError => e + event_id = Sentry.capture_exception(e) + puts "Exception raised and captured" + puts "Event ID: #{event_id}" + puts "Status: #{event_id ? 'Sent successfully!' : 'Failed to send (returned nil)'}" + puts "Note: Check Sentry dashboard in a few moments" + puts "=============================\n" + event_id end - - puts "\n" - puts "Event IDs returned: #{sent_ids.count}/#{count}" - puts "Note: Check Sentry dashboard in a few moments" - puts "==================================\n" - sent_ids end # Full diagnostic test @@ -111,9 +70,7 @@ def full_test def mask_dsn(dsn) return 'Not configured' if dsn.blank? - # Mask the secret part of the DSN - # DSN format: https://public_key@sentry.io/project_id - dsn.to_s.gsub(/(?<=https:\/\/)([^@]+)(?=@)/) { |match| "*" * match.length } + dsn.to_s.gsub(%r{(?<=https://)([^@]+)(?=@)}) { |match| '*' * match.length } rescue StandardError 'Error masking DSN' end diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake deleted file mode 100644 index e96283ea..00000000 --- a/lib/tasks/auto_annotate_models.rake +++ /dev/null @@ -1,59 +0,0 @@ -# NOTE: only doing this in development as some production environments (Heroku) -# NOTE: are sensitive to local FS writes, and besides -- it's just not proper -# NOTE: to have a dev-mode tool do its thing in production. -if Rails.env.development? - require 'annotate' - task :set_annotation_options do - # You can override any of these by setting an environment variable of the - # same name. - Annotate.set_defaults( - 'active_admin' => 'false', - 'additional_file_patterns' => [], - 'routes' => 'false', - 'models' => 'true', - 'position_in_routes' => 'before', - 'position_in_class' => 'before', - 'position_in_test' => 'before', - 'position_in_fixture' => 'before', - 'position_in_factory' => 'before', - 'position_in_serializer' => 'before', - 'show_foreign_keys' => 'true', - 'show_complete_foreign_keys' => 'false', - 'show_indexes' => 'true', - 'simple_indexes' => 'false', - 'model_dir' => 'app/models', - 'root_dir' => '', - 'include_version' => 'false', - 'require' => '', - 'exclude_tests' => 'false', - 'exclude_fixtures' => 'false', - 'exclude_factories' => 'false', - 'exclude_serializers' => 'false', - 'exclude_scaffolds' => 'true', - 'exclude_controllers' => 'true', - 'exclude_helpers' => 'true', - 'exclude_sti_subclasses' => 'false', - 'ignore_model_sub_dir' => 'false', - 'ignore_columns' => nil, - 'ignore_routes' => nil, - 'ignore_unknown_models' => 'false', - 'hide_limit_column_types' => 'integer,bigint,boolean', - 'hide_default_column_types' => 'json,jsonb,hstore', - 'skip_on_db_migrate' => 'false', - 'format_bare' => 'true', - 'format_rdoc' => 'false', - 'format_yard' => 'false', - 'format_markdown' => 'false', - 'sort' => 'false', - 'force' => 'false', - 'frozen' => 'false', - 'classified_sort' => 'true', - 'trace' => 'false', - 'wrapper_open' => nil, - 'wrapper_close' => nil, - 'with_comment' => 'true' - ) - end - - Annotate.load_tasks -end diff --git a/lib/tasks/email.rake b/lib/tasks/email.rake index b82bcbd9..b6edefd5 100644 --- a/lib/tasks/email.rake +++ b/lib/tasks/email.rake @@ -1,45 +1,30 @@ namespace :email do - # You can choose whether to queue the email through Sidekiq or send it directly: - # For direct delivery: + # Direct delivery: # bin/rails email:test[your@email.com] # - # To queue through Sidekiq (add true as a second argument): + # Queue via Active Job / Solid Queue: # bin/rails email:test[your@email.com,true] desc 'Send a test email to verify configuration' task :test, [ :email, :queue ] => :environment do |_t, args| recipient = args[:email] || Rails.application.credentials.dig(:sendgrid, :mailer_sender) - - # Check if Sidekiq is available - sidekiq_available = defined?(Sidekiq) == 'constant' && Sidekiq.class == Module - queue_delivery_requested = args[:queue]&.downcase == 'true' - - # Set queue_delivery to false if Sidekiq is not available - queue_delivery = queue_delivery_requested && sidekiq_available - - # Show a warning if user requested Sidekiq but it's not available - if queue_delivery_requested && !sidekiq_available - puts "WARNING: Sidekiq is not available. Email will be sent directly." - end + queue_delivery = args[:queue]&.downcase == 'true' if recipient.nil? puts 'ERROR: No recipient email provided and no default contact email configured.' - puts 'Usage: rake email:test[recipient@example.com] or rake email:test[recipient@example.com,true] to queue via Sidekiq' + puts 'Usage: rake email:test[recipient@example.com] or rake email:test[recipient@email.com,true] to queue via Active Job' exit 1 end puts "Sending test email to #{recipient}..." begin if queue_delivery - # For queued delivery, don't inspect the message before sending - puts 'Queuing email delivery through Sidekiq...' + puts "Queuing email delivery through Active Job (#{ActiveJob::Base.queue_adapter.class.name})..." TestMailer.test_email(recipient).deliver_later - puts 'Email successfully queued in Sidekiq!' + puts 'Email successfully queued!' else - # For direct delivery, we can inspect the message mail = TestMailer.test_email(recipient) - # Print message details before sending puts 'Message details:' puts " From: #{mail.from.first || 'Not set'}" puts " Reply-To: #{mail.reply_to&.first || 'Not set'}" @@ -50,7 +35,7 @@ namespace :email do end mail.deliver_now - puts 'Test email sent directly (bypassing Sidekiq)!' + puts 'Test email sent directly!' end rescue => e puts "ERROR: Failed to send test email: #{e.message}" diff --git a/package.json b/package.json index 118a6ea2..3f5406cf 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^8.0.10", "@popperjs/core": "^2.11.8", - "@rails/actiontext": "^7.0.8-3", + "@rails/actiontext": "^8.0.200", + "@sentry/browser": "^10.65.0", "autoprefixer": "^10.4.17", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", @@ -22,7 +23,8 @@ "build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets", "build:css:compile": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules", "build:css:prefix": "postcss ./app/assets/builds/application.css --use=autoprefixer --output=./app/assets/builds/application.css", - "build:css": "yarn build:css:compile && yarn build:css:prefix", + "build:css:copy-fonts": "mkdir -p app/assets/builds/fonts && cp -f node_modules/bootstrap-icons/font/fonts/* app/assets/builds/fonts/", + "build:css": "yarn build:css:compile && yarn build:css:prefix && yarn build:css:copy-fonts", "watch:css": "nodemon --watch ./app/assets/stylesheets/ --ext scss --exec \"yarn build:css\"", "test": "jest", "test:watch": "jest --watch" diff --git a/spec/middleware/rack_defense_spec.rb b/spec/middleware/rack_defense_spec.rb index 9af189d9..a2c41988 100644 --- a/spec/middleware/rack_defense_spec.rb +++ b/spec/middleware/rack_defense_spec.rb @@ -93,7 +93,7 @@ def call_with(path:, method: 'GET', headers: {}, input: nil) expect(env['rack.input']).to equal(original_input) end - it 'does not desync Rack POST cache from rack.input' do + it 'does not prevent Rack from parsing POST params after body inspection' do inspecting_app = ->(_env) { [200, {}, ['OK']] } env = Rack::MockRequest.env_for( '/entries', @@ -107,7 +107,7 @@ def call_with(path:, method: 'GET', headers: {}, input: nil) expect(status).to eq(200) request = Rack::Request.new(env) expect(request.POST).to eq('name' => 'test', 'value' => '1') - expect(env[Rack::RACK_REQUEST_FORM_INPUT]).to equal(env['rack.input']) + expect(env[Rack::RACK_REQUEST_FORM_HASH]).to eq('name' => 'test', 'value' => '1') end end diff --git a/yarn.lock b/yarn.lock index 04f71ae5..1a0c3335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1378,20 +1378,75 @@ resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.1.3.tgz#4db480347775aeecd4dde2405659eef74a458881" integrity sha512-ojNvnoZtPN0pYvVFtlO7dyEN9Oml1B6IDM+whGKVak69MMYW99lC2NOWXWeE3bmwEydbP/nn6ERcpfjHVjYQjA== -"@rails/actiontext@^7.0.8-3": - version "7.0.8-3" - resolved "https://registry.yarnpkg.com/@rails/actiontext/-/actiontext-7.0.8-3.tgz#b65765527aaa6b54f30f817713b15b1c1aad96dd" - integrity sha512-0+dCzCceGWKdScEGHU6tV5HuBVg9uIlBTB0XrR8QN/B8v2m7l0otbQ7CYV0+QllpDY89Y1voBhL/taJEzMk8WA== +"@rails/actiontext@^8.0.200": + version "8.1.300" + resolved "https://registry.yarnpkg.com/@rails/actiontext/-/actiontext-8.1.300.tgz#0b6d0a40e8ca8ba2caf5a0077e10b5503183a044" + integrity sha512-KPbLfZTt6aFKbdeTlU7ZzmX0uSSNhhfZx3G+2lfa6HltQ/r23HDcJnJOEVt72WKJo13IS5C5HK0sYwplVMCdNw== dependencies: - "@rails/activestorage" ">= 7.0.0-alpha1" + "@rails/activestorage" ">= 8.1.0-alpha" -"@rails/activestorage@>= 7.0.0-alpha1": - version "7.1.3" - resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-7.1.3.tgz#e83ece6c5fd94b3ddf30a8cf3b8f78cad049e596" - integrity sha512-B+RFYAU8vdTPFg0IJcRp2ey0Qw9hpcUOqHHcWqftDJ76ZMBi9+m/UUeMJlNsSd0l9eD+1HLlFSo1X//cY4yiDw== +"@rails/activestorage@>= 8.1.0-alpha": + version "8.1.300" + resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-8.1.300.tgz#624a6f291e0414c07c3d02ed3d1e6ee85c94277b" + integrity sha512-AOv3zZrTJjQlzm4L9uzOQtCz6zaM4IqUyux6yzSqmS+PZ8EzwD1F2JAc4LlJNJAv4MSyNYriG+CaCm1QbQTjsA== dependencies: spark-md5 "^3.0.1" +"@sentry/browser-utils@10.65.0": + version "10.65.0" + resolved "https://registry.yarnpkg.com/@sentry/browser-utils/-/browser-utils-10.65.0.tgz#e3b67c6b9dd05dad2ad7eee82ad873bfc9f8ab12" + integrity sha512-4J0mkfNJAGUOkpg1ZggizyftFTn9N20b+Jl87UnWsDUkNG0Ic1l/FIzMPTVxXrAnhBGu0ULO0TFWMoQ5s3QtZw== + dependencies: + "@sentry/conventions" "^0.15.1" + "@sentry/core" "10.65.0" + +"@sentry/browser@^10.65.0": + version "10.65.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.65.0.tgz#e2734a2effe051a9005afdd914fc07c68262f627" + integrity sha512-XUDDsx0qxzeIlcOu1fDEqTcDl0eiOqghsgV+ReuuNP4jYjZ9kUQxE3rXWM5mlT1pBi4VaQ4FHqvQZZrRXy+oDw== + dependencies: + "@sentry/browser-utils" "10.65.0" + "@sentry/conventions" "^0.15.1" + "@sentry/core" "10.65.0" + "@sentry/feedback" "10.65.0" + "@sentry/replay" "10.65.0" + "@sentry/replay-canvas" "10.65.0" + +"@sentry/conventions@^0.15.1": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@sentry/conventions/-/conventions-0.15.1.tgz#5b8888862d1e444678938f5afdd7842bca42f7cc" + integrity sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA== + +"@sentry/core@10.65.0": + version "10.65.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.65.0.tgz#a98cfd6e138d9658dd42d050902837ff591452e8" + integrity sha512-3aqtmM5NgNGo45BNaaBzi0LPQZAw//NEL4HKS5fXm12pJMa4KEkze8DEKnkTEIrGnWaOJKamecHKlnNg/Mqf/Q== + dependencies: + "@sentry/conventions" "^0.15.1" + +"@sentry/feedback@10.65.0": + version "10.65.0" + resolved "https://registry.yarnpkg.com/@sentry/feedback/-/feedback-10.65.0.tgz#a9418cc77c835e3a74cdc3fad2fb1098ecbc3887" + integrity sha512-ck8h7wgd3F3bYNk0v1OgohmyLBeXcKxqlfBJRtQq4k6KZUq+pXimOG7ckNguVMYjCo3PEfuG+ckKc21yqotKug== + dependencies: + "@sentry/core" "10.65.0" + +"@sentry/replay-canvas@10.65.0": + version "10.65.0" + resolved "https://registry.yarnpkg.com/@sentry/replay-canvas/-/replay-canvas-10.65.0.tgz#5ff20832ae8acacd38fe7f154dd7ac6c6cad1af2" + integrity sha512-A7X3RVk1Gk+knK8Ip/2EjejckNCLgCfRZo6eGlsy6qyz904KBpYmys1a0o7QkzFRjhIndjHAfcVxwt6jSLJlrQ== + dependencies: + "@sentry/core" "10.65.0" + "@sentry/replay" "10.65.0" + +"@sentry/replay@10.65.0": + version "10.65.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-10.65.0.tgz#8ed1255349e32faeac786e4ca2f4c2f56b98e64e" + integrity sha512-aW988CcQBNArbOMzOFOziipHz6uQyXSa4i5CPWsu+nhVPTJHafosi5Lv9n6NM/icDX5e23VdnX6mZd8SyJuo8A== + dependencies: + "@sentry/browser-utils" "10.65.0" + "@sentry/core" "10.65.0" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -3034,9 +3089,9 @@ js-tokens@^4.0.0: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: - version "3.14.2" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" - integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== + version "3.15.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.0.tgz#586e5214eafe3e893756a41e979b50d89d3e4a67" + integrity sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -4071,9 +4126,9 @@ write-file-atomic@^4.0.2: signal-exit "^3.0.7" ws@^8.11.0: - version "8.18.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" - integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + version "8.21.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.1.tgz#045650cd4b1207809e7547146223c3814a9af586" + integrity sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw== xml-name-validator@^4.0.0: version "4.0.0"