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.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +45 -22
  3. data/.claude/skills/sm-configure/SKILL.md +10 -1
  4. data/.claude/skills/sm-configure/reference/configuration-reference.md +44 -0
  5. data/.claude/skills/sm-host-setup/reference/initializer-template.md +17 -0
  6. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +2 -0
  7. data/.claude/skills/sm-job/reference/job-conventions.md +26 -0
  8. data/.claude/skills/sm-upgrade/reference/version-history.md +22 -0
  9. data/.gitignore +10 -0
  10. data/AGENTS.md +1 -1
  11. data/CHANGELOG.md +56 -0
  12. data/CLAUDE.md +11 -5
  13. data/Gemfile.lock +1 -1
  14. data/README.md +6 -4
  15. data/VERSION +1 -1
  16. data/app/assets/builds/source_monitor/application.css +43 -0
  17. data/app/assets/builds/source_monitor/application.js +127 -0
  18. data/app/assets/builds/source_monitor/application.js.map +3 -3
  19. data/app/assets/javascripts/source_monitor/application.js +2 -0
  20. data/app/assets/javascripts/source_monitor/controllers/notification_container_controller.js +138 -0
  21. data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +11 -0
  22. data/app/controllers/source_monitor/source_favicon_fetches_controller.rb +38 -0
  23. data/app/controllers/source_monitor/sources_controller.rb +11 -0
  24. data/app/helpers/source_monitor/application_helper.rb +51 -0
  25. data/app/jobs/source_monitor/favicon_fetch_job.rb +71 -0
  26. data/app/jobs/source_monitor/import_opml_job.rb +9 -0
  27. data/app/jobs/source_monitor/source_health_check_job.rb +10 -0
  28. data/app/models/source_monitor/source.rb +2 -0
  29. data/app/views/layouts/source_monitor/application.html.erb +23 -2
  30. data/app/views/source_monitor/shared/_toast.html.erb +1 -0
  31. data/app/views/source_monitor/sources/_details.html.erb +34 -5
  32. data/app/views/source_monitor/sources/_row.html.erb +11 -6
  33. data/config/routes.rb +1 -0
  34. data/docs/configuration.md +1 -1
  35. data/docs/upgrade.md +22 -0
  36. data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +15 -1
  37. data/lib/source_monitor/configuration/favicons_settings.rb +42 -0
  38. data/lib/source_monitor/configuration/http_settings.rb +1 -1
  39. data/lib/source_monitor/configuration/scraping_settings.rb +1 -1
  40. data/lib/source_monitor/configuration.rb +3 -1
  41. data/lib/source_monitor/favicons/discoverer.rb +196 -0
  42. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +21 -0
  43. data/lib/source_monitor/fetching/feed_fetcher.rb +1 -0
  44. data/lib/source_monitor/http.rb +5 -3
  45. data/lib/source_monitor/version.rb +1 -1
  46. data/lib/source_monitor.rb +4 -0
  47. data/lib/tasks/test_fast.rake +11 -0
  48. data/source_monitor.gemspec +1 -1
  49. metadata +7 -93
  50. data/.vbw-planning/PROJECT.md +0 -51
  51. data/.vbw-planning/ROADMAP.md +0 -32
  52. data/.vbw-planning/SHIPPED.md +0 -63
  53. data/.vbw-planning/STATE.md +0 -27
  54. data/.vbw-planning/codebase/ARCHITECTURE.md +0 -147
  55. data/.vbw-planning/codebase/CONCERNS.md +0 -99
  56. data/.vbw-planning/codebase/CONVENTIONS.md +0 -97
  57. data/.vbw-planning/codebase/DEPENDENCIES.md +0 -100
  58. data/.vbw-planning/codebase/INDEX.md +0 -86
  59. data/.vbw-planning/codebase/META.md +0 -42
  60. data/.vbw-planning/codebase/PATTERNS.md +0 -262
  61. data/.vbw-planning/codebase/STACK.md +0 -101
  62. data/.vbw-planning/codebase/STRUCTURE.md +0 -324
  63. data/.vbw-planning/codebase/TESTING.md +0 -154
  64. data/.vbw-planning/config.json +0 -53
  65. data/.vbw-planning/discovery.json +0 -26
  66. data/.vbw-planning/milestones/default/ROADMAP.md +0 -115
  67. data/.vbw-planning/milestones/default/STATE.md +0 -82
  68. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +0 -56
  69. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +0 -187
  70. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +0 -64
  71. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +0 -137
  72. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +0 -67
  73. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +0 -142
  74. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +0 -64
  75. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +0 -138
  76. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +0 -85
  77. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +0 -147
  78. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +0 -63
  79. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +0 -129
  80. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +0 -74
  81. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +0 -154
  82. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +0 -303
  83. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +0 -510
  84. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +0 -61
  85. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +0 -161
  86. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +0 -66
  87. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +0 -132
  88. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +0 -59
  89. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +0 -171
  90. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +0 -56
  91. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +0 -152
  92. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +0 -33
  93. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +0 -42
  94. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +0 -119
  95. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +0 -52
  96. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +0 -195
  97. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +0 -79
  98. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +0 -130
  99. data/.vbw-planning/milestones/generator-enhancements/REQUIREMENTS.md +0 -72
  100. data/.vbw-planning/milestones/generator-enhancements/ROADMAP.md +0 -125
  101. data/.vbw-planning/milestones/generator-enhancements/SHIPPED.md +0 -40
  102. data/.vbw-planning/milestones/generator-enhancements/STATE.md +0 -43
  103. data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-CONTEXT.md +0 -33
  104. data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-VERIFICATION.md +0 -86
  105. data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01-SUMMARY.md +0 -61
  106. data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01.md +0 -380
  107. data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/02-VERIFICATION.md +0 -78
  108. data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01-SUMMARY.md +0 -46
  109. data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01.md +0 -500
  110. data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/03-VERIFICATION.md +0 -89
  111. data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01-SUMMARY.md +0 -48
  112. data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01.md +0 -456
  113. data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/04-VERIFICATION.md +0 -129
  114. data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +0 -70
  115. data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01.md +0 -747
  116. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/05-VERIFICATION.md +0 -156
  117. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01-SUMMARY.md +0 -69
  118. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01.md +0 -455
  119. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02-SUMMARY.md +0 -39
  120. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02.md +0 -488
  121. data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/06-VERIFICATION.md +0 -100
  122. data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +0 -37
  123. data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01.md +0 -345
  124. data/.vbw-planning/milestones/upgrade-assurance/REQUIREMENTS.md +0 -80
  125. data/.vbw-planning/milestones/upgrade-assurance/ROADMAP.md +0 -75
  126. data/.vbw-planning/milestones/upgrade-assurance/STATE.md +0 -29
  127. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/01-VERIFICATION.md +0 -144
  128. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01-SUMMARY.md +0 -43
  129. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01.md +0 -405
  130. data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01-SUMMARY.md +0 -27
  131. data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01.md +0 -303
  132. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/03-VERIFICATION.md +0 -380
  133. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01-SUMMARY.md +0 -36
  134. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01.md +0 -652
  135. data/.vbw-planning/phases/01-aia-certificate-resolution/.context-dev.md +0 -17
  136. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01-SUMMARY.md +0 -26
  137. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01.md +0 -71
  138. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02-SUMMARY.md +0 -16
  139. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02.md +0 -56
  140. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03-SUMMARY.md +0 -17
  141. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03.md +0 -98
@@ -1,455 +0,0 @@
1
- ---
2
- phase: 5
3
- plan: "01"
4
- title: images-config-model-rewriter
5
- type: execute
6
- wave: 1
7
- depends_on: []
8
- cross_phase_deps: []
9
- autonomous: true
10
- effort_override: thorough
11
- skills_used:
12
- - sm-configure
13
- - sm-configuration-setting
14
- files_modified:
15
- - lib/source_monitor/configuration/images_settings.rb
16
- - lib/source_monitor/configuration.rb
17
- - lib/source_monitor.rb
18
- - app/models/source_monitor/item_content.rb
19
- - lib/source_monitor/images/content_rewriter.rb
20
- - test/lib/source_monitor/configuration/images_settings_test.rb
21
- - test/lib/source_monitor/images/content_rewriter_test.rb
22
- - test/models/source_monitor/item_content_test.rb
23
- must_haves:
24
- truths:
25
- - "Running `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/configuration/images_settings_test.rb` exits 0 with 0 failures"
26
- - "Running `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/images/content_rewriter_test.rb` exits 0 with 0 failures"
27
- - "Running `PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/item_content_test.rb` exits 0 with 0 failures"
28
- - "Running `bin/rubocop lib/source_monitor/configuration/images_settings.rb lib/source_monitor/images/content_rewriter.rb app/models/source_monitor/item_content.rb` exits 0 with no offenses"
29
- - "`SourceMonitor.config.images` returns an ImagesSettings instance"
30
- - "`SourceMonitor.config.images.download_to_active_storage` defaults to `false`"
31
- - "`SourceMonitor.reset_configuration!` resets images settings to defaults"
32
- - "ContentRewriter.new(html).image_urls returns an array of absolute image URLs found in img tags"
33
- - "ContentRewriter.new(html).rewrite { |url| new_url } replaces img src attributes with block return values"
34
- - "ItemContent responds to `images` (has_many_attached) when Active Storage is available"
35
- artifacts:
36
- - path: "lib/source_monitor/configuration/images_settings.rb"
37
- provides: "Image download configuration settings"
38
- contains: "class ImagesSettings"
39
- - path: "lib/source_monitor/images/content_rewriter.rb"
40
- provides: "HTML img tag parser and URL rewriter"
41
- contains: "class ContentRewriter"
42
- - path: "test/lib/source_monitor/configuration/images_settings_test.rb"
43
- provides: "Tests for ImagesSettings defaults, accessors, and reset"
44
- contains: "class ImagesSettingsTest"
45
- - path: "test/lib/source_monitor/images/content_rewriter_test.rb"
46
- provides: "Tests for image URL extraction and HTML rewriting"
47
- contains: "class ContentRewriterTest"
48
- key_links:
49
- - from: "images_settings.rb#download_to_active_storage"
50
- to: "REQ-24"
51
- via: "Configurable option defaults to false"
52
- - from: "content_rewriter.rb#image_urls"
53
- to: "REQ-24"
54
- via: "Detects inline images in item content"
55
- - from: "content_rewriter.rb#rewrite"
56
- to: "REQ-24"
57
- via: "Replaces original URLs with Active Storage URLs"
58
- - from: "item_content.rb#has_many_attached"
59
- to: "REQ-24"
60
- via: "Images attached to ItemContent via Active Storage"
61
- ---
62
- <objective>
63
- Create the configuration section, model attachment, and HTML content rewriter for downloading inline images to Active Storage. This plan establishes the foundational pieces that the download job (Plan 02) will use. REQ-24.
64
- </objective>
65
- <context>
66
- @lib/source_monitor/configuration.rb -- Main Configuration class. Has 10 sub-sections as attr_readers initialized in constructor. New `images` section follows the same pattern: add `require`, add `attr_reader :images`, initialize `@images = ImagesSettings.new` in constructor. The reset happens via `SourceMonitor.reset_configuration!` which creates a new Configuration instance.
67
-
68
- @lib/source_monitor/configuration/scraping_settings.rb -- Good pattern to follow for ImagesSettings. Simple settings class with `attr_accessor`, constants for defaults, `initialize` that calls `reset!`, and `reset!` that sets all defaults. Uses private `normalize_numeric` helper.
69
-
70
- @lib/source_monitor/configuration/http_settings.rb -- Another settings pattern. More accessors, same initialize/reset! structure.
71
-
72
- @app/models/source_monitor/item_content.rb -- Currently has `belongs_to :item` and `validates :item`. Need to add `has_many_attached :images` which requires Active Storage tables in the database. Since this is a mountable engine, Active Storage tables come from the host app -- they should already exist if the host uses `rails active_storage:install`. The dummy app has `config.active_storage.service` configured but NO Active Storage tables in schema.rb. We need to install them.
73
-
74
- @lib/source_monitor.rb -- Module autoload declarations. The new `Images` module should be added here as `module Images; autoload :ContentRewriter, "source_monitor/images/content_rewriter"; end`.
75
-
76
- @lib/source_monitor/items/item_creator/content_extractor.rb -- Uses Nokolexbor for HTML parsing. ContentRewriter should also use Nokolexbor (already a gemspec dependency) to parse HTML and find/rewrite img[src] attributes. Nokolexbor is a drop-in Nokogiri replacement with better performance.
77
-
78
- **Key design decisions:**
79
- 1. ImagesSettings has: `download_to_active_storage` (bool, default false), `max_download_size` (integer bytes, default 10MB), `download_timeout` (integer seconds, default 30), `allowed_content_types` (array, default %w[image/jpeg image/png image/gif image/webp image/svg+xml])
80
- 2. ContentRewriter is a pure HTML transformer -- no HTTP, no Active Storage. It takes HTML string, finds img[src], and provides `image_urls` (extraction) and `rewrite` (transformation via block).
81
- 3. `has_many_attached :images` on ItemContent is conditional -- only declared when Active Storage is loaded. This prevents errors in host apps that haven't installed Active Storage.
82
- 4. For the dummy app, install Active Storage migrations so tests can exercise attachments.
83
- 5. ContentRewriter handles relative URLs by requiring a `base_url` parameter for resolution. Feed items always have a source URL that can serve as base.
84
- </context>
85
- <tasks>
86
- <task type="auto">
87
- <name>create-images-settings</name>
88
- <files>
89
- lib/source_monitor/configuration/images_settings.rb
90
- lib/source_monitor/configuration.rb
91
- lib/source_monitor.rb
92
- test/lib/source_monitor/configuration/images_settings_test.rb
93
- </files>
94
- <action>
95
- **Create `lib/source_monitor/configuration/images_settings.rb`:**
96
-
97
- Follow the ScrapingSettings pattern. The class should have:
98
-
99
- ```ruby
100
- # frozen_string_literal: true
101
-
102
- module SourceMonitor
103
- class Configuration
104
- class ImagesSettings
105
- attr_accessor :download_to_active_storage,
106
- :max_download_size,
107
- :download_timeout,
108
- :allowed_content_types
109
-
110
- DEFAULT_MAX_DOWNLOAD_SIZE = 10 * 1024 * 1024 # 10 MB
111
- DEFAULT_DOWNLOAD_TIMEOUT = 30 # seconds
112
- DEFAULT_ALLOWED_CONTENT_TYPES = %w[
113
- image/jpeg
114
- image/png
115
- image/gif
116
- image/webp
117
- image/svg+xml
118
- ].freeze
119
-
120
- def initialize
121
- reset!
122
- end
123
-
124
- def reset!
125
- @download_to_active_storage = false
126
- @max_download_size = DEFAULT_MAX_DOWNLOAD_SIZE
127
- @download_timeout = DEFAULT_DOWNLOAD_TIMEOUT
128
- @allowed_content_types = DEFAULT_ALLOWED_CONTENT_TYPES.dup
129
- end
130
-
131
- def download_enabled?
132
- !!download_to_active_storage
133
- end
134
- end
135
- end
136
- end
137
- ```
138
-
139
- **Modify `lib/source_monitor/configuration.rb`:**
140
-
141
- 1. Add `require "source_monitor/configuration/images_settings"` after the other require lines.
142
- 2. Add `:images` to the `attr_reader` list (after `:scraping`).
143
- 3. Add `@images = ImagesSettings.new` in the `initialize` method (after `@scraping`).
144
-
145
- **Modify `lib/source_monitor.rb`:**
146
-
147
- Add a new `Images` module autoload block after the existing `Items` module:
148
-
149
- ```ruby
150
- module Images
151
- autoload :ContentRewriter, "source_monitor/images/content_rewriter"
152
- end
153
- ```
154
-
155
- **Create `test/lib/source_monitor/configuration/images_settings_test.rb`:**
156
-
157
- TDD tests covering:
158
- 1. Default values: `download_to_active_storage` is false, `max_download_size` is 10MB, `download_timeout` is 30, `allowed_content_types` includes the 5 types.
159
- 2. Accessors work: set each value, read it back.
160
- 3. `reset!` restores defaults after changes.
161
- 4. `download_enabled?` returns false by default, true when enabled.
162
- 5. Integration: `SourceMonitor.config.images` returns ImagesSettings instance.
163
- 6. `SourceMonitor.reset_configuration!` resets images settings.
164
- </action>
165
- <verify>
166
- Run `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/configuration/images_settings_test.rb` and confirm all tests pass. Run `bin/rubocop lib/source_monitor/configuration/images_settings.rb` and confirm no offenses. Verify `SourceMonitor.config.images` returns the settings object in a Rails console or by reading the code.
167
- </verify>
168
- <done>
169
- ImagesSettings class created with 4 configurable attributes, sensible defaults, reset! method, and download_enabled? convenience method. Wired into Configuration as `config.images`. Tests pass, RuboCop clean.
170
- </done>
171
- </task>
172
- <task type="auto">
173
- <name>install-active-storage-and-add-attachment</name>
174
- <files>
175
- app/models/source_monitor/item_content.rb
176
- test/models/source_monitor/item_content_test.rb
177
- </files>
178
- <action>
179
- **Install Active Storage tables in the dummy app:**
180
-
181
- Run `cd test/dummy && bin/rails active_storage:install` to generate the Active Storage migration, then run `cd test/dummy && bin/rails db:migrate` to apply it. This adds the `active_storage_blobs`, `active_storage_attachments`, and `active_storage_variant_records` tables to the dummy app's schema.
182
-
183
- If the above doesn't work due to engine test setup, manually create a migration in `test/dummy/db/migrate/` that creates the Active Storage tables, matching the standard Rails Active Storage migration content. Then run `bin/rails db:migrate` from the project root.
184
-
185
- **Modify `app/models/source_monitor/item_content.rb`:**
186
-
187
- Add `has_many_attached :images` conditionally. Since this is a mountable engine and the host app may or may not have Active Storage installed, wrap it:
188
-
189
- ```ruby
190
- # frozen_string_literal: true
191
-
192
- module SourceMonitor
193
- class ItemContent < ApplicationRecord
194
- belongs_to :item, class_name: "SourceMonitor::Item", inverse_of: :item_content, touch: true
195
-
196
- validates :item, presence: true
197
-
198
- # Active Storage attachment for downloaded inline images.
199
- # Only available when the host app has Active Storage installed.
200
- has_many_attached :images if respond_to?(:has_many_attached)
201
-
202
- SourceMonitor::ModelExtensions.register(self, :item_content)
203
- end
204
- end
205
- ```
206
-
207
- Note: `respond_to?(:has_many_attached)` is always true when `activestorage` is loaded (which it is via `rails/all`). If the host app explicitly excludes Active Storage, this gracefully skips. The important thing is that the Active Storage *tables* must exist for the attachment to work at runtime -- but the declaration itself is safe.
208
-
209
- Actually, since `rails/all` always loads Active Storage and our gemspec requires `rails >= 8.0.3`, `has_many_attached` will always be available. Use it unconditionally:
210
-
211
- ```ruby
212
- has_many_attached :images
213
- ```
214
-
215
- **Create or update `test/models/source_monitor/item_content_test.rb`:**
216
-
217
- This file does not currently exist. Create it with:
218
-
219
- 1. Test that ItemContent belongs_to :item.
220
- 2. Test that ItemContent responds to `images` (the attachment).
221
- 3. Test that `images` returns an empty collection by default.
222
- 4. Test that an image can be attached and retrieved (use `ActiveStorage::Blob.create_and_upload!` with a small test fixture).
223
-
224
- Create a small test image fixture: `test/fixtures/files/test_image.png` -- a 1x1 pixel PNG (use the smallest valid PNG binary).
225
-
226
- Use `fixture_file_upload` or `io: StringIO.new(...)` pattern for attaching test files.
227
- </action>
228
- <verify>
229
- Run `PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/item_content_test.rb` and confirm all tests pass. Verify Active Storage tables exist in `test/dummy/db/schema.rb`. Run `bin/rubocop app/models/source_monitor/item_content.rb` and confirm no offenses.
230
- </verify>
231
- <done>
232
- Active Storage tables installed in dummy app. ItemContent has `has_many_attached :images`. Tests verify attachment behavior. Schema updated.
233
- </done>
234
- </task>
235
- <task type="auto">
236
- <name>create-content-rewriter</name>
237
- <files>
238
- lib/source_monitor/images/content_rewriter.rb
239
- test/lib/source_monitor/images/content_rewriter_test.rb
240
- </files>
241
- <action>
242
- **Create `lib/source_monitor/images/content_rewriter.rb`:**
243
-
244
- A pure HTML transformer that uses Nokolexbor (already in gemspec) to find and rewrite `<img>` tag `src` attributes.
245
-
246
- ```ruby
247
- # frozen_string_literal: true
248
-
249
- require "nokolexbor"
250
- require "uri"
251
-
252
- module SourceMonitor
253
- module Images
254
- class ContentRewriter
255
- attr_reader :html, :base_url
256
-
257
- def initialize(html, base_url: nil)
258
- @html = html.to_s
259
- @base_url = base_url
260
- end
261
-
262
- # Returns an array of absolute image URLs found in <img> tags.
263
- # Skips data: URIs, blank src, and invalid URLs.
264
- def image_urls
265
- return [] if html.blank?
266
-
267
- doc = parse_fragment
268
- urls = []
269
-
270
- doc.css("img[src]").each do |img|
271
- url = resolve_url(img["src"])
272
- urls << url if url && downloadable_url?(url)
273
- end
274
-
275
- urls.uniq
276
- end
277
-
278
- # Rewrites <img src="..."> attributes by yielding each original URL
279
- # to the block and replacing with the block's return value.
280
- # Returns the rewritten HTML string.
281
- # If the block returns nil, the original URL is preserved (graceful fallback).
282
- def rewrite
283
- return html if html.blank?
284
-
285
- doc = parse_fragment
286
-
287
- doc.css("img[src]").each do |img|
288
- original_url = resolve_url(img["src"])
289
- next unless original_url && downloadable_url?(original_url)
290
-
291
- new_url = yield(original_url)
292
- img["src"] = new_url if new_url.present?
293
- end
294
-
295
- doc.to_html
296
- end
297
-
298
- private
299
-
300
- def parse_fragment
301
- Nokolexbor::DocumentFragment.parse(html)
302
- end
303
-
304
- def resolve_url(src)
305
- src = src.to_s.strip
306
- return nil if src.blank?
307
- return nil if src.start_with?("data:")
308
-
309
- uri = URI.parse(src)
310
- if uri.relative? && base_url.present?
311
- URI.join(base_url, src).to_s
312
- elsif uri.absolute?
313
- src
314
- end
315
- rescue URI::InvalidURIError
316
- nil
317
- end
318
-
319
- def downloadable_url?(url)
320
- uri = URI.parse(url)
321
- uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
322
- rescue URI::InvalidURIError
323
- false
324
- end
325
- end
326
- end
327
- end
328
- ```
329
-
330
- **Create `test/lib/source_monitor/images/content_rewriter_test.rb`:**
331
-
332
- Comprehensive tests:
333
-
334
- 1. **image_urls extraction:**
335
- - Returns empty array for nil/blank HTML
336
- - Extracts single img src URL
337
- - Extracts multiple img src URLs
338
- - Deduplicates identical URLs
339
- - Skips data: URIs
340
- - Skips img tags without src attribute
341
- - Skips blank src attributes
342
- - Resolves relative URLs when base_url provided
343
- - Skips relative URLs when no base_url provided
344
- - Handles malformed URLs gracefully (returns empty, no exception)
345
-
346
- 2. **rewrite:**
347
- - Returns original HTML when no img tags present
348
- - Returns original HTML when HTML is blank
349
- - Replaces img src with block return value
350
- - Preserves original URL when block returns nil (graceful fallback)
351
- - Handles multiple img tags
352
- - Preserves other img attributes (alt, class, etc.)
353
- - Skips data: URIs (does not yield them to block)
354
- - Handles mixed downloadable and non-downloadable URLs
355
-
356
- 3. **Edge cases:**
357
- - HTML with no images returns empty array from image_urls
358
- - Very large src attributes (truncated URLs) handled gracefully
359
- - HTML fragments (not full documents)
360
- - Self-closing img tags (`<img src="..." />`)
361
- </action>
362
- <verify>
363
- Run `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/images/content_rewriter_test.rb` and confirm all tests pass. Run `bin/rubocop lib/source_monitor/images/content_rewriter.rb` and confirm no offenses.
364
- </verify>
365
- <done>
366
- ContentRewriter class created with `image_urls` extraction and `rewrite` transformation methods. Uses Nokolexbor for HTML parsing. Handles relative URLs, data: URIs, and invalid URLs gracefully. Tests cover all scenarios.
367
- </done>
368
- </task>
369
- <task type="auto">
370
- <name>integration-test-and-config-test-update</name>
371
- <files>
372
- test/lib/source_monitor/configuration_test.rb
373
- test/lib/source_monitor/configuration/settings_test.rb
374
- </files>
375
- <action>
376
- Update existing configuration tests to cover the new `images` section.
377
-
378
- **Modify `test/lib/source_monitor/configuration_test.rb`:**
379
-
380
- 1. Find the test that checks all config sub-sections (likely iterating over attr_readers) and add `:images` to the list.
381
- 2. If there's a test for `reset_configuration!`, verify it also resets images settings.
382
-
383
- **Modify `test/lib/source_monitor/configuration/settings_test.rb`:**
384
-
385
- 1. Find where other settings classes are tested (like `assert_kind_of ModelDefinition, @models.item_content`) and add a test that `config.images` is an `ImagesSettings` instance.
386
- 2. Add a test that `config.images.download_to_active_storage` defaults to false.
387
-
388
- Run the full existing configuration test files to ensure no regressions.
389
- </action>
390
- <verify>
391
- Run `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/configuration_test.rb test/lib/source_monitor/configuration/settings_test.rb` and confirm all tests pass. No new RuboCop offenses.
392
- </verify>
393
- <done>
394
- Existing configuration tests updated to cover the new `images` settings section. No regressions in existing tests.
395
- </done>
396
- </task>
397
- <task type="auto">
398
- <name>full-plan-01-verification</name>
399
- <files>
400
- lib/source_monitor/configuration/images_settings.rb
401
- lib/source_monitor/configuration.rb
402
- lib/source_monitor.rb
403
- app/models/source_monitor/item_content.rb
404
- lib/source_monitor/images/content_rewriter.rb
405
- </files>
406
- <action>
407
- Run the full test suite and linting to confirm no regressions:
408
-
409
- 1. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/configuration/images_settings_test.rb test/lib/source_monitor/images/content_rewriter_test.rb test/models/source_monitor/item_content_test.rb` -- all new tests pass
410
- 2. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/configuration_test.rb test/lib/source_monitor/configuration/settings_test.rb` -- existing config tests pass
411
- 3. `bin/rails test` -- full suite passes with 874+ runs and 0 failures
412
- 4. `bin/rubocop` -- zero offenses
413
- 5. Review the final state:
414
- - `SourceMonitor.config.images` is accessible and has correct defaults
415
- - `SourceMonitor::Images::ContentRewriter` is autoloaded
416
- - `SourceMonitor::ItemContent` has `has_many_attached :images`
417
- - Active Storage tables exist in dummy app schema
418
- - All tests are isolated (use `SourceMonitor.reset_configuration!` in setup)
419
-
420
- If any test failures or RuboCop offenses are found, fix them before completing.
421
- </action>
422
- <verify>
423
- `bin/rails test` exits 0 with 874+ runs, 0 failures. `bin/rubocop` exits 0 with 0 offenses.
424
- </verify>
425
- <done>
426
- Plan 01 complete. Configuration, model attachment, and content rewriter are all in place. Full test suite passes. Ready for Plan 02 to build the download job and integration.
427
- </done>
428
- </task>
429
- </tasks>
430
- <verification>
431
- 1. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/configuration/images_settings_test.rb` -- all tests pass
432
- 2. `PARALLEL_WORKERS=1 bin/rails test test/lib/source_monitor/images/content_rewriter_test.rb` -- all tests pass
433
- 3. `PARALLEL_WORKERS=1 bin/rails test test/models/source_monitor/item_content_test.rb` -- all tests pass
434
- 4. `bin/rails test` -- 874+ runs, 0 failures
435
- 5. `bin/rubocop` -- 0 offenses
436
- 6. `grep -n 'class ImagesSettings' lib/source_monitor/configuration/images_settings.rb` returns a match
437
- 7. `grep -n 'attr_reader.*:images' lib/source_monitor/configuration.rb` returns a match
438
- 8. `grep -n 'has_many_attached :images' app/models/source_monitor/item_content.rb` returns a match
439
- 9. `grep -n 'class ContentRewriter' lib/source_monitor/images/content_rewriter.rb` returns a match
440
- 10. `grep -n 'autoload :ContentRewriter' lib/source_monitor.rb` returns a match
441
- </verification>
442
- <success_criteria>
443
- - ImagesSettings class exists with download_to_active_storage (default false), max_download_size, download_timeout, allowed_content_types (REQ-24 config)
444
- - Configuration.images is accessible and resets properly
445
- - ItemContent has has_many_attached :images (REQ-24 attachment)
446
- - Active Storage tables exist in dummy app for testing
447
- - ContentRewriter extracts image URLs from HTML content (REQ-24 detection)
448
- - ContentRewriter rewrites img src attributes via block (REQ-24 URL replacement)
449
- - ContentRewriter preserves original URLs when rewrite block returns nil (REQ-24 graceful fallback)
450
- - All existing tests pass (no regressions)
451
- - RuboCop clean
452
- </success_criteria>
453
- <output>
454
- .vbw-planning/phases/05-active-storage-images/PLAN-01-SUMMARY.md
455
- </output>
@@ -1,39 +0,0 @@
1
- ---
2
- plan: "02"
3
- phase: 5
4
- title: download-job-integration-docs
5
- status: COMPLETE
6
- requirement: REQ-24
7
- test_runs: 967
8
- test_assertions: 3100
9
- test_failures: 0
10
- rubocop_offenses: 0
11
- brakeman_warnings: 0
12
- commits:
13
- - hash: 2df856b
14
- message: "feat(05-02): create-image-downloader"
15
- - hash: 84e9493
16
- message: "feat(05-02): create-download-job"
17
- - hash: e97d2a4
18
- message: "feat(05-02): wire-integration-and-update-docs"
19
- deviations: none
20
- ---
21
-
22
- ## What Was Built
23
-
24
- - **Images::Downloader** -- Service object that downloads a single image via Faraday, validates content type against allowed list, enforces max_download_size, derives filenames from URL or generates random names. Returns nil on any failure for graceful fallback. 11 tests.
25
- - **DownloadContentImagesJob** -- Background job taking item_id. Reads item.content for inline images, downloads via Downloader, attaches blobs to item_content.images via Active Storage, rewrites item.content with blob serving URLs. Idempotent (skips if images already attached). Graceful per-image failure handling. Runs on fetch queue. 10 tests.
26
- - **EntryProcessor integration hook** -- enqueue_image_download called after item creation. Only fires when config.images.download_enabled? is true and item.content is non-blank. Wrapped in rescue so failures never break feed processing. 5 tests.
27
- - **sm-configure skill docs** -- Added config.images section to SKILL.md (table row, quick example, source file entry) and full ImagesSettings documentation to configuration-reference.md.
28
-
29
- ## Files Modified
30
-
31
- - `lib/source_monitor/images/downloader.rb` -- new (Downloader service)
32
- - `lib/source_monitor.rb` -- added Downloader autoload
33
- - `app/jobs/source_monitor/download_content_images_job.rb` -- new (background job)
34
- - `lib/source_monitor/fetching/feed_fetcher/entry_processor.rb` -- added enqueue_image_download hook + private method
35
- - `test/lib/source_monitor/images/downloader_test.rb` -- new (11 tests)
36
- - `test/jobs/source_monitor/download_content_images_job_test.rb` -- new (10 tests)
37
- - `test/lib/source_monitor/fetching/feed_fetcher/entry_processor_test.rb` -- new (5 tests)
38
- - `.claude/skills/sm-configure/SKILL.md` -- added Images row, quick example, source file
39
- - `.claude/skills/sm-configure/reference/configuration-reference.md` -- added ImagesSettings section