From 41cc4da44c5ef7d9a4adc2ff1d66f50c8b416ec7 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 14 Jul 2026 18:12:17 -0400 Subject: [PATCH 01/12] Upgrade Ruby and Rails versions, refactor dependencies, and remove Sidekiq integration - Updated Ruby version from 3.3.4 to 4.0.1 and Rails version from 7.2 to 8.1. - Refactored Gemfile to include new dependencies and remove Sidekiq, replacing it with Solid Queue for background job processing. - Adjusted database configurations to support multiple databases for primary, queue, cache, and cable. - Updated application and environment configurations to reflect changes in job processing and caching strategies. - Modified views and controllers to accommodate new pagination methods and removed deprecated assets. - Enhanced README documentation to reflect the updated technical specifications and installation instructions. --- .github/workflows/brakeman.yml | 2 +- .gitignore | 1 + .ruby-version | 2 +- .tool-versions | 2 +- Capfile | 4 - Gemfile | 27 +- Gemfile.lock | 275 ++++++++++-------- Procfile | 1 - README.md | 37 ++- app/assets/config/manifest.js | 4 - app/controllers/application_controller.rb | 2 +- app/controllers/users_dashboard_controller.rb | 15 +- app/helpers/users_dashboard_helper.rb | 2 - app/views/judge_dashboard/index.html.erb | 4 - app/views/layouts/application.html.erb | 2 +- app/views/users_dashboard/index.html.erb | 2 +- bin/jobs | 6 + config/application.rb | 20 +- config/cable.yml | 24 +- config/cache.yml | 20 ++ config/database.yml | 107 ++++++- config/deploy.rb | 19 +- config/deploy/templates/sidekiq.service.erb | 22 -- config/environments/development.rb | 5 +- config/environments/production.rb | 9 +- config/environments/staging.rb | 15 +- config/environments/test.rb | 2 +- config/initializers/action_mailer.rb | 29 +- config/initializers/assets.rb | 21 +- config/initializers/letter_opener_sidekiq.rb | 21 -- config/initializers/mission_control_jobs.rb | 4 + config/initializers/pagy.rb | 11 +- config/initializers/sidekiq.rb | 21 -- config/puma.rb | 3 + config/queue.yml | 18 ++ config/recurring.yml | 20 ++ config/routes.rb | 5 +- config/sidekiq.yml | 29 -- db/cable_schema.rb | 23 ++ db/cache_schema.rb | 24 ++ db/queue_schema.rb | 138 +++++++++ db/schema.rb | 222 +++++++------- lib/capistrano/tasks/redis.rake | 24 -- lib/tasks/auto_annotate_models.rake | 59 ---- lib/tasks/email.rake | 29 +- package.json | 5 +- yarn.lock | 18 +- 47 files changed, 739 insertions(+), 616 deletions(-) delete mode 100644 app/assets/config/manifest.js create mode 100755 bin/jobs create mode 100644 config/cache.yml delete mode 100644 config/deploy/templates/sidekiq.service.erb delete mode 100644 config/initializers/letter_opener_sidekiq.rb create mode 100644 config/initializers/mission_control_jobs.rb delete mode 100644 config/initializers/sidekiq.rb create mode 100644 config/queue.yml create mode 100644 config/recurring.yml delete mode 100644 config/sidekiq.yml create mode 100644 db/cable_schema.rb create mode 100644 db/cache_schema.rb create mode 100644 db/queue_schema.rb delete mode 100644 lib/capistrano/tasks/redis.rake delete mode 100644 lib/tasks/auto_annotate_models.rake diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml index aee38e13..06d18b81 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.1' 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..90cdbdcb 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-3.3.4 +ruby-4.0.1 diff --git a/.tool-versions b/.tool-versions index 276edf42..92fd4bdd 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -ruby 3.3.4 +ruby 4.0.1 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..d5276021 100644 --- a/Gemfile +++ b/Gemfile @@ -3,15 +3,19 @@ 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.1' gem 'actiontext' gem 'appsignal' gem 'bootsnap', require: false +gem 'benchmark' +gem 'cgi' gem 'country_select' gem 'cssbundling-rails' gem 'csv', '~> 3.2' +gem 'logger' +gem 'tsort' gem 'devise', '~> 4.9' gem 'google-cloud-storage', '~> 1.52' gem 'image_processing', '~> 1.2' @@ -19,17 +23,20 @@ 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..44991134 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) + uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.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) + 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) 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) 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,6 +121,7 @@ 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) @@ -143,7 +139,7 @@ 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) @@ -160,7 +156,9 @@ GEM rake (>= 12.0.0, < 14.0.0) docile (1.4.1) drb (2.2.3) - erubi (1.13.0) + 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) @@ -174,8 +172,10 @@ GEM logger faraday-net_http (3.3.0) net-http - ffi (1.17.0-arm64-darwin) - ffi (1.17.0-x86_64-linux-gnu) + 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) @@ -223,6 +223,10 @@ GEM image_processing (1.13.0) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) io-console (0.7.2) irb (1.14.0) rdoc (>= 4.0.0) @@ -264,7 +268,20 @@ GEM method_source (1.1.0) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.5) + mini_portile2 (2.8.9) + 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.7.2) multi_json (1.15.0) multi_xml (0.8.1) @@ -273,7 +290,7 @@ GEM mysql2 (0.5.6) net-http (0.4.1) uri - net-imap (0.4.20) + net-imap (0.6.4.1) date net-protocol net-pop (0.1.2) @@ -288,9 +305,8 @@ GEM net-protocol net-ssh (7.3.0) nio4r (2.7.3) - nokogiri (1.18.9-arm64-darwin) - racc (~> 1.4) - nokogiri (1.18.9-x86_64-linux-gnu) + nokogiri (1.18.9) + mini_portile2 (~> 2.8.2) racc (~> 1.4) omniauth (2.1.3) hashie (>= 3.4.6) @@ -305,17 +321,22 @@ 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 + 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) @@ -330,8 +351,9 @@ 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.1.21) rack-accept (0.4.5) rack (>= 0.4) rack-protection (4.1.1) @@ -346,20 +368,20 @@ GEM rackup (2.1.0) 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) @@ -371,13 +393,14 @@ GEM rails-html-sanitizer (1.6.1) loofah (~> 2.21) 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) @@ -398,8 +421,9 @@ GEM actionpack (>= 5.2) railties (>= 5.2) 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 +485,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 +499,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 +515,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 @@ -521,10 +542,12 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.1) + strscan (3.1.8) thor (1.4.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) @@ -548,36 +571,33 @@ 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) + yaml (0.4.0) zeitwerk (2.6.18) PLATFORMS - arm64-darwin-23 - x86_64-linux + arm64-darwin-25 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) @@ -591,19 +611,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 +634,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.1p0 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..421bef0c 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.1-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.1 +- **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.1 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..90e3e71e 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 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/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/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..b74c33de 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,7 +6,7 @@ <%= lsa_tdx_feedback_css %> <%= csrf_meta_tags %> <%= csp_meta_tag %> - <%= stylesheet_link_tag "application.bootstrap", "data-turbo-track": "reload" %> + <%= 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/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/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..4228b117 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -43,3 +43,6 @@ # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart + +# Run Solid Queue inside Puma for single-host Capistrano deploys. +plugin :solid_queue if ENV['SOLID_QUEUE_IN_PUMA'].present? || %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/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..b3f5ef02 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "@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", "autoprefixer": "^10.4.17", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", @@ -22,7 +22,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/yarn.lock b/yarn.lock index 04f71ae5..3fc44afe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1378,17 +1378,17 @@ 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" From bd077e1bb20489ef039a630c4c2136958a00647c Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 14 Jul 2026 18:18:50 -0400 Subject: [PATCH 02/12] Update Gemfile.lock to include x86_64-linux platform support - Added x86_64-linux to the list of supported platforms in Gemfile.lock to ensure compatibility with additional environments. --- Gemfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile.lock b/Gemfile.lock index 44991134..222d0f09 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -584,6 +584,7 @@ GEM PLATFORMS arm64-darwin-25 + x86_64-linux DEPENDENCIES actiontext From 93b584722518d7ff55563c487327df46b5b0e02f Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 14 Jul 2026 18:34:01 -0400 Subject: [PATCH 03/12] Update Puma configuration to avoid ActiveSupport methods during initialization - Modified the Puma configuration to prevent the use of ActiveSupport methods (e.g., present?) before Rails boots, ensuring compatibility and stability during server startup. --- config/puma.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/puma.rb b/config/puma.rb index 4228b117..d3a2dfe9 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -45,4 +45,5 @@ plugin :tmp_restart # Run Solid Queue inside Puma for single-host Capistrano deploys. -plugin :solid_queue if ENV['SOLID_QUEUE_IN_PUMA'].present? || %w[production staging].include?(ENV.fetch('RAILS_ENV', 'development')) +# 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')) From 7eae52daa3fd2e9edad683fa0eb6750777272f00 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 14 Jul 2026 18:43:11 -0400 Subject: [PATCH 04/12] Upgrade Ruby version from 4.0.1 to 4.0.6 across configuration files and documentation - Updated .ruby-version and .tool-versions to reflect the new Ruby version. - Modified Gemfile and Gemfile.lock to specify Ruby 4.0.6. - Updated README.md to include the new Ruby version in technical specifications and installation instructions. - Adjusted GitHub workflow configuration for Brakeman to use Ruby 4.0.6. --- .github/workflows/brakeman.yml | 2 +- .ruby-version | 2 +- .tool-versions | 2 +- Gemfile | 2 +- Gemfile.lock | 2 +- README.md | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml index 06d18b81..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: '4.0.1' + ruby-version: '4.0.6' bundler-cache: true - name: Run Brakeman diff --git a/.ruby-version b/.ruby-version index 90cdbdcb..1cf76f52 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-4.0.1 +ruby-4.0.6 diff --git a/.tool-versions b/.tool-versions index 92fd4bdd..c99a8bd2 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,4 +1,4 @@ -ruby 4.0.1 +ruby 4.0.6 nodejs 18.18.2 bundler 2.5.9 yarn 1.22.22 diff --git a/Gemfile b/Gemfile index d5276021..3a2afce6 100644 --- a/Gemfile +++ b/Gemfile @@ -4,7 +4,7 @@ source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } gem 'rails', '~> 8.1' -ruby '4.0.1' +ruby '4.0.6' gem 'actiontext' gem 'appsignal' diff --git a/Gemfile.lock b/Gemfile.lock index 222d0f09..3a7db877 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -653,7 +653,7 @@ DEPENDENCIES web-console RUBY VERSION - ruby 4.0.1p0 + ruby 4.0.6p0 BUNDLED WITH 2.5.9 diff --git a/README.md b/README.md index 421bef0c..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-8.1-red)](https://rubyonrails.org/) -[![Ruby](https://img.shields.io/badge/Ruby-4.0.1-blue)](https://www.ruby-lang.org/en/) +[![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. @@ -15,7 +15,7 @@ In its initial implementation, LSA Evaluate will serve the University of Michiga ## Technical Specifications - **Ruby on Rails:** 8.1 -- **Ruby Version:** 4.0.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) @@ -72,7 +72,7 @@ This application uses SendGrid for email delivery in the production environment. Before deploying: -1. Install Ruby 4.0.1 via asdf on the host. +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. From 83413e6f8195b6ba2403b58a0a8e51e714787b0d Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Tue, 14 Jul 2026 19:13:38 -0400 Subject: [PATCH 05/12] Update Gemfile.lock to upgrade bootsnap and msgpack versions - Upgraded bootsnap from 1.18.4 to 1.24.6 for improved performance. - Updated msgpack from 1.7.2 to 1.8.3 to ensure compatibility with dependencies. --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3a7db877..97ca677e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,7 +94,7 @@ GEM bindex (0.8.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 @@ -282,7 +282,7 @@ GEM railties (>= 7.1) stimulus-rails turbo-rails - msgpack (1.7.2) + msgpack (1.8.3) multi_json (1.15.0) multi_xml (0.8.1) bigdecimal (>= 3.1, < 5) From cf8b7dfa96a8cd31395ca3c930a89f47a0d15f15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:39:06 +0000 Subject: [PATCH 06/12] Bump js-yaml in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [js-yaml](https://github.com/nodeca/js-yaml). Updates `js-yaml` from 3.14.2 to 3.15.0 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.2...3.15.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.15.0 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3fc44afe..ad77442c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3034,9 +3034,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" From de3052440a9d103d443b0f9cace988c2e055614f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:39:44 +0000 Subject: [PATCH 07/12] Bump the bundler group across 1 directory with 9 updates Bumps the bundler group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [devise](https://github.com/heartcombo/devise) | `4.9.4` | `5.0.4` | | [puma](https://github.com/puma/puma) | `6.4.3` | `7.2.1` | | [addressable](https://github.com/sporkmonger/addressable) | `2.8.7` | `2.9.0` | | [faraday](https://github.com/lostisland/faraday) | `2.12.0` | `2.14.3` | | [jwt](https://github.com/jwt/ruby-jwt) | `2.9.3` | `2.10.3` | Updates `devise` from 4.9.4 to 5.0.4 - [Release notes](https://github.com/heartcombo/devise/releases) - [Changelog](https://github.com/heartcombo/devise/blob/main/CHANGELOG.md) - [Commits](https://github.com/heartcombo/devise/compare/v4.9.4...v5.0.4) Updates `puma` from 6.4.3 to 7.2.1 - [Release notes](https://github.com/puma/puma/releases) - [Changelog](https://github.com/puma/puma/blob/main/History.md) - [Commits](https://github.com/puma/puma/compare/v6.4.3...v7.2.1) Updates `addressable` from 2.8.7 to 2.9.0 - [Changelog](https://github.com/sporkmonger/addressable/blob/main/CHANGELOG.md) - [Commits](https://github.com/sporkmonger/addressable/compare/addressable-2.8.7...addressable-2.9.0) Updates `bcrypt` from 3.1.20 to 3.1.22 - [Release notes](https://github.com/bcrypt-ruby/bcrypt-ruby/releases) - [Changelog](https://github.com/bcrypt-ruby/bcrypt-ruby/blob/master/CHANGELOG) - [Commits](https://github.com/bcrypt-ruby/bcrypt-ruby/compare/v3.1.20...v3.1.22) Updates `concurrent-ruby` from 1.3.6 to 1.3.7 - [Release notes](https://github.com/ruby-concurrency/concurrent-ruby/releases) - [Changelog](https://github.com/ruby-concurrency/concurrent-ruby/blob/master/CHANGELOG.md) - [Commits](https://github.com/ruby-concurrency/concurrent-ruby/compare/v1.3.6...v1.3.7) Updates `faraday` from 2.12.0 to 2.14.3 - [Release notes](https://github.com/lostisland/faraday/releases) - [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md) - [Commits](https://github.com/lostisland/faraday/compare/v2.12.0...v2.14.3) Updates `jwt` from 2.9.3 to 2.10.3 - [Release notes](https://github.com/jwt/ruby-jwt/releases) - [Changelog](https://github.com/jwt/ruby-jwt/blob/main/CHANGELOG.md) - [Commits](https://github.com/jwt/ruby-jwt/compare/v2.9.3...v2.10.3) Updates `nokogiri` from 1.18.9 to 1.19.4 - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.9...v1.19.4) Updates `rack-session` from 2.1.1 to 2.1.2 - [Release notes](https://github.com/rack/rack-session/releases) - [Changelog](https://github.com/rack/rack-session/blob/main/releases.md) - [Commits](https://github.com/rack/rack-session/compare/v2.1.1...v2.1.2) --- updated-dependencies: - dependency-name: devise dependency-version: 5.0.4 dependency-type: direct:production dependency-group: bundler - dependency-name: puma dependency-version: 7.2.1 dependency-type: direct:production dependency-group: bundler - dependency-name: addressable dependency-version: 2.9.0 dependency-type: indirect dependency-group: bundler - dependency-name: bcrypt dependency-version: 3.1.22 dependency-type: indirect dependency-group: bundler - dependency-name: concurrent-ruby dependency-version: 1.3.7 dependency-type: indirect dependency-group: bundler - dependency-name: faraday dependency-version: 2.14.3 dependency-type: indirect dependency-group: bundler - dependency-name: jwt dependency-version: 2.10.3 dependency-type: indirect dependency-group: bundler - dependency-name: nokogiri dependency-version: 1.19.4 dependency-type: indirect dependency-group: bundler - dependency-name: rack-session dependency-version: 2.1.2 dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile | 2 +- Gemfile.lock | 108 +++++++++++++++++++++++++++------------------------ 2 files changed, 59 insertions(+), 51 deletions(-) diff --git a/Gemfile b/Gemfile index 3a2afce6..a45d7626 100644 --- a/Gemfile +++ b/Gemfile @@ -16,7 +16,7 @@ gem 'cssbundling-rails' gem 'csv', '~> 3.2' gem 'logger' gem 'tsort' -gem 'devise', '~> 4.9' +gem 'devise', '~> 5.0' gem 'google-cloud-storage', '~> 1.52' gem 'image_processing', '~> 1.2' gem 'jbuilder' diff --git a/Gemfile.lock b/Gemfile.lock index 97ca677e..1cb5c9ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -75,8 +75,8 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) airbrussh (1.6.0) sshkit (>= 1.6.1, != 1.7.0) appsignal (4.8.5) @@ -84,13 +84,13 @@ GEM rack (>= 2.0.0) ast (2.4.2) base64 (0.3.0) - bcrypt (3.1.20) + 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 (2.0.0) debug_inspector (>= 1.2.0) @@ -125,13 +125,13 @@ GEM 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) @@ -145,10 +145,10 @@ GEM 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) @@ -156,6 +156,7 @@ GEM rake (>= 12.0.0, < 14.0.0) docile (1.4.1) drb (2.2.3) + erb (6.0.4) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -166,12 +167,12 @@ 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 + faraday-net_http (3.4.4) + net-http (~> 0.5) ffi (1.17.0) fugit (1.13.0) et-orbi (~> 1.4) @@ -218,7 +219,7 @@ 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) @@ -227,8 +228,10 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.7.2) - irb (1.14.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) @@ -236,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) @@ -251,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) @@ -268,7 +271,6 @@ GEM method_source (1.1.0) mini_magick (4.13.2) mini_mime (1.1.5) - mini_portile2 (2.8.9) minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) @@ -288,8 +290,8 @@ GEM bigdecimal (>= 3.1, < 5) mutex_m (0.2.0) mysql2 (0.5.6) - net-http (0.4.1) - uri + net-http (0.9.1) + uri (>= 0.11.1) net-imap (0.6.4.1) date net-protocol @@ -304,9 +306,10 @@ GEM net-smtp (0.5.0) net-protocol net-ssh (7.3.0) - nio4r (2.7.3) - nokogiri (1.18.9) - mini_portile2 (~> 2.8.2) + nio4r (2.7.5) + nokogiri (1.19.4-arm64-darwin) + racc (~> 1.4) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) omniauth (2.1.3) hashie (>= 3.4.6) @@ -329,6 +332,9 @@ GEM 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) @@ -339,10 +345,8 @@ GEM method_source (~> 1.0) 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) @@ -353,21 +357,20 @@ GEM rspec-support (~> 3.12) raabro (1.4.0) racc (1.8.1) - rack (3.1.21) + 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 (8.1.3) actioncable (= 8.1.3) actionmailbox (= 8.1.3) @@ -386,12 +389,12 @@ GEM 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 (8.1.3) actionpack (= 8.1.3) @@ -403,23 +406,30 @@ GEM 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.4) rouge (5.0.0) @@ -541,9 +551,8 @@ GEM stackprof (0.2.27) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.1) strscan (3.1.8) - thor (1.4.0) + thor (1.5.0) tilt (2.4.0) timeout (0.6.1) trailblazer-option (0.1.2) @@ -562,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) @@ -571,7 +580,6 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webrick (1.8.2) websocket (1.2.11) websocket-driver (0.8.2) base64 @@ -580,7 +588,7 @@ GEM xpath (3.2.0) nokogiri (~> 1.8) yaml (0.4.0) - zeitwerk (2.6.18) + zeitwerk (2.8.2) PLATFORMS arm64-darwin-25 @@ -604,7 +612,7 @@ DEPENDENCIES 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) From 7354a1bc0c3df00b675223ab44b9b7a935874389 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 15 Jul 2026 09:42:24 -0400 Subject: [PATCH 08/12] Refactor Rack::Defense spec for clarity and accuracy - Updated test description to better reflect the behavior of Rack when parsing POST parameters after body inspection. - Changed expectation from `equal` to `eq` for comparing the parsed POST parameters, ensuring proper value comparison. --- spec/middleware/rack_defense_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 5089300dc2b225177f8e81b8e200b080efa5f31c Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 15 Jul 2026 10:22:52 -0400 Subject: [PATCH 09/12] Add affiliations display to user dashboard - Enhanced the user dashboard view to include a section for displaying user affiliations. - Implemented conditional rendering to show affiliations as badges or a "None" message if no affiliations exist. - Improved user experience by providing clear visibility of user affiliations. --- app/views/users_dashboard/show.html.erb | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 %> +
From a6684c5081a3d3bbad25247417be997d94ecb8f2 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 15 Jul 2026 11:37:18 -0400 Subject: [PATCH 10/12] Improve Sentry monitoring for high-traffic reliability. Capture all errors, track deploy releases, add browser SDK tracing, and stop scrubbing useful request context. Co-authored-by: Cursor --- app/controllers/application_controller.rb | 7 +- app/helpers/sentry_helper.rb | 16 ++++ app/javascript/application.js | 36 +++++++ app/views/layouts/application.html.erb | 1 + config/initializers/sentry.rb | 112 ++++++---------------- lib/capistrano/tasks/sentry.rake | 39 ++++++++ lib/sentry_release.rb | 28 ++++++ lib/sentry_test_helper.rb | 81 ++++------------ package.json | 1 + yarn.lock | 55 +++++++++++ 10 files changed, 231 insertions(+), 145 deletions(-) create mode 100644 lib/capistrano/tasks/sentry.rake create mode 100644 lib/sentry_release.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 90e3e71e..582b7917 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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/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/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/layouts/application.html.erb b/app/views/layouts/application.html.erb index b74c33de..74187792 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,6 +6,7 @@ <%= lsa_tdx_feedback_css %> <%= csrf_meta_tags %> <%= csp_meta_tag %> + <%= sentry_meta_tags %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %> <%= yield(:head) %> 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/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..825513b3 --- /dev/null +++ b/lib/sentry_release.rb @@ -0,0 +1,28 @@ +# 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 || + 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/package.json b/package.json index b3f5ef02..3f5406cf 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@hotwired/turbo-rails": "^8.0.10", "@popperjs/core": "^2.11.8", "@rails/actiontext": "^8.0.200", + "@sentry/browser": "^10.65.0", "autoprefixer": "^10.4.17", "bootstrap": "^5.3.3", "bootstrap-icons": "^1.11.3", diff --git a/yarn.lock b/yarn.lock index ad77442c..5117cd9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1392,6 +1392,61 @@ 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" From 310f82dd21ff7c228f5c240a0d954b3695d94475 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:39:43 +0000 Subject: [PATCH 11/12] Bump ws in the npm_and_yarn group across 1 directory Bumps the npm_and_yarn group with 1 update in the / directory: [ws](https://github.com/websockets/ws). Updates `ws` from 8.18.0 to 8.21.1 - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.18.0...8.21.1) --- updated-dependencies: - dependency-name: ws dependency-version: 8.21.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5117cd9a..1a0c3335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4126,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" From 4564e4361abb919cf726f37e68a4462cd204d538 Mon Sep 17 00:00:00 2001 From: rsmokeUM Date: Wed, 15 Jul 2026 12:22:40 -0400 Subject: [PATCH 12/12] Enhance Sentry release tracking by adding support for HATCHBOX_REVISION environment variable. This allows for improved release identification alongside existing methods. --- lib/sentry_release.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/sentry_release.rb b/lib/sentry_release.rb index 825513b3..84a4ee0d 100644 --- a/lib/sentry_release.rb +++ b/lib/sentry_release.rb @@ -6,6 +6,7 @@ module SentryRelease def current ENV['SENTRY_RELEASE'].presence || + ENV['HATCHBOX_REVISION'].presence || revision_file.presence || git_sha.presence end