source_monitor 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/commands/release.md +45 -22
- data/.claude/skills/sm-configure/SKILL.md +10 -1
- data/.claude/skills/sm-configure/reference/configuration-reference.md +44 -0
- data/.claude/skills/sm-host-setup/reference/initializer-template.md +17 -0
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +2 -0
- data/.claude/skills/sm-job/reference/job-conventions.md +26 -0
- data/.claude/skills/sm-upgrade/reference/version-history.md +22 -0
- data/.gitignore +10 -0
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +56 -0
- data/CLAUDE.md +11 -5
- data/Gemfile.lock +1 -1
- data/README.md +6 -4
- data/VERSION +1 -1
- data/app/assets/builds/source_monitor/application.css +43 -0
- data/app/assets/builds/source_monitor/application.js +127 -0
- data/app/assets/builds/source_monitor/application.js.map +3 -3
- data/app/assets/javascripts/source_monitor/application.js +2 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_container_controller.js +138 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +11 -0
- data/app/controllers/source_monitor/source_favicon_fetches_controller.rb +38 -0
- data/app/controllers/source_monitor/sources_controller.rb +11 -0
- data/app/helpers/source_monitor/application_helper.rb +51 -0
- data/app/jobs/source_monitor/favicon_fetch_job.rb +71 -0
- data/app/jobs/source_monitor/import_opml_job.rb +9 -0
- data/app/jobs/source_monitor/source_health_check_job.rb +10 -0
- data/app/models/source_monitor/source.rb +2 -0
- data/app/views/layouts/source_monitor/application.html.erb +23 -2
- data/app/views/source_monitor/shared/_toast.html.erb +1 -0
- data/app/views/source_monitor/sources/_details.html.erb +34 -5
- data/app/views/source_monitor/sources/_row.html.erb +11 -6
- data/config/routes.rb +1 -0
- data/docs/configuration.md +1 -1
- data/docs/upgrade.md +22 -0
- data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +15 -1
- data/lib/source_monitor/configuration/favicons_settings.rb +42 -0
- data/lib/source_monitor/configuration/http_settings.rb +1 -1
- data/lib/source_monitor/configuration/scraping_settings.rb +1 -1
- data/lib/source_monitor/configuration.rb +3 -1
- data/lib/source_monitor/favicons/discoverer.rb +196 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +21 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +1 -0
- data/lib/source_monitor/http.rb +5 -3
- data/lib/source_monitor/version.rb +1 -1
- data/lib/source_monitor.rb +4 -0
- data/lib/tasks/test_fast.rake +11 -0
- data/source_monitor.gemspec +1 -1
- metadata +7 -93
- data/.vbw-planning/PROJECT.md +0 -51
- data/.vbw-planning/ROADMAP.md +0 -32
- data/.vbw-planning/SHIPPED.md +0 -63
- data/.vbw-planning/STATE.md +0 -27
- data/.vbw-planning/codebase/ARCHITECTURE.md +0 -147
- data/.vbw-planning/codebase/CONCERNS.md +0 -99
- data/.vbw-planning/codebase/CONVENTIONS.md +0 -97
- data/.vbw-planning/codebase/DEPENDENCIES.md +0 -100
- data/.vbw-planning/codebase/INDEX.md +0 -86
- data/.vbw-planning/codebase/META.md +0 -42
- data/.vbw-planning/codebase/PATTERNS.md +0 -262
- data/.vbw-planning/codebase/STACK.md +0 -101
- data/.vbw-planning/codebase/STRUCTURE.md +0 -324
- data/.vbw-planning/codebase/TESTING.md +0 -154
- data/.vbw-planning/config.json +0 -53
- data/.vbw-planning/discovery.json +0 -26
- data/.vbw-planning/milestones/default/ROADMAP.md +0 -115
- data/.vbw-planning/milestones/default/STATE.md +0 -82
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +0 -56
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +0 -187
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +0 -64
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +0 -137
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +0 -67
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +0 -142
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +0 -64
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +0 -138
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +0 -85
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +0 -147
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +0 -63
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +0 -129
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +0 -74
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +0 -154
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +0 -303
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +0 -510
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +0 -61
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +0 -161
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +0 -66
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +0 -132
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +0 -59
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +0 -171
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +0 -56
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +0 -152
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +0 -33
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +0 -42
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +0 -119
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +0 -52
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +0 -195
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +0 -79
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +0 -130
- data/.vbw-planning/milestones/generator-enhancements/REQUIREMENTS.md +0 -72
- data/.vbw-planning/milestones/generator-enhancements/ROADMAP.md +0 -125
- data/.vbw-planning/milestones/generator-enhancements/SHIPPED.md +0 -40
- data/.vbw-planning/milestones/generator-enhancements/STATE.md +0 -43
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-CONTEXT.md +0 -33
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-VERIFICATION.md +0 -86
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01-SUMMARY.md +0 -61
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01.md +0 -380
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/02-VERIFICATION.md +0 -78
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01-SUMMARY.md +0 -46
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01.md +0 -500
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/03-VERIFICATION.md +0 -89
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01-SUMMARY.md +0 -48
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01.md +0 -456
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/04-VERIFICATION.md +0 -129
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +0 -70
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01.md +0 -747
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/05-VERIFICATION.md +0 -156
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01-SUMMARY.md +0 -69
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01.md +0 -455
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02-SUMMARY.md +0 -39
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02.md +0 -488
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/06-VERIFICATION.md +0 -100
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +0 -37
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01.md +0 -345
- data/.vbw-planning/milestones/upgrade-assurance/REQUIREMENTS.md +0 -80
- data/.vbw-planning/milestones/upgrade-assurance/ROADMAP.md +0 -75
- data/.vbw-planning/milestones/upgrade-assurance/STATE.md +0 -29
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/01-VERIFICATION.md +0 -144
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01-SUMMARY.md +0 -43
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01.md +0 -405
- data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01-SUMMARY.md +0 -27
- data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01.md +0 -303
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/03-VERIFICATION.md +0 -380
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01-SUMMARY.md +0 -36
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01.md +0 -652
- data/.vbw-planning/phases/01-aia-certificate-resolution/.context-dev.md +0 -17
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01-SUMMARY.md +0 -26
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01.md +0 -71
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02-SUMMARY.md +0 -16
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02.md +0 -56
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03-SUMMARY.md +0 -17
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03.md +0 -98
data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02.md
DELETED
|
@@ -1,488 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
phase: 5
|
|
3
|
-
plan: "02"
|
|
4
|
-
title: download-job-integration-docs
|
|
5
|
-
type: execute
|
|
6
|
-
wave: 2
|
|
7
|
-
depends_on:
|
|
8
|
-
- "PLAN-01"
|
|
9
|
-
cross_phase_deps: []
|
|
10
|
-
autonomous: true
|
|
11
|
-
effort_override: thorough
|
|
12
|
-
skills_used:
|
|
13
|
-
- sm-configure
|
|
14
|
-
- sm-job
|
|
15
|
-
- sm-engine-test
|
|
16
|
-
files_modified:
|
|
17
|
-
- lib/source_monitor/images/downloader.rb
|
|
18
|
-
- app/jobs/source_monitor/download_content_images_job.rb
|
|
19
|
-
- lib/source_monitor/fetching/feed_fetcher/entry_processor.rb
|
|
20
|
-
- lib/source_monitor.rb
|
|
21
|
-
- test/lib/source_monitor/images/downloader_test.rb
|
|
22
|
-
- test/jobs/source_monitor/download_content_images_job_test.rb
|
|
23
|
-
- test/lib/source_monitor/fetching/feed_fetcher/entry_processor_test.rb
|
|
24
|
-
- .claude/skills/sm-configure/SKILL.md
|
|
25
|
-
- .claude/skills/sm-configure/reference/configuration-reference.md
|
|
26
|
-
must_haves:
|
|
27
|
-
truths:
|
|
28
|
-
- "Running `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/images/downloader_test.rb` exits 0 with 0 failures"
|
|
29
|
-
- "Running `PARALLEL_WORKERS=1 bin/rails test test/jobs/source_monitor/download_content_images_job_test.rb` exits 0 with 0 failures"
|
|
30
|
-
- "Running `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/fetching/feed_fetcher/entry_processor_test.rb` exits 0 with 0 failures"
|
|
31
|
-
- "Running `bin/rubocop lib/source_monitor/images/downloader.rb app/jobs/source_monitor/download_content_images_job.rb lib/source_monitor/fetching/feed_fetcher/entry_processor.rb` exits 0 with no offenses"
|
|
32
|
-
- "When `config.images.download_to_active_storage` is false (default), no DownloadContentImagesJob is enqueued after item creation"
|
|
33
|
-
- "When `config.images.download_to_active_storage` is true, DownloadContentImagesJob is enqueued for newly created items that have HTML content"
|
|
34
|
-
- "DownloadContentImagesJob downloads images, attaches to ItemContent, and rewrites item.content with Active Storage URLs"
|
|
35
|
-
- "Images that fail to download are left with original URLs in the content (graceful fallback)"
|
|
36
|
-
- "Images larger than max_download_size are skipped and original URL preserved"
|
|
37
|
-
- "Images with disallowed content types are skipped and original URL preserved"
|
|
38
|
-
- "sm-configure skill documents the new config.images section"
|
|
39
|
-
artifacts:
|
|
40
|
-
- path: "lib/source_monitor/images/downloader.rb"
|
|
41
|
-
provides: "Downloads a single image via Faraday, validates size and content type"
|
|
42
|
-
contains: "class Downloader"
|
|
43
|
-
- path: "app/jobs/source_monitor/download_content_images_job.rb"
|
|
44
|
-
provides: "Background job to download and attach content images"
|
|
45
|
-
contains: "class DownloadContentImagesJob"
|
|
46
|
-
- path: "lib/source_monitor/fetching/feed_fetcher/entry_processor.rb"
|
|
47
|
-
provides: "Integration hook to enqueue image download after item creation"
|
|
48
|
-
contains: "enqueue_image_download"
|
|
49
|
-
- path: ".claude/skills/sm-configure/SKILL.md"
|
|
50
|
-
provides: "Updated skill with config.images section documented"
|
|
51
|
-
contains: "config.images"
|
|
52
|
-
key_links:
|
|
53
|
-
- from: "download_content_images_job.rb#perform"
|
|
54
|
-
to: "REQ-24"
|
|
55
|
-
via: "Downloads inline images to Active Storage"
|
|
56
|
-
- from: "entry_processor.rb#enqueue_image_download"
|
|
57
|
-
to: "REQ-24"
|
|
58
|
-
via: "Enqueues download job when config enabled and item has content"
|
|
59
|
-
- from: "downloader.rb"
|
|
60
|
-
to: "REQ-24"
|
|
61
|
-
via: "Validates size and content type before download"
|
|
62
|
-
- from: "sm-configure/SKILL.md"
|
|
63
|
-
to: "REQ-24"
|
|
64
|
-
via: "Configuration documented in sm-configure skill"
|
|
65
|
-
---
|
|
66
|
-
<objective>
|
|
67
|
-
Build the image download job, single-image downloader service, integration hook in the entry processor, and update the sm-configure skill. This plan connects Plan 01's foundational pieces into a working end-to-end image download pipeline. REQ-24.
|
|
68
|
-
</objective>
|
|
69
|
-
<context>
|
|
70
|
-
@lib/source_monitor/images/content_rewriter.rb -- (from Plan 01) Provides `image_urls` to get URLs and `rewrite { |url| new_url }` to transform HTML. The job uses `image_urls` to get the list, downloads each image, attaches to Active Storage, then uses `rewrite` to replace original URLs with Active Storage serving URLs.
|
|
71
|
-
|
|
72
|
-
@app/models/source_monitor/item_content.rb -- (from Plan 01) Has `has_many_attached :images`. The job attaches downloaded images here via `item_content.images.attach(blob)`.
|
|
73
|
-
|
|
74
|
-
@lib/source_monitor/configuration/images_settings.rb -- (from Plan 01) Provides `download_enabled?`, `max_download_size`, `download_timeout`, `allowed_content_types`.
|
|
75
|
-
|
|
76
|
-
@lib/source_monitor/fetching/feed_fetcher/entry_processor.rb -- The integration point. After `ItemCreator.call` returns a created item, if images download is enabled and the item has HTML content in `item.content`, enqueue `DownloadContentImagesJob.perform_later(item.id)`. Only for newly created items (not updates).
|
|
77
|
-
|
|
78
|
-
@lib/source_monitor/http.rb -- Faraday client factory. The Downloader creates its own Faraday connection: no retry (images are best-effort), short timeout from config, Accept header for images.
|
|
79
|
-
|
|
80
|
-
@app/jobs/source_monitor/application_job.rb -- Base job class. DownloadContentImagesJob inherits from this. Uses `source_monitor_queue :fetch` (reuse fetch queue since image downloads are I/O-bound like fetches).
|
|
81
|
-
|
|
82
|
-
@app/jobs/source_monitor/fetch_feed_job.rb -- Pattern to follow for job structure: `discard_on` for deserialization errors, simple `perform` that delegates to service objects.
|
|
83
|
-
|
|
84
|
-
@test/test_helper.rb -- WebMock disables external HTTP. Image download tests need WebMock stubs. Use `stub_request(:get, url).to_return(body: png_bytes, headers: { "Content-Type" => "image/png" })`.
|
|
85
|
-
|
|
86
|
-
@.claude/skills/sm-configure/SKILL.md -- Needs a new section for `config.images` with examples.
|
|
87
|
-
|
|
88
|
-
@app/models/source_monitor/item.rb -- The item model. `item.content` is a text column on `sourcemon_items` storing the feed entry content (HTML). This is where inline images live. The job reads `item.content`, rewrites it, and saves it back. `item_content` (separate table) stores scraped_html/scraped_content from scraping -- that happens later and is separate from feed content.
|
|
89
|
-
|
|
90
|
-
**Key design decisions:**
|
|
91
|
-
1. **Job takes `item_id`** (not item_content_id). The feed content with inline images is in `item.content`. The job reads `item.content`, downloads images, attaches blobs to `item_content.images` (building item_content if needed), and writes the rewritten HTML back to `item.content`.
|
|
92
|
-
2. Downloader is a service object that downloads one image: takes URL, returns `{io:, filename:, content_type:}` or nil on failure.
|
|
93
|
-
3. The job is idempotent: if `item_content.images.attached?`, it skips re-downloading.
|
|
94
|
-
4. The job runs on the fetch queue (I/O-bound work).
|
|
95
|
-
5. The job wraps each download in begin/rescue -- one failing image does not block others.
|
|
96
|
-
6. After all images are processed, rewrite the HTML once with all successful replacements. Failed images keep their original URLs.
|
|
97
|
-
7. Only newly created items trigger image downloads (not updates).
|
|
98
|
-
</context>
|
|
99
|
-
<tasks>
|
|
100
|
-
<task type="auto">
|
|
101
|
-
<name>create-image-downloader</name>
|
|
102
|
-
<files>
|
|
103
|
-
lib/source_monitor/images/downloader.rb
|
|
104
|
-
lib/source_monitor.rb
|
|
105
|
-
test/lib/source_monitor/images/downloader_test.rb
|
|
106
|
-
</files>
|
|
107
|
-
<action>
|
|
108
|
-
**Create `lib/source_monitor/images/downloader.rb`:**
|
|
109
|
-
|
|
110
|
-
A service object that downloads a single image from a URL, validates it, and returns the result.
|
|
111
|
-
|
|
112
|
-
```ruby
|
|
113
|
-
# frozen_string_literal: true
|
|
114
|
-
|
|
115
|
-
require "faraday"
|
|
116
|
-
require "securerandom"
|
|
117
|
-
|
|
118
|
-
module SourceMonitor
|
|
119
|
-
module Images
|
|
120
|
-
class Downloader
|
|
121
|
-
Result = Struct.new(:io, :filename, :content_type, :byte_size, keyword_init: true)
|
|
122
|
-
|
|
123
|
-
attr_reader :url, :settings
|
|
124
|
-
|
|
125
|
-
def initialize(url, settings: nil)
|
|
126
|
-
@url = url
|
|
127
|
-
@settings = settings || SourceMonitor.config.images
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Downloads the image and returns a Result, or nil if download fails
|
|
131
|
-
# or the image does not meet validation criteria.
|
|
132
|
-
def call
|
|
133
|
-
response = fetch_image
|
|
134
|
-
return unless response
|
|
135
|
-
|
|
136
|
-
content_type = response.headers["content-type"]&.split(";")&.first&.strip&.downcase
|
|
137
|
-
return unless allowed_content_type?(content_type)
|
|
138
|
-
|
|
139
|
-
body = response.body
|
|
140
|
-
return unless body && body.bytesize > 0
|
|
141
|
-
return if body.bytesize > settings.max_download_size
|
|
142
|
-
|
|
143
|
-
filename = derive_filename(url, content_type)
|
|
144
|
-
|
|
145
|
-
Result.new(
|
|
146
|
-
io: StringIO.new(body),
|
|
147
|
-
filename: filename,
|
|
148
|
-
content_type: content_type,
|
|
149
|
-
byte_size: body.bytesize
|
|
150
|
-
)
|
|
151
|
-
rescue Faraday::Error, URI::InvalidURIError, Timeout::Error => _error
|
|
152
|
-
nil
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
private
|
|
156
|
-
|
|
157
|
-
def fetch_image
|
|
158
|
-
connection = Faraday.new do |f|
|
|
159
|
-
f.options.timeout = settings.download_timeout
|
|
160
|
-
f.options.open_timeout = [settings.download_timeout / 2, 5].min
|
|
161
|
-
f.headers["User-Agent"] = SourceMonitor.config.http.user_agent || "SourceMonitor/#{SourceMonitor::VERSION}"
|
|
162
|
-
f.headers["Accept"] = "image/*"
|
|
163
|
-
f.adapter Faraday.default_adapter
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
response = connection.get(url)
|
|
167
|
-
return response if response.status == 200
|
|
168
|
-
|
|
169
|
-
nil
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def allowed_content_type?(content_type)
|
|
173
|
-
return false if content_type.blank?
|
|
174
|
-
|
|
175
|
-
settings.allowed_content_types.include?(content_type)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def derive_filename(image_url, content_type)
|
|
179
|
-
uri = URI.parse(image_url)
|
|
180
|
-
basename = File.basename(uri.path) if uri.path.present?
|
|
181
|
-
|
|
182
|
-
if basename.present? && basename.include?(".")
|
|
183
|
-
basename
|
|
184
|
-
else
|
|
185
|
-
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".bin"
|
|
186
|
-
"image-#{SecureRandom.hex(8)}#{ext}"
|
|
187
|
-
end
|
|
188
|
-
rescue URI::InvalidURIError
|
|
189
|
-
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".bin"
|
|
190
|
-
"image-#{SecureRandom.hex(8)}#{ext}"
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**Update `lib/source_monitor.rb`:**
|
|
198
|
-
|
|
199
|
-
Add `autoload :Downloader, "source_monitor/images/downloader"` inside the `module Images` block (added in Plan 01).
|
|
200
|
-
|
|
201
|
-
**Create `test/lib/source_monitor/images/downloader_test.rb`:**
|
|
202
|
-
|
|
203
|
-
Use WebMock stubs for all HTTP interactions. Tests:
|
|
204
|
-
|
|
205
|
-
1. Downloads valid image and returns Result with io, filename, content_type, byte_size
|
|
206
|
-
2. Returns nil for HTTP error (404, 500)
|
|
207
|
-
3. Returns nil for disallowed content type (e.g., text/html)
|
|
208
|
-
4. Returns nil for image exceeding max_download_size
|
|
209
|
-
5. Returns nil for empty response body
|
|
210
|
-
6. Returns nil for network timeout (stub with `to_timeout`)
|
|
211
|
-
7. Derives filename from URL path when available
|
|
212
|
-
8. Generates random filename when URL has no extension
|
|
213
|
-
9. Uses configured download_timeout
|
|
214
|
-
10. Uses configured allowed_content_types
|
|
215
|
-
</action>
|
|
216
|
-
<verify>
|
|
217
|
-
Run `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/images/downloader_test.rb` and confirm all tests pass. Run `bin/rubocop lib/source_monitor/images/downloader.rb` and confirm no offenses.
|
|
218
|
-
</verify>
|
|
219
|
-
<done>
|
|
220
|
-
Downloader service created. Downloads single images, validates content type and size, derives filenames. Returns nil on any failure for graceful fallback. Tests cover all success and failure scenarios.
|
|
221
|
-
</done>
|
|
222
|
-
</task>
|
|
223
|
-
<task type="auto">
|
|
224
|
-
<name>create-download-job</name>
|
|
225
|
-
<files>
|
|
226
|
-
app/jobs/source_monitor/download_content_images_job.rb
|
|
227
|
-
test/jobs/source_monitor/download_content_images_job_test.rb
|
|
228
|
-
</files>
|
|
229
|
-
<action>
|
|
230
|
-
**Create `app/jobs/source_monitor/download_content_images_job.rb`:**
|
|
231
|
-
|
|
232
|
-
The job takes `item_id`, reads `item.content` for inline images, downloads them, attaches to `item_content.images`, and rewrites `item.content` with Active Storage URLs.
|
|
233
|
-
|
|
234
|
-
```ruby
|
|
235
|
-
# frozen_string_literal: true
|
|
236
|
-
|
|
237
|
-
module SourceMonitor
|
|
238
|
-
class DownloadContentImagesJob < ApplicationJob
|
|
239
|
-
source_monitor_queue :fetch
|
|
240
|
-
|
|
241
|
-
discard_on ActiveJob::DeserializationError
|
|
242
|
-
|
|
243
|
-
def perform(item_id)
|
|
244
|
-
item = SourceMonitor::Item.find_by(id: item_id)
|
|
245
|
-
return unless item
|
|
246
|
-
return unless SourceMonitor.config.images.download_enabled?
|
|
247
|
-
|
|
248
|
-
html = item.content
|
|
249
|
-
return if html.blank?
|
|
250
|
-
|
|
251
|
-
# Build or find item_content for attachment storage
|
|
252
|
-
item_content = item.item_content || item.build_item_content
|
|
253
|
-
|
|
254
|
-
# Skip if images already attached (idempotency)
|
|
255
|
-
return if item_content.persisted? && item_content.images.attached?
|
|
256
|
-
|
|
257
|
-
base_url = item.url
|
|
258
|
-
rewriter = SourceMonitor::Images::ContentRewriter.new(html, base_url: base_url)
|
|
259
|
-
image_urls = rewriter.image_urls
|
|
260
|
-
return if image_urls.empty?
|
|
261
|
-
|
|
262
|
-
# Save item_content first so we can attach blobs to it
|
|
263
|
-
item_content.save! unless item_content.persisted?
|
|
264
|
-
|
|
265
|
-
# Download images and build URL mapping
|
|
266
|
-
url_mapping = download_images(item_content, image_urls)
|
|
267
|
-
return if url_mapping.empty?
|
|
268
|
-
|
|
269
|
-
# Rewrite HTML with Active Storage URLs
|
|
270
|
-
rewritten_html = rewriter.rewrite do |original_url|
|
|
271
|
-
url_mapping[original_url]
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Update the item content with rewritten HTML
|
|
275
|
-
item.update!(content: rewritten_html)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
private
|
|
279
|
-
|
|
280
|
-
def download_images(item_content, image_urls)
|
|
281
|
-
url_mapping = {}
|
|
282
|
-
settings = SourceMonitor.config.images
|
|
283
|
-
|
|
284
|
-
image_urls.each do |image_url|
|
|
285
|
-
result = SourceMonitor::Images::Downloader.new(image_url, settings: settings).call
|
|
286
|
-
next unless result
|
|
287
|
-
|
|
288
|
-
blob = ActiveStorage::Blob.create_and_upload!(
|
|
289
|
-
io: result.io,
|
|
290
|
-
filename: result.filename,
|
|
291
|
-
content_type: result.content_type
|
|
292
|
-
)
|
|
293
|
-
item_content.images.attach(blob)
|
|
294
|
-
|
|
295
|
-
# Generate a serving URL for the blob
|
|
296
|
-
url_mapping[image_url] = Rails.application.routes.url_helpers.rails_blob_path(blob, only_path: true)
|
|
297
|
-
rescue StandardError => _error
|
|
298
|
-
# Individual image failure should not block others.
|
|
299
|
-
# Original URL will be preserved (graceful fallback).
|
|
300
|
-
next
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
url_mapping
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
end
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
**Create `test/jobs/source_monitor/download_content_images_job_test.rb`:**
|
|
310
|
-
|
|
311
|
-
Tests using WebMock stubs and Active Storage test helpers:
|
|
312
|
-
|
|
313
|
-
1. Downloads images and rewrites item.content HTML when config enabled
|
|
314
|
-
2. Skips when config disabled (download_to_active_storage is false)
|
|
315
|
-
3. Skips when item not found
|
|
316
|
-
4. Skips when item.content is blank
|
|
317
|
-
5. Skips when images already attached (idempotency)
|
|
318
|
-
6. Skips when no image URLs found in content
|
|
319
|
-
7. Gracefully handles individual image download failure (other images still processed)
|
|
320
|
-
8. Preserves original URL for failed downloads in rewritten HTML
|
|
321
|
-
9. Attaches downloaded images to item_content.images
|
|
322
|
-
10. Creates item_content if it does not exist yet
|
|
323
|
-
|
|
324
|
-
For each test:
|
|
325
|
-
- Set up `SourceMonitor.configure { |c| c.images.download_to_active_storage = true }` where needed
|
|
326
|
-
- Create a source and item with `content: '<p><img src="https://example.com/photo.jpg"></p>'`
|
|
327
|
-
- Stub WebMock for the image URL returning a small PNG binary
|
|
328
|
-
- Call `DownloadContentImagesJob.perform_now(item.id)`
|
|
329
|
-
- Assert on `item.reload.content` for rewritten URLs and `item.item_content.images.count`
|
|
330
|
-
</action>
|
|
331
|
-
<verify>
|
|
332
|
-
Run `PARALLEL_WORKERS=1 bin/rails test test/jobs/source_monitor/download_content_images_job_test.rb` and confirm all tests pass. Run `bin/rubocop app/jobs/source_monitor/download_content_images_job.rb` and confirm no offenses.
|
|
333
|
-
</verify>
|
|
334
|
-
<done>
|
|
335
|
-
DownloadContentImagesJob created. Takes item_id, reads item.content, downloads images via Downloader, attaches to item_content via Active Storage, rewrites HTML with blob paths. Idempotent, graceful failure handling. Tests cover all scenarios.
|
|
336
|
-
</done>
|
|
337
|
-
</task>
|
|
338
|
-
<task type="auto">
|
|
339
|
-
<name>wire-integration-and-update-docs</name>
|
|
340
|
-
<files>
|
|
341
|
-
lib/source_monitor/fetching/feed_fetcher/entry_processor.rb
|
|
342
|
-
test/lib/source_monitor/fetching/feed_fetcher/entry_processor_test.rb
|
|
343
|
-
.claude/skills/sm-configure/SKILL.md
|
|
344
|
-
.claude/skills/sm-configure/reference/configuration-reference.md
|
|
345
|
-
</files>
|
|
346
|
-
<action>
|
|
347
|
-
**Modify `lib/source_monitor/fetching/feed_fetcher/entry_processor.rb`:**
|
|
348
|
-
|
|
349
|
-
Add an integration hook after item creation. In the `process_feed_entries` method, after the `SourceMonitor::Events.after_item_created` call (line 40), add:
|
|
350
|
-
|
|
351
|
-
```ruby
|
|
352
|
-
enqueue_image_download(result.item)
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
This is inside the `if result.created?` block, so it only fires for new items.
|
|
356
|
-
|
|
357
|
-
Add a private method:
|
|
358
|
-
|
|
359
|
-
```ruby
|
|
360
|
-
def enqueue_image_download(item)
|
|
361
|
-
return unless SourceMonitor.config.images.download_enabled?
|
|
362
|
-
return if item.content.blank?
|
|
363
|
-
|
|
364
|
-
SourceMonitor::DownloadContentImagesJob.perform_later(item.id)
|
|
365
|
-
rescue StandardError => error
|
|
366
|
-
# Image download enqueue failure must never break feed processing
|
|
367
|
-
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
368
|
-
Rails.logger.error("[SourceMonitor] Failed to enqueue image download for item #{item.id}: #{error.message}")
|
|
369
|
-
end
|
|
370
|
-
end
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
**Create/update entry processor test:**
|
|
374
|
-
|
|
375
|
-
Check if `test/lib/source_monitor/fetching/feed_fetcher/entry_processor_test.rb` exists. If not, create it with a proper test class. Add tests:
|
|
376
|
-
|
|
377
|
-
1. When images download enabled AND item created with content containing img tags, asserts DownloadContentImagesJob is enqueued with the item's ID
|
|
378
|
-
2. When images download disabled (default), asserts no DownloadContentImagesJob enqueued
|
|
379
|
-
3. When item is updated (not created), asserts no job enqueued
|
|
380
|
-
4. When item.content is blank, asserts no job enqueued
|
|
381
|
-
5. When enqueue raises an error, item creation still succeeds (graceful failure)
|
|
382
|
-
|
|
383
|
-
Use `assert_enqueued_with(job: SourceMonitor::DownloadContentImagesJob, args: [item.id])` and `assert_no_enqueued_jobs(only: SourceMonitor::DownloadContentImagesJob)`.
|
|
384
|
-
|
|
385
|
-
Test setup needs a source, a mock feed with entries (use Feedjira or a mock object), and configure images download as needed per test.
|
|
386
|
-
|
|
387
|
-
**Update `.claude/skills/sm-configure/SKILL.md`:**
|
|
388
|
-
|
|
389
|
-
1. Add `| Images | \`config.images\` | \`ImagesSettings\` |` to the Configuration Sections table.
|
|
390
|
-
2. Add a new Quick Example section after "Authentication (Devise)":
|
|
391
|
-
|
|
392
|
-
```markdown
|
|
393
|
-
### Image Downloads (Active Storage)
|
|
394
|
-
```ruby
|
|
395
|
-
config.images.download_to_active_storage = true
|
|
396
|
-
config.images.max_download_size = 5 * 1024 * 1024 # 5 MB
|
|
397
|
-
config.images.download_timeout = 15
|
|
398
|
-
config.images.allowed_content_types = %w[image/jpeg image/png image/webp]
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
3. Add `| \`lib/source_monitor/configuration/images_settings.rb\` | Image download settings |` to the Key Source Files table.
|
|
402
|
-
|
|
403
|
-
**Update `.claude/skills/sm-configure/reference/configuration-reference.md`:**
|
|
404
|
-
|
|
405
|
-
Add a complete "Images Settings" section documenting all ImagesSettings options:
|
|
406
|
-
|
|
407
|
-
| Setting | Type | Default | Description |
|
|
408
|
-
|---|---|---|---|
|
|
409
|
-
| download_to_active_storage | Boolean | false | Enable background image downloading |
|
|
410
|
-
| max_download_size | Integer | 10485760 (10 MB) | Maximum image file size in bytes |
|
|
411
|
-
| download_timeout | Integer | 30 | HTTP timeout for image downloads in seconds |
|
|
412
|
-
| allowed_content_types | Array | ["image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"] | Permitted MIME types |
|
|
413
|
-
|
|
414
|
-
Include a usage example and note about Active Storage prerequisites (host app must have Active Storage installed).
|
|
415
|
-
</action>
|
|
416
|
-
<verify>
|
|
417
|
-
Run `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/fetching/feed_fetcher/entry_processor_test.rb` and confirm all tests pass. Run `bin/rubocop lib/source_monitor/fetching/feed_fetcher/entry_processor.rb` and confirm no offenses. Verify `grep -n 'config.images' .claude/skills/sm-configure/SKILL.md` returns matches.
|
|
418
|
-
</verify>
|
|
419
|
-
<done>
|
|
420
|
-
Integration hook wired in entry_processor. DownloadContentImagesJob enqueued with item.id for newly created items with HTML content when config enabled. Entry processor tests verify all scenarios. sm-configure skill and reference updated with config.images documentation.
|
|
421
|
-
</done>
|
|
422
|
-
</task>
|
|
423
|
-
<task type="auto">
|
|
424
|
-
<name>full-plan-02-verification</name>
|
|
425
|
-
<files>
|
|
426
|
-
lib/source_monitor/images/downloader.rb
|
|
427
|
-
app/jobs/source_monitor/download_content_images_job.rb
|
|
428
|
-
lib/source_monitor/fetching/feed_fetcher/entry_processor.rb
|
|
429
|
-
.claude/skills/sm-configure/SKILL.md
|
|
430
|
-
</files>
|
|
431
|
-
<action>
|
|
432
|
-
Run the full test suite and linting to confirm no regressions:
|
|
433
|
-
|
|
434
|
-
1. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/images/downloader_test.rb test/jobs/source_monitor/download_content_images_job_test.rb test/lib/source_monitor/fetching/feed_fetcher/entry_processor_test.rb` -- all new tests pass
|
|
435
|
-
2. `bin/rails test` -- full suite passes with 874+ runs and 0 failures
|
|
436
|
-
3. `bin/rubocop` -- zero offenses
|
|
437
|
-
4. `bin/brakeman --no-pager` -- zero warnings
|
|
438
|
-
5. End-to-end verification:
|
|
439
|
-
- Confirm `config.images.download_to_active_storage = true` enables image downloads
|
|
440
|
-
- Confirm default (false) means no jobs enqueued
|
|
441
|
-
- Confirm the job downloads images, attaches them, and rewrites item.content
|
|
442
|
-
- Confirm failed downloads preserve original URLs
|
|
443
|
-
- Confirm sm-configure skill documents `config.images` section
|
|
444
|
-
6. Review all modified files for consistency:
|
|
445
|
-
- Job inherits from ApplicationJob, uses source_monitor_queue :fetch
|
|
446
|
-
- Job takes item_id, reads item.content, rewrites item.content
|
|
447
|
-
- Downloader handles all failure modes gracefully (returns nil)
|
|
448
|
-
- Entry processor integration is wrapped in rescue (never breaks feed processing)
|
|
449
|
-
- ContentRewriter preserves non-image HTML attributes and structure
|
|
450
|
-
|
|
451
|
-
If any test failures, RuboCop offenses, or Brakeman warnings are found, fix them before completing.
|
|
452
|
-
</action>
|
|
453
|
-
<verify>
|
|
454
|
-
`bin/rails test` exits 0 with 874+ runs, 0 failures. `bin/rubocop` exits 0 with 0 offenses. `bin/brakeman --no-pager` exits 0 with 0 warnings. `grep -n 'config.images' .claude/skills/sm-configure/SKILL.md` returns matches.
|
|
455
|
-
</verify>
|
|
456
|
-
<done>
|
|
457
|
-
Plan 02 complete. Full image download pipeline is operational: config enables feature, entry processor enqueues job for new items with content, job downloads images via Downloader, attaches to item_content via Active Storage, rewrites item.content with blob URLs. Graceful fallback on all failure modes. Documentation updated. Full test suite passes.
|
|
458
|
-
</done>
|
|
459
|
-
</task>
|
|
460
|
-
</tasks>
|
|
461
|
-
<verification>
|
|
462
|
-
1. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/images/downloader_test.rb` -- all tests pass
|
|
463
|
-
2. `PARALLEL_WORKERS=1 bin/rails test test/jobs/source_monitor/download_content_images_job_test.rb` -- all tests pass
|
|
464
|
-
3. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/fetching/feed_fetcher/entry_processor_test.rb` -- all tests pass
|
|
465
|
-
4. `bin/rails test` -- 874+ runs, 0 failures
|
|
466
|
-
5. `bin/rubocop` -- 0 offenses
|
|
467
|
-
6. `bin/brakeman --no-pager` -- 0 warnings
|
|
468
|
-
7. `grep -n 'class Downloader' lib/source_monitor/images/downloader.rb` returns a match
|
|
469
|
-
8. `grep -n 'class DownloadContentImagesJob' app/jobs/source_monitor/download_content_images_job.rb` returns a match
|
|
470
|
-
9. `grep -n 'enqueue_image_download' lib/source_monitor/fetching/feed_fetcher/entry_processor.rb` returns a match
|
|
471
|
-
10. `grep -n 'config.images' .claude/skills/sm-configure/SKILL.md` returns matches
|
|
472
|
-
11. `grep -n 'ImagesSettings' .claude/skills/sm-configure/reference/configuration-reference.md` returns matches
|
|
473
|
-
</verification>
|
|
474
|
-
<success_criteria>
|
|
475
|
-
- Downloader service downloads images, validates size/content-type, returns nil on failure (REQ-24)
|
|
476
|
-
- DownloadContentImagesJob takes item_id, orchestrates download/attach/rewrite pipeline on item.content (REQ-24)
|
|
477
|
-
- Job is idempotent (skips if images already attached) (REQ-24)
|
|
478
|
-
- Failed individual downloads preserve original URLs in content (REQ-24 graceful fallback)
|
|
479
|
-
- Entry processor enqueues job only for newly created items when config enabled (REQ-24)
|
|
480
|
-
- Entry processor integration never breaks feed processing on failure (REQ-24 graceful)
|
|
481
|
-
- Default config (disabled) means zero behavior change to existing pipeline (REQ-24 defaults false)
|
|
482
|
-
- sm-configure skill documents config.images section (REQ-24 documentation)
|
|
483
|
-
- All existing tests pass (no regressions)
|
|
484
|
-
- RuboCop clean, Brakeman clean
|
|
485
|
-
</success_criteria>
|
|
486
|
-
<output>
|
|
487
|
-
.vbw-planning/phases/05-active-storage-images/PLAN-02-SUMMARY.md
|
|
488
|
-
</output>
|
data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/06-VERIFICATION.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
phase: 06-netflix-feed-fix
|
|
3
|
-
plan: PLAN-01
|
|
4
|
-
tier: standard
|
|
5
|
-
result: PASS
|
|
6
|
-
passed: 18
|
|
7
|
-
failed: 0
|
|
8
|
-
total: 18
|
|
9
|
-
date: 2026-02-12
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Must-Have Checks
|
|
13
|
-
|
|
14
|
-
| # | Truth/Condition | Status | Evidence |
|
|
15
|
-
|---|----------------|--------|----------|
|
|
16
|
-
| 1 | HTTP tests pass | PASS | `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/http_test.rb` - 13 runs, 45 assertions, 0 failures |
|
|
17
|
-
| 2 | FeedFetcher tests pass | PASS | `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb` - 65 runs, 277 assertions, 0 failures |
|
|
18
|
-
| 3 | RuboCop passes | PASS | 4 files inspected, no offenses detected |
|
|
19
|
-
| 4 | Full test suite passes | PASS | 973 runs, 3114 assertions, 0 failures, 0 errors, 0 skips |
|
|
20
|
-
| 5 | cert_store in http.rb | PASS | Found at lines 78 and 83: `connection.ssl.cert_store` and `def default_cert_store` |
|
|
21
|
-
| 6 | ssl_ca_file in http_settings.rb | PASS | Found at lines 17 and 37: `attr_accessor :ssl_ca_file` and initialization |
|
|
22
|
-
| 7 | Netflix VCR cassette exists | PASS | File exists at `test/vcr_cassettes/source_monitor/fetching/netflix_medium_rss.yml` and contains "netflixtechblog" |
|
|
23
|
-
|
|
24
|
-
## Artifact Checks
|
|
25
|
-
|
|
26
|
-
| Artifact | Exists | Contains | Status |
|
|
27
|
-
|----------|--------|----------|--------|
|
|
28
|
-
| lib/source_monitor/http.rb | YES | "cert_store" | PASS |
|
|
29
|
-
| lib/source_monitor/configuration/http_settings.rb | YES | "ssl_ca_file" | PASS |
|
|
30
|
-
| test/lib/source_monitor/http_test.rb | YES | "ssl" | PASS |
|
|
31
|
-
| test/lib/source_monitor/fetching/feed_fetcher_test.rb | YES | "netflix" | PASS |
|
|
32
|
-
| test/vcr_cassettes/source_monitor/fetching/netflix_medium_rss.yml | YES | "netflixtechblog" | PASS |
|
|
33
|
-
|
|
34
|
-
## Key Link Checks
|
|
35
|
-
|
|
36
|
-
| From | To | Via | Status |
|
|
37
|
-
|------|----|----|--------|
|
|
38
|
-
| http.rb#configure_ssl | REQ-25 | Configures Faraday SSL with system cert store (lines 66-80) | PASS |
|
|
39
|
-
| http_settings.rb#ssl_ca_file | REQ-25 | Exposes configurable SSL CA file/path (lines 17-19, 37-39) | PASS |
|
|
40
|
-
| feed_fetcher_test.rb#netflix_regression | REQ-25 | VCR cassette proves Netflix Tech Blog feed parses successfully (lines 1142-1158) | PASS |
|
|
41
|
-
|
|
42
|
-
## Convention Compliance
|
|
43
|
-
|
|
44
|
-
| Convention | File | Status | Detail |
|
|
45
|
-
|-----------|------|--------|--------|
|
|
46
|
-
| frozen_string_literal | lib/source_monitor/http.rb | PASS | Line 1 |
|
|
47
|
-
| frozen_string_literal | lib/source_monitor/configuration/http_settings.rb | PASS | Line 1 |
|
|
48
|
-
| frozen_string_literal | test/lib/source_monitor/http_test.rb | PASS | Line 1 |
|
|
49
|
-
| frozen_string_literal | test/lib/source_monitor/fetching/feed_fetcher_test.rb | PASS | Line 1 |
|
|
50
|
-
| RuboCop omakase | All modified files | PASS | 0 offenses |
|
|
51
|
-
| Minitest | test/lib/source_monitor/http_test.rb | PASS | 13 tests, all passing |
|
|
52
|
-
|
|
53
|
-
## Anti-Pattern Scan
|
|
54
|
-
|
|
55
|
-
| Pattern | Found | Location | Severity |
|
|
56
|
-
|---------|-------|----------|----------|
|
|
57
|
-
| Hard-coded credentials | NO | N/A | - |
|
|
58
|
-
| Boolean state columns | NO | N/A | - |
|
|
59
|
-
| Service object business logic | NO | N/A | - |
|
|
60
|
-
| N+1 queries | NO | N/A | - |
|
|
61
|
-
|
|
62
|
-
## Requirement Mapping
|
|
63
|
-
|
|
64
|
-
| Requirement | Plan Ref | Artifact Evidence | Status |
|
|
65
|
-
|-------------|----------|------------------|--------|
|
|
66
|
-
| REQ-25: Fix Netflix Tech Blog feed SSL errors | PLAN-01 objective | http.rb lines 66-84: SSL cert store configuration | PASS |
|
|
67
|
-
| REQ-25: Configurable SSL options | PLAN-01 must_have | http_settings.rb lines 17-19, 37-39: ssl_ca_file, ssl_ca_path, ssl_verify | PASS |
|
|
68
|
-
| REQ-25: Netflix regression test | PLAN-01 must_have | feed_fetcher_test.rb lines 1142-1158 + VCR cassette | PASS |
|
|
69
|
-
|
|
70
|
-
## Summary
|
|
71
|
-
|
|
72
|
-
**Tier:** Standard (15-25 checks)
|
|
73
|
-
|
|
74
|
-
**Result:** PASS
|
|
75
|
-
|
|
76
|
-
**Passed:** 18/18
|
|
77
|
-
|
|
78
|
-
**Failed:** None
|
|
79
|
-
|
|
80
|
-
All must-have truths verified successfully:
|
|
81
|
-
- HTTP and FeedFetcher tests pass with zero failures
|
|
82
|
-
- RuboCop passes with zero offenses across all modified files
|
|
83
|
-
- Full test suite passes (973 runs, 0 failures)
|
|
84
|
-
- SSL cert store configuration present in http.rb
|
|
85
|
-
- Configurable SSL options (ssl_ca_file, ssl_ca_path, ssl_verify) present in http_settings.rb
|
|
86
|
-
- Netflix Tech Blog VCR cassette exists and contains expected content
|
|
87
|
-
|
|
88
|
-
All artifacts verified:
|
|
89
|
-
- lib/source_monitor/http.rb implements SSL cert store configuration
|
|
90
|
-
- lib/source_monitor/configuration/http_settings.rb exposes SSL configuration options
|
|
91
|
-
- test/lib/source_monitor/http_test.rb contains 6 SSL-specific tests (lines 117-153)
|
|
92
|
-
- test/lib/source_monitor/fetching/feed_fetcher_test.rb contains Netflix regression test (lines 1142-1158)
|
|
93
|
-
- test/vcr_cassettes/source_monitor/fetching/netflix_medium_rss.yml captured from real Netflix Tech Blog feed
|
|
94
|
-
|
|
95
|
-
All key links verified:
|
|
96
|
-
- configure_ssl method (http.rb lines 66-80) solves REQ-25 by using OpenSSL::X509::Store with set_default_paths
|
|
97
|
-
- HTTPSettings attributes (http_settings.rb) provide configurability for non-standard environments
|
|
98
|
-
- Netflix regression test with VCR cassette proves the fix works in practice
|
|
99
|
-
|
|
100
|
-
No regressions detected. All conventions followed. REQ-25 fully satisfied.
|
data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
plan: "01"
|
|
3
|
-
phase: 6
|
|
4
|
-
title: ssl-cert-store-configuration
|
|
5
|
-
status: COMPLETE
|
|
6
|
-
requirement: REQ-25
|
|
7
|
-
test_runs: 973
|
|
8
|
-
test_assertions: 3114
|
|
9
|
-
test_failures: 0
|
|
10
|
-
rubocop_offenses: 0
|
|
11
|
-
brakeman_warnings: 0
|
|
12
|
-
commits:
|
|
13
|
-
- hash: c673d00
|
|
14
|
-
message: "feat(06-01): add-ssl-settings-to-http-settings"
|
|
15
|
-
- hash: f084129
|
|
16
|
-
message: "feat(06-01): configure-faraday-ssl-cert-store"
|
|
17
|
-
- hash: d2e3997
|
|
18
|
-
message: "test(06-01): add-ssl-unit-tests"
|
|
19
|
-
- hash: 6f0bbe8
|
|
20
|
-
message: "test(06-01): record-netflix-vcr-cassette-and-regression-test"
|
|
21
|
-
deviations: none
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## What Was Built
|
|
25
|
-
|
|
26
|
-
- **HTTPSettings SSL options** -- Added ssl_ca_file, ssl_ca_path, ssl_verify attr_accessors with safe defaults (verify=true, ca_file/ca_path=nil). Follows existing settings pattern.
|
|
27
|
-
- **Faraday SSL cert store** -- Every Faraday connection now gets an OpenSSL::X509::Store initialized with set_default_paths, loading all system-trusted CAs including intermediates. ssl_ca_file and ssl_ca_path override the store when set. General fix applying to ALL connections, not Netflix-specific.
|
|
28
|
-
- **SSL unit tests** -- 5 new tests covering default cert store, ca_file override, ca_path override, verify-true default, verify-false escape hatch. 13 total HTTP tests.
|
|
29
|
-
- **Netflix VCR cassette** -- Recorded from real Netflix Tech Blog feed (netflixtechblog.com/feed). Trimmed to 3 entries for manageable fixture size. Regression test parses as RSS and validates Netflix title and entries.
|
|
30
|
-
|
|
31
|
-
## Files Modified
|
|
32
|
-
|
|
33
|
-
- `lib/source_monitor/configuration/http_settings.rb` -- added ssl_ca_file, ssl_ca_path, ssl_verify settings
|
|
34
|
-
- `lib/source_monitor/http.rb` -- added require "openssl", configure_ssl method, default_cert_store method
|
|
35
|
-
- `test/lib/source_monitor/http_test.rb` -- 5 new SSL configuration tests
|
|
36
|
-
- `test/lib/source_monitor/fetching/feed_fetcher_test.rb` -- Netflix Tech Blog regression test
|
|
37
|
-
- `test/vcr_cassettes/source_monitor/fetching/netflix_medium_rss.yml` -- new VCR cassette
|