Back to Source Monitor

.Context Dev

.vbw-planning/milestones/polish-and-reliability/phases/02-favicon-support/.context-dev.md

0.13.024.4 KB
Original Source

Phase 02 Context

Goal

Not available

Codebase Map Available

Codebase mapping exists in .vbw-planning/codebase/. Key files:

  • ARCHITECTURE.md
  • CONCERNS.md
  • PATTERNS.md
  • DEPENDENCIES.md
  • STRUCTURE.md
  • CONVENTIONS.md
  • TESTING.md
  • STACK.md

Read CONVENTIONS.md, PATTERNS.md, STRUCTURE.md, and DEPENDENCIES.md first to bootstrap codebase understanding.

Changed Files (Delta)

  • .gitignore
  • .vbw-planning/config.json
  • .vbw-planning/discovery.json
  • .vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md
  • .vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md
  • .vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md
  • .vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md
  • .vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md
  • .vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md
  • .vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md
  • .vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md
  • .vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md
  • .vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md
  • .vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md
  • .vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md
  • .vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md
  • .vbw-planning/milestones/default/ROADMAP.md
  • .vbw-planning/milestones/default/STATE.md
  • .vbw-planning/phases/01-aia-certificate-resolution/.context-dev.md
  • .vbw-planning/phases/01-aia-certificate-resolution/PLAN-01-SUMMARY.md
  • .vbw-planning/phases/01-aia-certificate-resolution/PLAN-01.md
  • .vbw-planning/phases/01-aia-certificate-resolution/PLAN-02-SUMMARY.md
  • .vbw-planning/phases/01-aia-certificate-resolution/PLAN-02.md
  • .vbw-planning/phases/01-aia-certificate-resolution/PLAN-03-SUMMARY.md
  • .vbw-planning/phases/01-aia-certificate-resolution/PLAN-03.md
  • .vbw-planning/phases/02-test-performance/.context-dev.md
  • .vbw-planning/phases/02-test-performance/.context-lead.md
  • .vbw-planning/phases/02-test-performance/.context-qa.md
  • .vbw-planning/phases/02-test-performance/02-RESEARCH.md
  • .vbw-planning/phases/02-test-performance/02-VERIFICATION.md
  • .vbw-planning/phases/02-test-performance/PLAN-01-SUMMARY.md
  • .vbw-planning/phases/02-test-performance/PLAN-01.md
  • .vbw-planning/phases/02-test-performance/PLAN-02-SUMMARY.md
  • .vbw-planning/phases/02-test-performance/PLAN-02.md
  • .vbw-planning/phases/02-test-performance/PLAN-03-SUMMARY.md
  • .vbw-planning/phases/02-test-performance/PLAN-03.md
  • .vbw-planning/phases/02-test-performance/PLAN-04-SUMMARY.md
  • .vbw-planning/phases/02-test-performance/PLAN-04.md
  • .vbw-planning/ROADMAP.md
  • .vbw-planning/STATE.md
  • CLAUDE.md
  • test/lib/tmp/install_generator/config/recurring.yml

Code Slices

.gitignore (32 lines)

/.bundle/
/doc/
/log/*.log
/pkg/
/tmp/
/node_modules/
/coverage/
/test/dummy/db/*.sqlite3
/test/dummy/db/*.sqlite3-*
/test/dummy/log/*.log*
/test/dummy/storage/
/test/dummy/tmp/
/test/tmp/
/test/lib/tmp/install_generator/config/routes.rb
/app/assets/builds/*
!/app/assets/builds/.keep
!/app/assets/builds/source_monitor/
.vbw-planning/.cost-ledger.json
.vbw-planning/.notification-log.jsonl
.vbw-planning/.session-log.jsonl
.vbw-planning/.hook-errors.log
.vbw-planning/.claude-md-migrated
.vbw-planning/.watchdog-pid
.vbw-planning/.watchdog.log
.vbw-planning/.agent-pids
.vbw-planning/.agent-panes
.vbw-planning/.active-agent
.vbw-planning/.active-agent-count
.vbw-planning/.todo-flat-migrated
/codebase_analysis.md
*.gem
.vbw-worktrees/

.vbw-planning/config.json (44 lines)

{
  "effort": "thorough",
  "autonomy": "standard",
  "auto_commit": true,
  "planning_tracking": "manual",
  "auto_push": "never",
  "verification_tier": "standard",
  "skill_suggestions": true,
  "auto_install_skills": false,
  "discovery_questions": true,
  "context_compiler": true,
  "visual_format": "unicode",
  "max_tasks_per_plan": 5,
  "prefer_teams": "always",
  "branch_per_milestone": false,
  "plain_summary": true,
  "active_profile": "default",
  "custom_profiles": {},
  "model_profile": "quality",
  "model_overrides": {},
  "agent_max_turns": {
    "scout": 15,
    "qa": 25,
    "architect": 30,
    "debugger": 80,
    "lead": 50,
    "dev": 75
  },
  "qa_skip_agents": [
    "docs"
  ],
  "worktree_isolation": "on",
  "token_budgets": false,
  "two_phase_completion": false,
  "metrics": false,
  "smart_routing": false,
  "validation_gates": false,
  "snapshot_resume": false,
  "lease_locks": false,
  "event_recovery": false,
  "monorepo_routing": false,
  "rolling_summary": false,
  "compaction_trigger": 130000
}

.vbw-planning/discovery.json (65 lines, first 30 shown)

{
  "answered": [
    {
      "question": "What matters most in the conventions cleanup?",
      "answer": "All of the above: Model conventions, Controller patterns, Dead code removal",
      "category": "scope",
      "phase": "4",
      "date": "2026-02-10"
    },
    {
      "question": "How should we handle convention violations that would change public API behavior?",
      "answer": "Fix everything -- rename/restructure even if it changes method signatures or route patterns",
      "category": "api-policy",
      "phase": "4",
      "date": "2026-02-10"
    },
    {
      "question": "Favicon discovery strategy?",
      "answer": "Multi-strategy cascade: /favicon.ico -> HTML parsing (full GET, Nokogiri, prefer largest) -> Google Favicon API. Skip DuckDuckGo.",
      "area": "favicon-discovery",
      "phase": "02",
      "date": "2026-02-20"
    },
    {
      "question": "How to handle downloaded favicons before storage?",
      "answer": "Store raw original via Active Storage, define two variants: 32x32 (standard) and 64x64 (retina). SVGs stored as-is AND rasterized to PNG.",
      "area": "image-processing",
      "phase": "02",
      "date": "2026-02-20"
    },

.vbw-planning/ROADMAP.md (72 lines, first 30 shown)

# Roadmap

## Milestone: polish-and-reliability

### Phases

1. [x] **Backend Fixes** -- Fix browser User-Agent default, health check status transitions, and smarter scrape rate limiting
2. [ ] **Favicon Support** -- Automatically save source favicons via Active Storage with background fetch job
3. [ ] **Toast Stacking** -- Cap visible toast notifications with hover-to-expand for bulk operation UX

### Phase Details

#### Phase 1: Backend Fixes

**Goal:** Fix three independent backend issues: bot-blocked feeds due to User-Agent, health check not updating status, and overly aggressive scrape limiting.

**Requirements:**
- REQ-UA-01: Change default User-Agent from "SourceMonitor/VERSION" to a browser-like string
- REQ-HC-01: After a successful manual health check on a declining/critical/warning source, trigger SourceHealthMonitor re-evaluation or directly transition status to "improving"
- REQ-SL-01: Refine max_in_flight_per_source to only count actively-running scrape jobs (not queued ones)

**Success Criteria:**
- [ ] Default UA string resembles a real browser (e.g., Mozilla/5.0 compatible)
- [ ] Successful manual health check on a declining source transitions it to improving
- [ ] Scrape limit counts only actively-running jobs, queued items don't count toward the cap
- [ ] All existing tests pass, new tests cover changed behavior
- [ ] RuboCop zero offenses, Brakeman zero warnings

#### Phase 2: Favicon Support

.vbw-planning/STATE.md (30 lines)

# State

## Current Position

- **Milestone:** polish-and-reliability
- **Phase:** 2 -- Favicon Support
- **Status:** Planned
- **Progress:** 33%
- **Plans:** 3 (0/3 complete)

## Decisions

| Decision | Date | Context |
|----------|------|---------|
| Active Storage for favicons | 2026-02-20 | has_one_attached with guard, consistent with ItemContent pattern |
| Smarter scrape limit | 2026-02-20 | Count only running jobs, not queued; keeps safety but removes false bottleneck |
| Browser-like default UA | 2026-02-20 | Simple global fix for bot-blocked feeds like Uber |
| Health check triggers status update | 2026-02-20 | Successful manual health check should transition declining -> improving |
| Toast cap + hover expand | 2026-02-20 | Max 3 visible, +N more badge, hover to see all |

## Todos

## Metrics

- **Started:** 2026-02-20
- **Phases:** 3
- **Tests at start:** 1033

## Blockers
None

CLAUDE.md (224 lines, first 30 shown)

# SourceMonitor

**Core value:** Drop-in Rails engine for feed monitoring, content scraping, and operational dashboards.

## Active Context

**Milestone:** polish-and-reliability
**Phase:** 1 -- Backend Fixes (pending planning)
**Last shipped:** aia-ssl-fix (2026-02-20) -- 2 phases, 7 plans, 8 commits
**Previous:** upgrade-assurance (2026-02-13), generator-enhancements (2026-02-12)

## Key Decisions

- Keep PostgreSQL-only for now
- Keep host-app auth model
- Ruby autoload for lib/ modules (not Zeitwerk)
- PG parallel fork segfault resolved: switched to thread-based parallelism in aia-ssl-fix milestone

## Installed Skills

- agent-browser (global)
- flowdeck (global)
- ralph-tui-create-json (global)
- ralph-tui-prd (global)
- vastai (global)
- find-skills (global)

## Learned Patterns

- Sub-module extraction: create `module/submodule.rb` with `require_relative`, lazy accessors, forwarding methods for backward compat

test/lib/tmp/install_generator/config/recurring.yml (28 lines)

default: &default
  my_existing_job:
    class: MyJob
    schedule: every hour

  source_monitor_schedule_fetches:
    class: SourceMonitor::ScheduleFetchesJob
    args:
    - limit: 100
    schedule: every minute

  source_monitor_schedule_scrapes:
    command: 'SourceMonitor::Scraping::Scheduler.run(limit: 100)'
    schedule: every 2 minutes

  source_monitor_item_cleanup:
    class: SourceMonitor::ItemCleanupJob
    schedule: at 2am every day

  source_monitor_log_cleanup:
    class: SourceMonitor::LogCleanupJob
    args:
    - fetch_logs_older_than_days: 90
      scrape_logs_older_than_days: 60
    schedule: at 3am every day

development:
  <<: *default

Active Plan


phase: 2 plan: 3 title: "Favicon Fetch Triggers: Source Creation and Feed Success" wave: 2 depends_on: [1] must_haves:

  • "FaviconFetchJob enqueued after source creation in SourcesController#create"
  • "FaviconFetchJob enqueued after successful feed fetch in SourceUpdater#update_source_for_success when favicon not attached"
  • "Favicon fetch only triggered when favicons enabled and ActiveStorage defined"
  • "Import OPML flow triggers favicon fetch for each created source"
  • "Cooldown respected: favicon not re-fetched within retry_cooldown_days of last failed attempt"
  • "Integration tests verify end-to-end favicon trigger flow"
  • "All existing tests pass, bin/rubocop zero offenses" skills_used: []

Plan 03: Favicon Fetch Triggers: Source Creation and Feed Success

Objective

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.

Context

  • @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.

Tasks

Task 1: Trigger favicon fetch on manual source creation

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:

ruby
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:

ruby
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:

ruby
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:

  • Test create with website_url enqueues FaviconFetchJob (use assert_enqueued_with)
  • Test create without website_url does not enqueue FaviconFetchJob
  • Test create with favicons disabled does not enqueue FaviconFetchJob
  • Test create failure (invalid source) does not enqueue FaviconFetchJob

Task 2: Trigger favicon fetch on successful feed fetch

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):

ruby
enqueue_favicon_fetch_if_needed

Add a private method to the class:

ruby
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:

  • Test update_source_for_success enqueues FaviconFetchJob when favicon not attached
  • Test update_source_for_success does NOT enqueue when favicon already attached
  • Test update_source_for_success does NOT enqueue when within cooldown period
  • Test update_source_for_success does NOT enqueue when favicons disabled
  • Test update_source_for_success does NOT enqueue when website_url blank
  • Test update_source_for_success does NOT error when enqueue fails (rescued)
  • Test update_source_for_not_modified does NOT enqueue favicon (we only trigger on success with content)

Task 3: Trigger favicon fetch for OPML-imported sources

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!:

ruby
SourceMonitor::FaviconFetchJob.perform_later(source.id) if should_fetch_favicon?(source)

Add a private method:

ruby
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

  • Test that OPML import with sources having website_url enqueues FaviconFetchJob for each
  • Test that OPML import with sources lacking website_url does not enqueue
  • Test that OPML import with favicons disabled does not enqueue

Task 4: Integration test for end-to-end favicon flow

Files: test/integration/source_monitor/favicon_integration_test.rb

Create an integration test that verifies the full flow:

  1. Create a source via POST to sources_controller
  2. Assert FaviconFetchJob was enqueued
  3. Perform the job with WebMock stubs for favicon discovery
  4. Assert favicon is attached to the source
  5. Verify the source show page renders without error

Use with_queue_adapter(:test) and assert_enqueued_with for job assertions.

Also test the negative path:

  • Create source without website_url, verify no job enqueued
  • Create source with favicons disabled, verify no job enqueued

Tests: This task IS the test.

Files

ActionPath
MODIFYapp/controllers/source_monitor/sources_controller.rb
MODIFYlib/source_monitor/fetching/feed_fetcher/source_updater.rb
MODIFYapp/jobs/source_monitor/import_opml_job.rb
CREATEtest/controllers/source_monitor/sources_controller_favicon_test.rb
CREATEtest/lib/source_monitor/fetching/feed_fetcher/source_updater_favicon_test.rb
CREATEtest/jobs/source_monitor/import_opml_favicon_test.rb
CREATEtest/integration/source_monitor/favicon_integration_test.rb

Verification

bash
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

Success Criteria

  • Creating a source via UI enqueues FaviconFetchJob when website_url present
  • Successful feed fetch enqueues FaviconFetchJob when favicon not attached and outside cooldown
  • OPML import enqueues FaviconFetchJob for each imported source with website_url
  • All triggers respect the enabled? guard and ActiveStorage check
  • All triggers are wrapped in rescue to never break the main flow on failure
  • Existing controller, job, and updater tests still pass
  • Zero RuboCop offenses

Research Findings

Phase 2: Favicon Support — Research

Researched: 2026-02-20

Findings

Source Model Structure

  • Location: app/models/source_monitor/source.rb
  • Existing columns: name, feed_url, website_url, active, feed_format, fetch_interval_hours, next_fetch_at, last_fetched_at, last_error, failure_count, metadata (JSONB), custom_headers (JSONB), scrape_settings (JSONB)
  • No existing Active Storage attachments on Source model
  • ItemContent model uses has_many_attached :images if defined?(ActiveStorage) guard pattern (proven)
  • ModelExtensions.register called at line 53 for host app extensibility

FeedFetcher Pipeline (Success Flow)

  • FeedFetcher#callperform_fetchhandle_response
  • On HTTP 200: handle_success calls entry_processor.process_feed_entries then source_updater.update_source_for_success
  • source_updater.update_source_for_success (lines 14-40 of source_updater.rb) is the hooking point for favicon triggering
  • Source metadata JSONB can track favicon_last_attempted_at for cooldown

Active Storage Patterns

  • DownloadContentImagesJob pattern: ActiveStorage::Blob.create_and_upload!(io:, filename:, content_type:) then model.images.attach(blob)
  • ImagesSettings defines DEFAULT_ALLOWED_CONTENT_TYPES including image/svg+xml

HTTP Module

  • SourceMonitor::HTTP.client() provides Faraday with retry (4x), gzip, redirect following (5 max), SSL, custom headers
  • Can directly use for favicon HTML fetch and image download

Source Views

  • Row template: app/views/source_monitor/sources/_row.html.erb (lines 24-102)
  • Shows source.name with link, feed_url, health/fetch status badges
  • No current favicon display — simple <div class="font-medium text-slate-900"> wrapper for name
  • Tailwind CSS utility classes throughout

Job Patterns

  • All inherit from ApplicationJob, use source_monitor_queue :role
  • DownloadContentImagesJob: perform(item_id) with model lookup and early returns
  • discard_on ActiveJob::DeserializationError for resilience

Configuration DSL

  • Settings pattern: class with attr_accessors, initialize calls reset!, constants for defaults
  • ImagesSettings attributes: download_to_active_storage, max_download_size, download_timeout, allowed_content_types
  • Access via: SourceMonitor.config.images

Relevant Patterns

  1. Conditional feature guard: has_one_attached :favicon if defined?(ActiveStorage) + job early return
  2. Blob attachment: create_and_upload! then model.attachment.attach(blob) (from DownloadContentImagesJob)
  3. HTTP client reuse: SourceMonitor::HTTP.client(headers: ...) for all network requests
  4. Metadata JSONB state: favicon_last_attempted_at for cooldown tracking (no schema change needed)
  5. Settings class pattern: FaviconsSettings following ImagesSettings template
  6. Nokolexbor: Already in gemspec as Nokogiri-compatible HTML parser

Risks

  1. Network timeouts in cascade: Favicon discovery adds HTTP requests. Mitigate with aggressive timeout (5s) and async job (not blocking feed fetch)
  2. SVG rasterization complexity: Rails Active Storage has content-type quirks with SVG. Mitigate by storing raw + optional rasterization
  3. Storage quota: Large favicons (512x512+). Mitigate with max_download_size (1MB) and dimension validation
  4. Cooldown state in metadata: Could be cleared if source metadata is modified. Acceptable risk for MVP

Recommendations

  1. Create FaviconsSettings configuration class with: enabled, fetch_timeout (5s), max_download_size (1MB), retry_cooldown_days (7), allowed_content_types
  2. Use has_one_attached :favicon (not has_many) with ActiveStorage guard
  3. Create FetchFaviconJob on :fetch queue, triggered from source_updater after successful fetch when favicon blank
  4. Create Favicons::Discoverer module with cascade: /favicon.ico → HTML parsing (Nokogiri, prefer largest) → Google Favicon API
  5. Store cooldown state in metadata JSONB (no migration needed for source table)
  6. View: conditional favicon display with initials fallback placeholder