source_monitor 0.7.1 → 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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/sm-configure/SKILL.md +10 -1
  3. data/.claude/skills/sm-configure/reference/configuration-reference.md +44 -0
  4. data/.claude/skills/sm-host-setup/reference/initializer-template.md +17 -0
  5. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +2 -0
  6. data/.claude/skills/sm-job/reference/job-conventions.md +26 -0
  7. data/.claude/skills/sm-upgrade/reference/version-history.md +22 -0
  8. data/.gitignore +10 -0
  9. data/AGENTS.md +1 -1
  10. data/CHANGELOG.md +35 -0
  11. data/CLAUDE.md +11 -5
  12. data/Gemfile.lock +1 -1
  13. data/README.md +6 -4
  14. data/VERSION +1 -1
  15. data/app/assets/builds/source_monitor/application.css +43 -0
  16. data/app/assets/builds/source_monitor/application.js +127 -0
  17. data/app/assets/builds/source_monitor/application.js.map +3 -3
  18. data/app/assets/javascripts/source_monitor/application.js +2 -0
  19. data/app/assets/javascripts/source_monitor/controllers/notification_container_controller.js +138 -0
  20. data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +11 -0
  21. data/app/controllers/source_monitor/source_favicon_fetches_controller.rb +38 -0
  22. data/app/controllers/source_monitor/sources_controller.rb +11 -0
  23. data/app/helpers/source_monitor/application_helper.rb +51 -0
  24. data/app/jobs/source_monitor/favicon_fetch_job.rb +71 -0
  25. data/app/jobs/source_monitor/import_opml_job.rb +9 -0
  26. data/app/jobs/source_monitor/source_health_check_job.rb +10 -0
  27. data/app/models/source_monitor/source.rb +2 -0
  28. data/app/views/layouts/source_monitor/application.html.erb +23 -2
  29. data/app/views/source_monitor/shared/_toast.html.erb +1 -0
  30. data/app/views/source_monitor/sources/_details.html.erb +34 -5
  31. data/app/views/source_monitor/sources/_row.html.erb +11 -6
  32. data/config/routes.rb +1 -0
  33. data/docs/configuration.md +1 -1
  34. data/docs/upgrade.md +22 -0
  35. data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +15 -1
  36. data/lib/source_monitor/configuration/favicons_settings.rb +42 -0
  37. data/lib/source_monitor/configuration/http_settings.rb +1 -1
  38. data/lib/source_monitor/configuration/scraping_settings.rb +1 -1
  39. data/lib/source_monitor/configuration.rb +3 -1
  40. data/lib/source_monitor/favicons/discoverer.rb +196 -0
  41. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +21 -0
  42. data/lib/source_monitor/fetching/feed_fetcher.rb +1 -0
  43. data/lib/source_monitor/http.rb +5 -3
  44. data/lib/source_monitor/version.rb +1 -1
  45. data/lib/source_monitor.rb +4 -0
  46. data/source_monitor.gemspec +1 -1
  47. metadata +6 -106
  48. data/.vbw-planning/PROJECT.md +0 -51
  49. data/.vbw-planning/ROADMAP.md +0 -53
  50. data/.vbw-planning/SHIPPED.md +0 -63
  51. data/.vbw-planning/STATE.md +0 -27
  52. data/.vbw-planning/codebase/ARCHITECTURE.md +0 -147
  53. data/.vbw-planning/codebase/CONCERNS.md +0 -99
  54. data/.vbw-planning/codebase/CONVENTIONS.md +0 -97
  55. data/.vbw-planning/codebase/DEPENDENCIES.md +0 -100
  56. data/.vbw-planning/codebase/INDEX.md +0 -86
  57. data/.vbw-planning/codebase/META.md +0 -42
  58. data/.vbw-planning/codebase/PATTERNS.md +0 -262
  59. data/.vbw-planning/codebase/STACK.md +0 -101
  60. data/.vbw-planning/codebase/STRUCTURE.md +0 -324
  61. data/.vbw-planning/codebase/TESTING.md +0 -154
  62. data/.vbw-planning/config.json +0 -53
  63. data/.vbw-planning/discovery.json +0 -26
  64. data/.vbw-planning/milestones/default/ROADMAP.md +0 -115
  65. data/.vbw-planning/milestones/default/STATE.md +0 -82
  66. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +0 -56
  67. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +0 -187
  68. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +0 -64
  69. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +0 -137
  70. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +0 -67
  71. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +0 -142
  72. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +0 -64
  73. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +0 -138
  74. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +0 -85
  75. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +0 -147
  76. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +0 -63
  77. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +0 -129
  78. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +0 -74
  79. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +0 -154
  80. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +0 -303
  81. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +0 -510
  82. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +0 -61
  83. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +0 -161
  84. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +0 -66
  85. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +0 -132
  86. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +0 -59
  87. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +0 -171
  88. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +0 -56
  89. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +0 -152
  90. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +0 -33
  91. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +0 -42
  92. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +0 -119
  93. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +0 -52
  94. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +0 -195
  95. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +0 -79
  96. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +0 -130
  97. data/.vbw-planning/milestones/generator-enhancements/REQUIREMENTS.md +0 -72
  98. data/.vbw-planning/milestones/generator-enhancements/ROADMAP.md +0 -125
  99. data/.vbw-planning/milestones/generator-enhancements/SHIPPED.md +0 -40
  100. data/.vbw-planning/milestones/generator-enhancements/STATE.md +0 -43
  101. data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-CONTEXT.md +0 -33
  102. data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-VERIFICATION.md +0 -86
  103. data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01-SUMMARY.md +0 -61
  104. data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01.md +0 -380
  105. data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/02-VERIFICATION.md +0 -78
  106. data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01-SUMMARY.md +0 -46
  107. data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01.md +0 -500
  108. data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/03-VERIFICATION.md +0 -89
  109. data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01-SUMMARY.md +0 -48
  110. data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01.md +0 -456
  111. data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/04-VERIFICATION.md +0 -129
  112. data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +0 -70
  113. data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01.md +0 -747
  114. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/05-VERIFICATION.md +0 -156
  115. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01-SUMMARY.md +0 -69
  116. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01.md +0 -455
  117. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02-SUMMARY.md +0 -39
  118. data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02.md +0 -488
  119. data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/06-VERIFICATION.md +0 -100
  120. data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +0 -37
  121. data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01.md +0 -345
  122. data/.vbw-planning/milestones/upgrade-assurance/REQUIREMENTS.md +0 -80
  123. data/.vbw-planning/milestones/upgrade-assurance/ROADMAP.md +0 -75
  124. data/.vbw-planning/milestones/upgrade-assurance/STATE.md +0 -29
  125. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/01-VERIFICATION.md +0 -144
  126. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01-SUMMARY.md +0 -43
  127. data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01.md +0 -405
  128. data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01-SUMMARY.md +0 -27
  129. data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01.md +0 -303
  130. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/03-VERIFICATION.md +0 -380
  131. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01-SUMMARY.md +0 -36
  132. data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01.md +0 -652
  133. data/.vbw-planning/phases/01-aia-certificate-resolution/.context-dev.md +0 -17
  134. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01-SUMMARY.md +0 -26
  135. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01.md +0 -71
  136. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02-SUMMARY.md +0 -16
  137. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02.md +0 -56
  138. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03-SUMMARY.md +0 -17
  139. data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03.md +0 -98
  140. data/.vbw-planning/phases/02-test-performance/.context-dev.md +0 -75
  141. data/.vbw-planning/phases/02-test-performance/.context-lead.md +0 -89
  142. data/.vbw-planning/phases/02-test-performance/.context-qa.md +0 -23
  143. data/.vbw-planning/phases/02-test-performance/02-RESEARCH.md +0 -56
  144. data/.vbw-planning/phases/02-test-performance/02-VERIFICATION.md +0 -51
  145. data/.vbw-planning/phases/02-test-performance/PLAN-01-SUMMARY.md +0 -37
  146. data/.vbw-planning/phases/02-test-performance/PLAN-01.md +0 -156
  147. data/.vbw-planning/phases/02-test-performance/PLAN-02-SUMMARY.md +0 -33
  148. data/.vbw-planning/phases/02-test-performance/PLAN-02.md +0 -120
  149. data/.vbw-planning/phases/02-test-performance/PLAN-03-SUMMARY.md +0 -30
  150. data/.vbw-planning/phases/02-test-performance/PLAN-03.md +0 -154
  151. data/.vbw-planning/phases/02-test-performance/PLAN-04-SUMMARY.md +0 -28
  152. data/.vbw-planning/phases/02-test-performance/PLAN-04.md +0 -133
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 135499910675f0d424b88ec25e1503f3068fdbd78ffa553942438efd886b72bd
4
- data.tar.gz: a6b03ef217569a206d7521f2d8b42f3c300ab6ddf41e144cabaf24535f92b8dc
3
+ metadata.gz: 932dca7d7b8f754262dd37aac3cf722aee017ec53e662116dd97527ec2a8a1f3
4
+ data.tar.gz: 7d6ff568a5e3eb1cf5269736771474f19da9e82b18759232b03855735f284819
5
5
  SHA512:
6
- metadata.gz: b4e0ebe10a0211760f42d6a131cbf61eb09bf3f268006fbefe51636137c10ba0afc0413e2c39a46a8d3ff39037db07ba680a6f059599d7751b56a79d5deef1a3
7
- data.tar.gz: 7bdfa9ca0995bb91757be78271ca171a14ec83cc54aef8d13b083446e3c23a96d4568718b40a944c83c802173124823609a36c405a68b8a0c81bb138891aa65b
6
+ metadata.gz: 1318387b90d5811d52fc5d852a2be9578515a0d69f77baa90ecc1fbab930a3d2067a1e7056f8d2bd15940de2c0799e147203b31e4d91784e3078fb0fa8edc6e5
7
+ data.tar.gz: b35f81be14c230f1865642b5aa9f8fd462791c8fe3db293bf0f6326f2fc9fc1295eaef5f9899488d298e01e6eb3e6b316c00ed53595341a17973edd50cbb7b10
@@ -29,7 +29,7 @@ After the block executes, `ModelExtensions.reload!` runs automatically to apply
29
29
 
30
30
  ## Configuration Sections
31
31
 
32
- The `config` object (`SourceMonitor::Configuration`) has 11 sub-sections plus top-level queue/job settings:
32
+ The `config` object (`SourceMonitor::Configuration`) has 13 sub-sections plus top-level queue/job settings:
33
33
 
34
34
  | Section | Accessor | Class |
35
35
  |---|---|---|
@@ -45,6 +45,7 @@ The `config` object (`SourceMonitor::Configuration`) has 11 sub-sections plus to
45
45
  | Realtime | `config.realtime` | `RealtimeSettings` |
46
46
  | Authentication | `config.authentication` | `AuthenticationSettings` |
47
47
  | Images | `config.images` | `ImagesSettings` |
48
+ | Favicons | `config.favicons` | `FaviconsSettings` |
48
49
 
49
50
  See `reference/configuration-reference.md` for every setting with types, defaults, and examples.
50
51
 
@@ -91,6 +92,14 @@ config.models.source.include_concern "MyApp::SourceExtension"
91
92
  config.models.item.validate :custom_check
92
93
  ```
93
94
 
95
+ ### Favicons (Active Storage)
96
+ ```ruby
97
+ config.favicons.enabled = true
98
+ config.favicons.fetch_timeout = 10
99
+ config.favicons.max_download_size = 512 * 1024 # 512 KB
100
+ config.favicons.retry_cooldown_days = 14
101
+ ```
102
+
94
103
  ### Realtime
95
104
  ```ruby
96
105
  config.realtime.adapter = :redis
@@ -342,6 +342,50 @@ When enabled, `DownloadContentImagesJob` is automatically enqueued after new ite
342
342
 
343
343
  ---
344
344
 
345
+ ## Favicons Settings (`config.favicons`)
346
+
347
+ Class: `SourceMonitor::Configuration::FaviconsSettings`
348
+
349
+ Controls automatic favicon fetching and storage for sources via Active Storage.
350
+
351
+ **Prerequisite:** The host app must have Active Storage installed (`rails active_storage:install` + migrations). Without Active Storage, favicons are silently disabled and colored initials placeholders are shown instead.
352
+
353
+ | Setting | Type | Default | Description |
354
+ |---|---|---|---|
355
+ | `enabled` | Boolean | `true` | Enable automatic favicon fetching |
356
+ | `fetch_timeout` | Integer | `5` | HTTP timeout for favicon requests (seconds) |
357
+ | `max_download_size` | Integer | `1048576` (1 MB) | Maximum favicon file size in bytes; larger files are skipped |
358
+ | `retry_cooldown_days` | Integer | `7` | Days to wait before retrying a failed favicon fetch |
359
+ | `allowed_content_types` | Array | `["image/x-icon", "image/vnd.microsoft.icon", "image/png", "image/jpeg", "image/gif", "image/svg+xml", "image/webp"]` | Permitted MIME types for downloaded favicons |
360
+
361
+ ### Helper Method
362
+
363
+ | Method | Returns | Description |
364
+ |---|---|---|
365
+ | `enabled?` | Boolean | Returns `true` when `enabled` is truthy AND `ActiveStorage` is defined |
366
+
367
+ ```ruby
368
+ # Customize favicon settings
369
+ config.favicons.enabled = true
370
+ config.favicons.fetch_timeout = 10
371
+ config.favicons.max_download_size = 512 * 1024 # 512 KB
372
+ config.favicons.retry_cooldown_days = 14
373
+ config.favicons.allowed_content_types = %w[image/png image/x-icon image/svg+xml]
374
+ ```
375
+
376
+ When enabled, `FaviconFetchJob` is automatically enqueued:
377
+ 1. After a new source is created (via UI or OPML import) with a `website_url`
378
+ 2. After a successful feed fetch when the source has no favicon attached and is outside the retry cooldown
379
+
380
+ The job uses `Favicons::Discoverer` which tries three strategies in order:
381
+ 1. Direct `/favicon.ico` fetch from the source's domain
382
+ 2. HTML page parsing for `<link rel="icon">`, `<link rel="apple-touch-icon">`, and similar tags (prefers largest by `sizes` attribute)
383
+ 3. Google Favicon API as a last resort
384
+
385
+ Failed attempts are tracked in the source's `metadata` JSONB column (`favicon_last_attempted_at`) to respect the cooldown period.
386
+
387
+ ---
388
+
345
389
  ## Environment Variables
346
390
 
347
391
  | Variable | Purpose |
@@ -176,6 +176,23 @@ SourceMonitor.configure do |config|
176
176
  # record.errors.add(:base, "custom error") unless record.valid_for_my_app?
177
177
  # }
178
178
 
179
+ # ===========================================================================
180
+ # Favicons (Active Storage)
181
+ # ===========================================================================
182
+ # Automatically fetch and store source favicons via Active Storage.
183
+ # Requires Active Storage in the host app (rails active_storage:install).
184
+ # Without Active Storage, favicons are silently disabled -- colored
185
+ # initials placeholders are shown instead.
186
+
187
+ # config.favicons.enabled = true # default: true
188
+ # config.favicons.fetch_timeout = 5 # seconds
189
+ # config.favicons.max_download_size = 1_048_576 # 1 MB
190
+ # config.favicons.retry_cooldown_days = 7
191
+ # config.favicons.allowed_content_types = %w[
192
+ # image/x-icon image/vnd.microsoft.icon image/png
193
+ # image/jpeg image/gif image/svg+xml image/webp
194
+ # ]
195
+
179
196
  # ===========================================================================
180
197
  # Realtime (Action Cable) Adapter
181
198
  # ===========================================================================
@@ -155,6 +155,8 @@ bin/source_monitor verify
155
155
  - [ ] Event callbacks wired for host integration
156
156
  - [ ] Realtime adapter confirmed (Solid Cable or Redis)
157
157
  - [ ] Mission Control integration enabled (if desired)
158
+ - [ ] Active Storage installed (required for favicons and image downloads)
159
+ - [ ] Favicon settings configured (`config.favicons.*`) if customization needed
158
160
 
159
161
  ## Troubleshooting
160
162
 
@@ -164,6 +164,32 @@ class ScheduleFetchesJob < ApplicationJob
164
164
  end
165
165
  ```
166
166
 
167
+ ### Lightweight Fetch Job (FaviconFetchJob)
168
+
169
+ Demonstrates multi-strategy cascade with guard clauses:
170
+
171
+ ```ruby
172
+ class FaviconFetchJob < ApplicationJob
173
+ source_monitor_queue :fetch
174
+ discard_on ActiveJob::DeserializationError
175
+
176
+ def perform(source_id)
177
+ source = Source.find_by(id: source_id)
178
+ return unless source
179
+ return unless should_fetch?(source)
180
+
181
+ result = Favicons::Discoverer.new(source: source).call
182
+ attach_favicon(source, result) if result.success?
183
+ end
184
+ end
185
+ ```
186
+
187
+ Notable patterns:
188
+ - Multiple guard clauses: source exists, Active Storage defined, no existing favicon, outside cooldown period
189
+ - Uses `Favicons::Discoverer` service with 3-strategy cascade (direct `/favicon.ico`, HTML parsing, Google API)
190
+ - Failed attempts tracked in source `metadata` JSONB (`favicon_last_attempted_at`) for retry cooldown
191
+ - Graceful degradation: host apps without Active Storage never enqueue this job
192
+
167
193
  ### Broadcast Job (SourceHealthCheckJob)
168
194
 
169
195
  Demonstrates result broadcasting:
@@ -2,6 +2,28 @@
2
2
 
3
3
  Version-specific migration notes for each major/minor version transition. Agents should reference this file when guiding users through multi-version upgrades.
4
4
 
5
+ ## 0.7.x to 0.8.0
6
+
7
+ **Key changes:**
8
+ - Default HTTP User-Agent changed from `SourceMonitor/<version>` to `Mozilla/5.0 (compatible; SourceMonitor/<version>)` with browser-like headers (Accept-Language, DNT, Referer). Prevents bot-blocking by feed servers.
9
+ - Default `max_in_flight_per_source` changed from `25` to `nil` (unlimited). If you relied on the previous default for per-source rate limiting, set it explicitly.
10
+ - Successful manual health checks on degraded sources now trigger a feed fetch for faster recovery.
11
+ - Automatic source favicons via Active Storage with multi-strategy discovery (direct `/favicon.ico`, HTML `<link>` parsing, Google Favicon API fallback)
12
+ - New configuration section: `config.favicons` with `enabled`, `fetch_timeout`, `max_download_size`, `retry_cooldown_days`, and `allowed_content_types` settings
13
+ - Colored initials placeholder shown when no favicon is available or Active Storage is not installed
14
+ - OPML imports trigger favicon fetches for each imported source with a `website_url`
15
+ - Toast notifications capped at 3 visible with "+N more" badge, click-to-expand, and "Clear all" button
16
+ - Error-level toasts auto-dismiss after 10 seconds (vs 5 seconds for info/success)
17
+
18
+ **Action items:**
19
+ 1. Re-run `bin/rails source_monitor:upgrade` to get updated initializer template
20
+ 2. If you explicitly set `config.http.user_agent`, your value is preserved. Otherwise the new browser-like default applies automatically.
21
+ 3. If you need per-source scrape rate limiting, add `config.scraping.max_in_flight_per_source = 25` (or your preferred value) to your initializer
22
+ 4. If using Active Storage, favicons are enabled by default -- no action needed
23
+ 5. If NOT using Active Storage, favicons are silently disabled -- no action needed
24
+ 6. Toast stacking is automatic -- no configuration needed
25
+ 7. No breaking changes -- all existing configuration remains valid
26
+
5
27
  ## 0.3.x to 0.4.0
6
28
 
7
29
  **Released:** 2026-02-12
data/.gitignore CHANGED
@@ -27,5 +27,15 @@
27
27
  .vbw-planning/.active-agent
28
28
  .vbw-planning/.active-agent-count
29
29
  .vbw-planning/.todo-flat-migrated
30
+ .vbw-planning/.agent-worktrees/
31
+ .vbw-planning/.cache/
32
+ .vbw-planning/.context-usage
33
+ .vbw-planning/.contracts/
34
+ .vbw-planning/.events/
35
+ .vbw-planning/.execution-state.json
30
36
  /codebase_analysis.md
37
+ /VERIFICATION.md
38
+ /test/dummy/public/assets/
39
+ /test/lib/tmp/
31
40
  *.gem
41
+ .vbw-worktrees/
data/AGENTS.md CHANGED
@@ -83,7 +83,7 @@ Store secrets (API keys, webhook tokens) in `config/credentials/` and never comm
83
83
 
84
84
  ## Claude Code Skills
85
85
 
86
- SourceMonitor ships 14 engine-specific Claude Code skills (`sm-*` prefix) covering the domain model, configuration DSL, pipeline stages, testing conventions, and more. Skills are distributed with the gem and installed into `.claude/skills/` via rake tasks:
86
+ SourceMonitor ships 15 engine-specific Claude Code skills (`sm-*` prefix) covering the domain model, configuration DSL, pipeline stages, testing conventions, and more. Skills are distributed with the gem and installed into `.claude/skills/` via rake tasks:
87
87
 
88
88
  ```bash
89
89
  bin/rails source_monitor:skills:install # Consumer skills (host app integration)
data/CHANGELOG.md CHANGED
@@ -15,6 +15,41 @@ All notable changes to this project are documented below. The format follows [Ke
15
15
 
16
16
  - No unreleased changes yet.
17
17
 
18
+ ## [0.8.0] - 2026-02-21
19
+
20
+ ### Added
21
+
22
+ - **Automatic source favicons.** Sources now display favicons next to their names in list and detail views. Favicons are fetched automatically via background job on source creation and successful feed fetches using a multi-strategy cascade: `/favicon.ico` direct fetch, HTML `<link>` tag parsing (preferring largest available), and Google Favicon API fallback. Requires Active Storage in the host app.
23
+ - New configuration section: `config.favicons` with `enabled` (default: `true`), `fetch_timeout` (5s), `max_download_size` (1MB), `retry_cooldown_days` (7), and `allowed_content_types` settings.
24
+ - Colored initials placeholder shown when no favicon is available (consistent HSL color derived from source name).
25
+ - Graceful degradation: host apps without Active Storage see placeholders only, no errors.
26
+ - OPML imports also trigger favicon fetches for each imported source with a `website_url`.
27
+ - Manual "Fetch Favicon" button on source detail pages; favicon fetch also triggered on 304 Not Modified responses when missing.
28
+ - Redirect-following in favicon discoverer for domains that redirect (e.g., `reddit.com` -> `www.reddit.com`).
29
+ - **Toast notification stacking.** Bulk operations no longer flood the screen with overlapping toasts. At most 3 toasts are visible at a time; overflow is shown as a "+N more" badge that expands the full stack on click. "Clear all" button dismisses every toast at once.
30
+ - Error-level toasts persist for 10 seconds (vs 5 seconds for info/success).
31
+ - Hidden toasts promote into visible slots as earlier toasts auto-dismiss.
32
+ - Container controller tracks DOM changes via MutationObserver and properly cleans up event listeners on disconnect.
33
+
34
+ ### Changed
35
+
36
+ - **Browser-like default User-Agent.** Default HTTP User-Agent changed from `SourceMonitor/<version>` to `Mozilla/5.0 (compatible; SourceMonitor/<version>)` with full browser-like headers (Accept, Accept-Language, DNT, Referer from source `website_url`). This prevents bot-blocking by feed servers.
37
+ - **Smarter scrape rate limiting.** Default `max_in_flight_per_source` changed from `25` to `nil` (unlimited). The previous default unnecessarily throttled scraping for sources with many items. Set an explicit value in your initializer if you need per-source caps.
38
+ - **Health check triggers status re-evaluation.** A successful manual health check on a degraded (declining/critical/warning) source now triggers a feed fetch, allowing the health monitor to transition the source back to "improving" status instead of requiring the source to recover on its own schedule.
39
+
40
+ ### Fixed
41
+
42
+ - Favicon discoverer properly follows HTTP redirects (e.g., `reddit.com` -> `www.reddit.com`).
43
+ - Favicon fetch uses `rails_blob_path` for correct routing within the engine context.
44
+ - Favicon display prefers PNG format (via Google Favicon API) over raw ICO for better browser compatibility.
45
+ - Gemspec excludes `.vbw-planning/` from gem package to reduce gem size.
46
+
47
+ ### Testing
48
+
49
+ - 1,125 tests, 0 failures.
50
+ - RuboCop: 0 offenses.
51
+ - Brakeman: 0 warnings.
52
+
18
53
  ## [0.7.1] - 2026-02-18
19
54
 
20
55
  ### Changed
data/CLAUDE.md CHANGED
@@ -4,17 +4,17 @@
4
4
 
5
5
  ## Active Context
6
6
 
7
- **Milestone:** (none active)
8
- **Last shipped:** upgrade-assurance (2026-02-13) -- 3 phases, 14 tasks, 12 commits
9
- **Previous:** generator-enhancements (2026-02-12) -- v0.4.0
10
- **Next action:** /vbw:vibe to start a new milestone
7
+ **Milestone:** polish-and-reliability
8
+ **Phase:** 1 -- Backend Fixes (pending planning)
9
+ **Last shipped:** aia-ssl-fix (2026-02-20) -- 2 phases, 7 plans, 8 commits
10
+ **Previous:** upgrade-assurance (2026-02-13), generator-enhancements (2026-02-12)
11
11
 
12
12
  ## Key Decisions
13
13
 
14
14
  - Keep PostgreSQL-only for now
15
15
  - Keep host-app auth model
16
16
  - Ruby autoload for lib/ modules (not Zeitwerk)
17
- - PG parallel fork segfault when running single test files; use PARALLEL_WORKERS=1 or full suite
17
+ - PG parallel fork segfault resolved: switched to thread-based parallelism in aia-ssl-fix milestone
18
18
 
19
19
  ## Installed Skills
20
20
 
@@ -100,6 +100,12 @@ Run /vbw:help for all commands.
100
100
  - No N+1 queries (use `includes`/`preload`).
101
101
  - No hardcoded credentials (use Rails credentials or ENV).
102
102
 
103
+ ## QA and UAT Rules
104
+
105
+ - **Browser-first verification:** During VBW QA (`/vbw:qa`) and UAT (`/vbw:verify`), ALWAYS start by using `agent-browser` to test UI scenarios yourself before presenting checkpoints to the user. Navigate to the dummy app (port 3002), take snapshots/screenshots, and verify visual and functional behavior with agents first.
106
+ - **Automate what you can:** Any test that can be verified programmatically (config defaults, job enqueue behavior, controller responses) should be automated -- only present truly visual/interactive tests to the user.
107
+ - **Dummy app port:** The SourceMonitor dummy app runs on port 3002 (`cd test/dummy && bin/rails server -p 3002`).
108
+
103
109
  ## Security Rules
104
110
 
105
111
  ### Protected Files (NEVER read or output)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- source_monitor (0.7.1)
4
+ source_monitor (0.8.0)
5
5
  cssbundling-rails (~> 1.4)
6
6
  faraday (~> 2.9)
7
7
  faraday-follow_redirects (~> 0.4)
data/README.md CHANGED
@@ -9,8 +9,8 @@ SourceMonitor is a production-ready Rails 8 mountable engine for ingesting, norm
9
9
  In your host Rails app:
10
10
 
11
11
  ```bash
12
- bundle add source_monitor --version "~> 0.3.1"
13
- # or add `gem "source_monitor", "~> 0.3.1"` manually, then run:
12
+ bundle add source_monitor --version "~> 0.7.1"
13
+ # or add `gem "source_monitor", "~> 0.7.1"` manually, then run:
14
14
  bundle install
15
15
  ```
16
16
 
@@ -19,7 +19,9 @@ This exposes `bin/source_monitor` (via Bundler binstubs) so you can run the guid
19
19
  ## Highlights
20
20
  - Full-featured source and item administration backed by Turbo Streams and Tailwind UI components
21
21
  - Adaptive fetch pipeline (Feedjira + Faraday) with conditional GETs, retention pruning, and scrape orchestration
22
+ - Automatic source favicons via Active Storage with multi-strategy discovery and graceful fallback
22
23
  - Realtime dashboard metrics, batching/caching query layer, and Mission Control integration hooks
24
+ - Smart toast notification stacking (max 3 visible, "+N more" overflow badge, click-to-expand)
23
25
  - Extensible scraper adapters (Readability included) with per-source settings and structured result metadata
24
26
  - Declarative configuration DSL covering queues, HTTP, retention, events, model extensions, authentication, and realtime transports
25
27
  - First-class observability through ActiveSupport notifications and `SourceMonitor::Metrics` counters/gauges
@@ -41,7 +43,7 @@ This exposes `bin/source_monitor` (via Bundler binstubs) so you can run the guid
41
43
  Before running any SourceMonitor commands inside your host app, add the gem and install dependencies:
42
44
 
43
45
  ```bash
44
- bundle add source_monitor --version "~> 0.3.1"
46
+ bundle add source_monitor --version "~> 0.7.1"
45
47
  # or edit your Gemfile, then run
46
48
  bundle install
47
49
  ```
@@ -113,7 +115,7 @@ See [docs/configuration.md](docs/configuration.md) for exhaustive coverage and e
113
115
 
114
116
  ## Claude Code Skills
115
117
 
116
- SourceMonitor ships 14 engine-specific Claude Code skills (`sm-*` prefix) that give AI agents deep context about the engine's domain model, configuration DSL, pipeline stages, and testing conventions. Skills are bundled with the gem and installed into your host app's `.claude/skills/` directory.
118
+ SourceMonitor ships 15 engine-specific Claude Code skills (`sm-*` prefix) that give AI agents deep context about the engine's domain model, configuration DSL, pipeline stages, and testing conventions. Skills are bundled with the gem and installed into your host app's `.claude/skills/` directory.
117
119
 
118
120
  ```bash
119
121
  bin/rails source_monitor:skills:install # Consumer skills (host app integration)
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.1
1
+ 0.8.0
@@ -651,6 +651,14 @@ video {
651
651
  right: 0px;
652
652
  }
653
653
 
654
+ .fm-admin .-bottom-1 {
655
+ bottom: -0.25rem;
656
+ }
657
+
658
+ .fm-admin .-right-1 {
659
+ right: -0.25rem;
660
+ }
661
+
654
662
  .fm-admin .left-4 {
655
663
  left: 1rem;
656
664
  }
@@ -957,6 +965,10 @@ video {
957
965
  align-items: flex-start;
958
966
  }
959
967
 
968
+ .fm-admin .items-end {
969
+ align-items: flex-end;
970
+ }
971
+
960
972
  .fm-admin .items-center {
961
973
  align-items: center;
962
974
  }
@@ -977,6 +989,10 @@ video {
977
989
  gap: 0.25rem;
978
990
  }
979
991
 
992
+ .fm-admin .gap-1\.5 {
993
+ gap: 0.375rem;
994
+ }
995
+
980
996
  .fm-admin .gap-2 {
981
997
  gap: 0.5rem;
982
998
  }
@@ -1348,6 +1364,11 @@ video {
1348
1364
  background-color: rgb(248 250 252 / var(--tw-bg-opacity, 1));
1349
1365
  }
1350
1366
 
1367
+ .fm-admin .bg-slate-700 {
1368
+ --tw-bg-opacity: 1;
1369
+ background-color: rgb(51 65 85 / var(--tw-bg-opacity, 1));
1370
+ }
1371
+
1351
1372
  .fm-admin .bg-slate-800 {
1352
1373
  --tw-bg-opacity: 1;
1353
1374
  background-color: rgb(30 41 59 / var(--tw-bg-opacity, 1));
@@ -1367,6 +1388,15 @@ video {
1367
1388
  background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
1368
1389
  }
1369
1390
 
1391
+ .fm-admin .object-contain {
1392
+ -o-object-fit: contain;
1393
+ object-fit: contain;
1394
+ }
1395
+
1396
+ .fm-admin .p-0\.5 {
1397
+ padding: 0.125rem;
1398
+ }
1399
+
1370
1400
  .fm-admin .p-3 {
1371
1401
  padding: 0.75rem;
1372
1402
  }
@@ -1726,6 +1756,10 @@ video {
1726
1756
  color: rgb(255 255 255 / var(--tw-text-opacity, 1));
1727
1757
  }
1728
1758
 
1759
+ .fm-admin .underline {
1760
+ text-decoration-line: underline;
1761
+ }
1762
+
1729
1763
  .fm-admin .opacity-0 {
1730
1764
  opacity: 0;
1731
1765
  }
@@ -1848,6 +1882,11 @@ video {
1848
1882
  background-color: rgb(248 250 252 / var(--tw-bg-opacity, 1));
1849
1883
  }
1850
1884
 
1885
+ .fm-admin .hover\:bg-slate-600:hover {
1886
+ --tw-bg-opacity: 1;
1887
+ background-color: rgb(71 85 105 / var(--tw-bg-opacity, 1));
1888
+ }
1889
+
1851
1890
  .fm-admin .hover\:bg-slate-700:hover {
1852
1891
  --tw-bg-opacity: 1;
1853
1892
  background-color: rgb(51 65 85 / var(--tw-bg-opacity, 1));
@@ -1962,6 +2001,10 @@ video {
1962
2001
  background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));
1963
2002
  }
1964
2003
 
2004
+ .fm-admin :is(.group:hover .group-hover\:inline-flex) {
2005
+ display: inline-flex;
2006
+ }
2007
+
1965
2008
  .fm-admin :is(.peer:checked ~ .peer-checked\:border-blue-500) {
1966
2009
  --tw-border-opacity: 1;
1967
2010
  border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
@@ -2488,6 +2488,7 @@ var notification_controller_default = class extends Controller {
2488
2488
  }
2489
2489
  this.clearTimeout();
2490
2490
  this.registerController();
2491
+ this.applyLevelDelay();
2491
2492
  this.startTimer();
2492
2493
  }
2493
2494
  disconnect() {
@@ -2505,8 +2506,17 @@ var notification_controller_default = class extends Controller {
2505
2506
  if (this.delayValue <= 0) return;
2506
2507
  this.timeoutId = window.setTimeout(() => this.dismiss(), this.delayValue);
2507
2508
  }
2509
+ applyLevelDelay() {
2510
+ const level = this.element.dataset.level;
2511
+ if (level === "error" && this.delayValue === 5e3) {
2512
+ this.delayValue = 1e4;
2513
+ }
2514
+ }
2508
2515
  dismiss() {
2509
2516
  if (!this.element) return;
2517
+ this.element.dispatchEvent(
2518
+ new CustomEvent("notification:dismissed", { bubbles: true })
2519
+ );
2510
2520
  this.element.classList.add("opacity-0", "translate-y-2");
2511
2521
  window.setTimeout(() => {
2512
2522
  if (this.element && this.element.remove) {
@@ -2521,6 +2531,122 @@ var notification_controller_default = class extends Controller {
2521
2531
  }
2522
2532
  };
2523
2533
 
2534
+ // app/assets/javascripts/source_monitor/controllers/notification_container_controller.js
2535
+ var notification_container_controller_default = class extends Controller {
2536
+ static values = {
2537
+ maxVisible: { default: 3, type: Number },
2538
+ expanded: { default: false, type: Boolean }
2539
+ };
2540
+ static targets = ["list", "badge", "badgeCount", "clearAll"];
2541
+ connect() {
2542
+ this.rafId = null;
2543
+ this.boundHandleClickOutside = this.handleClickOutside.bind(this);
2544
+ this.boundScheduleRecalculate = () => this.scheduleRecalculate();
2545
+ this.observer = new MutationObserver(this.boundScheduleRecalculate);
2546
+ this.observer.observe(this.listTarget, { childList: true });
2547
+ this.listTarget.addEventListener(
2548
+ "notification:dismissed",
2549
+ this.boundScheduleRecalculate
2550
+ );
2551
+ this.recalculateVisibility();
2552
+ }
2553
+ disconnect() {
2554
+ if (this.observer) {
2555
+ this.observer.disconnect();
2556
+ this.observer = null;
2557
+ }
2558
+ if (this.rafId) {
2559
+ cancelAnimationFrame(this.rafId);
2560
+ this.rafId = null;
2561
+ }
2562
+ this.listTarget.removeEventListener(
2563
+ "notification:dismissed",
2564
+ this.boundScheduleRecalculate
2565
+ );
2566
+ document.removeEventListener("click", this.boundHandleClickOutside);
2567
+ }
2568
+ scheduleRecalculate() {
2569
+ if (this.rafId) {
2570
+ cancelAnimationFrame(this.rafId);
2571
+ }
2572
+ this.rafId = requestAnimationFrame(() => {
2573
+ this.rafId = null;
2574
+ this.recalculateVisibility();
2575
+ });
2576
+ }
2577
+ recalculateVisibility() {
2578
+ const toasts = Array.from(this.listTarget.children);
2579
+ const total = toasts.length;
2580
+ if (this.expandedValue) {
2581
+ toasts.forEach((toast) => {
2582
+ toast.classList.remove("hidden");
2583
+ toast.removeAttribute("aria-hidden");
2584
+ toast.removeAttribute("inert");
2585
+ });
2586
+ } else {
2587
+ toasts.forEach((toast, index) => {
2588
+ if (index < this.maxVisibleValue) {
2589
+ toast.classList.remove("hidden");
2590
+ toast.removeAttribute("aria-hidden");
2591
+ toast.removeAttribute("inert");
2592
+ } else {
2593
+ toast.classList.add("hidden");
2594
+ toast.setAttribute("aria-hidden", "true");
2595
+ toast.setAttribute("inert", "");
2596
+ }
2597
+ });
2598
+ }
2599
+ const hiddenCount = this.expandedValue ? 0 : Math.max(0, total - this.maxVisibleValue);
2600
+ if (this.hasBadgeTarget) {
2601
+ if (this.hasBadgeCountTarget) {
2602
+ this.badgeCountTarget.textContent = `+${hiddenCount} more`;
2603
+ }
2604
+ if (hiddenCount > 0) {
2605
+ this.badgeTarget.classList.remove("hidden");
2606
+ } else {
2607
+ this.badgeTarget.classList.add("hidden");
2608
+ }
2609
+ }
2610
+ if (this.hasClearAllTarget) {
2611
+ const showClearAll = total > 0 && (hiddenCount > 0 || this.expandedValue);
2612
+ if (showClearAll) {
2613
+ this.clearAllTarget.classList.remove("hidden");
2614
+ } else {
2615
+ this.clearAllTarget.classList.add("hidden");
2616
+ }
2617
+ }
2618
+ }
2619
+ toggleExpand(event) {
2620
+ event.preventDefault();
2621
+ if (this.expandedValue) {
2622
+ this.collapseStack();
2623
+ } else {
2624
+ this.expandStack();
2625
+ }
2626
+ }
2627
+ expandStack() {
2628
+ this.expandedValue = true;
2629
+ this.recalculateVisibility();
2630
+ document.addEventListener("click", this.boundHandleClickOutside);
2631
+ }
2632
+ collapseStack() {
2633
+ this.expandedValue = false;
2634
+ document.removeEventListener("click", this.boundHandleClickOutside);
2635
+ this.recalculateVisibility();
2636
+ }
2637
+ handleClickOutside(event) {
2638
+ if (!this.element.contains(event.target)) {
2639
+ this.collapseStack();
2640
+ }
2641
+ }
2642
+ clearAll(event) {
2643
+ event.preventDefault();
2644
+ const toasts = Array.from(this.listTarget.children);
2645
+ toasts.forEach((toast) => toast.remove());
2646
+ this.collapseStack();
2647
+ }
2648
+ };
2649
+
2524
2650
  // app/assets/javascripts/source_monitor/controllers/dropdown_controller.js
2525
2651
  var dropdown_controller_default = class extends Controller {
2526
2652
  static targets = ["menu"];
@@ -2746,6 +2872,7 @@ if (!existingApplication) {
2746
2872
  window.SourceMonitorStimulus = application;
2747
2873
  }
2748
2874
  application.register("notification", notification_controller_default);
2875
+ application.register("notification-container", notification_container_controller_default);
2749
2876
  application.register("async-submit", async_submit_controller_default);
2750
2877
  application.register("dropdown", dropdown_controller_default);
2751
2878
  application.register("modal", modal_controller_default);