Skip to content

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)

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.

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/bar across the entire repo, and
  • the short name bar within the app/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.erb
  • edit.html.erb
  • index.html.erb
  • new.html.erb
  • show.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.erb
  • edit.html.erb
  • index.html.erb
  • new.html.erb
  • show.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#….

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.

  • 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_configurations
26 app/views/www/products
23 app/views/menus
19 app/views/uploads
13 app/views/preset_jobs
11 app/views/employee_events
10 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/shared
25 app/views/shared/heat_loss
21 app/views/pages
17 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.
  1. 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.
  2. After Tier 1 ships, regenerate the candidate list (production ships features weekly). Re-run the analysis script on the current master so we don’t ship a stale list.
  3. Land Tier 2 in directory-grouped commits. One commit per area is fine (CRM / www / employee / accounting). Smoke-test each feature as it ships.
  4. Run bundle exec rake traceroute to drive the Tier 4 routes / actions cleanup as a separate follow-up.
  5. Save Tier 3 for last — it’s the smallest reward per hour of review and the highest false-positive risk.
scripts/find_unused_partials.rb
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)
end
end
corpus.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 found
end
puts unused

Run as ruby scripts/find_unused_partials.rb > /tmp/unused.txt.

Section titled “Out of scope (related cleanups noticed in passing)”
  • Gemfile.lock still has a GIT remote: https://github.com/warmlyyours/dragonfly-s3_data_store entry for the dragonfly-s3_data_store gem. 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.rb serialize :paramscoder: YAML
    • app/models/seo_page_keyword.rb enum keyword_target: { … } → positional form
    • lib/online_migrations/.../backfill_google_ads_visit_sources.rb connectionlease_connection