Findings from code review. Work through sequentially, top to bottom.
Once done with an item, commit and dont co-author, do only titles, and keep it simple. One commit per TODO item.
- Negative limit returns 500.
app/controllers/api/v1/facts_controller.rb:19,app/controllers/api/v2/facts_controller.rb:14.limit.to_iof-1is neither zero nor> 5, so it reaches the query and emitsLIMIT -1, which Postgres rejects (ActiveRecord::StatementInvalid). Replace withlimit.to_i.clamp(1, 5). -
raw=trueon empty facts table raises NoMethodError.app/controllers/api/v1/facts_controller.rb:12calls@facts.first.bodywithout a nil guard. Now returns 404. - Clamp semantics are surprising.
?limit=10currently returns 1 fact instead of 5. Fixed by theclamp(1, 5)change above;swagger/already documented min 1 / max 5, so no doc change was needed. - Add request specs covering all three cases (see item 23). Added
spec/requests/api/v{1,2}/facts_behavior_spec.rb, separate from the rswag doc files.
- Restrict CORS.
config/initializers/cors.rballowspost patch putfromorigins "*"on every resource. The API is read-only — allowgetandoptionsonly. - Add
/.envto.gitignore. It is currently untracked but holdsUMAMI_PASSWORD; onegit add -Aleaks it. Commit a.env.examplewith the fourUMAMI_*keys and no values. - Enable production hardening.
config/environments/production.rbhasassume_ssl,force_ssl, andconfig.hostsall commented out. App sits behind Traefik TLS — enableassume_sslandforce_ssl, and setconfig.hoststodogapi.dogfor Host-header / DNS-rebinding protection. - Bump framework defaults.
config/application.rbpinsconfig.load_defaults 7.0while running Rails 8.1. Step up usingconfig/initializers/new_framework_defaults_8_1.rb. - Define a CSP.
config/initializers/content_security_policy.rbis entirely commented out, and the public pages carry inline JS.
- Add
.dockerignore. Build context currently ships.git/,log/, and hundreds oftmp/miniprofiler/files. - Run as non-root.
Dockerfilehas noUSER. - Multi-stage build. Single stage leaves
build-essentialandgitin the runtime image. - Delete
entrypoint.sh. It runsbundle installat container start and is referenced by neither the Dockerfile nordocker-compose.yml.
- Fix N+1 in v2 index actions.
GroupSerializerdeclareshas_many :breedsandBreedSerializerdeclaresbelongs_to :group, but neither index preloads.breeds#indexpermitspage[size]=1000, so that is up to 1000 group lookups. Add.includes(:group)/.includes(:breeds). - Move rack-attack off MemoryStore.
config/initializers/rack_attack.rb:6uses an in-process store, so the real limit is 300/min per puma worker and resets on restart. Redis is already running indocker-compose.yml. - Replace
ORDER BY RANDOM(). Used in v1 facts, v2 facts, andpages#index(homepage, every hit). Full sort per request, no usable index. UseTABLESAMPLE, a random offset, or a cached id list. - Move Umami tracking off raw threads.
app/controllers/concerns/umami_trackable.rb:19spawns an unboundedThread.newper API response, with no pool, killed mid-flight on shutdown. Sidekiq is already in the Gemfile. Also drop the per-requestRails.logger.debuginterpolation on line 14. - Add HTTP caching. No ETag or Cache-Control on breeds/groups, which are effectively static. Cheap CDN win.
- Add missing indexes.
facts.uuid(public-facing id, currently unindexed),breeds.nameandgroups.name(both ordered on).
- Drop unused gems. No references anywhere in the codebase:
redis,sidekiq,groupdate,ransack,jbuilder,image_processing,aws-sdk-s3, andsqlite3(in the test group, but the suite runs on Postgres). Keepredis/sidekiqonly if the rack-attack and Umami items above are done first. Each unused gem is CVE surface and image weight. - Remove the
Usermodel anduserstable. No auth, no routes, nohas_secure_password(bcrypt is not even a dependency), yetpassword_digestandremember_tokencolumns exist. - Resolve
pg_search. Scopes are defined on all three models and called nowhere; there is no search endpoint. Ship search or drop the gem. - Resolve
JSONAPI::Filtering. Included inApi::V2::BaseControllerbutjsonapi_filteris never called, so clients sendingfilter[...]silently get unfiltered results. Implement it or remove the include. - Mount or delete rswag UI.
config/initializers/rswag_ui.rbconfigures endpoints, butRswag::Ui::Engineis never mounted inconfig/routes.rb— the docs UI is unreachable. - De-duplicate pagination.
jsonapi_page_sizeis identical inbreeds_controller.rbandgroups_controller.rb. Move it toApi::V2::BaseController.
- Add
/uphealth endpoint.config/environments/production.rb:47silences/up, but no such route exists anddocker-compose.ymlhas no healthcheck. Addget "up" => "rails/health#show". - Write real request specs. Everything under
spec/requests/is an rswag doc-generator block asserting only a200. Nothing covers limit clamping,raw=true, 404 paths, or pagination bounds — which is why the bugs in section 1 exist. Keep the rswag blocks for doc generation, add separate specs for behavior. - Set a simplecov minimum threshold. Installed, but currently enforces nothing.
- Fill in the near-empty model specs.
spec/models/fact_spec.rbandspec/models/user_spec.rbare 3 lines each (the latter goes away ifUseris removed). - Bump Postgres off 13. EOL as of November 2025. Pinned in both
.github/workflows/ci.ymlanddocker-compose.yml.