diff --git a/.github/workflows/brakeman.yml b/.github/workflows/brakeman.yml
index aee38e13..54f61e76 100644
--- a/.github/workflows/brakeman.yml
+++ b/.github/workflows/brakeman.yml
@@ -21,7 +21,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: '3.3.4'
+ ruby-version: '4.0.6'
bundler-cache: true
- name: Run Brakeman
diff --git a/.gitignore b/.gitignore
index d2a365de..a94c835c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
# Ignore bundler config.
/.bundle
+/vendor/bundle
# Ignore all logfiles and tempfiles.
/log/*
diff --git a/.ruby-version b/.ruby-version
index 6d5369b9..1cf76f52 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-ruby-3.3.4
+ruby-4.0.6
diff --git a/.tool-versions b/.tool-versions
index 276edf42..c99a8bd2 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,4 +1,4 @@
-ruby 3.3.4
+ruby 4.0.6
nodejs 18.18.2
bundler 2.5.9
yarn 1.22.22
diff --git a/Capfile b/Capfile
index cf18f3ec..189af7ac 100644
--- a/Capfile
+++ b/Capfile
@@ -9,13 +9,9 @@ require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git
# Include tasks from other gems included in your Gemfile
-
require 'capistrano/rails'
require 'capistrano/bundler'
require 'capistrano/asdf'
-# require 'capistrano/sidekiq'
-# install_plugin Capistrano::Sidekiq
-# install_plugin Capistrano::Sidekiq::Systemd
# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
diff --git a/Gemfile b/Gemfile
index 96338b45..a45d7626 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,33 +3,40 @@
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
-gem 'rails', '~> 7.2'
-ruby '3.3.4'
+gem 'rails', '~> 8.1'
+ruby '4.0.6'
gem 'actiontext'
gem 'appsignal'
gem 'bootsnap', require: false
+gem 'benchmark'
+gem 'cgi'
gem 'country_select'
gem 'cssbundling-rails'
gem 'csv', '~> 3.2'
-gem 'devise', '~> 4.9'
+gem 'logger'
+gem 'tsort'
+gem 'devise', '~> 5.0'
gem 'google-cloud-storage', '~> 1.52'
gem 'image_processing', '~> 1.2'
gem 'jbuilder'
gem 'jsbundling-rails'
gem 'lsa_tdx_feedback'
gem 'mysql2', '~> 0.5.3'
+gem 'net-imap', '>= 0.5'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-saml', '~> 2.1'
-gem 'pagy', '~> 6.4'
+gem 'pagy', '~> 43.0'
+gem 'propshaft'
gem 'puma'
gem 'pundit'
-gem 'redis', '~> 5.0'
gem 'sentry-ruby'
gem 'sentry-rails'
gem 'stackprof'
-gem 'sidekiq', '~> 7.3'
-gem 'sassc-rails'
+gem 'solid_cable'
+gem 'solid_cache'
+gem 'solid_queue'
+gem 'mission_control-jobs'
gem 'simple_form', '~> 5.3'
gem 'stimulus-rails'
gem 'skylight'
@@ -38,14 +45,12 @@ gem 'turnout2024', require: 'turnout'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
group :development do
- gem 'annotate'
- gem 'better_errors'
- gem 'binding_of_caller'
+ gem 'better_errors', '>= 2.10'
+ gem 'binding_of_caller', '>= 2.0'
gem 'brakeman'
gem 'capistrano', '~> 3.17', require: false
gem 'capistrano-rails', '~> 1.6', '>= 1.6.1', require: false
gem 'capistrano-asdf', require: false
- gem 'capistrano-sidekiq', '~> 2.0', require: false
gem 'web-console'
end
@@ -55,7 +60,6 @@ group :test do
gem 'rails-controller-testing'
gem 'selenium-webdriver'
gem 'simplecov', require: false
- gem 'webdrivers'
end
group :development, :staging do
@@ -65,7 +69,6 @@ end
group :development, :test do
gem 'capybara'
gem 'debug', platforms: %i[mri mingw x64_mingw]
- gem 'pry-byebug'
gem 'pry-rails'
gem 'pundit-matchers'
gem 'rspec-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index d3fea7d0..1cb5c9ef 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,105 +1,104 @@
GEM
remote: https://rubygems.org/
specs:
- actioncable (7.2.2.2)
- actionpack (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ action_text-trix (2.1.19)
+ railties
+ actioncable (8.1.3)
+ actionpack (= 8.1.3)
+ activesupport (= 8.1.3)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
- actionmailbox (7.2.2.2)
- actionpack (= 7.2.2.2)
- activejob (= 7.2.2.2)
- activerecord (= 7.2.2.2)
- activestorage (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ actionmailbox (8.1.3)
+ actionpack (= 8.1.3)
+ activejob (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
mail (>= 2.8.0)
- actionmailer (7.2.2.2)
- actionpack (= 7.2.2.2)
- actionview (= 7.2.2.2)
- activejob (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ actionmailer (8.1.3)
+ actionpack (= 8.1.3)
+ actionview (= 8.1.3)
+ activejob (= 8.1.3)
+ activesupport (= 8.1.3)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
- actionpack (7.2.2.2)
- actionview (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ actionpack (8.1.3)
+ actionview (= 8.1.3)
+ activesupport (= 8.1.3)
nokogiri (>= 1.8.5)
- racc
- rack (>= 2.2.4, < 3.2)
+ rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
- actiontext (7.2.2.2)
- actionpack (= 7.2.2.2)
- activerecord (= 7.2.2.2)
- activestorage (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ actiontext (8.1.3)
+ action_text-trix (~> 2.1.15)
+ actionpack (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.2.2.2)
- activesupport (= 7.2.2.2)
+ actionview (8.1.3)
+ activesupport (= 8.1.3)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- activejob (7.2.2.2)
- activesupport (= 7.2.2.2)
+ activejob (8.1.3)
+ activesupport (= 8.1.3)
globalid (>= 0.3.6)
- activemodel (7.2.2.2)
- activesupport (= 7.2.2.2)
- activerecord (7.2.2.2)
- activemodel (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ activemodel (8.1.3)
+ activesupport (= 8.1.3)
+ activerecord (8.1.3)
+ activemodel (= 8.1.3)
+ activesupport (= 8.1.3)
timeout (>= 0.4.0)
- activestorage (7.2.2.2)
- actionpack (= 7.2.2.2)
- activejob (= 7.2.2.2)
- activerecord (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ activestorage (8.1.3)
+ actionpack (= 8.1.3)
+ activejob (= 8.1.3)
+ activerecord (= 8.1.3)
+ activesupport (= 8.1.3)
marcel (~> 1.0)
- activesupport (7.2.2.2)
+ activesupport (8.1.3)
base64
- benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
+ json
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
- addressable (2.8.7)
- public_suffix (>= 2.0.2, < 7.0)
+ uri (>= 0.13.1)
+ addressable (2.9.0)
+ public_suffix (>= 2.0.2, < 8.0)
airbrussh (1.6.0)
sshkit (>= 1.6.1, != 1.7.0)
- annotate (3.2.0)
- activerecord (>= 3.2, < 8.0)
- rake (>= 10.4, < 14.0)
appsignal (4.8.5)
logger
rack (>= 2.0.0)
ast (2.4.2)
base64 (0.3.0)
- bcrypt (3.1.20)
- benchmark (0.4.1)
+ bcrypt (3.1.22)
+ benchmark (0.5.0)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
- bigdecimal (3.2.2)
+ bigdecimal (4.1.2)
bindex (0.8.1)
- binding_of_caller (1.0.1)
+ binding_of_caller (2.0.0)
debug_inspector (>= 1.2.0)
- bootsnap (1.18.4)
+ bootsnap (1.24.6)
msgpack (~> 1.2)
brakeman (7.0.2)
racc
builder (3.3.0)
- byebug (11.1.3)
capistrano (3.20.0)
airbrussh (>= 1.0.0)
i18n
@@ -113,10 +112,6 @@ GEM
capistrano-rails (1.6.3)
capistrano (~> 3.1)
capistrano-bundler (>= 1.1, < 3)
- capistrano-sidekiq (2.3.1)
- capistrano (>= 3.9.0)
- capistrano-bundler
- sidekiq (>= 6.0)
capybara (3.40.0)
addressable
matrix
@@ -126,16 +121,17 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
+ cgi (0.5.2)
childprocess (5.1.0)
logger (~> 1.5)
coderay (1.1.3)
- concurrent-ruby (1.3.6)
- connection_pool (2.5.3)
+ concurrent-ruby (1.3.7)
+ connection_pool (3.0.2)
countries (6.0.1)
unaccent (~> 0.3)
country_select (9.0.0)
countries (> 5.0, < 7.0)
- crass (1.0.6)
+ crass (1.0.7)
cssbundling-rails (1.4.1)
railties (>= 6.0.0)
csv (3.3.3)
@@ -143,16 +139,16 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
- date (3.4.1)
+ date (3.5.1)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
debug_inspector (1.2.0)
declarative (0.0.20)
- devise (4.9.4)
+ devise (5.0.4)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
- railties (>= 4.1.0)
+ railties (>= 7.0)
responders
warden (~> 1.2.3)
diff-lcs (1.5.1)
@@ -160,7 +156,10 @@ GEM
rake (>= 12.0.0, < 14.0.0)
docile (1.4.1)
drb (2.2.3)
- erubi (1.13.0)
+ erb (6.0.4)
+ erubi (1.13.1)
+ et-orbi (1.4.0)
+ tzinfo
factory_bot (6.4.6)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
@@ -168,14 +167,16 @@ GEM
railties (>= 5.0.0)
faker (3.4.2)
i18n (>= 1.8.11, < 2)
- faraday (2.12.0)
- faraday-net_http (>= 2.0, < 3.4)
+ faraday (2.14.3)
+ faraday-net_http (>= 2.0, < 3.5)
json
logger
- faraday-net_http (3.3.0)
- net-http
- ffi (1.17.0-arm64-darwin)
- ffi (1.17.0-x86_64-linux-gnu)
+ faraday-net_http (3.4.4)
+ net-http (~> 0.5)
+ ffi (1.17.0)
+ fugit (1.13.0)
+ et-orbi (~> 1.4)
+ raabro (~> 1.4)
globalid (1.2.1)
activesupport (>= 6.1)
google-apis-core (0.15.1)
@@ -218,13 +219,19 @@ GEM
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (1.14.8)
+ i18n (1.15.2)
concurrent-ruby (~> 1.0)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
- io-console (0.7.2)
- irb (1.14.0)
+ importmap-rails (2.2.3)
+ actionpack (>= 6.0.0)
+ activesupport (>= 6.0.0)
+ railties (>= 6.0.0)
+ io-console (0.8.2)
+ irb (1.18.0)
+ pp (>= 0.6.0)
+ prism (>= 1.3.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.12.0)
@@ -232,8 +239,8 @@ GEM
activesupport (>= 5.0.0)
jsbundling-rails (1.3.1)
railties (>= 6.0.0)
- json (2.7.2)
- jwt (2.9.3)
+ json (2.21.1)
+ jwt (2.10.3)
base64
language_server-protocol (3.17.0.3)
launchy (3.0.1)
@@ -247,7 +254,7 @@ GEM
railties (>= 6.1)
rexml
logger (1.7.0)
- loofah (2.23.1)
+ loofah (2.25.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
lsa_tdx_feedback (1.0.3)
@@ -264,16 +271,28 @@ GEM
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
- minitest (5.25.5)
- msgpack (1.7.2)
+ minitest (6.0.6)
+ drb (~> 2.0)
+ prism (~> 1.5)
+ mission_control-jobs (1.1.0)
+ actioncable (>= 7.1)
+ actionpack (>= 7.1)
+ activejob (>= 7.1)
+ activerecord (>= 7.1)
+ importmap-rails (>= 1.2.1)
+ irb (~> 1.13)
+ railties (>= 7.1)
+ stimulus-rails
+ turbo-rails
+ msgpack (1.8.3)
multi_json (1.15.0)
multi_xml (0.8.1)
bigdecimal (>= 3.1, < 5)
mutex_m (0.2.0)
mysql2 (0.5.6)
- net-http (0.4.1)
- uri
- net-imap (0.4.20)
+ net-http (0.9.1)
+ uri (>= 0.11.1)
+ net-imap (0.6.4.1)
date
net-protocol
net-pop (0.1.2)
@@ -287,10 +306,10 @@ GEM
net-smtp (0.5.0)
net-protocol
net-ssh (7.3.0)
- nio4r (2.7.3)
- nokogiri (1.18.9-arm64-darwin)
+ nio4r (2.7.5)
+ nokogiri (1.19.4-arm64-darwin)
racc (~> 1.4)
- nokogiri (1.18.9-x86_64-linux-gnu)
+ nokogiri (1.19.4-x86_64-linux-gnu)
racc (~> 1.4)
omniauth (2.1.3)
hashie (>= 3.4.6)
@@ -305,23 +324,29 @@ GEM
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.3)
- pagy (6.5.0)
+ pagy (43.6.0)
+ json
+ uri
+ yaml
parallel (1.26.3)
parser (3.3.5.0)
ast (~> 2.4.1)
racc
+ pp (0.6.4)
+ prettyprint
+ prettyprint (0.2.0)
+ prism (1.9.0)
+ propshaft (1.3.2)
+ actionpack (>= 7.0.0)
+ activesupport (>= 7.0.0)
+ rack
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
- pry-byebug (3.10.1)
- byebug (~> 11.0)
- pry (>= 0.13, < 0.15)
pry-rails (0.3.11)
pry (>= 0.13.0)
- psych (5.1.2)
- stringio
- public_suffix (6.0.1)
- puma (6.4.3)
+ public_suffix (7.0.5)
+ puma (7.2.1)
nio4r (~> 2.0)
pundit (2.4.0)
activesupport (>= 3.0.0)
@@ -330,76 +355,85 @@ GEM
rspec-expectations (~> 3.12)
rspec-mocks (~> 3.12)
rspec-support (~> 3.12)
+ raabro (1.4.0)
racc (1.8.1)
- rack (3.1.18)
+ rack (3.2.6)
rack-accept (0.4.5)
rack (>= 0.4)
rack-protection (4.1.1)
base64 (>= 0.1.0)
logger (>= 1.6.0)
rack (>= 3.0.0, < 4)
- rack-session (2.1.1)
+ rack-session (2.1.2)
base64 (>= 0.1.0)
rack (>= 3.0.0)
- rack-test (2.1.0)
+ rack-test (2.2.0)
rack (>= 1.3)
- rackup (2.1.0)
+ rackup (2.3.1)
rack (>= 3)
- webrick (~> 1.8)
- rails (7.2.2.2)
- actioncable (= 7.2.2.2)
- actionmailbox (= 7.2.2.2)
- actionmailer (= 7.2.2.2)
- actionpack (= 7.2.2.2)
- actiontext (= 7.2.2.2)
- actionview (= 7.2.2.2)
- activejob (= 7.2.2.2)
- activemodel (= 7.2.2.2)
- activerecord (= 7.2.2.2)
- activestorage (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ rails (8.1.3)
+ actioncable (= 8.1.3)
+ actionmailbox (= 8.1.3)
+ actionmailer (= 8.1.3)
+ actionpack (= 8.1.3)
+ actiontext (= 8.1.3)
+ actionview (= 8.1.3)
+ activejob (= 8.1.3)
+ activemodel (= 8.1.3)
+ activerecord (= 8.1.3)
+ activestorage (= 8.1.3)
+ activesupport (= 8.1.3)
bundler (>= 1.15.0)
- railties (= 7.2.2.2)
+ railties (= 8.1.3)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
- rails-dom-testing (2.2.0)
+ rails-dom-testing (2.3.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
- rails-html-sanitizer (1.6.1)
- loofah (~> 2.21)
+ rails-html-sanitizer (1.7.0)
+ loofah (~> 2.25)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
- railties (7.2.2.2)
- actionpack (= 7.2.2.2)
- activesupport (= 7.2.2.2)
+ railties (8.1.3)
+ actionpack (= 8.1.3)
+ activesupport (= 8.1.3)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
+ tsort (>= 0.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
- rake (13.3.1)
- rdoc (6.7.0)
- psych (>= 4.0.0)
+ rake (13.4.2)
+ rbs (4.0.3)
+ logger
+ prism (>= 1.6.0)
+ tsort
+ rdoc (8.0.0)
+ erb
+ prism (>= 1.6.0)
+ rbs (>= 4.0.0)
+ tsort
redis (5.3.0)
redis-client (>= 0.22.0)
redis-client (0.24.0)
connection_pool
regexp_parser (2.9.2)
- reline (0.5.9)
+ reline (0.6.3)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
- responders (3.1.1)
- actionpack (>= 5.2)
- railties (>= 5.2)
+ responders (3.2.0)
+ actionpack (>= 7.0)
+ railties (>= 7.0)
retriable (3.1.2)
- rexml (3.4.2)
- rouge (4.3.0)
+ rexml (3.4.4)
+ rouge (5.0.0)
+ strscan (~> 3.1)
rspec-core (3.13.1)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.2)
@@ -461,19 +495,13 @@ GEM
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
- rubyzip (2.3.2)
- sassc (2.4.0)
- ffi (~> 1.9)
- sassc-rails (2.1.2)
- railties (>= 4.0.0)
- sassc (>= 2.0)
- sprockets (> 3.0)
- sprockets-rails
- tilt
+ rubyzip (3.4.1)
securerandom (0.4.1)
- selenium-webdriver (4.10.0)
+ selenium-webdriver (4.46.0)
+ base64 (~> 0.2)
+ logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
- rubyzip (>= 1.2.2, < 3.0)
+ rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
sentry-rails (5.23.0)
railties (>= 5.0)
@@ -481,12 +509,6 @@ GEM
sentry-ruby (5.23.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
- sidekiq (7.3.9)
- base64
- connection_pool (>= 2.3.0)
- logger
- rack (>= 2.2.4)
- redis-client (>= 0.22.2)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
@@ -503,13 +525,22 @@ GEM
simplecov_json_formatter (0.1.4)
skylight (6.0.4)
activesupport (>= 5.2.0)
- sprockets (4.2.1)
- concurrent-ruby (~> 1.0)
- rack (>= 2.2.4, < 4)
- sprockets-rails (3.5.2)
- actionpack (>= 6.1)
- activesupport (>= 6.1)
- sprockets (>= 3.0.0)
+ solid_cable (4.0.0)
+ actioncable (>= 7.2)
+ activejob (>= 7.2)
+ activerecord (>= 7.2)
+ railties (>= 7.2)
+ solid_cache (1.0.10)
+ activejob (>= 7.2)
+ activerecord (>= 7.2)
+ railties (>= 7.2)
+ solid_queue (1.4.0)
+ activejob (>= 7.1)
+ activerecord (>= 7.1)
+ concurrent-ruby (>= 1.3.1)
+ fugit (~> 1.11)
+ railties (>= 7.1)
+ thor (>= 1.3.1)
sshkit (1.25.0)
base64
logger
@@ -520,11 +551,12 @@ GEM
stackprof (0.2.27)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
- stringio (3.1.1)
- thor (1.4.0)
+ strscan (3.1.8)
+ thor (1.5.0)
tilt (2.4.0)
- timeout (0.4.3)
+ timeout (0.6.1)
trailblazer-option (0.1.2)
+ tsort (0.2.0)
turbo-rails (2.0.6)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
@@ -539,7 +571,7 @@ GEM
uber (0.1.0)
unaccent (0.4.0)
unicode-display_width (2.5.0)
- uri (0.13.3)
+ uri (1.1.1)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
@@ -548,42 +580,39 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
- webdrivers (5.3.1)
- nokogiri (~> 1.6)
- rubyzip (>= 1.3.0)
- selenium-webdriver (~> 4.0, < 4.11)
- webrick (1.8.2)
websocket (1.2.11)
- websocket-driver (0.7.6)
+ websocket-driver (0.8.2)
+ base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.6.18)
+ yaml (0.4.0)
+ zeitwerk (2.8.2)
PLATFORMS
- arm64-darwin-23
+ arm64-darwin-25
x86_64-linux
DEPENDENCIES
actiontext
- annotate
appsignal
- better_errors
- binding_of_caller
+ benchmark
+ better_errors (>= 2.10)
+ binding_of_caller (>= 2.0)
bootsnap
brakeman
capistrano (~> 3.17)
capistrano-asdf
capistrano-rails (~> 1.6, >= 1.6.1)
- capistrano-sidekiq (~> 2.0)
capybara
+ cgi
country_select
cssbundling-rails
csv (~> 3.2)
database_cleaner-active_record (~> 2.0)
debug
- devise (~> 4.9)
+ devise (~> 5.0)
factory_bot_rails
faker
google-cloud-storage (~> 1.52)
@@ -591,19 +620,21 @@ DEPENDENCIES
jbuilder
jsbundling-rails
letter_opener_web
+ logger
lsa_tdx_feedback
+ mission_control-jobs
mysql2 (~> 0.5.3)
+ net-imap (>= 0.5)
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml (~> 2.1)
- pagy (~> 6.4)
- pry-byebug
+ pagy (~> 43.0)
+ propshaft
pry-rails
puma
pundit
pundit-matchers
- rails (~> 7.2)
+ rails (~> 8.1)
rails-controller-testing
- redis (~> 5.0)
rspec-rails
rubocop
rubocop-capybara
@@ -612,24 +643,25 @@ DEPENDENCIES
rubocop-rails-omakase
rubocop-rspec
rubocop-rspec_rails
- sassc-rails
selenium-webdriver
sentry-rails
sentry-ruby
- sidekiq (~> 7.3)
simple_form (~> 5.3)
simplecov
skylight
+ solid_cable
+ solid_cache
+ solid_queue
stackprof
stimulus-rails
+ tsort
turbo-rails
turnout2024
tzinfo-data
web-console
- webdrivers
RUBY VERSION
- ruby 3.3.4p94
+ ruby 4.0.6p0
BUNDLED WITH
2.5.9
diff --git a/Procfile b/Procfile
index 6d19a6cf..c2c566e8 100644
--- a/Procfile
+++ b/Procfile
@@ -1,2 +1 @@
web: bundle exec puma -C config/puma.rb
-worker: bundle exec sidekiq -e ${RAILS_ENV:-development} -C config/sidekiq.yml
diff --git a/README.md b/README.md
index f924b2b2..7b195291 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# LSA Evaluate
-[](https://rubyonrails.org/)
-[](https://www.ruby-lang.org/en/)
+[](https://rubyonrails.org/)
+[](https://www.ruby-lang.org/en/)
[](https://www.mysql.com/)
LSA Evaluate is a Ruby on Rails application designed to facilitate a comprehensive submission and evaluation process. The application allows entrants to complete a profile, submit a body of work electronically, and have that work evaluated. Key features include the maintenance of entry forms, management of evaluations, and visual cues to entrants regarding the status of their submissions.
@@ -14,9 +14,14 @@ In its initial implementation, LSA Evaluate will serve the University of Michiga
## Technical Specifications
-- **Ruby on Rails:** 7.2
-- **Ruby Version:** 3.3.4
-- **Database:** MySQL 8
+- **Ruby on Rails:** 8.1
+- **Ruby Version:** 4.0.6
+- **Database:** MySQL 8 (primary + Solid Queue / Cache / Cable databases)
+- **Assets:** Propshaft + cssbundling-rails / jsbundling-rails (esbuild + Dart Sass)
+- **Jobs:** Solid Queue (via Puma plugin in production/staging)
+- **Cache / Cable:** Solid Cache and Solid Cable (no Redis)
+
+> Note: `lsa_tdx_feedback` still declares a Redis gem dependency, so `redis` may appear transitively in `Gemfile.lock`, but the application no longer uses Redis for jobs, cache, or Action Cable.
## Installation
@@ -37,14 +42,16 @@ In its initial implementation, LSA Evaluate will serve the University of Michiga
3. **Set up the database:**
```bash
- rails db:create db:migrate
-
+ rails db:create db:prepare
```
-4. **Run the server:**
+ This creates the primary database plus queue, cache, and cable databases and loads their schemas.
+
+4. **Build assets and run the server:**
```bash
- rails server
+ yarn build && yarn build:css
+ bin/dev
```
## Email Configuration with SendGrid
@@ -59,9 +66,17 @@ This application uses SendGrid for email delivery in the production environment.
SENDGRID_API_KEY=your_sendgrid_api_key_here
DOMAIN_NAME=yourdomain.com
```
-4. Ensure Sidekiq is set up and running to process emails asynchronously
+4. Solid Queue runs inside Puma in production/staging and processes `deliver_later` mailers asynchronously. Monitor jobs at `/jobs` (axis_mundi users).
+
+## Production / Staging Ops Notes
+
+Before deploying:
-Emails are automatically configured to be sent asynchronously through Sidekiq background jobs.
+1. Install Ruby 4.0.6 via asdf on the host.
+2. Create MySQL databases: `evaluate_db_prod_queue`, `evaluate_db_prod_cache`, `evaluate_db_prod_cable` (and staging equivalents / `QUEUE_DATABASE_URL`, `CACHE_DATABASE_URL`, `CABLE_DATABASE_URL` if separate).
+3. Run `rails db:prepare` (or load `db/queue_schema.rb`, `db/cache_schema.rb`, `db/cable_schema.rb`) on those databases.
+4. Stop and remove Sidekiq/Redis once in-flight Sidekiq jobs are drained.
+5. Restart Puma after deploy — Solid Queue is enabled via the Puma plugin.
## Protected Branches and Pre-Push Hook
diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js
deleted file mode 100644
index 6d8bb9fb..00000000
--- a/app/assets/config/manifest.js
+++ /dev/null
@@ -1,4 +0,0 @@
-//= link_tree ../images
-//= link_tree ../builds
-//= link_tree ../stylesheets
-//= link judge_dashboard.css
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 8b9a45a9..582b7917 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -2,7 +2,7 @@
class ApplicationController < ActionController::Base
include Pundit::Authorization
include ApplicationHelper
- include Pagy::Backend
+ include Pagy::Method
before_action :authenticate_user!
before_action :set_sentry_context
@@ -68,11 +68,14 @@ def log_exception(exception)
end
def set_sentry_context
- Sentry.set_user(id: current_user.id, email: current_user.email) if current_user
+ if current_user
+ Sentry.set_user(id: current_user.id, email: current_user.email)
+ end
+
Sentry.set_context('request', {
controller: controller_name,
action: action_name,
- params: params.except(:controller, :action)
+ params: request.filtered_parameters.except('controller', 'action')
})
end
end
diff --git a/app/controllers/users_dashboard_controller.rb b/app/controllers/users_dashboard_controller.rb
index 0b4a1ddf..2889e3f0 100644
--- a/app/controllers/users_dashboard_controller.rb
+++ b/app/controllers/users_dashboard_controller.rb
@@ -16,12 +16,17 @@ def index
end
@pagy, @users = pagy(
+ :offset,
users.order(sort_column => sort_direction),
- items: 20, # Explicitly set number of items per page
- params: {
- principal_name_filter: params[:principal_name_filter],
- email_filter: params[:email_filter]
- }.compact # Pass filters to pagination links
+ limit: 20,
+ querify: lambda { |p|
+ p.merge!(
+ {
+ 'principal_name_filter' => params[:principal_name_filter],
+ 'email_filter' => params[:email_filter]
+ }.compact
+ )
+ }
)
end
diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb
index 7aa73495..9c620dc9 100644
--- a/app/helpers/sentry_helper.rb
+++ b/app/helpers/sentry_helper.rb
@@ -1,4 +1,20 @@
+# frozen_string_literal: true
+
module SentryHelper
+ def sentry_meta_tags
+ dsn = Rails.application.credentials.dig(:sentry, :dsn)
+ return if dsn.blank?
+ return unless Sentry.configuration.enabled_environments.include?(Rails.env)
+
+ safe_join(
+ [
+ tag.meta(name: 'sentry-dsn', content: dsn),
+ tag.meta(name: 'sentry-environment', content: Rails.env),
+ tag.meta(name: 'sentry-release', content: SentryRelease.current.to_s)
+ ]
+ )
+ end
+
def set_sentry_user(user)
Sentry.set_user(id: user.id, email: user.email)
end
diff --git a/app/helpers/users_dashboard_helper.rb b/app/helpers/users_dashboard_helper.rb
index 980213c8..78618034 100644
--- a/app/helpers/users_dashboard_helper.rb
+++ b/app/helpers/users_dashboard_helper.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
module UsersDashboardHelper
- include Pagy::Frontend
-
def sort_link(column, title = nil)
title ||= column.titleize
diff --git a/app/javascript/application.js b/app/javascript/application.js
index dc744f82..94b59ef5 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -1,4 +1,5 @@
// Entry point for the build script in your package.json
+import * as Sentry from "@sentry/browser"
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
@@ -7,4 +8,39 @@ import Sortable from "sortablejs"
import "trix"
import "@rails/actiontext"
+const sentryDsn = document.querySelector('meta[name="sentry-dsn"]')?.content
+
+if (sentryDsn) {
+ const environment =
+ document.querySelector('meta[name="sentry-environment"]')?.content || "production"
+ const release = document.querySelector('meta[name="sentry-release"]')?.content
+ const isProduction = environment === "production"
+
+ Sentry.init({
+ dsn: sentryDsn,
+ environment,
+ release: release || undefined,
+ integrations: [Sentry.browserTracingIntegration()],
+ // Match server-side sampling; keep all browser errors
+ tracesSampleRate: isProduction ? 0.1 : 1.0,
+ // Ignore common extension / noise errors
+ ignoreErrors: [
+ "ResizeObserver loop limit exceeded",
+ "ResizeObserver loop completed with undelivered notifications",
+ "Non-Error promise rejection captured",
+ /^Loading CSS chunk/,
+ /^Loading chunk/
+ ],
+ beforeSend(event) {
+ // Drop events from browser extensions
+ if (event.exception?.values?.some((ex) =>
+ /chrome-extension:|moz-extension:|safari-extension:/i.test(ex.stacktrace?.frames?.[0]?.filename || "")
+ )) {
+ return null
+ }
+ return event
+ }
+ })
+}
+
window.Sortable = Sortable;
diff --git a/app/views/judge_dashboard/index.html.erb b/app/views/judge_dashboard/index.html.erb
index b610b0d9..bd84f036 100644
--- a/app/views/judge_dashboard/index.html.erb
+++ b/app/views/judge_dashboard/index.html.erb
@@ -1,7 +1,3 @@
-<% content_for :head do %>
- <%= stylesheet_link_tag "judge_dashboard", "data-turbo-track": "reload" %>
-<% end %>
-
Judge Dashboard
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 28cc247e..74187792 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -6,7 +6,8 @@
<%= lsa_tdx_feedback_css %>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
- <%= stylesheet_link_tag "application.bootstrap", "data-turbo-track": "reload" %>
+ <%= sentry_meta_tags %>
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
<%= yield(:head) %>
<%= Sentry.get_trace_propagation_meta.html_safe %>
diff --git a/app/views/users_dashboard/index.html.erb b/app/views/users_dashboard/index.html.erb
index 3e15f096..4d7f201c 100644
--- a/app/views/users_dashboard/index.html.erb
+++ b/app/views/users_dashboard/index.html.erb
@@ -77,7 +77,7 @@
- <%== pagy_bootstrap_nav(@pagy) %>
+ <%== @pagy.series_nav(:bootstrap) %>
diff --git a/app/views/users_dashboard/show.html.erb b/app/views/users_dashboard/show.html.erb
index 42162a92..dd565e95 100644
--- a/app/views/users_dashboard/show.html.erb
+++ b/app/views/users_dashboard/show.html.erb
@@ -28,6 +28,17 @@
Uniqname
<%= @user.uniqname %>
+
+ Affiliations
+
+ <% if @user.affiliations.any? %>
+ <% @user.affiliations.each do |affiliation| %>
+ <%= affiliation.name %>
+ <% end %>
+ <% else %>
+ None
+ <% end %>
+
diff --git a/bin/jobs b/bin/jobs
new file mode 100755
index 00000000..dcf59f30
--- /dev/null
+++ b/bin/jobs
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+
+require_relative "../config/environment"
+require "solid_queue/cli"
+
+SolidQueue::Cli.start(ARGV)
diff --git a/config/application.rb b/config/application.rb
index c8ac06d7..dd748614 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -25,9 +25,11 @@
module LsaEvaluate
class Application < Rails::Application # rubocop:disable Style/Documentation
# Initialize configuration defaults for originally generated Rails version.
- config.load_defaults 7.0
- # Add lib to the eager load paths
- config.eager_load_paths << Rails.root.join('lib')
+ config.load_defaults 8.1
+
+ # Autoload lib/ (ignore non-Ruby dirs). Rack::Defense is also required above for boot.
+ config.autoload_lib(ignore: %w[assets tasks capistrano])
+
config.time_zone = 'Eastern Time (US & Canada)'
config.active_record.default_timezone = :utc
config.exceptions_app = self.routes
@@ -40,21 +42,9 @@ class Application < Rails::Application # rubocop:disable Style/Documentation
#
# These settings can be overridden in specific environments using the files
# in config/environments, which are processed later.
- #
- # config.time_zone = "Central Time (US & Canada)"
- # config.eager_load_paths << Rails.root.join("extras")
- # Don't generate system test files.
config.generators.system_tests = nil
# Add security middleware
config.middleware.use Rack::Defense
-
- # Configure rate limiting
- config.action_dispatch.rate_limiter = {
- limit: 300,
- period: 5.minutes,
- store: :redis,
- key: ->(request) { request.ip }
- }
end
end
diff --git a/config/cable.yml b/config/cable.yml
index 8acd62fe..8fffefde 100644
--- a/config/cable.yml
+++ b/config/cable.yml
@@ -1,11 +1,25 @@
+# Async adapter only works within the same process, so for manually triggering cable updates from a console,
+# and seeing results in the browser, you must do so from the web console (running inside the dev process),
+# not a terminal started via bin/rails console! Add "console" to any action or any ERB template view
+# to make the web console appear.
development:
- adapter: redis
- url: redis://:<%= Rails.application.credentials.redis_password %>@localhost:6379/1
+ adapter: async
test:
adapter: test
+staging:
+ adapter: solid_cable
+ connects_to:
+ database:
+ writing: cable
+ polling_interval: 0.1.seconds
+ message_retention: 1.day
+
production:
- adapter: redis
- url: <%= ENV.fetch("REDIS_URL") { "redis://:#{Rails.application.credentials.redis_password}@localhost:6379/1" } %>
- channel_prefix: lsa_evaluate_production
+ adapter: solid_cable
+ connects_to:
+ database:
+ writing: cable
+ polling_interval: 0.1.seconds
+ message_retention: 1.day
diff --git a/config/cache.yml b/config/cache.yml
new file mode 100644
index 00000000..344c2ccf
--- /dev/null
+++ b/config/cache.yml
@@ -0,0 +1,20 @@
+default: &default
+ store_options:
+ # Cap age of oldest cache entry to fulfill retention policies
+ # max_age: <%= 60.days.to_i %>
+ max_size: <%= 256.megabytes %>
+ namespace: <%= Rails.env %>
+
+development:
+ <<: *default
+
+test:
+ <<: *default
+
+production:
+ database: cache
+ <<: *default
+
+staging:
+ database: cache
+ <<: *default
diff --git a/config/database.yml b/config/database.yml
index f82e2e44..f2391174 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -1,4 +1,3 @@
-
default: &default
adapter: mysql2
encoding: utf8mb4
@@ -9,26 +8,102 @@ default: &default
port: 3306
development:
- <<: *default
- database: lsa_evaluate_development
+ primary:
+ <<: *default
+ database: lsa_evaluate_development
+ queue:
+ <<: *default
+ database: lsa_evaluate_development_queue
+ migrations_paths: db/queue_migrate
+ cache:
+ <<: *default
+ database: lsa_evaluate_development_cache
+ migrations_paths: db/cache_migrate
+ cable:
+ <<: *default
+ database: lsa_evaluate_development_cable
+ migrations_paths: db/cable_migrate
development_feature:
- <<: *default
- database: lsa_evaluate_development_feature
+ primary:
+ <<: *default
+ database: lsa_evaluate_development_feature
+ queue:
+ <<: *default
+ database: lsa_evaluate_development_feature_queue
+ migrations_paths: db/queue_migrate
+ cache:
+ <<: *default
+ database: lsa_evaluate_development_feature_cache
+ migrations_paths: db/cache_migrate
+ cable:
+ <<: *default
+ database: lsa_evaluate_development_feature_cable
+ migrations_paths: db/cable_migrate
test:
- <<: *default
- database: lsa_evaluate_test
+ primary:
+ <<: *default
+ database: lsa_evaluate_test
+ queue:
+ <<: *default
+ database: lsa_evaluate_test_queue
+ migrations_paths: db/queue_migrate
+ cache:
+ <<: *default
+ database: lsa_evaluate_test_cache
+ migrations_paths: db/cache_migrate
+ cable:
+ <<: *default
+ database: lsa_evaluate_test_cable
+ migrations_paths: db/cable_migrate
staging:
- <<: *default
- url: <%= ENV["DATABASE_URL"] %>
+ primary:
+ <<: *default
+ url: <%= ENV["DATABASE_URL"] %>
+ queue:
+ <<: *default
+ url: <%= ENV.fetch("QUEUE_DATABASE_URL") { ENV["DATABASE_URL"] } %>
+ migrations_paths: db/queue_migrate
+ cache:
+ <<: *default
+ url: <%= ENV.fetch("CACHE_DATABASE_URL") { ENV["DATABASE_URL"] } %>
+ migrations_paths: db/cache_migrate
+ cable:
+ <<: *default
+ url: <%= ENV.fetch("CABLE_DATABASE_URL") { ENV["DATABASE_URL"] } %>
+ migrations_paths: db/cable_migrate
production:
- <<: *default
- # url: <%= ENV["DATABASE_URL"] %>
- database: evaluate_db_prod
- username: <%= Rails.application.credentials.mysql[:prod_user] %>
- password: <%= Rails.application.credentials.mysql[:prod_password] %>
- host: <%= Rails.application.credentials.mysql[:prod_servername] %>
- sslca: <%= Rails.application.credentials.mysql[:prod_sslca] %>
+ primary:
+ <<: *default
+ database: evaluate_db_prod
+ username: <%= Rails.application.credentials.dig(:mysql, :prod_user) %>
+ password: <%= Rails.application.credentials.dig(:mysql, :prod_password) %>
+ host: <%= Rails.application.credentials.dig(:mysql, :prod_servername) %>
+ sslca: <%= Rails.application.credentials.dig(:mysql, :prod_sslca) %>
+ queue:
+ <<: *default
+ database: evaluate_db_prod_queue
+ username: <%= Rails.application.credentials.dig(:mysql, :prod_user) %>
+ password: <%= Rails.application.credentials.dig(:mysql, :prod_password) %>
+ host: <%= Rails.application.credentials.dig(:mysql, :prod_servername) %>
+ sslca: <%= Rails.application.credentials.dig(:mysql, :prod_sslca) %>
+ migrations_paths: db/queue_migrate
+ cache:
+ <<: *default
+ database: evaluate_db_prod_cache
+ username: <%= Rails.application.credentials.dig(:mysql, :prod_user) %>
+ password: <%= Rails.application.credentials.dig(:mysql, :prod_password) %>
+ host: <%= Rails.application.credentials.dig(:mysql, :prod_servername) %>
+ sslca: <%= Rails.application.credentials.dig(:mysql, :prod_sslca) %>
+ migrations_paths: db/cache_migrate
+ cable:
+ <<: *default
+ database: evaluate_db_prod_cable
+ username: <%= Rails.application.credentials.dig(:mysql, :prod_user) %>
+ password: <%= Rails.application.credentials.dig(:mysql, :prod_password) %>
+ host: <%= Rails.application.credentials.dig(:mysql, :prod_servername) %>
+ sslca: <%= Rails.application.credentials.dig(:mysql, :prod_sslca) %>
+ migrations_paths: db/cable_migrate
diff --git a/config/deploy.rb b/config/deploy.rb
index e17536a5..4fb00258 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -2,7 +2,6 @@
lock '~> 3.20.0'
set :default_env, {
- 'NODE_OPTIONS' => '--openssl-legacy-provider',
'PATH' => '$HOME/.asdf/shims:$HOME/.asdf/bin:$PATH',
'SKIP_ECHO_ENV' => 'true',
'SKIP_SEEDS' => 'true'
@@ -48,7 +47,7 @@
desc 'Start the PUMA service'
task :start do
on roles(:app) do
- puts 'You must intially start the puma service using sudo on the server'
+ puts 'You must initially start the puma service using sudo on the server'
end
end
end
@@ -78,7 +77,6 @@
before 'bundler:install', 'debug:print_ruby_version'
before :starting, :check_revision
after :finishing, 'puma:restart'
- after :finishing, 'sidekiq:manual_restart'
end
namespace :debug do
@@ -95,13 +93,11 @@
on roles(:app) do
within current_path do
with rails_env: fetch(:rails_env) do
- # Capture the output of each command
ruby_version = capture(:ruby, '-v')
which_ruby = capture(:which, 'ruby')
asdf_ruby_list = capture(:asdf, 'list ruby')
rails_version = capture(:bundle, 'exec rails -v')
- # Log the captured outputs
info "Ruby Version: #{ruby_version.strip}"
info "Ruby Path: #{which_ruby.strip}"
info "asdf Ruby Versions: #{asdf_ruby_list.strip}"
@@ -112,19 +108,6 @@
end
end
-namespace :sidekiq do
- desc 'Manually restart Sidekiq service'
- task :manual_restart do
- on roles(:app) do
- puts "\n\n=========================================================="
- puts "IMPORTANT: Sidekiq needs to be restarted manually!"
- puts "Please run the following command on the server as a user with sudo privileges:"
- puts "sudo systemctl restart sidekiq"
- puts "==========================================================\n\n"
- end
- end
-end
-
namespace :maintenance do
desc 'Maintenance start (edit config/maintenance_template.yml to provide parameters)'
task :start do
diff --git a/config/deploy/templates/sidekiq.service.erb b/config/deploy/templates/sidekiq.service.erb
deleted file mode 100644
index cacc0102..00000000
--- a/config/deploy/templates/sidekiq.service.erb
+++ /dev/null
@@ -1,22 +0,0 @@
-[Unit]
-Description=sidekiq
-After=syslog.target network.target
-
-[Service]
-Type=simple
-WorkingDirectory=<%= current_path %>
-ExecStart=/bin/bash -lc 'bundle exec sidekiq -e <%= fetch(:rails_env) %> -C <%= current_path %>/config/sidekiq.yml'
-User=<%= fetch(:user) %>
-Group=<%= fetch(:user) %>
-UMask=0002
-RestartSec=1
-Restart=on-failure
-StandardOutput=append:<%= shared_path %>/log/sidekiq.log
-StandardError=append:<%= shared_path %>/log/sidekiq.error.log
-
-# Greatly reduce Ruby memory fragmentation and heap usage
-# https://www.mikeperham.com/2018/04/25/taming-rails-memory-bloat/
-Environment=MALLOC_ARENA_MAX=2
-
-[Install]
-WantedBy=multi-user.target
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 7715d246..62db8f8e 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -8,7 +8,7 @@
# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
- config.cache_classes = false
+ config.enable_reloading = true
# Do not eager load code on boot.
config.eager_load = false
@@ -70,9 +70,6 @@
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
- # Suppress logger output for asset requests.
- config.assets.quiet = true
-
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 92c4f0f3..e69144f1 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -6,7 +6,7 @@
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
- config.cache_classes = true
+ config.enable_reloading = false
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
@@ -30,7 +30,7 @@
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
- config.assets.compile = false
+ # config.assets.compile = false
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.asset_host = "http://assets.example.com"
@@ -62,10 +62,11 @@
config.log_tags = [ :request_id ]
# Use a different cache store in production.
- # config.cache_store = :mem_cache_store
+ config.cache_store = :solid_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment).
- config.active_job.queue_adapter = :sidekiq
+ config.active_job.queue_adapter = :solid_queue
+ config.solid_queue.connects_to = { database: { writing: :queue } }
config.active_job.queue_name_prefix = 'lsa_evaluate_production'
config.action_mailer.perform_caching = false
diff --git a/config/environments/staging.rb b/config/environments/staging.rb
index 51ad6747..c210ef76 100644
--- a/config/environments/staging.rb
+++ b/config/environments/staging.rb
@@ -6,7 +6,7 @@
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
- config.cache_classes = true
+ config.enable_reloading = false
# Eager load code on boot.
config.eager_load = true
@@ -26,7 +26,7 @@
# config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
- config.assets.compile = false
+ # config.assets.compile = false
# Store uploaded files on the local file system.
config.active_storage.service = :local
@@ -47,9 +47,6 @@
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = Logger::Formatter.new
- # Caching
- # config.cache_store = :mem_cache_store
-
# Action Mailer
host = 'https://evaluate-staging.lsa.umich.edu/'
config.action_mailer.default_url_options = { host: host }
@@ -58,7 +55,7 @@
config.action_mailer.delivery_method = :letter_opener_web
config.action_mailer.perform_deliveries = true
-# Enable mailer previews (protected by authorization in config/initializers/mailer_previews.rb)
+ # Enable mailer previews (protected by authorization in config/initializers/mailer_previews.rb)
config.action_mailer.show_previews = true
# I18n
@@ -67,8 +64,10 @@
# Deprecations
config.active_support.report_deprecations = false
- # Active Job
- config.active_job.queue_adapter = :sidekiq
+ # Use Solid Cache and Solid Queue (no Redis / Sidekiq)
+ config.cache_store = :solid_cache_store
+ config.active_job.queue_adapter = :solid_queue
+ config.solid_queue.connects_to = { database: { writing: :queue } }
config.active_job.queue_name_prefix = 'lsa_evaluate_staging'
# Do not dump schema after migrations.
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 56275067..761a60d1 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -11,7 +11,7 @@
# Settings specified here will take precedence over those in config/application.rb.
# Turn false under Spring and add config.action_view.cache_template_loading = true.
- config.cache_classes = true
+ config.enable_reloading = false
# Eager loading loads your whole application. When running a single test locally,
# this probably isn't necessary. It's a good idea to do in a continuous integration
diff --git a/config/initializers/action_mailer.rb b/config/initializers/action_mailer.rb
index 77ee2e8c..e5b37775 100644
--- a/config/initializers/action_mailer.rb
+++ b/config/initializers/action_mailer.rb
@@ -1,32 +1,19 @@
-# frozen_string_literal: true
-
-# Configure ActionMailer to deliver emails asynchronously using Sidekiq
+# Configure ActionMailer to deliver emails asynchronously via Active Job / Solid Queue
Rails.application.config.action_mailer.deliver_later_queue_name = :default
# Extend ActionMailer to use mail delivery logging
-ActionMailer::Base.class_eval do
- # Add delivery tracking method
- def notify_delivered
- if self.class.delivery_notification_method
- send(self.class.delivery_notification_method, message)
- end
- end
-
- # Hook into the delivery process
- # Remove problematic observer and interceptor
- # self.register_observer(-> (message) { message.delivery_method.settings[:return_response] = true })
- # self.register_interceptor(-> (message) { message })
- # Remove problematic observer that causes recursion
- # self.register_observer(-> (message) {
- # message.deliver_later unless Rails.env.test?
- # })
-
+ActiveSupport.on_load(:action_mailer) do
class << self
attr_accessor :delivery_notification_method
- # Method to set notification callback
def notify_on_delivery(method_name)
self.delivery_notification_method = method_name
end
end
+
+ def notify_delivered
+ return unless self.class.delivery_notification_method
+
+ send(self.class.delivery_notification_method, message)
+ end
end
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
index 3dc3e275..6e95be16 100644
--- a/config/initializers/assets.rb
+++ b/config/initializers/assets.rb
@@ -1,21 +1,4 @@
# frozen_string_literal: true
-# Be sure to restart your server when you modify this file.
-
-# Version of your assets, change this if you want to expire all your assets.
-Rails.application.config.assets.version = '1.0'
-
-# Add node_modules to the asset load path for Sprockets
-Rails.application.config.assets.paths << Rails.root.join('node_modules')
-
-# Add additional assets to the asset load path.
-Rails.application.config.assets.paths << Rails.root.join('node_modules/bootstrap-icons/font')
-
-# Adjust precompilation settings
-Rails.application.config.assets.precompile -= %w[ actiontext.css actiontext.scss ]
-Rails.application.config.assets.precompile += %w[ application.bootstrap.css ]
-
-# Precompile additional assets.
-# application.js, application.bootstrap.css, and all non-JS/CSS in the app/assets
-# folder are already added.
-# Rails.application.config.assets.precompile += %w( admin.js admin.css )
+# Propshaft serves digests from app/assets and app/assets/builds.
+# CSS/JS are built by cssbundling-rails and jsbundling-rails into app/assets/builds.
diff --git a/config/initializers/letter_opener_sidekiq.rb b/config/initializers/letter_opener_sidekiq.rb
deleted file mode 100644
index bc987821..00000000
--- a/config/initializers/letter_opener_sidekiq.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# Special configuration for letter_opener with Sidekiq in staging
-if Rails.env.staging?
- require 'letter_opener_web'
-
- # Ensure delivery_method settings are accessible in Sidekiq threads
- Sidekiq.configure_server do |config|
- config.client_middleware do |chain|
- chain.add(Class.new do
- def call(worker_class, job, queue, redis_pool)
- if worker_class == 'ActionMailer::MailDeliveryJob' && Rails.application.config.action_mailer.delivery_method == :letter_opener_web
- # Ensure ActionMailer's settings are correctly set in the Sidekiq context
- ActionMailer::Base.delivery_method = :letter_opener_web
- end
- yield
- end
- end)
- end
- end
-end
diff --git a/config/initializers/mission_control_jobs.rb b/config/initializers/mission_control_jobs.rb
new file mode 100644
index 00000000..ebcb3910
--- /dev/null
+++ b/config/initializers/mission_control_jobs.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+# Devise already protects the /jobs mount; disable Mission Control HTTP basic auth.
+MissionControl::Jobs.http_basic_auth_enabled = false
diff --git a/config/initializers/pagy.rb b/config/initializers/pagy.rb
index 96edce74..16e75dfd 100644
--- a/config/initializers/pagy.rb
+++ b/config/initializers/pagy.rb
@@ -1,10 +1,7 @@
# frozen_string_literal: true
-# Pagy initializer file
-# See https://ddnexus.github.io/pagy/api/pagy#configuration
+# Pagy 43+ configuration
+# See https://ddnexus.github.io/pagy/guides/upgrade-guide/
-require 'pagy/extras/bootstrap' # Add bootstrap extra for styled pagination
-require 'pagy/extras/items' # Allow users to change the number of items per page
-
-Pagy::DEFAULT[:items] = 20 # Default items per page
-Pagy::DEFAULT[:max_items] = 100 # Max items per page to prevent abuse
+Pagy::OPTIONS[:limit] = 20
+Pagy::OPTIONS[:max_limit] = 100
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 7e93053b..bb70110a 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -1,121 +1,71 @@
# frozen_string_literal: true
+require_relative '../../lib/sentry_release'
+
Sentry.init do |config|
- # Use credentials instead of ENV or hardcoded value
config.dsn = Rails.application.credentials.dig(:sentry, :dsn)
# Only enable in production and staging environments
config.enabled_environments = %w[production staging]
- # Environment-specific configuration
config.environment = Rails.env
- config.release = Rails.application.class.module_parent_name.underscore.dasherize
+ config.release = SentryRelease.current
+ config.server_name = Socket.gethostname
- # Logging configuration
- config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
+ config.breadcrumbs_logger = %i[active_support_logger http_logger]
- # Enable logging integration (this is the correct way for sentry-rails)
- # The sentry-rails gem automatically integrates with Rails logger
- # To send logs to Sentry, use: Sentry.capture_message("Your log message", level: :info)
+ # Keep PII off by default; user id/email are set explicitly in ApplicationController
+ config.send_default_pii = false
- # Add user context data (PII) - be careful with this in production
- config.send_default_pii = Rails.env.development? || Rails.env.test?
+ # Capture all errors — sample only performance data (high-traffic apps still need full error coverage)
+ config.sample_rate = 1.0
- # Server name for better debugging
- config.server_name = Socket.gethostname
+ config.max_breadcrumbs = 50
- # Performance monitoring
- # In production, you might want to lower this to something like 0.1 (10%)
- # depending on your traffic volume
- config.traces_sample_rate = Rails.env.production? ? 0.1 : 1.0
+ # Dynamic trace sampling (takes precedence over traces_sample_rate)
+ # sentry-rails already excludes RoutingError / UnknownFormat / etc. from error reporting
+ config.traces_sampler = lambda do |context|
+ transaction_name = context.dig(:transaction_context, :name).to_s
- # Profile sampling - adjust based on your needs
- config.profiles_sample_rate = Rails.env.production? ? 0.1 : 1.0
+ return 0.0 if transaction_name.include?('health_check')
+ return 0.0 if transaction_name.match?(/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$/i)
- # Custom sampling logic if needed
- config.traces_sampler = lambda do |context|
- # Don't sample health check endpoints
- if context[:transaction_context][:name]&.include?('health_check')
- 0.0
- else
- # Sample based on environment
- Rails.env.production? ? 0.1 : 1.0
- end
+ # Respect parent sampling decision for distributed traces (browser → Rails)
+ return context[:parent_sampled] unless context[:parent_sampled].nil?
+
+ Rails.env.production? ? 0.1 : 1.0
end
- # Add additional context to errors
+ # Relative to sampled traces; requires the stackprof gem (already in Gemfile)
+ config.profiles_sample_rate = Rails.env.production? ? 0.1 : 1.0
+
config.before_send = lambda do |event, _hint|
- # Add request context
- if event.request
- event.request.data = {
- user_agent: event.request.headers['User-Agent'],
- referer: event.request.headers['Referer'],
- remote_addr: event.request.headers['X-Forwarded-For'] || event.request.headers['Remote-Addr']
- }
- end
-
- # Add user context if available
- if defined?(Current) && Current.user
- event.user = {
- id: Current.user.id,
- email: Current.user.email,
- username: Current.user.try(:username)
- }
- end
-
- # Add custom tags
- event.tags = event.tags.merge(
+ event.tags = (event.tags || {}).merge(
environment: Rails.env,
- version: Rails.application.class.module_parent_name.underscore.dasherize
+ app: 'lsa-evaluate'
)
- # Filter out sensitive data
- if event.exception
- event.exception.values.each do |exception|
- next unless exception.value
- # Remove potential passwords, tokens, etc.
- exception.value.gsub!(/password[=:]\s*[^\s&]+/i, 'password=[FILTERED]')
- exception.value.gsub!(/token[=:]\s*[^\s&]+/i, 'token=[FILTERED]')
- exception.value.gsub!(/secret[=:]\s*[^\s&]+/i, 'secret=[FILTERED]')
- end
- end
-
event
end
- # Configure backtrace cleanup
- config.backtrace_cleanup_callback = lambda do |backtrace|
- Rails.backtrace_cleaner.clean(backtrace)
- end
-
- # Configure error filtering
config.before_send_transaction = lambda do |event, _hint|
- # Filter out health check transactions
return nil if event.transaction&.include?('health_check')
-
- # Filter out static asset requests
- return nil if event.transaction&.match?(/\.(css|js|png|jpg|jpeg|gif|ico|svg)$/)
+ return nil if event.transaction&.match?(/\.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$/i)
event
end
- # Configure breadcrumb filtering
config.before_breadcrumb = lambda do |breadcrumb, _hint|
- # Filter out sensitive breadcrumbs
return nil if breadcrumb.message&.match?(/password|token|secret/i)
-
- # Filter out noisy breadcrumbs
return nil if breadcrumb.message&.match?(/SELECT.*FROM.*users/i)
breadcrumb
end
- # Configure sampling for different types of events
- config.sample_rate = Rails.env.production? ? 0.1 : 1.0
-
- # Configure max breadcrumbs
- config.max_breadcrumbs = 50
+ config.backtrace_cleanup_callback = lambda do |backtrace|
+ Rails.backtrace_cleaner.clean(backtrace)
+ end
- # Configure debug mode (only in development)
- config.debug = Rails.env.development?
+ # Useful on staging when diagnosing SDK issues (Sentry is disabled in development)
+ config.debug = Rails.env.staging?
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
deleted file mode 100644
index f1b4755b..00000000
--- a/config/initializers/sidekiq.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'sidekiq'
-
-# Configure Redis connection
-redis_config = {
- url: ENV.fetch('REDIS_URL') { "redis://:#{Rails.application.credentials.redis_password}@localhost:6379/1" },
- ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE } # Only if using Redis over SSL
-}
-
-Sidekiq.configure_server do |config|
- config.redis = redis_config
-
- # Set default retry count for Sidekiq 7.3.x
- # The retry option is now set in the sidekiq.yml file
-end
-
-Sidekiq.configure_client do |config|
- config.redis = redis_config
-end
-
-# Set Sidekiq as the ActiveJob queue adapter
-Rails.application.config.active_job.queue_adapter = :sidekiq
diff --git a/config/puma.rb b/config/puma.rb
index 1713441e..d3a2dfe9 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -43,3 +43,7 @@
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
+
+# Run Solid Queue inside Puma for single-host Capistrano deploys.
+# Avoid ActiveSupport (e.g. present?) here — Puma loads this file before Rails boots.
+plugin :solid_queue if ENV['SOLID_QUEUE_IN_PUMA'] || %w[production staging].include?(ENV.fetch('RAILS_ENV', 'development'))
diff --git a/config/queue.yml b/config/queue.yml
new file mode 100644
index 00000000..9eace59c
--- /dev/null
+++ b/config/queue.yml
@@ -0,0 +1,18 @@
+default: &default
+ dispatchers:
+ - polling_interval: 1
+ batch_size: 500
+ workers:
+ - queues: "*"
+ threads: 3
+ processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
+ polling_interval: 0.1
+
+development:
+ <<: *default
+
+test:
+ <<: *default
+
+production:
+ <<: *default
diff --git a/config/recurring.yml b/config/recurring.yml
new file mode 100644
index 00000000..a54c4bef
--- /dev/null
+++ b/config/recurring.yml
@@ -0,0 +1,20 @@
+# examples:
+# periodic_cleanup:
+# class: CleanSoftDeletedRecordsJob
+# queue: background
+# args: [ 1000, { batch_size: 500 } ]
+# schedule: every hour
+# periodic_cleanup_with_command:
+# command: "SoftDeletedRecord.due.delete_all"
+# priority: 2
+# schedule: at 5am every day
+
+production:
+ clear_solid_queue_finished_jobs:
+ command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
+ schedule: every hour at minute 12
+
+staging:
+ clear_solid_queue_finished_jobs:
+ command: "SolidQueue::Job.clear_finished_in_batches(sleep_between_batches: 0.3)"
+ schedule: every hour at minute 12
diff --git a/config/routes.rb b/config/routes.rb
index db7af590..647d1c10 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -6,10 +6,9 @@
get 'bulk_contest_instances/create'
root 'static_pages#home'
- # Sidekiq Web UI
- require 'sidekiq/web'
+ # Mission Control Jobs UI (Solid Queue)
authenticate :user, ->(user) { user.axis_mundi? } do
- mount Sidekiq::Web => '/sidekiq'
+ mount MissionControl::Jobs::Engine, at: '/jobs'
end
resources :entries do
diff --git a/config/sidekiq.yml b/config/sidekiq.yml
deleted file mode 100644
index 3c3ec642..00000000
--- a/config/sidekiq.yml
+++ /dev/null
@@ -1,29 +0,0 @@
----
-# Default configuration
-:concurrency: 5
-:retry: 3
-
-# Sidekiq will run this file through ERB when loading it
-:queues:
- - [critical, 3]
- - [default, 2]
- - [<%= ENV['RAILS_ENV'] || 'development' %>, 2]
- - [<%= ENV['RAILS_ENV'] || 'development' %>_mailers, 2]
- - [lsa_evaluate_<%= ENV['RAILS_ENV'] || 'development' %>_default, 2]
- - [lsa_evaluate_<%= ENV['RAILS_ENV'] || 'development' %>_mailers, 2]
- - [low, 1]
-
-# Environment-specific configurations
-development:
- :concurrency: 5
-
-test:
- :concurrency: 1
-
-staging:
- :concurrency: 10
- :retry: 5
-
-production:
- :concurrency: 20
- :retry: 10
diff --git a/db/cable_schema.rb b/db/cable_schema.rb
new file mode 100644
index 00000000..145052dd
--- /dev/null
+++ b/db/cable_schema.rb
@@ -0,0 +1,23 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[8.1].define(version: 1) do
+ create_table "solid_cable_messages", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.binary "channel", limit: 1024, null: false
+ t.bigint "channel_hash", null: false
+ t.datetime "created_at", null: false
+ t.binary "payload", size: :long, null: false
+ t.index ["channel"], name: "index_solid_cable_messages_on_channel"
+ t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash"
+ t.index ["created_at"], name: "index_solid_cable_messages_on_created_at"
+ end
+end
diff --git a/db/cache_schema.rb b/db/cache_schema.rb
new file mode 100644
index 00000000..c3f885ac
--- /dev/null
+++ b/db/cache_schema.rb
@@ -0,0 +1,24 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[8.1].define(version: 1) do
+ create_table "solid_cache_entries", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.integer "byte_size", null: false
+ t.datetime "created_at", null: false
+ t.binary "key", limit: 1024, null: false
+ t.bigint "key_hash", null: false
+ t.binary "value", size: :long, null: false
+ t.index ["byte_size"], name: "index_solid_cache_entries_on_byte_size"
+ t.index ["key_hash", "byte_size"], name: "index_solid_cache_entries_on_key_hash_and_byte_size"
+ t.index ["key_hash"], name: "index_solid_cache_entries_on_key_hash", unique: true
+ end
+end
diff --git a/db/queue_schema.rb b/db/queue_schema.rb
new file mode 100644
index 00000000..73eef229
--- /dev/null
+++ b/db/queue_schema.rb
@@ -0,0 +1,138 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[8.1].define(version: 1) do
+ create_table "solid_queue_blocked_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.string "concurrency_key", null: false
+ t.datetime "created_at", null: false
+ t.datetime "expires_at", null: false
+ t.bigint "job_id", null: false
+ t.integer "priority", default: 0, null: false
+ t.string "queue_name", null: false
+ t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release"
+ t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance"
+ t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true
+ end
+
+ create_table "solid_queue_claimed_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "job_id", null: false
+ t.bigint "process_id"
+ t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true
+ t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id"
+ end
+
+ create_table "solid_queue_failed_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.text "error"
+ t.bigint "job_id", null: false
+ t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true
+ end
+
+ create_table "solid_queue_jobs", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.string "active_job_id"
+ t.text "arguments"
+ t.string "class_name", null: false
+ t.string "concurrency_key"
+ t.datetime "created_at", null: false
+ t.datetime "finished_at"
+ t.integer "priority", default: 0, null: false
+ t.string "queue_name", null: false
+ t.datetime "scheduled_at"
+ t.datetime "updated_at", null: false
+ t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id"
+ t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name"
+ t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at"
+ t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering"
+ t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting"
+ end
+
+ create_table "solid_queue_pauses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.string "queue_name", null: false
+ t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true
+ end
+
+ create_table "solid_queue_processes", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.string "hostname"
+ t.string "kind", null: false
+ t.datetime "last_heartbeat_at", null: false
+ t.text "metadata"
+ t.string "name", null: false
+ t.integer "pid", null: false
+ t.bigint "supervisor_id"
+ t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at"
+ t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true
+ t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id"
+ end
+
+ create_table "solid_queue_ready_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "job_id", null: false
+ t.integer "priority", default: 0, null: false
+ t.string "queue_name", null: false
+ t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true
+ t.index ["priority", "job_id"], name: "index_solid_queue_poll_all"
+ t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue"
+ end
+
+ create_table "solid_queue_recurring_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "job_id", null: false
+ t.datetime "run_at", null: false
+ t.string "task_key", null: false
+ t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true
+ t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true
+ end
+
+ create_table "solid_queue_recurring_tasks", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.text "arguments"
+ t.string "class_name"
+ t.string "command", limit: 2048
+ t.datetime "created_at", null: false
+ t.text "description"
+ t.string "key", null: false
+ t.integer "priority", default: 0
+ t.string "queue_name"
+ t.string "schedule", null: false
+ t.boolean "static", default: true, null: false
+ t.datetime "updated_at", null: false
+ t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true
+ t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static"
+ end
+
+ create_table "solid_queue_scheduled_executions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.bigint "job_id", null: false
+ t.integer "priority", default: 0, null: false
+ t.string "queue_name", null: false
+ t.datetime "scheduled_at", null: false
+ t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true
+ t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all"
+ end
+
+ create_table "solid_queue_semaphores", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "expires_at", null: false
+ t.string "key", null: false
+ t.datetime "updated_at", null: false
+ t.integer "value", default: 1, null: false
+ t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at"
+ t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value"
+ t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true
+ end
+
+ add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+ add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade
+end
diff --git a/db/schema.rb b/db/schema.rb
index 7b6ddcba..de7e0f1f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,36 +10,36 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[7.2].define(version: 2026_01_13_165213) do
+ActiveRecord::Schema[8.1].define(version: 2026_01_13_165213) do
create_table "action_text_rich_texts", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "name", null: false
t.text "body", size: :long
- t.string "record_type", null: false
- t.bigint "record_id", null: false
t.datetime "created_at", null: false
+ t.string "name", null: false
+ t.bigint "record_id", null: false
+ t.string "record_type", null: false
t.datetime "updated_at", null: false
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "name", null: false
- t.string "record_type", null: false
- t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
+ t.string "name", null: false
+ t.bigint "record_id", null: false
+ t.string "record_type", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "key", null: false
- t.string "filename", null: false
- t.string "content_type"
- t.text "metadata"
- t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
+ t.string "content_type"
t.datetime "created_at", null: false
+ t.string "filename", null: false
+ t.string "key", null: false
+ t.text "metadata"
+ t.string "service_name", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
@@ -50,19 +50,19 @@
end
create_table "affiliations", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "name"
- t.bigint "user_id", null: false
t.datetime "created_at", null: false
+ t.string "name"
t.datetime "updated_at", null: false
+ t.bigint "user_id", null: false
t.index ["user_id"], name: "index_affiliations_on_user_id"
end
create_table "assignments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.bigint "user_id", null: false
t.bigint "container_id", null: false
- t.bigint "role_id", null: false
t.datetime "created_at", null: false
+ t.bigint "role_id", null: false
t.datetime "updated_at", null: false
+ t.bigint "user_id", null: false
t.index ["container_id"], name: "index_assignments_on_container_id"
t.index ["role_id", "user_id", "container_id"], name: "index_assignments_on_role_user_container", unique: true
t.index ["role_id"], name: "index_assignments_on_role_id"
@@ -71,8 +71,8 @@
end
create_table "campuses", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "campus_descr", null: false
t.integer "campus_cd", null: false
+ t.string "campus_descr", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["campus_cd"], name: "campus_cd_unq_idx", unique: true
@@ -82,9 +82,9 @@
end
create_table "categories", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "kind"
- t.text "description"
t.datetime "created_at", null: false
+ t.text "description"
+ t.string "kind"
t.datetime "updated_at", null: false
t.index ["kind"], name: "index_categories_on_kind", unique: true
end
@@ -100,8 +100,8 @@
end
create_table "class_level_requirements", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.bigint "contest_instance_id", null: false
t.bigint "class_level_id", null: false
+ t.bigint "contest_instance_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["class_level_id"], name: "index_class_level_requirements_on_class_level_id"
@@ -110,89 +110,89 @@
end
create_table "class_levels", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "name", null: false
- t.text "description", null: false
t.datetime "created_at", null: false
+ t.text "description", null: false
+ t.string "name", null: false
t.datetime "updated_at", null: false
end
create_table "containers", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.string "contact_email"
+ t.datetime "created_at", null: false
+ t.bigint "department_id", null: false
t.string "name"
t.text "notes"
- t.bigint "department_id", null: false
- t.bigint "visibility_id", null: false
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.string "contact_email"
+ t.bigint "visibility_id", null: false
t.index ["department_id"], name: "index_containers_on_department_id"
t.index ["visibility_id"], name: "index_containers_on_visibility_id"
end
create_table "contest_descriptions", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.boolean "active", default: false, null: false
+ t.boolean "archived", default: false, null: false
t.bigint "container_id", null: false
+ t.datetime "created_at", null: false
+ t.string "created_by", null: false
t.string "name", null: false
t.string "short_name"
- t.string "created_by", null: false
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.boolean "active", default: false, null: false
- t.boolean "archived", default: false, null: false
t.index ["container_id"], name: "index_contest_descriptions_on_container_id"
end
create_table "contest_instances", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.boolean "active", default: false, null: false
+ t.boolean "archived", default: false, null: false
t.bigint "contest_description_id", null: false
- t.datetime "date_open", null: false
+ t.text "course_requirement_description"
+ t.datetime "created_at", null: false
+ t.string "created_by"
t.datetime "date_closed", null: false
- t.text "notes"
+ t.datetime "date_open", null: false
t.boolean "has_course_requirement", default: false, null: false
- t.text "course_requirement_description"
+ t.integer "maximum_number_entries_per_applicant", default: 1, null: false
+ t.text "notes"
t.boolean "recletter_required", default: false, null: false
+ t.boolean "require_campus_employment_info", default: false, null: false
+ t.boolean "require_finaid_info", default: false, null: false
+ t.boolean "require_pen_name", default: false, null: false
t.boolean "transcript_required", default: false, null: false
- t.integer "maximum_number_entries_per_applicant", default: 1, null: false
- t.string "created_by"
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.boolean "active", default: false, null: false
- t.boolean "archived", default: false, null: false
- t.boolean "require_pen_name", default: false, null: false
- t.boolean "require_finaid_info", default: false, null: false
- t.boolean "require_campus_employment_info", default: false, null: false
t.index ["contest_description_id"], name: "contest_description_id_idx"
t.index ["contest_description_id"], name: "index_contest_instances_on_contest_description_id"
t.index ["id"], name: "id_unq_idx", unique: true
end
create_table "departments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.integer "dept_id", null: false
- t.text "dept_description"
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.text "dept_description"
+ t.integer "dept_id", null: false
t.string "name"
+ t.datetime "updated_at", null: false
end
create_table "editable_contents", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
t.string "page", null: false
t.string "section", null: false
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["page", "section"], name: "index_editable_contents_on_page_and_section", unique: true
end
create_table "entries", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "title", null: false
- t.bigint "contest_instance_id", null: false
- t.bigint "profile_id", null: false
+ t.boolean "accepted_financial_aid_notice", default: false, null: false
+ t.boolean "campus_employee", default: false, null: false
t.bigint "category_id", null: false
+ t.bigint "contest_instance_id", null: false
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "disqualified", default: false, null: false
t.boolean "deleted", default: false, null: false
+ t.boolean "disqualified", default: false, null: false
+ t.text "financial_aid_description"
t.string "pen_name"
- t.boolean "campus_employee", default: false, null: false
- t.boolean "accepted_financial_aid_notice", default: false, null: false
+ t.bigint "profile_id", null: false
t.boolean "receiving_financial_aid", default: false, null: false
- t.text "financial_aid_description"
+ t.string "title", null: false
+ t.datetime "updated_at", null: false
t.index ["category_id"], name: "category_id_idx"
t.index ["category_id"], name: "index_entries_on_category_id"
t.index ["contest_instance_id"], name: "contest_instance_id_idx"
@@ -203,17 +203,17 @@
end
create_table "entry_rankings", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
t.bigint "entry_id", null: false
+ t.text "external_comments"
+ t.boolean "finalized", default: false, null: false
+ t.text "internal_comments"
t.bigint "judging_round_id", null: false
- t.bigint "user_id", null: false
- t.integer "rank"
t.text "notes"
+ t.integer "rank"
t.boolean "selected_for_next_round", default: false, null: false
- t.datetime "created_at", null: false
t.datetime "updated_at", null: false
- t.text "internal_comments"
- t.text "external_comments"
- t.boolean "finalized", default: false, null: false
+ t.bigint "user_id", null: false
t.index ["entry_id", "judging_round_id", "user_id"], name: "index_entry_rankings_uniqueness", unique: true
t.index ["entry_id"], name: "index_entry_rankings_on_entry_id"
t.index ["judging_round_id"], name: "index_entry_rankings_on_judging_round_id"
@@ -221,58 +221,58 @@
end
create_table "judging_assignments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.bigint "user_id", null: false
- t.bigint "contest_instance_id", null: false
t.boolean "active", default: true, null: false
+ t.bigint "contest_instance_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.bigint "user_id", null: false
t.index ["contest_instance_id"], name: "index_judging_assignments_on_contest_instance_id"
t.index ["user_id", "contest_instance_id"], name: "index_judging_assignments_on_user_id_and_contest_instance_id", unique: true
t.index ["user_id"], name: "index_judging_assignments_on_user_id"
end
create_table "judging_rounds", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.bigint "contest_instance_id", null: false
- t.integer "round_number", null: false
- t.datetime "start_date"
- t.datetime "end_date"
t.boolean "active", default: false, null: false
t.boolean "completed", default: false, null: false
+ t.bigint "contest_instance_id", null: false
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "require_internal_comments", default: false, null: false
- t.boolean "require_external_comments", default: false, null: false
- t.integer "min_internal_comment_words", default: 0, null: false
- t.integer "min_external_comment_words", default: 0, null: false
- t.text "special_instructions"
- t.integer "required_entries_count", default: 0, null: false
t.integer "emails_sent_count", default: 0, null: false
- t.boolean "include_average_ranking", default: false
+ t.datetime "end_date"
t.boolean "include_advancement_status", default: false
+ t.boolean "include_average_ranking", default: false
+ t.integer "min_external_comment_words", default: 0, null: false
+ t.integer "min_internal_comment_words", default: 0, null: false
+ t.boolean "require_external_comments", default: false, null: false
+ t.boolean "require_internal_comments", default: false, null: false
+ t.integer "required_entries_count", default: 0, null: false
+ t.integer "round_number", null: false
+ t.text "special_instructions"
+ t.datetime "start_date"
+ t.datetime "updated_at", null: false
t.index ["contest_instance_id", "round_number"], name: "index_judging_rounds_on_contest_instance_id_and_round_number", unique: true
t.index ["contest_instance_id"], name: "index_judging_rounds_on_contest_instance_id"
end
create_table "profiles", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.bigint "user_id", null: false
- t.string "preferred_first_name", default: "", null: false
- t.string "preferred_last_name", default: "", null: false
- t.bigint "class_level_id"
- t.bigint "school_id"
+ t.boolean "accepted_financial_aid_notice", default: false, null: false
+ t.boolean "campus_employee", default: false, null: false
t.bigint "campus_id"
- t.string "major"
- t.date "grad_date", null: false
+ t.bigint "class_level_id"
+ t.datetime "created_at", null: false
t.string "degree", null: false
- t.boolean "receiving_financial_aid", default: false, null: false
- t.boolean "accepted_financial_aid_notice", default: false, null: false
+ t.string "department"
t.text "financial_aid_description"
+ t.date "grad_date", null: false
t.string "hometown_publication"
+ t.string "major"
t.string "pen_name"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.boolean "campus_employee", default: false, null: false
- t.string "department"
+ t.string "preferred_first_name", default: "", null: false
+ t.string "preferred_last_name", default: "", null: false
+ t.boolean "receiving_financial_aid", default: false, null: false
+ t.bigint "school_id"
t.string "umid"
+ t.datetime "updated_at", null: false
+ t.bigint "user_id", null: false
t.index ["campus_id"], name: "campus_id_idx"
t.index ["campus_id"], name: "index_profiles_on_campus_id"
t.index ["class_level_id"], name: "class_level_id_idx"
@@ -286,73 +286,73 @@
end
create_table "roles", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "kind"
- t.text "description"
t.datetime "created_at", null: false
+ t.text "description"
+ t.string "kind"
t.datetime "updated_at", null: false
end
create_table "round_judge_assignments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.bigint "judging_round_id", null: false
- t.bigint "user_id", null: false
t.boolean "active", default: true
t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
t.datetime "instructions_sent_at"
+ t.bigint "judging_round_id", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "user_id", null: false
t.index ["judging_round_id", "user_id"], name: "index_round_judge_assignments_on_judging_round_id_and_user_id", unique: true
t.index ["judging_round_id"], name: "index_round_judge_assignments_on_judging_round_id"
t.index ["user_id"], name: "index_round_judge_assignments_on_user_id"
end
create_table "schools", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "name"
t.datetime "created_at", null: false
+ t.string "name"
t.datetime "updated_at", null: false
t.index ["id"], name: "id_unq_idx", unique: true
end
create_table "user_roles", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.bigint "user_id", null: false
- t.bigint "role_id", null: false
t.datetime "created_at", null: false
+ t.bigint "role_id", null: false
t.datetime "updated_at", null: false
+ t.bigint "user_id", null: false
t.index ["role_id"], name: "index_user_roles_on_role_id"
t.index ["user_id", "role_id"], name: "index_user_roles_on_user_id_and_role_id", unique: true
t.index ["user_id"], name: "index_user_roles_on_user_id"
end
create_table "users", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
+ t.datetime "created_at", null: false
+ t.datetime "current_sign_in_at"
+ t.string "current_sign_in_ip"
+ t.string "display_name"
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
- t.string "reset_password_token"
- t.datetime "reset_password_sent_at"
- t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0, null: false
- t.datetime "current_sign_in_at"
+ t.integer "failed_attempts", default: 0, null: false
+ t.string "first_name", default: "", null: false
+ t.string "last_name", default: "", null: false
t.datetime "last_sign_in_at"
- t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
- t.integer "failed_attempts", default: 0, null: false
- t.string "unlock_token"
t.datetime "locked_at"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
- t.string "uniqname"
+ t.string "principal_name"
t.string "provider"
+ t.datetime "remember_created_at"
+ t.datetime "reset_password_sent_at"
+ t.string "reset_password_token"
+ t.integer "sign_in_count", default: 0, null: false
t.string "uid"
- t.string "principal_name"
- t.string "display_name"
- t.string "first_name", default: "", null: false
- t.string "last_name", default: "", null: false
+ t.string "uniqname"
+ t.string "unlock_token"
+ t.datetime "updated_at", null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true
end
create_table "visibilities", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
- t.string "kind", null: false
- t.text "description"
t.datetime "created_at", null: false
+ t.text "description"
+ t.string "kind", null: false
t.datetime "updated_at", null: false
end
diff --git a/lib/capistrano/tasks/redis.rake b/lib/capistrano/tasks/redis.rake
deleted file mode 100644
index 76f6d15e..00000000
--- a/lib/capistrano/tasks/redis.rake
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace :redis do
- desc 'Check if Redis is installed'
- task :check do
- on roles(:app) do
- unless test('which redis-server')
- error 'Redis is not installed. Please install Redis before deploying.'
- exit 1
- end
- end
- end
-
- desc 'Install Redis'
- task :install do
- on roles(:app) do
- execute :sudo, 'apt-get update'
- execute :sudo, 'apt-get install -y redis-server'
- execute :sudo, 'systemctl enable redis-server'
- execute :sudo, 'systemctl start redis-server'
- end
- end
-end
-
-# Hook into the deployment flow
-before 'deploy:check', 'redis:check'
diff --git a/lib/capistrano/tasks/sentry.rake b/lib/capistrano/tasks/sentry.rake
new file mode 100644
index 00000000..f7591143
--- /dev/null
+++ b/lib/capistrano/tasks/sentry.rake
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+namespace :sentry do
+ desc 'Write REVISION file so Rails can report a deploy-specific Sentry release'
+ task :write_revision do
+ on roles(:app) do
+ revision = fetch(:current_revision)
+ upload! StringIO.new(revision), "#{release_path}/REVISION"
+ end
+ end
+
+ desc 'Notify Sentry of a new deploy (requires SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT)'
+ task :notify_deploy do
+ auth_token = ENV['SENTRY_AUTH_TOKEN']
+ org = ENV['SENTRY_ORG']
+ project = ENV['SENTRY_PROJECT']
+ next unless auth_token.present? && org.present? && project.present?
+
+ revision = fetch(:current_revision)
+ environment = fetch(:rails_env, 'production')
+
+ run_locally do
+ execute :curl, '-sS', '-X', 'POST',
+ "https://sentry.io/api/0/organizations/#{org}/releases/",
+ '-H', "Authorization: Bearer #{auth_token}",
+ '-H', 'Content-Type: application/json',
+ '-d', "'{\"version\":\"#{revision}\",\"projects\":[\"#{project}\"]}'"
+
+ execute :curl, '-sS', '-X', 'POST',
+ "https://sentry.io/api/0/organizations/#{org}/releases/#{revision}/deploys/",
+ '-H', "Authorization: Bearer #{auth_token}",
+ '-H', 'Content-Type: application/json',
+ '-d', "'{\"environment\":\"#{environment}\"}'"
+ end
+ end
+end
+
+after 'deploy:updated', 'sentry:write_revision'
+after 'deploy:finished', 'sentry:notify_deploy'
diff --git a/lib/sentry_release.rb b/lib/sentry_release.rb
new file mode 100644
index 00000000..84a4ee0d
--- /dev/null
+++ b/lib/sentry_release.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# Resolves a deploy-specific release id for Sentry (git SHA preferred).
+module SentryRelease
+ module_function
+
+ def current
+ ENV['SENTRY_RELEASE'].presence ||
+ ENV['HATCHBOX_REVISION'].presence ||
+ revision_file.presence ||
+ git_sha.presence
+ end
+
+ def revision_file
+ path = Rails.root.join('REVISION')
+ return unless path.file?
+
+ path.read.strip.presence
+ end
+ private_class_method :revision_file
+
+ def git_sha
+ sha = `git rev-parse HEAD 2>/dev/null`.strip
+ sha.presence
+ rescue StandardError
+ nil
+ end
+ private_class_method :git_sha
+end
diff --git a/lib/sentry_test_helper.rb b/lib/sentry_test_helper.rb
index f98df42b..0fdae0d0 100644
--- a/lib/sentry_test_helper.rb
+++ b/lib/sentry_test_helper.rb
@@ -6,7 +6,6 @@
# SentryTestHelper.verify_config
# SentryTestHelper.test_message
# SentryTestHelper.test_exception
-# SentryTestHelper.test_with_sampling
# SentryTestHelper.full_test
module SentryTestHelper
class << self
@@ -18,82 +17,42 @@ def verify_config
puts "Is enabled: #{Sentry.configuration.enabled_environments.include?(Rails.env)}"
puts "DSN configured: #{Sentry.configuration.dsn.present?}"
puts "DSN (masked): #{mask_dsn(Sentry.configuration.dsn)}" if Sentry.configuration.dsn
- puts "Sample rate: #{Sentry.configuration.sample_rate}"
- puts "Traces sample rate: #{Sentry.configuration.traces_sample_rate}"
+ puts "Error sample rate: #{Sentry.configuration.sample_rate}"
+ puts "Traces sampler configured: #{!Sentry.configuration.traces_sampler.nil?}"
+ puts "Profiles sample rate: #{Sentry.configuration.profiles_sample_rate}"
puts "Release: #{Sentry.configuration.release}"
puts "Environment: #{Sentry.configuration.environment}"
puts "==========================\n"
end
- # Send a test message that bypasses sampling by sending to a transaction
- # This increases the likelihood of delivery
+ # Send a test message
def test_message(message = "Test message from #{Rails.env} at #{Time.current}")
puts "\n=== Testing Sentry Message ==="
puts "Sending message: #{message}"
- # Temporarily override sample rate
- original_sample_rate = Sentry.configuration.sample_rate
- Sentry.configuration.sample_rate = 1.0
-
- begin
- event_id = Sentry.capture_message(message, level: :info)
- puts "Event ID: #{event_id}"
- puts "Status: #{event_id ? 'Sent successfully!' : 'Failed to send (returned nil)'}"
- puts "Note: Check Sentry dashboard in a few moments"
- ensure
- # Restore original sample rate
- Sentry.configuration.sample_rate = original_sample_rate
- end
-
+ event_id = Sentry.capture_message(message, level: :info)
+ puts "Event ID: #{event_id}"
+ puts "Status: #{event_id ? 'Sent successfully!' : 'Failed to send (returned nil)'}"
+ puts "Note: Check Sentry dashboard in a few moments"
puts "===========================\n"
event_id
end
- # Send a test exception that bypasses sampling
+ # Send a test exception
def test_exception
puts "\n=== Testing Sentry Exception ==="
- # Temporarily override sample rate
- original_sample_rate = Sentry.configuration.sample_rate
- Sentry.configuration.sample_rate = 1.0
-
begin
- begin
- raise StandardError, "Test exception from #{Rails.env} at #{Time.current}"
- rescue StandardError => e
- event_id = Sentry.capture_exception(e)
- puts "Exception raised and captured"
- puts "Event ID: #{event_id}"
- puts "Status: #{event_id ? 'Sent successfully!' : 'Failed to send (returned nil)'}"
- puts "Note: Check Sentry dashboard in a few moments"
- event_id
- end
- ensure
- # Restore original sample rate
- Sentry.configuration.sample_rate = original_sample_rate
- end
-
- puts "=============================\n"
- end
-
- # Send multiple messages to test sampling (some should get through)
- def test_with_sampling(count = 20)
- puts "\n=== Testing with Normal Sampling ==="
- puts "Sending #{count} messages with #{(Sentry.configuration.sample_rate * 100).to_i}% sample rate"
- puts "Expected to see approximately #{(count * Sentry.configuration.sample_rate).round} events"
-
- sent_ids = []
- count.times do |i|
- event_id = Sentry.capture_message("Sampled test message #{i + 1}/#{count} at #{Time.current}")
- sent_ids << event_id if event_id
- print "."
+ raise StandardError, "Test exception from #{Rails.env} at #{Time.current}"
+ rescue StandardError => e
+ event_id = Sentry.capture_exception(e)
+ puts "Exception raised and captured"
+ puts "Event ID: #{event_id}"
+ puts "Status: #{event_id ? 'Sent successfully!' : 'Failed to send (returned nil)'}"
+ puts "Note: Check Sentry dashboard in a few moments"
+ puts "=============================\n"
+ event_id
end
-
- puts "\n"
- puts "Event IDs returned: #{sent_ids.count}/#{count}"
- puts "Note: Check Sentry dashboard in a few moments"
- puts "==================================\n"
- sent_ids
end
# Full diagnostic test
@@ -111,9 +70,7 @@ def full_test
def mask_dsn(dsn)
return 'Not configured' if dsn.blank?
- # Mask the secret part of the DSN
- # DSN format: https://public_key@sentry.io/project_id
- dsn.to_s.gsub(/(?<=https:\/\/)([^@]+)(?=@)/) { |match| "*" * match.length }
+ dsn.to_s.gsub(%r{(?<=https://)([^@]+)(?=@)}) { |match| '*' * match.length }
rescue StandardError
'Error masking DSN'
end
diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake
deleted file mode 100644
index e96283ea..00000000
--- a/lib/tasks/auto_annotate_models.rake
+++ /dev/null
@@ -1,59 +0,0 @@
-# NOTE: only doing this in development as some production environments (Heroku)
-# NOTE: are sensitive to local FS writes, and besides -- it's just not proper
-# NOTE: to have a dev-mode tool do its thing in production.
-if Rails.env.development?
- require 'annotate'
- task :set_annotation_options do
- # You can override any of these by setting an environment variable of the
- # same name.
- Annotate.set_defaults(
- 'active_admin' => 'false',
- 'additional_file_patterns' => [],
- 'routes' => 'false',
- 'models' => 'true',
- 'position_in_routes' => 'before',
- 'position_in_class' => 'before',
- 'position_in_test' => 'before',
- 'position_in_fixture' => 'before',
- 'position_in_factory' => 'before',
- 'position_in_serializer' => 'before',
- 'show_foreign_keys' => 'true',
- 'show_complete_foreign_keys' => 'false',
- 'show_indexes' => 'true',
- 'simple_indexes' => 'false',
- 'model_dir' => 'app/models',
- 'root_dir' => '',
- 'include_version' => 'false',
- 'require' => '',
- 'exclude_tests' => 'false',
- 'exclude_fixtures' => 'false',
- 'exclude_factories' => 'false',
- 'exclude_serializers' => 'false',
- 'exclude_scaffolds' => 'true',
- 'exclude_controllers' => 'true',
- 'exclude_helpers' => 'true',
- 'exclude_sti_subclasses' => 'false',
- 'ignore_model_sub_dir' => 'false',
- 'ignore_columns' => nil,
- 'ignore_routes' => nil,
- 'ignore_unknown_models' => 'false',
- 'hide_limit_column_types' => 'integer,bigint,boolean',
- 'hide_default_column_types' => 'json,jsonb,hstore',
- 'skip_on_db_migrate' => 'false',
- 'format_bare' => 'true',
- 'format_rdoc' => 'false',
- 'format_yard' => 'false',
- 'format_markdown' => 'false',
- 'sort' => 'false',
- 'force' => 'false',
- 'frozen' => 'false',
- 'classified_sort' => 'true',
- 'trace' => 'false',
- 'wrapper_open' => nil,
- 'wrapper_close' => nil,
- 'with_comment' => 'true'
- )
- end
-
- Annotate.load_tasks
-end
diff --git a/lib/tasks/email.rake b/lib/tasks/email.rake
index b82bcbd9..b6edefd5 100644
--- a/lib/tasks/email.rake
+++ b/lib/tasks/email.rake
@@ -1,45 +1,30 @@
namespace :email do
- # You can choose whether to queue the email through Sidekiq or send it directly:
- # For direct delivery:
+ # Direct delivery:
# bin/rails email:test[your@email.com]
#
- # To queue through Sidekiq (add true as a second argument):
+ # Queue via Active Job / Solid Queue:
# bin/rails email:test[your@email.com,true]
desc 'Send a test email to verify configuration'
task :test, [ :email, :queue ] => :environment do |_t, args|
recipient = args[:email] || Rails.application.credentials.dig(:sendgrid, :mailer_sender)
-
- # Check if Sidekiq is available
- sidekiq_available = defined?(Sidekiq) == 'constant' && Sidekiq.class == Module
- queue_delivery_requested = args[:queue]&.downcase == 'true'
-
- # Set queue_delivery to false if Sidekiq is not available
- queue_delivery = queue_delivery_requested && sidekiq_available
-
- # Show a warning if user requested Sidekiq but it's not available
- if queue_delivery_requested && !sidekiq_available
- puts "WARNING: Sidekiq is not available. Email will be sent directly."
- end
+ queue_delivery = args[:queue]&.downcase == 'true'
if recipient.nil?
puts 'ERROR: No recipient email provided and no default contact email configured.'
- puts 'Usage: rake email:test[recipient@example.com] or rake email:test[recipient@example.com,true] to queue via Sidekiq'
+ puts 'Usage: rake email:test[recipient@example.com] or rake email:test[recipient@email.com,true] to queue via Active Job'
exit 1
end
puts "Sending test email to #{recipient}..."
begin
if queue_delivery
- # For queued delivery, don't inspect the message before sending
- puts 'Queuing email delivery through Sidekiq...'
+ puts "Queuing email delivery through Active Job (#{ActiveJob::Base.queue_adapter.class.name})..."
TestMailer.test_email(recipient).deliver_later
- puts 'Email successfully queued in Sidekiq!'
+ puts 'Email successfully queued!'
else
- # For direct delivery, we can inspect the message
mail = TestMailer.test_email(recipient)
- # Print message details before sending
puts 'Message details:'
puts " From: #{mail.from.first || 'Not set'}"
puts " Reply-To: #{mail.reply_to&.first || 'Not set'}"
@@ -50,7 +35,7 @@ namespace :email do
end
mail.deliver_now
- puts 'Test email sent directly (bypassing Sidekiq)!'
+ puts 'Test email sent directly!'
end
rescue => e
puts "ERROR: Failed to send test email: #{e.message}"
diff --git a/package.json b/package.json
index 118a6ea2..3f5406cf 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,8 @@
"@hotwired/stimulus": "^3.2.2",
"@hotwired/turbo-rails": "^8.0.10",
"@popperjs/core": "^2.11.8",
- "@rails/actiontext": "^7.0.8-3",
+ "@rails/actiontext": "^8.0.200",
+ "@sentry/browser": "^10.65.0",
"autoprefixer": "^10.4.17",
"bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3",
@@ -22,7 +23,8 @@
"build": "esbuild app/javascript/*.* --bundle --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets",
"build:css:compile": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules",
"build:css:prefix": "postcss ./app/assets/builds/application.css --use=autoprefixer --output=./app/assets/builds/application.css",
- "build:css": "yarn build:css:compile && yarn build:css:prefix",
+ "build:css:copy-fonts": "mkdir -p app/assets/builds/fonts && cp -f node_modules/bootstrap-icons/font/fonts/* app/assets/builds/fonts/",
+ "build:css": "yarn build:css:compile && yarn build:css:prefix && yarn build:css:copy-fonts",
"watch:css": "nodemon --watch ./app/assets/stylesheets/ --ext scss --exec \"yarn build:css\"",
"test": "jest",
"test:watch": "jest --watch"
diff --git a/spec/middleware/rack_defense_spec.rb b/spec/middleware/rack_defense_spec.rb
index 9af189d9..a2c41988 100644
--- a/spec/middleware/rack_defense_spec.rb
+++ b/spec/middleware/rack_defense_spec.rb
@@ -93,7 +93,7 @@ def call_with(path:, method: 'GET', headers: {}, input: nil)
expect(env['rack.input']).to equal(original_input)
end
- it 'does not desync Rack POST cache from rack.input' do
+ it 'does not prevent Rack from parsing POST params after body inspection' do
inspecting_app = ->(_env) { [200, {}, ['OK']] }
env = Rack::MockRequest.env_for(
'/entries',
@@ -107,7 +107,7 @@ def call_with(path:, method: 'GET', headers: {}, input: nil)
expect(status).to eq(200)
request = Rack::Request.new(env)
expect(request.POST).to eq('name' => 'test', 'value' => '1')
- expect(env[Rack::RACK_REQUEST_FORM_INPUT]).to equal(env['rack.input'])
+ expect(env[Rack::RACK_REQUEST_FORM_HASH]).to eq('name' => 'test', 'value' => '1')
end
end
diff --git a/yarn.lock b/yarn.lock
index 04f71ae5..1a0c3335 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1378,20 +1378,75 @@
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-7.1.3.tgz#4db480347775aeecd4dde2405659eef74a458881"
integrity sha512-ojNvnoZtPN0pYvVFtlO7dyEN9Oml1B6IDM+whGKVak69MMYW99lC2NOWXWeE3bmwEydbP/nn6ERcpfjHVjYQjA==
-"@rails/actiontext@^7.0.8-3":
- version "7.0.8-3"
- resolved "https://registry.yarnpkg.com/@rails/actiontext/-/actiontext-7.0.8-3.tgz#b65765527aaa6b54f30f817713b15b1c1aad96dd"
- integrity sha512-0+dCzCceGWKdScEGHU6tV5HuBVg9uIlBTB0XrR8QN/B8v2m7l0otbQ7CYV0+QllpDY89Y1voBhL/taJEzMk8WA==
+"@rails/actiontext@^8.0.200":
+ version "8.1.300"
+ resolved "https://registry.yarnpkg.com/@rails/actiontext/-/actiontext-8.1.300.tgz#0b6d0a40e8ca8ba2caf5a0077e10b5503183a044"
+ integrity sha512-KPbLfZTt6aFKbdeTlU7ZzmX0uSSNhhfZx3G+2lfa6HltQ/r23HDcJnJOEVt72WKJo13IS5C5HK0sYwplVMCdNw==
dependencies:
- "@rails/activestorage" ">= 7.0.0-alpha1"
+ "@rails/activestorage" ">= 8.1.0-alpha"
-"@rails/activestorage@>= 7.0.0-alpha1":
- version "7.1.3"
- resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-7.1.3.tgz#e83ece6c5fd94b3ddf30a8cf3b8f78cad049e596"
- integrity sha512-B+RFYAU8vdTPFg0IJcRp2ey0Qw9hpcUOqHHcWqftDJ76ZMBi9+m/UUeMJlNsSd0l9eD+1HLlFSo1X//cY4yiDw==
+"@rails/activestorage@>= 8.1.0-alpha":
+ version "8.1.300"
+ resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-8.1.300.tgz#624a6f291e0414c07c3d02ed3d1e6ee85c94277b"
+ integrity sha512-AOv3zZrTJjQlzm4L9uzOQtCz6zaM4IqUyux6yzSqmS+PZ8EzwD1F2JAc4LlJNJAv4MSyNYriG+CaCm1QbQTjsA==
dependencies:
spark-md5 "^3.0.1"
+"@sentry/browser-utils@10.65.0":
+ version "10.65.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser-utils/-/browser-utils-10.65.0.tgz#e3b67c6b9dd05dad2ad7eee82ad873bfc9f8ab12"
+ integrity sha512-4J0mkfNJAGUOkpg1ZggizyftFTn9N20b+Jl87UnWsDUkNG0Ic1l/FIzMPTVxXrAnhBGu0ULO0TFWMoQ5s3QtZw==
+ dependencies:
+ "@sentry/conventions" "^0.15.1"
+ "@sentry/core" "10.65.0"
+
+"@sentry/browser@^10.65.0":
+ version "10.65.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.65.0.tgz#e2734a2effe051a9005afdd914fc07c68262f627"
+ integrity sha512-XUDDsx0qxzeIlcOu1fDEqTcDl0eiOqghsgV+ReuuNP4jYjZ9kUQxE3rXWM5mlT1pBi4VaQ4FHqvQZZrRXy+oDw==
+ dependencies:
+ "@sentry/browser-utils" "10.65.0"
+ "@sentry/conventions" "^0.15.1"
+ "@sentry/core" "10.65.0"
+ "@sentry/feedback" "10.65.0"
+ "@sentry/replay" "10.65.0"
+ "@sentry/replay-canvas" "10.65.0"
+
+"@sentry/conventions@^0.15.1":
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/@sentry/conventions/-/conventions-0.15.1.tgz#5b8888862d1e444678938f5afdd7842bca42f7cc"
+ integrity sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==
+
+"@sentry/core@10.65.0":
+ version "10.65.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.65.0.tgz#a98cfd6e138d9658dd42d050902837ff591452e8"
+ integrity sha512-3aqtmM5NgNGo45BNaaBzi0LPQZAw//NEL4HKS5fXm12pJMa4KEkze8DEKnkTEIrGnWaOJKamecHKlnNg/Mqf/Q==
+ dependencies:
+ "@sentry/conventions" "^0.15.1"
+
+"@sentry/feedback@10.65.0":
+ version "10.65.0"
+ resolved "https://registry.yarnpkg.com/@sentry/feedback/-/feedback-10.65.0.tgz#a9418cc77c835e3a74cdc3fad2fb1098ecbc3887"
+ integrity sha512-ck8h7wgd3F3bYNk0v1OgohmyLBeXcKxqlfBJRtQq4k6KZUq+pXimOG7ckNguVMYjCo3PEfuG+ckKc21yqotKug==
+ dependencies:
+ "@sentry/core" "10.65.0"
+
+"@sentry/replay-canvas@10.65.0":
+ version "10.65.0"
+ resolved "https://registry.yarnpkg.com/@sentry/replay-canvas/-/replay-canvas-10.65.0.tgz#5ff20832ae8acacd38fe7f154dd7ac6c6cad1af2"
+ integrity sha512-A7X3RVk1Gk+knK8Ip/2EjejckNCLgCfRZo6eGlsy6qyz904KBpYmys1a0o7QkzFRjhIndjHAfcVxwt6jSLJlrQ==
+ dependencies:
+ "@sentry/core" "10.65.0"
+ "@sentry/replay" "10.65.0"
+
+"@sentry/replay@10.65.0":
+ version "10.65.0"
+ resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-10.65.0.tgz#8ed1255349e32faeac786e4ca2f4c2f56b98e64e"
+ integrity sha512-aW988CcQBNArbOMzOFOziipHz6uQyXSa4i5CPWsu+nhVPTJHafosi5Lv9n6NM/icDX5e23VdnX6mZd8SyJuo8A==
+ dependencies:
+ "@sentry/browser-utils" "10.65.0"
+ "@sentry/core" "10.65.0"
+
"@sinclair/typebox@^0.27.8":
version "0.27.8"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
@@ -3034,9 +3089,9 @@ js-tokens@^4.0.0:
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.13.1:
- version "3.14.2"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0"
- integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.15.0.tgz#586e5214eafe3e893756a41e979b50d89d3e4a67"
+ integrity sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
@@ -4071,9 +4126,9 @@ write-file-atomic@^4.0.2:
signal-exit "^3.0.7"
ws@^8.11.0:
- version "8.18.0"
- resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
- integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
+ version "8.21.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.1.tgz#045650cd4b1207809e7547146223c3814a9af586"
+ integrity sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==
xml-name-validator@^4.0.0:
version "4.0.0"