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-01.md
DELETED
|
@@ -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
|