Task: Dead Views and Partials Cleanup
Created: April 25, 2026 Priority: Low Estimated Savings: ~13 view files (Tier 1, zero risk) up to ~700 view files (full sweep) Risk: Low for Tier 1, Medium for Tier 2, Higher for Tier 3 (dynamic-render directories)
Overview
Section titled “Overview”Static analysis of app/views/ against controller / route / render call
sites surfaced ~678 partial files and several full view directories with
no obvious references. Sample-verifying 15 random partials from the
candidate list found 15/15 truly unreferenced, so the signal is good —
but the long tail (shared/, pages/, root-level partials) carries
false-positive risk because of dynamic render "#{prefix}_partial"
patterns and gem-rendered views.
This task is a follow-up to the Rails 7.2 upgrade work. It is not a prerequisite for Rails 8.x — it just removes dead code so the eventual 8.x bump has less surface to worry about.
How the candidate list was built
Section titled “How the candidate list was built”Tight regex over every .rb, .erb, .haml in app/, lib/, config/:
render(?:\s+|\s*\(\s*)(?:(?:partial|template|layout)\s*:\s*)?["']<name>["']| partial\s*:\s*["']<name>["']A partial at app/views/foo/_bar.html.erb was searched by both:
- the qualified name
foo/baracross the entire repo, and - the short name
barwithin theapp/views/foo/**subtree.
Auto-collection rendering (render @foos) was filtered by skipping
partials whose short name matches a model name and whose containing
directory is the plural form of that name.
The output list lives in /tmp/unused_partials_tight.txt from the
analysis session — regenerate it before deleting anything (production
adds and removes views constantly).
Tier 1 — Definitely dead (zero risk, ~13 files)
Section titled “Tier 1 — Definitely dead (zero risk, ~13 files)”Verified three ways: no controller on disk, no route entry, no
render call referencing the file.
app/views/articles/ (7 files — delete the entire directory)
Section titled “app/views/articles/ (7 files — delete the entire directory)”_article.html.erb_form.html.erb_product_linking_form.html.erbedit.html.erbindex.html.erbnew.html.erbshow.html.erb
Notes: there is no ArticlesController. The only /articles/:id route
in config/routes/www.rb points at www/support_articles#show — a
different controller that has its own views under
app/views/www/support_articles/.
app/views/blocks/ (5 files — delete the entire directory)
Section titled “app/views/blocks/ (5 files — delete the entire directory)”_form.html.erbedit.html.erbindex.html.erbnew.html.erbshow.html.erb
Notes: no BlocksController, no Block model in app/models/ (the
only Block class on disk is lib/dxf/floor_plan.rb:343 — a DXF
geometry primitive, unrelated). No routes target blocks#….
Cascade-dead from the blocks/ removal
Section titled “Cascade-dead from the blocks/ removal”app/views/employee_work_schedules/_full_calendar.html.erb (1 file)
calls render partial: 'blocks/calendar' (line 2). That target
partial does not exist either — the call would 500 if the partial
were ever rendered. The _full_calendar.html.erb partial itself is
not rendered from anywhere. Delete it.
Other confirmed-dead one-offs
Section titled “Other confirmed-dead one-offs”app/views/employee_events/_new_event_modal.html.erb— no caller.
Tier 2 — Low risk, per-file review (~250 files)
Section titled “Tier 2 — Low risk, per-file review (~250 files)”These are partials inside directories where a live controller exists and live siblings are still rendered — meaning the feature is alive but specific partials inside it have no callers. Bucket counts from the candidate list:
26 app/views/room_configurations26 app/views/www/products23 app/views/menus19 app/views/uploads13 app/views/preset_jobs11 app/views/employee_events10 app/views/line_items 9 app/views/crm/payments/gateways 9 app/views/opportunities 9 app/views/orders 8 app/views/praises 8 app/views/crm/amazon_products 8 app/views/coupons 8 app/views/communications 8 app/views/images 8 app/views/www/leads 7 app/views/quotes 7 app/views/my_accounts 6 app/views/customers 6 app/views/deliveries 6 app/views/my_carts 6 app/views/crm/reports/opportunities_report 6 app/views/crm/reports/reports 6 app/views/searches/item_search 6 app/views/searches/customer_search (subset of the searches/ tree) 5 app/views/posts 5 app/views/my_orders 5 app/views/my_addresses 5 app/views/items 5 app/views/www/payments/gateways 4 app/views/invoices…Recommended approach: drop directory-by-directory, run the full test suite per drop, smoke the corresponding feature in QA. Since CRM and www are different deployment targets, group commits by area.
Tier 3 — Dynamic-render risk, manual eyeball needed (~400 files)
Section titled “Tier 3 — Dynamic-render risk, manual eyeball needed (~400 files)”46 app/views/shared25 app/views/shared/heat_loss21 app/views/pages17 app/views (root-level _foo.html.erb partials)…The risk in these directories is render "#{prefix}_partial" and
render "shared/#{type}"-style calls. Before deleting any file in
shared/, pages/, or the root, run:
git grep -E 'render(?:\s+|\s*\(\s*)["][^"]*#\{' -- '*.erb' '*.rb' | head -40…to find all dynamic-render call sites and audit them by hand.
The ~17 root-level app/views/_*.erb partials are the cleanest place
to start in this tier — they are referenced (if at all) as bare
render 'live_chat' style, which the static scan does pick up. Most
of them appear to be CMS leftovers from before the pages/ refactor
and are likely safe to delete after a quick eyeball.
Tier 4 — Routes / actions / controllers (separate task)
Section titled “Tier 4 — Routes / actions / controllers (separate task)”Not addressed by this task. The right tool is the traceroute gem
(already in the :development group of the Gemfile). Once the app
boots locally:
bundle exec rake traceroute…produces an authoritative list of Unused routes and Routes without action. Static analysis without booting is unreliable because:
- Many routes use
controller: 'admin/foo'syntax. - Devise / Doorkeeper inject controllers via
devise_for :foo, controllers: { … }. BasePortalController-style abstract classes have no direct route but are inherited by live controllers.
Plan of attack
Section titled “Plan of attack”- Land Tier 1 as one commit on the Rails 7.2 PR branch (or a new small PR) — 13 file deletions, no behavior change. Smoke test: none required; nothing else references these files.
- After Tier 1 ships, regenerate the candidate list (production
ships features weekly). Re-run the analysis script on the current
masterso we don’t ship a stale list. - Land Tier 2 in directory-grouped commits. One commit per area is fine (CRM / www / employee / accounting). Smoke-test each feature as it ships.
- Run
bundle exec rake tracerouteto drive the Tier 4 routes / actions cleanup as a separate follow-up. - Save Tier 3 for last — it’s the smallest reward per hour of review and the highest false-positive risk.
Regenerating the candidate list
Section titled “Regenerating the candidate list”require 'pathname'
views_root = Pathname('app/views')partials = []Dir.glob('app/views/**/_*.{erb,haml}').sort.each do |path| pn = Pathname(path) basename = pn.basename.to_s next unless basename =~ /^_(.+?)(?:\.([a-z_]+))?\.(erb|haml)$/ short = $1 dirpath = pn.dirname.relative_path_from(views_root).to_s qualified = dirpath == '.' ? short : "#{dirpath}/#{short}" partials << { path: path, short: short, qualified: qualified, dirpath: dirpath }end
corpus = {}%w[app lib config].each do |d| Dir.glob("#{d}/**/*.{rb,erb,haml}").each do |f| corpus[f] = (File.read(f, encoding: 'UTF-8') rescue nil) endendcorpus.compact!
def render_pattern(name) esc = Regexp.escape(name) Regexp.new( "render(?:\\s+|\\s*\\(\\s*)(?:(?:partial|template|layout)\\s*:\\s*)?[\"']#{esc}[\"']" \ "|partial\\s*:\\s*[\"']#{esc}[\"']" )end
unused = []partials.each do |p| patt_q = render_pattern(p[:qualified]) patt_s = (p[:dirpath] != '.') ? render_pattern(p[:short]) : nil found = false corpus.each do |f, src| next if f == p[:path] if src.match?(patt_q) found = true break end if patt_s && f.start_with?("app/views/#{p[:dirpath]}/") && src.match?(patt_s) found = true break end end unused << p[:path] unless foundendputs unusedRun as ruby scripts/find_unused_partials.rb > /tmp/unused.txt.
Out of scope (related cleanups noticed in passing)
Section titled “Out of scope (related cleanups noticed in passing)”Gemfile.lockstill has aGIT remote: https://github.com/warmlyyours/dragonfly-s3_data_storeentry for thedragonfly-s3_data_storegem. Worth confirming dragonfly itself is still in use; if not, this and its transitive deps can be dropped.- Two PR-scope gaps from the earlier static audit are already fixed in the Rails 7.2 PR follow-up commits:
app/models/order_transaction.rbserialize :params→coder: YAMLapp/models/seo_page_keyword.rbenum keyword_target: { … }→ positional formlib/online_migrations/.../backfill_google_ads_visit_sources.rbconnection→lease_connection