.vbw-planning/milestones/ui-fixes-and-smart-scraping/phases/02-feed-reliability/03-PLAN.md
Before giving up on a Cloudflare-challenged feed, attempt light bypass techniques: UA rotation, cookie persistence, and alternate request headers. If all fail, raise BlockedError (from Plan 01) with clear diagnostics. Show a "Blocked" badge on the source UI.
Files to create:
lib/source_monitor/fetching/cloudflare_bypass.rbSteps:
SourceMonitor::Fetching::CloudflareBypass classresponse: (the initial blocked response) and feed_url:#call method tries bypass strategies in order, returns the successful response or nil:
a. Cookie replay: Extract Set-Cookie headers from initial response, re-request with those cookies set
b. UA rotation: Try 3-4 different real browser UA strings (Chrome, Firefox, Safari, Edge -- recent versions)
c. Cache-busting headers: Add Cache-Control: no-cache, Pragma: no-cache headersSourceMonitor::HTTP.client with modified headersUSER_AGENTS with 4-5 curated, real browser UA stringsFiles to modify:
lib/source_monitor/fetching/feed_fetcher.rbSteps:
parse_feed, after detect_blocked_response identifies a Cloudflare block (blocked_by == "cloudflare"):
CloudflareBypass.new(response:, feed_url: source.feed_url).callFeedjira.parseBlockedError as before@bypass_attempted is set, skip bypassFiles to modify:
app/views/source_monitor/sources/_details.html.erb (or wherever source status badges are rendered)app/views/source_monitor/sources/_row.html.erb (list view)Steps:
last_error contains "blocked" or if latest fetch_log has error_category == "blocked"last_error field which already stores the error message, and check for the BlockedError class name in the latest fetch_log's error_classFiles to create:
test/lib/source_monitor/fetching/cloudflare_bypass_test.rbFiles to modify:
test/lib/source_monitor/fetching/feed_fetcher_test.rbSteps:
bin/rubocop passes with zero offensesbin/rails test passes