Engineering finding — home page

#WEWILL video panel: what's slowing it down

Three concrete issues found while reviewing how HomeController#index loads and renders the WeWill video carousel — an unbounded query, an avoidable network round trip on every asset, and a client-side fallback that redoes work on every page load.

Scope app/controllers/home_controller.rb → app/views/home/_wewill.html.haml Dated 2026‑08‑25 Findings 3 Status resolution decided — see below
FindingSeverityLocationFix
No cap on the WeWill video query — every active video loads on every homepage hit high home_controller.rb:14 Add a for_panel scope, matching every sibling model
Video/poster URLs use Active Storage's redirect helper — two round trips per asset medium _wewill.html.haml Switch to rails_storage_proxy_path
Missing thumbnails are captured client-side, from scratch, on every visit low video_thumbnail_controller.js Generate + attach a real thumbnail server-side via a background job
01 high

The query is the only one of its kind with no cap

app/controllers/home_controller.rb:14

Where

app/controllers/home_controller.rbcurrent
@wewill_videos = WewillVideo.active.in_display_order.with_attached_video_file.with_attached_thumbnail

Why it matters

Every other collection rendered on this same page — Deliverable, GroundReport, TrailMix, CanWeTalk — goes through a for_panel scope capped at PANEL_LIMIT = 10 (CanWeTalk even has a separate VIDEO_PANEL_LIMIT = 5 just for its video carousel). WewillVideo is the one exception: no limit anywhere in the chain. Every active video ever uploaded — and both its video_file and thumbnail blobs — loads on every single homepage request, and the cost only grows as the admin adds more over time.

The established pattern (for comparison)

app/models/ground_report.rbreference
PANEL_LIMIT = 10
scope :for_panel, -> { visible.limit(PANEL_LIMIT) }

Fix

app/models/wewill_video.rbadd
PANEL_LIMIT = 12

scope :for_panel, -> { active.in_display_order.limit(PANEL_LIMIT) }
app/controllers/home_controller.rbchange
@wewill_videos = WewillVideo.for_panel
  .with_attached_video_file
  .with_attached_thumbnail

12 keeps two full carousel pages at the widest breakpoint (4 visible × ~3) without pulling in everything the admin has ever uploaded.

02 medium

Video and poster URLs cost two round trips instead of one

app/views/home/_wewill.html.haml

Where

app/views/home/_wewill.html.hamlcurrent
%video{ poster: (url_for(video.thumbnail) if video.thumbnail.attached?), preload: "metadata", ... }
  %source{ src: url_for(video.video_file), type: video.video_file.content_type }

Why it matters

url_for(attachment) is Active Storage's redirect helper. On the app's current Disk service, that means: the browser requests the video, Rails looks up the blob and responds with a signed 302, then the browser makes a second request to actually fetch the bytes. Two full round trips per asset, both served by the same Rails process — there's no external CDN yet for the redirect to usefully hand off to. Because preload="metadata" already fires a request per visible slide, this doubles the round trips for every video and every thumbnail in the carousel.

Fix

_wewill.html.haml− url_for (redirect, 2 requests)
poster: (url_for(video.thumbnail) if video.thumbnail.attached?)
%source{ src: url_for(video.video_file), ... }
_wewill.html.haml+ rails_storage_proxy_path (1 request)
poster: (rails_storage_proxy_path(video.thumbnail) if video.thumbnail.attached?)
%source{ src: rails_storage_proxy_path(video.video_file), ... }

Leave the download link and the share button as they are — rails_blob_path(video.video_file, disposition: "attachment") is a deliberate, on-click, one-off request where the Content-Disposition header from redirect mode is actually wanted, not part of the page-load path.

03 low

Missing thumbnails are recomputed client-side on every load

app/javascript/controllers/video_thumbnail_controller.js

Where

When a WewillVideo has no thumbnail attached, this Stimulus controller takes over as the poster fallback:

video_thumbnail_controller.jscurrent behavior
seekToFrame = () => {
  // downloads video metadata, then seeks ~2s in — a real byte-range fetch
  this.element.currentTime = Math.min(this.seekValue, (this.element.duration || 0) / 2)
}

captureFrame = () => {
  // draws the frame to a <canvas> and re-paints it as the poster — thrown away on reload
  canvas.getContext("2d").drawImage(this.element, 0, 0, ...)
}

Why it matters

Nothing persists the captured frame — it's redone from scratch, per video, on every single page load for any video missing a thumbnail. It's a genuine network + CPU cost (the metadata fetch and the seek are real requests, not free), and it's pure repeated waste since the outcome is identical every time.

Decision — generate the thumbnail server-side, once, via a background job

Two options were on the table: require the thumbnail at upload, or generate it automatically. Requiring it doesn't actually work cleanly here — the fallback exists precisely because a WewillVideo record can be created before any thumbnail is available, and a presence validation can't apply to a file a background job hasn't produced yet. Generating it server-side removes the fallback path entirely, for every video, without depending on an admin remembering an extra step — including the videos already uploaded without one.

Shape: a job triggered right after upload, following the same pattern SecurityEventGeocodeJob already uses elsewhere in this app — attach-time side effect handled asynchronously, not inline in the request.

app/jobs/wewill_video_thumbnail_job.rbnew
class WewillVideoThumbnailJob < ApplicationJob
  def perform(video)
    return if video.thumbnail.attached?

    preview = video.video_file.preview(resize_to_limit: [720, 1280]).processed
    video.thumbnail.attach(
      io: preview.image.download_to_tempfile,
      filename: "#{video.id}-thumbnail.jpg",
      content_type: "image/jpeg"
    )
  end
end
app/models/wewill_video.rbadd
after_create_commit -> { WewillVideoThumbnailJob.perform_later(self) }, unless: :thumbnail?
Dockerfileadd ffmpeg alongside the existing libvips line
apt-get install --no-install-recommends -y curl libjemalloc2 libvips ffmpeg postgresql-client

Rollout: ship the job + Dockerfile change, then backfill existing videos once — WewillVideo.active.where.missing(:thumbnail_attachment).find_each { |v| WewillVideoThumbnailJob.perform_later(v) } from the console. Once every active video has a real attached thumbnail, video_thumbnail_controller.js and its wiring in the view have no remaining callers and can be deleted outright.

Already working well — worth knowing before "fixing" anything else

Resolution plan

#ChangeEffortRisk
01Add for_panel scope + use it in the controllerSmallLow
02Swap url_forrails_storage_proxy_path for src/posterSmallLow
03aAdd ffmpeg to the Dockerfile; ship WewillVideoThumbnailJob + the after_create_commit hookMediumLow — additive, no existing behavior removed yet
03bBackfill thumbnails for existing videos from the console, confirm every active video has oneSmallLow
03cDelete video_thumbnail_controller.js and its wiring in _wewill.html.hamlSmallLow — only safe once 03b is confirmed

01 and 02 have no sequencing dependency on 03 and can ship independently, any time.

Findings from a review of the Invisible President Rails app's home page media pipeline. Scope limited to the #WEWILL video panel; not a full-page audit.