.vbw-planning/milestones/polish-and-reliability/phases/02-favicon-support/.context-dev.md
Not available
Codebase mapping exists in .vbw-planning/codebase/. Key files:
ARCHITECTURE.mdCONCERNS.mdPATTERNS.mdDEPENDENCIES.mdSTRUCTURE.mdCONVENTIONS.mdTESTING.mdSTACK.mdRead CONVENTIONS.md, PATTERNS.md, STRUCTURE.md, and DEPENDENCIES.md first to bootstrap codebase understanding.
.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.mdCLAUDE.mdtest/lib/tmp/install_generator/config/recurring.yml.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
phase: 2 plan: 3 title: "Favicon Fetch Triggers: Source Creation and Feed Success" wave: 2 depends_on: [1] must_haves:
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
Researched: 2026-02-20
app/models/source_monitor/source.rbhas_many_attached :images if defined?(ActiveStorage) guard pattern (proven)FeedFetcher#call → perform_fetch → handle_responsehandle_success calls entry_processor.process_feed_entries then source_updater.update_source_for_successActiveStorage::Blob.create_and_upload!(io:, filename:, content_type:) then model.images.attach(blob)SourceMonitor::HTTP.client() provides Faraday with retry (4x), gzip, redirect following (5 max), SSL, custom headersapp/views/source_monitor/sources/_row.html.erb (lines 24-102)<div class="font-medium text-slate-900"> wrapper for namesource_monitor_queue :roleperform(item_id) with model lookup and early returnsdiscard_on ActiveJob::DeserializationError for resilienceSourceMonitor.config.imageshas_one_attached :favicon if defined?(ActiveStorage) + job early returnSourceMonitor::HTTP.client(headers: ...) for all network requestsFaviconsSettings configuration class with: enabled, fetch_timeout (5s), max_download_size (1MB), retry_cooldown_days (7), allowed_content_typeshas_one_attached :favicon (not has_many) with ActiveStorage guardFetchFaviconJob on :fetch queue, triggered from source_updater after successful fetch when favicon blankFavicons::Discoverer module with cascade: /favicon.ico → HTML parsing (Nokogiri, prefer largest) → Google Favicon API