.vbw-planning/milestones/polish-and-reliability/phases/02-favicon-support/PLAN-03.md
Wire FaviconFetchJob into the source lifecycle: trigger on source creation (controller + OPML import) and on successful feed fetches when favicon is missing. REQ-FAV-03.
@app/controllers/source_monitor/sources_controller.rb -- create action (lines 54-62) for manual source creation trigger@lib/source_monitor/fetching/feed_fetcher/source_updater.rb -- update_source_for_success (lines 14-39) for feed success trigger@app/jobs/source_monitor/import_opml_job.rb -- OPML import creates sources in bulk@app/jobs/source_monitor/favicon_fetch_job.rb -- the job created in Plan 01 (must exist before this plan executes)This plan depends on Plan 01 because it references FaviconFetchJob which is created there. No file overlap with Plan 02 (which modifies views/helpers only). This plan modifies: sources_controller.rb, source_updater.rb, import_opml_job.rb, and creates integration tests.
Files: app/controllers/source_monitor/sources_controller.rb
In the create action (line 54-62), after @source.save succeeds but before the redirect, enqueue the favicon job:
Current:
def create
@source = Source.new(source_params)
if @source.save
redirect_to source_monitor.source_path(@source), notice: "Source created successfully"
else
render :new, status: :unprocessable_entity
end
end
Replace with:
def create
@source = Source.new(source_params)
if @source.save
enqueue_favicon_fetch(@source)
redirect_to source_monitor.source_path(@source), notice: "Source created successfully"
else
render :new, status: :unprocessable_entity
end
end
Add a private method:
def enqueue_favicon_fetch(source)
return unless defined?(ActiveStorage)
return unless SourceMonitor.config.favicons.enabled?
return if source.website_url.blank?
SourceMonitor::FaviconFetchJob.perform_later(source.id)
rescue StandardError => error
Rails.logger.warn("[SourceMonitor] Failed to enqueue favicon fetch: #{error.message}") if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
end
Tests: test/controllers/source_monitor/sources_controller_favicon_test.rb
Create a separate controller test file to avoid merge conflicts:
Files: lib/source_monitor/fetching/feed_fetcher/source_updater.rb
In update_source_for_success (lines 14-39), after source.update!(attributes) on line 39, add favicon fetch enqueue:
Add after source.update!(attributes) (line 39):
enqueue_favicon_fetch_if_needed
Add a private method to the class:
def enqueue_favicon_fetch_if_needed
return unless defined?(ActiveStorage)
return unless SourceMonitor.config.favicons.enabled?
return if source.website_url.blank?
return if source.respond_to?(:favicon) && source.favicon.attached?
# Check cooldown via metadata
last_attempt = source.metadata&.dig("favicon_last_attempted_at")
if last_attempt.present?
cooldown_days = SourceMonitor.config.favicons.retry_cooldown_days
return if Time.parse(last_attempt) > cooldown_days.days.ago
end
SourceMonitor::FaviconFetchJob.perform_later(source.id)
rescue StandardError => error
Rails.logger.warn(
"[SourceMonitor::SourceUpdater] Failed to enqueue favicon fetch for source #{source.id}: #{error.message}"
) if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
end
This duplicates some of the cooldown logic from the job itself (belt-and-suspenders). The reason is to avoid enqueuing unnecessary jobs when we can cheaply check in the updater. The job also checks on its own as a safety net.
Tests: test/lib/source_monitor/fetching/feed_fetcher/source_updater_favicon_test.rb
Create a separate test file:
Files: app/jobs/source_monitor/import_opml_job.rb
Read the existing import_opml_job.rb to understand where sources are created. After each source is successfully created/saved in the import loop, enqueue a favicon fetch.
Find the source creation loop and add after each successful source.save! or source.create!:
SourceMonitor::FaviconFetchJob.perform_later(source.id) if should_fetch_favicon?(source)
Add a private method:
def should_fetch_favicon?(source)
defined?(ActiveStorage) &&
SourceMonitor.config.favicons.enabled? &&
source.website_url.present?
rescue StandardError
false
end
Tests: test/jobs/source_monitor/import_opml_favicon_test.rb
Files: test/integration/source_monitor/favicon_integration_test.rb
Create an integration test that verifies the full flow:
Use with_queue_adapter(:test) and assert_enqueued_with for job assertions.
Also test the negative path:
Tests: This task IS the test.
| Action | Path |
|---|---|
| MODIFY | app/controllers/source_monitor/sources_controller.rb |
| MODIFY | lib/source_monitor/fetching/feed_fetcher/source_updater.rb |
| MODIFY | app/jobs/source_monitor/import_opml_job.rb |
| CREATE | test/controllers/source_monitor/sources_controller_favicon_test.rb |
| CREATE | test/lib/source_monitor/fetching/feed_fetcher/source_updater_favicon_test.rb |
| CREATE | test/jobs/source_monitor/import_opml_favicon_test.rb |
| CREATE | test/integration/source_monitor/favicon_integration_test.rb |
bin/rails test test/controllers/source_monitor/sources_controller_favicon_test.rb test/lib/source_monitor/fetching/feed_fetcher/source_updater_favicon_test.rb test/jobs/source_monitor/import_opml_favicon_test.rb test/integration/source_monitor/favicon_integration_test.rb
bin/rails test test/controllers/source_monitor/sources_controller_test.rb
bin/rubocop app/controllers/source_monitor/sources_controller.rb lib/source_monitor/fetching/feed_fetcher/source_updater.rb app/jobs/source_monitor/import_opml_job.rb