diff --git a/Gemfile b/Gemfile index b76f160..42b37e7 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source "https://rubygems.org" # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 7.2.2", ">= 7.2.2.1" +gem "rails", "~> 8.1.3", ">= 8.1.3.1" # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] gem "sprockets-rails" # Use postgresql as the database for Active Record @@ -42,8 +42,8 @@ gem "bootsnap", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] # gem "image_processing", "~> 1.2" -# Sass 編譯器 -gem "sassc-rails" +# Sass 編譯器(dart-sass 官方實作;sassc/libsass 已於 2024 停止維護) +gem "dartsass-rails" # TailwindCSS via a standalone CLI executable, integrated with the asset # pipeline (no Node/npm required) [https://github.com/rails/tailwindcss-rails] diff --git a/Gemfile.lock b/Gemfile.lock index 4958175..f5635a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,84 +1,86 @@ GEM remote: https://rubygems.org/ specs: - actioncable (7.2.2.1) - actionpack (= 7.2.2.1) - activesupport (= 7.2.2.1) + action_text-trix (2.1.19) + railties + actioncable (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.2.1) - actionpack (= 7.2.2.1) - activejob (= 7.2.2.1) - activerecord (= 7.2.2.1) - activestorage (= 7.2.2.1) - activesupport (= 7.2.2.1) + actionmailbox (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) - actionmailer (7.2.2.1) - actionpack (= 7.2.2.1) - actionview (= 7.2.2.1) - activejob (= 7.2.2.1) - activesupport (= 7.2.2.1) + actionmailer (8.1.3.1) + actionpack (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.2.1) - actionview (= 7.2.2.1) - activesupport (= 7.2.2.1) + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) 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.1) - actionpack (= 7.2.2.1) - activerecord (= 7.2.2.1) - activestorage (= 7.2.2.1) - activesupport (= 7.2.2.1) + actiontext (8.1.3.1) + action_text-trix (~> 2.1.15) + actionpack (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.2.1) - activesupport (= 7.2.2.1) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.2.2.1) - activesupport (= 7.2.2.1) + activejob (8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.3.6) - activemodel (7.2.2.1) - activesupport (= 7.2.2.1) - activerecord (7.2.2.1) - activemodel (= 7.2.2.1) - activesupport (= 7.2.2.1) + activemodel (8.1.3.1) + activesupport (= 8.1.3.1) + activerecord (8.1.3.1) + activemodel (= 8.1.3.1) + activesupport (= 8.1.3.1) timeout (>= 0.4.0) - activestorage (7.2.2.1) - actionpack (= 7.2.2.1) - activejob (= 7.2.2.1) - activerecord (= 7.2.2.1) - activesupport (= 7.2.2.1) + activestorage (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activesupport (= 8.1.3.1) marcel (~> 1.0) - activesupport (7.2.2.1) + activesupport (8.1.3.1) base64 - benchmark (>= 0.3) bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) connection_pool (>= 2.2.5) drb i18n (>= 1.6, < 2) + json logger (>= 1.4.2) minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) + uri (>= 0.13.1) addressable (2.8.7) public_suffix (>= 2.0.2, < 7.0) ast (2.4.3) base64 (0.3.0) bcrypt (3.1.22) - benchmark (0.4.1) - bigdecimal (3.2.2) + bigdecimal (4.1.2) bindex (0.8.1) bootsnap (1.18.6) msgpack (~> 1.2) @@ -95,35 +97,52 @@ GEM regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) chunky_png (1.4.0) - concurrent-ruby (1.3.5) - connection_pool (2.5.3) - crass (1.0.6) - date (3.4.1) + concurrent-ruby (1.3.8) + connection_pool (3.0.2) + crass (1.0.7) + dartsass-rails (0.5.1) + railties (>= 6.0.0) + sass-embedded (~> 1.63) + date (3.5.1) debug (1.11.0) irb (~> 1.10) reline (>= 0.3.8) drb (2.2.3) - erb (5.0.1) + erb (6.0.7) erubi (1.13.1) - ffi (1.17.2-aarch64-linux-gnu) - ffi (1.17.2-aarch64-linux-musl) - ffi (1.17.2-arm-linux-gnu) - ffi (1.17.2-arm-linux-musl) - ffi (1.17.2-arm64-darwin) - ffi (1.17.2-x86_64-darwin) - ffi (1.17.2-x86_64-linux-gnu) - ffi (1.17.2-x86_64-linux-musl) - globalid (1.2.1) + globalid (1.4.0) activesupport (>= 6.1) - i18n (1.14.7) + google-protobuf (4.36.1) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.1-aarch64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.1-aarch64-linux-musl) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.1-arm64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.1-x86_64-darwin) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.1-x86_64-linux-gnu) + bigdecimal + rake (~> 13.3) + google-protobuf (4.36.1-x86_64-linux-musl) + bigdecimal + rake (~> 13.3) + i18n (1.15.2) concurrent-ruby (~> 1.0) importmap-rails (2.1.0) actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.8.0) - irb (1.15.2) + io-console (0.9.2) + irb (1.18.0) pp (>= 0.6.0) + prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) jbuilder (2.13.0) @@ -133,107 +152,114 @@ GEM language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) - loofah (2.24.1) + loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.8.1) + mail (2.9.1) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.2.1) matrix (0.4.3) mini_mime (1.1.5) - minitest (5.25.5) + minitest (6.0.6) + drb (~> 2.0) + prism (~> 1.5) msgpack (1.8.0) - net-imap (0.5.9) + net-imap (0.6.6) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.2) + net-protocol (0.3.0) timeout net-smtp (0.5.1) net-protocol - nio4r (2.7.4) - nokogiri (1.18.8-aarch64-linux-gnu) + nio4r (2.7.5) + nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-aarch64-linux-musl) + nokogiri (1.19.4-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.18.8-arm-linux-gnu) + nokogiri (1.19.4-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-arm-linux-musl) + nokogiri (1.19.4-arm-linux-musl) racc (~> 1.4) - nokogiri (1.18.8-arm64-darwin) + nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-darwin) + nokogiri (1.19.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-gnu) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.18.8-x86_64-linux-musl) + nokogiri (1.19.4-x86_64-linux-musl) racc (~> 1.4) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) racc pg (1.5.9) - pp (0.6.2) + pp (0.6.4) prettyprint prettyprint (0.2.0) - prism (1.4.0) - psych (5.2.6) - date - stringio + prism (1.9.0) public_suffix (6.0.2) puma (6.6.0) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.16) + rack (3.2.7) rack-attack (6.8.0) rack (>= 1.0, < 4) - rack-session (2.1.1) + rack-session (2.1.2) base64 (>= 0.1.0) rack (>= 3.0.0) rack-test (2.2.0) rack (>= 1.3) - rackup (2.2.1) + rackup (2.3.1) rack (>= 3) - rails (7.2.2.1) - actioncable (= 7.2.2.1) - actionmailbox (= 7.2.2.1) - actionmailer (= 7.2.2.1) - actionpack (= 7.2.2.1) - actiontext (= 7.2.2.1) - actionview (= 7.2.2.1) - activejob (= 7.2.2.1) - activemodel (= 7.2.2.1) - activerecord (= 7.2.2.1) - activestorage (= 7.2.2.1) - activesupport (= 7.2.2.1) + rails (8.1.3.1) + actioncable (= 8.1.3.1) + actionmailbox (= 8.1.3.1) + actionmailer (= 8.1.3.1) + actionpack (= 8.1.3.1) + actiontext (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activemodel (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) bundler (>= 1.15.0) - railties (= 7.2.2.1) + railties (= 8.1.3.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.2) - loofah (~> 2.21) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) 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.1) - actionpack (= 7.2.2.1) - activesupport (= 7.2.2.1) + railties (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) 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.0) - rdoc (6.14.2) + rake (13.4.2) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) erb - psych (>= 4.0.0) + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort regexp_parser (2.10.0) - reline (0.6.1) + reline (0.7.0) io-console (~> 0.5) rexml (3.4.1) rqrcode (3.2.0) @@ -270,14 +296,22 @@ GEM rubocop-rails (>= 2.30) ruby-progressbar (1.13.0) rubyzip (2.4.1) - 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 + sass-embedded (1.103.1-aarch64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-aarch64-linux-musl) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-arm-linux-gnueabihf) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-arm-linux-musleabihf) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-arm64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-x86_64-darwin) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-x86_64-linux-gnu) + google-protobuf (~> 4.31) + sass-embedded (1.103.1-x86_64-linux-musl) + google-protobuf (~> 4.31) securerandom (0.4.1) selenium-webdriver (4.34.0) base64 (~> 0.2) @@ -295,7 +329,6 @@ GEM sprockets (>= 3.0.0) stimulus-rails (1.3.4) railties (>= 6.0.0) - stringio (3.1.7) tailwindcss-rails (4.6.0) railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) @@ -306,9 +339,9 @@ GEM tailwindcss-ruby (4.3.3-x86_64-darwin) tailwindcss-ruby (4.3.3-x86_64-linux-gnu) tailwindcss-ruby (4.3.3-x86_64-linux-musl) - thor (1.3.2) - tilt (2.6.1) - timeout (0.4.3) + thor (1.5.0) + timeout (0.6.1) + tsort (0.2.0) turbo-rails (2.0.16) actionpack (>= 7.1.0) railties (>= 7.1.0) @@ -317,6 +350,7 @@ GEM unicode-display_width (3.1.4) unicode-emoji (~> 4.0, >= 4.0.4) unicode-emoji (4.0.4) + uri (1.1.1) useragent (0.16.11) web-console (4.2.1) actionview (>= 6.0.0) @@ -324,13 +358,13 @@ GEM bindex (>= 0.4.0) railties (>= 6.0.0) websocket (1.2.11) - websocket-driver (0.8.0) + 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.7.3) + zeitwerk (2.8.3) PLATFORMS aarch64-linux @@ -349,16 +383,16 @@ DEPENDENCIES bootsnap brakeman capybara + dartsass-rails debug importmap-rails jbuilder pg (~> 1.1) puma (>= 5.0) rack-attack - rails (~> 7.2.2, >= 7.2.2.1) + rails (~> 8.1.3, >= 8.1.3.1) rqrcode rubocop-rails-omakase - sassc-rails selenium-webdriver sprockets-rails stimulus-rails @@ -368,4 +402,4 @@ DEPENDENCIES web-console BUNDLED WITH - 2.5.16 + 2.7.2 diff --git a/Procfile.dev b/Procfile.dev index da151fe..2007e54 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,2 +1,3 @@ web: bin/rails server css: bin/rails tailwindcss:watch +sass: bin/rails dartsass:watch diff --git a/README.md b/README.md index ab57f25..e4e8cfa 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [▶ Live Demo](https://venture-ferris.curihaosity.xyz) 一個把 2018 年台北市商業處「勇闖摩天輪」團隊實境解謎活動網站,從 2018 年老舊 -的 CodeIgniter 3 專案重構為 Rails 7.2 的技術作品集專案。 +的 CodeIgniter 3 專案重構為 Rails 8.1 的技術作品集專案。 ![遊戲流程展示](docs/images/demo.gif) @@ -36,7 +36,7 @@ 完整論述、架構總覽(Models/ERD/路由)、相較原作的新增功能全文,見 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)。 -**技術棧**:Rails 7.2/PostgreSQL/Hotwire(Turbo + Stimulus)/Tailwind CSS v4/部署 Fly.io(東京)+ Cloudflare。 +**技術棧**:Rails 8.1/PostgreSQL/Hotwire(Turbo + Stimulus)/Tailwind CSS v4/Dart Sass/部署 Fly.io(東京)+ Cloudflare。 ## 本機啟動 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index b06fc42..4028c22 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,5 +1,4 @@ //= link_tree ../images -//= link_directory ../stylesheets .css //= link_tree ../../javascript .js //= link_tree ../../../vendor/javascript .js //= link_tree ../builds diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css deleted file mode 100644 index 288b9ab..0000000 --- a/app/assets/stylesheets/application.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's - * vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/app/assets/stylesheets/boss.scss b/app/assets/stylesheets/boss.scss index 37b5495..58c5598 100644 --- a/app/assets/stylesheets/boss.scss +++ b/app/assets/stylesheets/boss.scss @@ -3,8 +3,11 @@ *{ font-family: 'Noto Sans TC'; } /*----- 基本設定 -----*/ -body { - background-image: asset-url("bossBg.png"); +body { + /* dart-sass has no sprockets `asset-url()` helper, so the fingerprinted + URL is injected from the boss page's `yield :head` block as a custom + property. See app/views/game/bosses/show.html.erb. */ + background-image: var(--boss-bg-image); background-repeat: no-repeat; background-attachment: fixed; background-position: center top; @@ -50,7 +53,7 @@ p.text-shadow{ color: white; text-shadow: 1px 0 5px black, 0 1px 5px black; } isolation: isolate; width: 100%; aspect-ratio: 509 / 720; - background-image: asset-url("bossBg.png"); + background-image: var(--boss-bg-image); background-repeat: no-repeat; background-position: center; background-size: cover; diff --git a/app/assets/stylesheets/site.scss b/app/assets/stylesheets/site.scss index 07faff2..aebe141 100644 --- a/app/assets/stylesheets/site.scss +++ b/app/assets/stylesheets/site.scss @@ -3,8 +3,12 @@ *{ font-family: 'Noto Sans TC'; } /*----- 基本設定 -----*/ -body { - background-image: asset-url("bg.jpg"); +body { + /* dart-sass has no sprockets `asset-url()` helper (it would pass the + function through verbatim and the browser could not resolve it), so the + fingerprinted URL is injected from the layout as a custom property. + See app/views/layouts/application.html.erb. */ + background-image: var(--site-bg-image); background-repeat: repeat; background-attachment: fixed; background-position: center top; diff --git a/app/views/game/bosses/show.html.erb b/app/views/game/bosses/show.html.erb index c04d951..cf94a88 100644 --- a/app/views/game/bosses/show.html.erb +++ b/app/views/game/bosses/show.html.erb @@ -8,6 +8,10 @@ stylesheet without editing app/views/layouts/. %> <% content_for :head do %> <%= stylesheet_link_tag "boss" %> + <%# boss.scss is compiled by dart-sass, which has no sprockets `asset-url()` + helper, so the fingerprinted background URL is handed to it here as a + custom property (see app/assets/stylesheets/boss.scss). %> + <% end %>
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 6681b32..2b2c946 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -14,6 +14,10 @@ <%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "site" %> + <%# site.scss is compiled by dart-sass, which has no sprockets + `asset-url()` helper, so the fingerprinted background URL is handed to + it here as a custom property (see app/assets/stylesheets/site.scss). %> + <%= javascript_importmap_tags %> <%= yield :head %> diff --git a/bin/rubocop b/bin/rubocop index 40330c0..5a20504 100755 --- a/bin/rubocop +++ b/bin/rubocop @@ -2,7 +2,7 @@ require "rubygems" require "bundler/setup" -# explicit rubocop config increases performance slightly while avoiding config confusion. +# Explicit RuboCop config increases performance slightly while avoiding config confusion. ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) load Gem.bin_path("rubocop", "rubocop") diff --git a/bin/setup b/bin/setup index 907b2d9..81be011 100755 --- a/bin/setup +++ b/bin/setup @@ -2,7 +2,6 @@ require "fileutils" APP_ROOT = File.expand_path("..", __dir__) -APP_NAME = "venture-ferris" def system!(*args) system(*args, exception: true) @@ -14,7 +13,6 @@ FileUtils.chdir APP_ROOT do # Add necessary setup steps to this file. puts "== Installing dependencies ==" - system! "gem install bundler --conservative" system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" @@ -24,14 +22,14 @@ FileUtils.chdir APP_ROOT do puts "\n== Preparing database ==" system! "bin/rails db:prepare" + system! "bin/rails db:reset" if ARGV.include?("--reset") puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" - puts "\n== Restarting application server ==" - system! "bin/rails restart" - - # puts "\n== Configuring puma-dev ==" - # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" - # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" + unless ARGV.include?("--skip-server") + puts "\n== Starting development server ==" + STDOUT.flush # flush the output before exec(2) so that it displays + exec "bin/dev" + end end diff --git a/config/application.rb b/config/application.rb index aaf7ac8..943c27d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -9,7 +9,7 @@ module VentureFerris class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 7.2 + config.load_defaults 8.1 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. @@ -26,17 +26,5 @@ class Application < Rails::Application # ActiveRecord still stores UTC as usual. config.time_zone = "Taipei" # config.eager_load_paths << Rails.root.join("extras") - - # sassc-rails defaults `config.assets.css_compressor` to `:sass` outside - # development (see sassc/rails/railtie.rb#setup_compression), which - # routes every compiled CSS asset — including the pre-built, already - # minified `tailwind.css` from tailwindcss-rails — back through the - # libsass compressor. Tailwind v4's output uses modern CSS syntax - # (e.g. `@media (width >= 40rem)` range syntax) that libsass cannot - # parse, raising "unclosed parenthesis in media query expression" in - # test/production. Disable the compressor so built assets are served - # as-is; sassc-rails' own .scss compilation (site.scss/boss.scss) is - # unaffected since that happens at the engine/transformer step, not here. - config.assets.css_compressor = nil end end diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 220996d..9be5989 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -1,24 +1,4 @@ { - "ignored_warnings": [ - { - "warning_type": "Unmaintained Dependency", - "warning_code": 120, - "fingerprint": "d84924377155b41e094acae7404ec2e521629d86f97b0ff628e3d1b263f8101c", - "check_name": "EOLRails", - "message": "Support for Rails 7.2.2.1 ended on 2026-08-09", - "file": "Gemfile.lock", - "line": 199, - "link": "https://brakemanscanner.org/docs/warning_types/unmaintained_dependency/", - "code": null, - "render_path": null, - "location": null, - "user_input": null, - "confidence": "High", - "cwe_id": [ - 1104 - ], - "note": "Rails 7.2.2.1 EOL is a known, accepted gap for this portfolio rebuild (see README.md's \"CSS 編譯器\" section for the same kind of documented-but-out-of-scope upstream EOL). Bumping the Rails minor/major version is out of scope for this batch of fixes; tracked as a follow-up, not something to silently hide by loosening the Gemfile lock." - } - ], + "ignored_warnings": [], "brakeman_version": "7.0.2" } diff --git a/config/environments/development.rb b/config/environments/development.rb index 5d5d683..cffd7bb 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -3,9 +3,7 @@ Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # 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. + # Make code changes take effect immediately without server restart. config.enable_reloading = true # Do not eager load code on boot. @@ -17,50 +15,51 @@ # Enable server timing. config.server_timing = true - # Enable/disable caching. By default caching is disabled. - # Run rails dev:cache to toggle caching. + # Enable/disable Action Controller caching. By default Action Controller caching is disabled. + # Run rails dev:cache to toggle Action Controller caching. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true - - config.cache_store = :memory_store - config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } + config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false - - config.cache_store = :null_store end + # Change to :null_store to avoid any caching. + config.cache_store = :memory_store + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false - # Disable caching for Action Mailer templates even if Action Controller - # caching is enabled. + # Make template changes take effect immediately. config.action_mailer.perform_caching = false + # Set localhost to be used by links generated in mailer templates. config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true + # Append comments with runtime information tags to SQL queries in logs. + config.active_record.query_log_tags_enabled = true + # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true + # Highlight code that triggered redirect in logs. + config.action_dispatch.verbose_redirect_logs = true + + # Sprockets settings. The Rails 8 template omits these because it assumes + # Propshaft; this app still runs the sprockets pipeline (see Gemfile). # Suppress logger output for asset requests. config.assets.quiet = true config.assets.debug = true diff --git a/config/environments/production.rb b/config/environments/production.rb index e3aaecb..1050129 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -6,44 +6,29 @@ # Code is not reloaded between requests. 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 - # and those relying on copy on write to perform better. - # Rake tasks automatically ignore this option for performance. + # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). config.eager_load = true - # Full error reports are disabled and caching is turned on. + # Full error reports are disabled. config.consider_all_requests_local = false - config.action_controller.perform_caching = true - - # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment - # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). - # config.require_master_key = true - # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. - # config.public_file_server.enabled = false + # Turn on fragment caching in view templates. + config.action_controller.perform_caching = true - # Compress CSS using a preprocessor. - # config.assets.css_compressor = :sass + # Cache assets for far-future expiry since they are all digest stamped. + config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } - # Do not fall back to assets pipeline if a precompiled asset is missed. + # Do not fall back to the asset pipeline if a precompiled asset is missed. + # (Sprockets defaults this to true; the Rails 8 template omits the line + # because it assumes Propshaft, which has no runtime compilation at all.) config.assets.compile = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" - # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache - # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX - # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local - # Mount Action Cable outside main process or domain. - # config.action_cable.mount_path = nil - # config.action_cable.url = "wss://example.com/cable" - # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] - # Assume all access to the app is happening through a SSL-terminating reverse proxy. # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. # Fly.io's edge proxy terminates TLS and forwards plain HTTP to the app over @@ -58,41 +43,48 @@ # Skip http-to-https redirect for the default health check endpoint. # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } - # Log to STDOUT by default - config.logger = ActiveSupport::Logger.new(STDOUT) - .tap { |logger| logger.formatter = ::Logger::Formatter.new } - .then { |logger| ActiveSupport::TaggedLogging.new(logger) } - - # Prepend all log lines with the following tags. + # Log to STDOUT with the current request id as a default log tag. config.log_tags = [ :request_id ] + config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) - # "info" includes generic and useful information about system operation, but avoids logging too much - # information to avoid inadvertent exposure of personally identifiable information (PII). If you - # want to log everything, set the level to "debug". + # Change to "debug" to log everything (including potentially personally-identifiable information!). config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") - # Use a different cache store in production. + # Prevent health checks from clogging up the logs. + config.silence_healthcheck_path = "/up" + + # Don't log any deprecations. + config.active_support.report_deprecations = false + + # Replace the default in-process memory cache store with a durable alternative. # config.cache_store = :mem_cache_store - # Use a real queuing backend for Active Job (and separate queues per environment). + # Replace the default in-process and non-durable queuing backend for Active Job. # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "venture_ferris_production" - - # Disable caching for Action Mailer templates even if Action Controller - # caching is enabled. - config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false + # Set host to be used by links generated in mailer templates. + # This app ships no mailers (only the generated ApplicationMailer skeleton), + # so the Rails 8 template's `host: "example.com"` placeholder is left off + # rather than committing a hostname the deployment does not actually use. + # config.action_mailer.default_url_options = { host: "example.com" } + + # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. + # config.action_mailer.smtp_settings = { + # user_name: Rails.application.credentials.dig(:smtp, :user_name), + # password: Rails.application.credentials.dig(:smtp, :password), + # address: "smtp.example.com", + # port: 587, + # authentication: :plain + # } + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true - # Don't log any deprecations. - config.active_support.report_deprecations = false - # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false diff --git a/config/environments/test.rb b/config/environments/test.rb index 0c616a1..c2095b1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/integer/time" - # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped @@ -17,12 +15,11 @@ # loading is working properly before deploying your code. config.eager_load = ENV["CI"].present? - # Configure public file server for tests with Cache-Control for performance. - config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } + # Configure public file server for tests with cache-control for performance. + config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } - # Show full error reports and disable caching. + # Show full error reports. config.consider_all_requests_local = true - config.action_controller.perform_caching = false config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. @@ -34,28 +31,17 @@ # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test - # Disable caching for Action Mailer templates even if Action Controller - # caching is enabled. - config.action_mailer.perform_caching = false - # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test - # Unlike controllers, the mailer instance doesn't have any context about the - # incoming request so you'll need to provide the :host parameter yourself. - config.action_mailer.default_url_options = { host: "www.example.com" } + # Set host to be used by links generated in mailer templates. + config.action_mailer.default_url_options = { host: "example.com" } # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raise exceptions for disallowed deprecations. - config.active_support.disallowed_deprecation = :raise - - # Tell Active Support which deprecation messages to disallow. - config.active_support.disallowed_deprecation_warnings = [] - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index bd5bcd2..4873244 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -5,8 +5,3 @@ # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in the app/assets -# folder are already added. -# Rails.application.config.assets.precompile += %w[ admin.js admin.css ] diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index b3076b3..d51d713 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -20,6 +20,10 @@ # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src style-src) # +# # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` +# # if the corresponding directives are specified in `content_security_policy_nonce_directives`. +# # config.content_security_policy_nonce_auto = true +# # # Report violations without enforcing the policy. # # config.content_security_policy_report_only = true # end diff --git a/config/initializers/dartsass.rb b/config/initializers/dartsass.rb new file mode 100644 index 0000000..c89bafa --- /dev/null +++ b/config/initializers/dartsass.rb @@ -0,0 +1,13 @@ +# dartsass-rails compiles each entry point in app/assets/stylesheets/ into +# app/assets/builds/, which sprockets then fingerprints and serves (see +# app/assets/config/manifest.js's `link_tree ../builds`). `dartsass:build` is +# attached to `assets:precompile`, so production builds pick this up +# automatically; `bin/dev` runs `dartsass:watch` alongside the server. +# +# These two entry points are linked directly from views rather than being +# bundled together: "site" from the shared layout, "boss" only from the boss +# battle page (app/views/game/bosses/show.html.erb). +Rails.application.config.dartsass.builds = { + "site.scss" => "site.css", + "boss.scss" => "boss.css" +} diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index c010b83..c0b717f 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -4,5 +4,5 @@ # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += [ - :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn + :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc ] diff --git a/config/puma.rb b/config/puma.rb index 03c166f..1c317b4 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,13 +1,18 @@ # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. - +# # Puma starts a configurable number of processes (workers) and each process # serves each request in a thread from an internal thread pool. # +# You can control the number of workers using ENV["WEB_CONCURRENCY"]. You +# should only set this value when you want to run 2 or more workers. The +# default is already 1. You can set it to `auto` to automatically start a worker +# for each available processor. +# # The ideal number of threads per worker depends both on how much time the # application spends waiting for IO operations and on how much you wish to -# to prioritize throughput over latency. +# prioritize throughput over latency. # # As a rule of thumb, increasing the number of threads will increase how much # traffic a given process can handle (throughput), but due to CRuby's diff --git a/db/schema.rb b/db/schema.rb index b9997fe..3a3ce3f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,31 +10,31 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 2026_09_01_000000) do +ActiveRecord::Schema[8.1].define(version: 2026_09_01_000000) do # These are extensions that must be enabled in order to support this database - enable_extension "plpgsql" + enable_extension "pg_catalog.plpgsql" create_table "admins", force: :cascade do |t| + t.datetime "created_at", null: false t.string "email", null: false t.string "password_digest", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false t.integer "role", default: 0, null: false + t.datetime "updated_at", null: false t.index ["email"], name: "index_admins_on_email", unique: true end create_table "boss_battles", force: :cascade do |t| - t.bigint "team_id", null: false - t.datetime "started_at" - t.datetime "ended_at" t.integer "attack_count", default: 0, null: false - t.integer "hp", default: 120, null: false + t.integer "bonus_time_seconds", default: 0, null: false t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "ended_at" + t.integer "hp", default: 120, null: false t.datetime "last_critical_at" t.bigint "question_id", null: false - t.integer "bonus_time_seconds", default: 0, null: false t.datetime "spotlight_until" + t.datetime "started_at" + t.bigint "team_id", null: false + t.datetime "updated_at", null: false t.index ["question_id"], name: "index_boss_battles_on_question_id" t.index ["team_id", "question_id"], name: "index_boss_battles_on_team_id_and_question_id", unique: true t.index ["team_id"], name: "index_boss_battles_on_team_id" @@ -42,8 +42,8 @@ create_table "boss_readies", force: :cascade do |t| t.bigint "boss_battle_id", null: false - t.bigint "player_id", null: false t.datetime "created_at", null: false + t.bigint "player_id", null: false t.datetime "updated_at", null: false t.index ["boss_battle_id", "player_id"], name: "index_boss_readies_on_battle_and_player", unique: true t.index ["boss_battle_id"], name: "index_boss_readies_on_boss_battle_id" @@ -52,10 +52,10 @@ create_table "boss_skill_uses", force: :cascade do |t| t.bigint "boss_battle_id", null: false - t.bigint "player_id", null: false - t.string "skill", null: false t.datetime "consumed_at" t.datetime "created_at", null: false + t.bigint "player_id", null: false + t.string "skill", null: false t.datetime "updated_at", null: false t.index ["boss_battle_id", "player_id"], name: "index_boss_skill_uses_on_battle_and_player", unique: true t.index ["boss_battle_id"], name: "index_boss_skill_uses_on_boss_battle_id" @@ -63,21 +63,21 @@ end create_table "bosses", force: :cascade do |t| - t.string "sprite", null: false t.datetime "created_at", null: false + t.string "sprite", null: false t.datetime "updated_at", null: false t.index ["sprite"], name: "index_bosses_on_sprite", unique: true end create_table "players", force: :cascade do |t| - t.bigint "team_id", null: false - t.integer "role", null: false + t.datetime "created_at", null: false t.string "email", null: false - t.integer "job" - t.string "name" t.integer "gender", default: 0, null: false + t.integer "job" t.string "mobile" - t.datetime "created_at", null: false + t.string "name" + t.integer "role", null: false + t.bigint "team_id", null: false t.datetime "updated_at", null: false t.index ["team_id", "email"], name: "index_players_on_team_id_and_email", unique: true t.index ["team_id", "job"], name: "index_players_on_team_id_and_job", unique: true, where: "(job IS NOT NULL)" @@ -85,12 +85,12 @@ end create_table "question_attempts", force: :cascade do |t| - t.bigint "team_id", null: false - t.bigint "question_id", null: false - t.datetime "started_at" + t.datetime "created_at", null: false t.datetime "ended_at" t.integer "hint_count", default: 0, null: false - t.datetime "created_at", null: false + t.bigint "question_id", null: false + t.datetime "started_at" + t.bigint "team_id", null: false t.datetime "updated_at", null: false t.index ["question_id"], name: "index_question_attempts_on_question_id" t.index ["team_id", "question_id"], name: "index_question_attempts_on_team_id_and_question_id", unique: true @@ -98,69 +98,69 @@ end create_table "question_hints", force: :cascade do |t| - t.bigint "question_id", null: false - t.integer "position", null: false t.text "content", null: false t.datetime "created_at", null: false + t.integer "position", null: false + t.bigint "question_id", null: false t.datetime "updated_at", null: false t.index ["question_id", "position"], name: "index_question_hints_on_question_id_and_position", unique: true t.index ["question_id"], name: "index_question_hints_on_question_id" end create_table "questions", force: :cascade do |t| - t.integer "number", null: false - t.integer "kind", default: 1, null: false - t.string "title", null: false t.string "answer_digest" - t.text "content" - t.string "level" - t.text "explanation" t.boolean "auto_start", default: false, null: false t.integer "base_score", default: 1000, null: false - t.integer "puzzle_rows" - t.integer "puzzle_cols" t.integer "boss_hp", default: 120, null: false + t.bigint "boss_id", null: false + t.integer "boss_phase" t.integer "boss_time_limit", default: 30, null: false + t.text "content" t.datetime "created_at", null: false + t.text "explanation" + t.integer "kind", default: 1, null: false + t.string "level" + t.integer "number", null: false + t.integer "puzzle_cols" + t.integer "puzzle_rows" + t.string "title", null: false t.datetime "updated_at", null: false - t.bigint "boss_id", null: false - t.integer "boss_phase" t.index ["boss_id"], name: "index_questions_on_boss_id" t.index ["number"], name: "index_questions_on_number", unique: true end create_table "reward_codes", force: :cascade do |t| - t.string "code", null: false - t.boolean "test_mode", default: false, null: false - t.string "player_email" t.datetime "claimed_at" + t.string "code", null: false t.datetime "created_at", null: false + t.string "player_email" + t.boolean "test_mode", default: false, null: false t.datetime "updated_at", null: false t.index ["code"], name: "index_reward_codes_on_code", unique: true t.index ["player_email"], name: "index_reward_codes_on_player_email" end create_table "score_entries", force: :cascade do |t| - t.bigint "team_id", null: false - t.integer "question_score", default: 0, null: false - t.integer "time_score", default: 0, null: false - t.integer "hint_score", default: 0, null: false t.integer "boss_score", default: 0, null: false + t.datetime "created_at", null: false + t.integer "hint_score", default: 0, null: false t.integer "job_score", default: 0, null: false + t.bigint "question_id", null: false + t.integer "question_score", default: 0, null: false + t.bigint "team_id", null: false + t.integer "time_score", default: 0, null: false t.integer "total_score", default: 0, null: false - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.bigint "question_id", null: false t.index ["question_id"], name: "index_score_entries_on_question_id" t.index ["team_id", "question_id"], name: "index_score_entries_on_team_id_and_question_id", unique: true t.index ["team_id"], name: "index_score_entries_on_team_id" end create_table "teams", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "name" t.string "serial_no", null: false t.boolean "test_mode", default: false, null: false - t.string "name" - t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["serial_no"], name: "index_teams_on_serial_no", unique: true end diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cff2fd7..d9d933f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -175,11 +175,23 @@ Bootstrap 4.1.3(含 Font Awesome)全套 CDN,終點是: `app/assets/tailwind/application.css`(`@import "tailwindcss"`)經 `tailwindcss-rails` 編譯出 `app/assets/builds/tailwind.css`,負責全站絕大部分 -樣式。`sassc-rails`(`libsass` 綁定)現在只剩下編譯上一節提到的兩份少量自訂 -CSS(`site.scss`/`boss.scss`)——拼圖/Boss 疊圖等 Tailwind utility 表達不了 -或不划算表達的功能性定位樣式。`sassc`/`sassc-rails` 上游已宣告 EOL、不再 -維護;**未來待辦**:換成 `dartsass-rails`(官方目前建議的替代方案),屆時 -只需要把兩個 `.scss` 檔案原樣搬過去、調整 Gemfile,不影響任何其他程式碼。 +樣式。`dartsass-rails`(Dart Sass 官方實作)現在只剩下編譯上一節提到的兩份 +少量自訂 CSS(`site.scss`/`boss.scss`)——拼圖/Boss 疊圖等 Tailwind utility +表達不了或不划算表達的功能性定位樣式。原本使用的 `sassc`/`sassc-rails` +(`libsass` 綁定)上游已宣告 EOL,已於 Rails 8 升級時一併換掉。 + +編譯路徑:`config/initializers/dartsass.rb` 定義兩個 entry point,dart-sass +把它們編進 `app/assets/builds/`,再由 sprockets 加上 digest 後輸出(與 +`tailwind.css` 同一條路徑,見 `app/assets/config/manifest.js` 的 +`link_tree ../builds`)。`dartsass:build` 已自動掛在 `assets:precompile` 與 +`test:prepare` 之後,`bin/dev`(`Procfile.dev`)則會另起 `dartsass:watch`。 + +**一個換裝時的取捨要知道**:dart-sass 沒有 sprockets 的 `asset-url()` helper +(會被當成未知的 CSS 函式原樣輸出,瀏覽器解不開)。兩份 scss 原本共 3 處 +`asset-url()` 引用 `bg.jpg`/`bossBg.png`,改成讀 CSS custom property +(`--site-bg-image`/`--boss-bg-image`),由 layout 與 Boss 頁的 `yield :head` +用 `image_path` 注入帶 digest 的網址——這樣才能同時保留 dart-sass 與資產 +指紋。新增背景圖引用時請沿用這個作法,不要寫死路徑。 ## 相較原作的新增功能 diff --git a/docs/RAILS_UPGRADE_RESEARCH.md b/docs/RAILS_UPGRADE_RESEARCH.md new file mode 100644 index 0000000..96962d1 --- /dev/null +++ b/docs/RAILS_UPGRADE_RESEARCH.md @@ -0,0 +1,104 @@ +# Rails 7.2 EOL 與升級路徑查證 + +> 查閱日期:2026-09-02。來源:rubyonrails.org/maintenance、guides.rubyonrails.org/upgrading_ruby_on_rails.html、rubygems.org(皆官方)。 +> 註:原委派的研究 agent 因機器休眠中斷,本文件由主對話直接以官方來源完成查證。 + +## 結論摘要 + +1. **Rails 7.2.x 已完全 EOL**:安全修補止於 2026-08-09(官方 maintenance 頁;與 brakeman `EOLRails` 警告一致)。**必須升級**。 +2. **升級目標選 8.1.x(現行 8.1.3.1)**,不要停在 8.0:8.0.x 的安全修補只到 **2026-11-07**(剩兩個月),8.1.x 的 bug 修補到 2026-10-10、安全修補到 **2027-10-10**。 +3. **Ruby 3.3.5 可直上**:Rails 8.0/8.1 最低要求 Ruby 3.2.0。(Ruby 版本升級可另案,非本次前置。) +4. **官方建議逐個 minor 走**:7.2 → 8.0.5.1(app:update、修 deprecation、測試綠)→ 8.1.3.1(同流程)。 +5. **Sprockets 在 Rails 8 仍是可用的 optional gem**(7.0 起即為 optional dependency);官方升級指南對 8.x 未要求換 Propshaft。是否順勢把 sassc-rails→dartsass(README 既列的未來項)併入本次:**建議併入**——避免「升了框架還掛著 EOL 編譯器」的半吊子狀態,但作為升級後的獨立 commit,失敗可單獨退回。 +6. 官方指南明列的 8.0→8.1 變更:`schema.rb` 欄位改字母排序(會有一次性的大 schema diff,屬預期)。7.2→8.0 的細節在 release notes(實作時逐項對照 `load_defaults 8.0` 的 new_framework_defaults 檔案)。 + +## 版本支援時間表(官方 maintenance 頁) + +| 系列 | bug 修補 | 安全修補 | 狀態 | +|---|---|---|---| +| 8.1.x(8.1.3.1) | 至 2026-10-10 | 至 **2027-10-10** | ✅ 建議目標 | +| 8.0.x(8.0.5.1) | 已結束(2026-05-07) | 至 2026-11-07 | ⚠️ 僅升到此=兩個月後再升 | +| 7.2.x(本專案) | 已結束 | **已結束(2026-08-09)** | ❌ 完全 EOL | +| ≤7.1 | — | — | ❌ 完全 EOL | + +## 本專案受影響面初盤 + +- **Gemfile**:`rails ~> 7.2.2` → `~> 8.1.3`;其餘 gem(pg、puma、importmap/turbo/stimulus、rqrcode、rack-attack、tailwindcss-rails、bcrypt、sprockets-rails、sassc-rails)皆為活躍維護、預期相容(bundle update 時驗證)。 +- **config**:`load_defaults 7.2` → 逐步 8.0 → 8.1;`app:update` 產生的 new_framework_defaults 逐項審閱(本專案自訂點:`assume_ssl`、`ALLOWED_HOSTS`+`/up` 豁免、Taipei 時區、css_compressor nil——最後一項若換 dartsass 可移除)。 +- **schema.rb**:8.1 起欄位字母排序——升級後 `db:migrate` 會產生一次大而無害的 schema diff。 +- **測試安全網**:222 integration+13 system+M0 計分黃金值(升級全程不得改其字面量)。 +- **enum**:已是新語法,無虞。無 ActiveStorage/ActionCable/ActionMailbox 實際使用,升級面小。 +- **資產管線**(若併入換裝):sassc-rails → dartsass-rails(沿用 sprockets);`site.scss`/`boss.scss` 語法為一般 CSS+巢狀,dart-sass 相容性風險低;`config.assets.css_compressor = nil` workaround 屆時可望移除(dart-sass 輸出不再經 libsass 壓縮——實作時驗證)。 + +## 建議實作批次 + +1. **B1:7.2 → 8.0.5.1**(Gemfile、bundle update、app:update 逐檔審閱、deprecation 清零、全測試綠+M0 不變) +2. **B2:8.0 → 8.1.3.1**(同流程+接受 schema 排序 diff) +3. **B3(可選但建議):sassc-rails → dartsass-rails**(獨立 commit,失敗可退) +4. 全程一個 PR、三個 commit;verifier 總驗收(含 production boot、assets:precompile、demo 全鏈 curl)後合併——自動部署上線。 + +--- + +## 實作附錄(2026-09-02 完成) + +升級已完成並落地為三個 commit,全程未修改 M0 計分黃金值測試 +(`test/integration/game/scoring_golden_test.rb`,MD5 前後皆為 +`b5f9bf50204b0b7f74d063c26734d602`)。 + +| 批次 | commit | 內容 | +|---|---|---| +| B1 | `chore: 升級 Rails 7.2.2.1 至 8.0.5.1` | Gemfile、app:update 逐檔取捨、`load_defaults 8.0` | +| B2 | `chore: 升級 Rails 8.0.5.1 至 8.1.3.1` | 同流程、`load_defaults 8.1`、schema 字母排序重排 | +| B3 | `feat: 以 dartsass-rails 取代 EOL 的 sassc-rails` | CSS 編譯器換裝、移除 css_compressor workaround | + +### app:update 的通則:範本會覆蓋自訂設定,每一批都要重做一次 + +`app:update` 在 B1 與 B2 都把下列自訂設定整段刪掉或註解掉,兩批都必須手動復原 +(B2 甚至把 `assume_ssl`/`force_ssl` 兩行一起註解掉——若沒發現,production +會退回無 TLS 假設的狀態): + +- `config/application.rb`:Taipei 時區 +- `config/environments/production.rb`:`assume_ssl`/`force_ssl`、 + `ALLOWED_HOSTS` 與 `/up` 的 host authorization 豁免、`assets.compile = false` +- `config/environments/development.rb`:sprockets 的 + `assets.quiet`/`assets.debug`/`assets.compile` +- `bin/brakeman`(範本會把移除過的 `--ensure-latest` 加回來)、 + `bin/dev`(範本改成直接 exec rails server,會廢掉 Procfile.dev) + +**Rails 8 範本假設 Propshaft**,所以凡是 sprockets 專屬的設定(上面 +`assets.*` 那幾項)範本都不會有,升級時要自己補回去,不能當成「範本刪掉了 +表示不需要」。另外每批 `app:update` 都會重新複製三個 ActiveStorage +migration,本專案沒有相關資料表,兩批都需刪除。 + +### 8.1 的 schema.rb 重排 + +除了預期的欄位字母排序外,實際還有兩處差異(已逐行比對確認無欄位增刪): +`ActiveRecord::Schema[7.2]` → `[8.1]`,以及 `enable_extension "plpgsql"` → +`"pg_catalog.plpgsql"`(8.1 改為輸出 schema 限定的擴充名稱)。 + +### dartsass 換裝:唯一的實質不相容是 `asset-url()` + +兩份 scss 的語法 dart-sass 完全吃得下,沒有任何語法錯誤。真正的坑是 +**dart-sass 沒有 sprockets 的 `asset-url()` helper**——它不會報錯,而是把 +`asset-url("bg.jpg")` 當成未知的 CSS 函式原樣輸出,瀏覽器解不開,背景圖靜默 +消失。3 處引用改為 CSS custom property(`--site-bg-image`/`--boss-bg-image`), +由 layout 與 Boss 頁 `yield :head` 用 `image_path` 注入帶 digest 的網址, +兼顧 dart-sass 與資產指紋。 + +連帶處理:`app/assets/config/manifest.js` 移除 `link_directory ../stylesheets .css` +(否則 sprockets 會試圖自己編 scss 而找不到 sassc);刪除沒有任何地方引用、 +且 `require_tree .` 會把 scss 拉進 sprockets 的 `app/assets/stylesheets/application.css`; +`Procfile.dev` 補上 `dartsass:watch`。 + +`config.assets.css_compressor = nil` 的 workaround 已移除並實測:libsass 不再 +參與,production precompile 後 `tailwind.css` 的 digest 與移除前完全相同, +確認沒有壓縮器介入。 + +### 一個與 Rails 無關、但會污染輸出的坑 + +`dartsass:build` 會 `system()` 出一個新的 Ruby 行程,於是 `Gemfile.lock` 的 +`BUNDLED WITH`(原為 2.5.16)與實際執行的 bundler(2.7.2)版本不一致就會在 +每次測試輸出裡噴 8 對 `already initialized constant Gem::Platform::*` 警告。 +把 `BUNDLED WITH` 對齊為 2.7.2 即可清零。注意 `bundle update --bundler` 會直接 +跳到當時最新的 bundler 4.0.19(major 版本,Dockerfile 有 `BUNDLE_DEPLOYMENT=1`, +風險高),不要照單全收,手動寫回實際在跑的版本比較安全。 diff --git a/docs/REFACTOR_PLAN.md b/docs/REFACTOR_PLAN.md index b682de4..7aa4fb6 100644 --- a/docs/REFACTOR_PLAN.md +++ b/docs/REFACTOR_PLAN.md @@ -82,7 +82,7 @@ seeds 明訂中文顯示名(阿北/鄉民/鞋姊/罔美)↔ enum 對映。 - **jQuery 全站保留,走 classic script(不走 importmap ESM**,minified plugin 非 ESM 且 Bootstrap 4 modal 硬依賴 jQuery):jQuery 3.3.1 / jQuery UI 1.12.1 / Bootstrap 4.1.3 **沿用舊站的 CDN `