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.
| Finding | Severity | Location | Fix |
|---|---|---|---|
| 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 |
@wewill_videos = WewillVideo.active.in_display_order.with_attached_video_file.with_attached_thumbnail
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.
PANEL_LIMIT = 10
scope :for_panel, -> { visible.limit(PANEL_LIMIT) }
PANEL_LIMIT = 12
scope :for_panel, -> { active.in_display_order.limit(PANEL_LIMIT) }
@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.
%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 }
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.
poster: (url_for(video.thumbnail) if video.thumbnail.attached?)
%source{ src: url_for(video.video_file), ... }
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.
When a WewillVideo has no thumbnail attached, this Stimulus controller takes over as the poster fallback:
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, ...)
}
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.
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.
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
after_create_commit -> { WewillVideoThumbnailJob.perform_later(self) }, unless: :thumbnail?
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.
| # | Change | Effort | Risk |
|---|---|---|---|
| 01 | Add for_panel scope + use it in the controller | Small | Low |
| 02 | Swap url_for → rails_storage_proxy_path for src/poster | Small | Low |
| 03a | Add ffmpeg to the Dockerfile; ship WewillVideoThumbnailJob + the after_create_commit hook | Medium | Low — additive, no existing behavior removed yet |
| 03b | Backfill thumbnails for existing videos from the console, confirm every active video has one | Small | Low |
| 03c | Delete video_thumbnail_controller.js and its wiring in _wewill.html.haml | Small | Low — 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.