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.
- checksums.yaml +4 -4
- 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 +35 -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/source_monitor.gemspec +1 -1
- metadata +6 -106
- data/.vbw-planning/PROJECT.md +0 -51
- data/.vbw-planning/ROADMAP.md +0 -53
- 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/phases/02-test-performance/.context-dev.md +0 -75
- data/.vbw-planning/phases/02-test-performance/.context-lead.md +0 -89
- data/.vbw-planning/phases/02-test-performance/.context-qa.md +0 -23
- data/.vbw-planning/phases/02-test-performance/02-RESEARCH.md +0 -56
- data/.vbw-planning/phases/02-test-performance/02-VERIFICATION.md +0 -51
- data/.vbw-planning/phases/02-test-performance/PLAN-01-SUMMARY.md +0 -37
- data/.vbw-planning/phases/02-test-performance/PLAN-01.md +0 -156
- data/.vbw-planning/phases/02-test-performance/PLAN-02-SUMMARY.md +0 -33
- data/.vbw-planning/phases/02-test-performance/PLAN-02.md +0 -120
- data/.vbw-planning/phases/02-test-performance/PLAN-03-SUMMARY.md +0 -30
- data/.vbw-planning/phases/02-test-performance/PLAN-03.md +0 -154
- data/.vbw-planning/phases/02-test-performance/PLAN-04-SUMMARY.md +0 -28
- data/.vbw-planning/phases/02-test-performance/PLAN-04.md +0 -133
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SourceMonitor
|
|
4
|
+
class Configuration
|
|
5
|
+
class FaviconsSettings
|
|
6
|
+
attr_accessor :enabled,
|
|
7
|
+
:fetch_timeout,
|
|
8
|
+
:max_download_size,
|
|
9
|
+
:retry_cooldown_days,
|
|
10
|
+
:allowed_content_types
|
|
11
|
+
|
|
12
|
+
DEFAULT_FETCH_TIMEOUT = 5 # seconds
|
|
13
|
+
DEFAULT_MAX_DOWNLOAD_SIZE = 1 * 1024 * 1024 # 1 MB
|
|
14
|
+
DEFAULT_RETRY_COOLDOWN_DAYS = 7
|
|
15
|
+
DEFAULT_ALLOWED_CONTENT_TYPES = %w[
|
|
16
|
+
image/x-icon
|
|
17
|
+
image/vnd.microsoft.icon
|
|
18
|
+
image/png
|
|
19
|
+
image/jpeg
|
|
20
|
+
image/gif
|
|
21
|
+
image/svg+xml
|
|
22
|
+
image/webp
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
reset!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset!
|
|
30
|
+
@enabled = true
|
|
31
|
+
@fetch_timeout = DEFAULT_FETCH_TIMEOUT
|
|
32
|
+
@max_download_size = DEFAULT_MAX_DOWNLOAD_SIZE
|
|
33
|
+
@retry_cooldown_days = DEFAULT_RETRY_COOLDOWN_DAYS
|
|
34
|
+
@allowed_content_types = DEFAULT_ALLOWED_CONTENT_TYPES.dup
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def enabled?
|
|
38
|
+
!!enabled && !!defined?(ActiveStorage)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -9,6 +9,7 @@ require "source_monitor/configuration/realtime_settings"
|
|
|
9
9
|
require "source_monitor/configuration/retention_settings"
|
|
10
10
|
require "source_monitor/configuration/authentication_settings"
|
|
11
11
|
require "source_monitor/configuration/images_settings"
|
|
12
|
+
require "source_monitor/configuration/favicons_settings"
|
|
12
13
|
require "source_monitor/configuration/scraper_registry"
|
|
13
14
|
require "source_monitor/configuration/events"
|
|
14
15
|
require "source_monitor/configuration/validation_definition"
|
|
@@ -28,7 +29,7 @@ module SourceMonitor
|
|
|
28
29
|
:mission_control_enabled,
|
|
29
30
|
:mission_control_dashboard_path
|
|
30
31
|
|
|
31
|
-
attr_reader :http, :scrapers, :retention, :events, :models, :realtime, :fetching, :health, :authentication, :scraping, :images
|
|
32
|
+
attr_reader :http, :scrapers, :retention, :events, :models, :realtime, :fetching, :health, :authentication, :scraping, :images, :favicons
|
|
32
33
|
|
|
33
34
|
DEFAULT_QUEUE_NAMESPACE = "source_monitor"
|
|
34
35
|
|
|
@@ -53,6 +54,7 @@ module SourceMonitor
|
|
|
53
54
|
@authentication = AuthenticationSettings.new
|
|
54
55
|
@scraping = ScrapingSettings.new
|
|
55
56
|
@images = ImagesSettings.new
|
|
57
|
+
@favicons = FaviconsSettings.new
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def queue_name_for(role)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/follow_redirects"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "nokogiri"
|
|
7
|
+
|
|
8
|
+
module SourceMonitor
|
|
9
|
+
module Favicons
|
|
10
|
+
class Discoverer
|
|
11
|
+
Result = Struct.new(:io, :filename, :content_type, :url, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
attr_reader :website_url, :settings
|
|
14
|
+
|
|
15
|
+
def initialize(website_url, settings: nil)
|
|
16
|
+
@website_url = website_url
|
|
17
|
+
@settings = settings || SourceMonitor.config.favicons
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
return if website_url.blank?
|
|
22
|
+
|
|
23
|
+
try_html_link_tags || try_google_favicon_api || try_favicon_ico
|
|
24
|
+
rescue Faraday::Error, URI::InvalidURIError, Timeout::Error
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def try_favicon_ico
|
|
31
|
+
uri = URI.parse(website_url)
|
|
32
|
+
favicon_url = "#{uri.scheme}://#{uri.host}/favicon.ico"
|
|
33
|
+
download_favicon(favicon_url)
|
|
34
|
+
rescue URI::InvalidURIError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def try_html_link_tags
|
|
39
|
+
response = html_client.get(website_url)
|
|
40
|
+
return unless response.status == 200
|
|
41
|
+
|
|
42
|
+
doc = Nokogiri::HTML(response.body)
|
|
43
|
+
candidates = extract_icon_candidates(doc)
|
|
44
|
+
return if candidates.empty?
|
|
45
|
+
|
|
46
|
+
candidates.each do |candidate_url|
|
|
47
|
+
result = download_favicon(candidate_url)
|
|
48
|
+
return result if result
|
|
49
|
+
end
|
|
50
|
+
nil
|
|
51
|
+
rescue Faraday::Error, Nokogiri::SyntaxError
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def try_google_favicon_api
|
|
56
|
+
uri = URI.parse(website_url)
|
|
57
|
+
api_url = "https://www.google.com/s2/favicons?domain=#{uri.host}&sz=64"
|
|
58
|
+
download_favicon(api_url)
|
|
59
|
+
rescue URI::InvalidURIError
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def extract_icon_candidates(doc)
|
|
64
|
+
candidates = []
|
|
65
|
+
|
|
66
|
+
# Search link[rel] tags for icon types
|
|
67
|
+
icon_selectors = [
|
|
68
|
+
'link[rel*="icon"]',
|
|
69
|
+
'link[rel="apple-touch-icon"]',
|
|
70
|
+
'link[rel="apple-touch-icon-precomposed"]',
|
|
71
|
+
'link[rel="mask-icon"]'
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
icon_selectors.each do |selector|
|
|
75
|
+
doc.css(selector).each do |link|
|
|
76
|
+
href = link["href"]
|
|
77
|
+
next if href.blank?
|
|
78
|
+
|
|
79
|
+
absolute_url = resolve_url(href)
|
|
80
|
+
next unless absolute_url
|
|
81
|
+
|
|
82
|
+
sizes = parse_sizes(link["sizes"])
|
|
83
|
+
candidates << { url: absolute_url, size: sizes }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Search meta tags for msapplication-TileImage
|
|
88
|
+
doc.css('meta[name="msapplication-TileImage"]').each do |meta|
|
|
89
|
+
content = meta["content"]
|
|
90
|
+
next if content.blank?
|
|
91
|
+
|
|
92
|
+
absolute_url = resolve_url(content)
|
|
93
|
+
candidates << { url: absolute_url, size: 0 } if absolute_url
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# og:image as last resort
|
|
97
|
+
doc.css('meta[property="og:image"]').each do |meta|
|
|
98
|
+
content = meta["content"]
|
|
99
|
+
next if content.blank?
|
|
100
|
+
|
|
101
|
+
absolute_url = resolve_url(content)
|
|
102
|
+
candidates << { url: absolute_url, size: -1 } if absolute_url
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Sort by size descending (prefer larger), deduplicate by URL
|
|
106
|
+
candidates
|
|
107
|
+
.sort_by { |c| -(c[:size] || 0) }
|
|
108
|
+
.uniq { |c| c[:url] }
|
|
109
|
+
.map { |c| c[:url] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def parse_sizes(sizes_attr)
|
|
113
|
+
return 0 if sizes_attr.blank?
|
|
114
|
+
return 0 if sizes_attr.casecmp("any").zero?
|
|
115
|
+
|
|
116
|
+
# Parse "32x32", "256x256", etc. -- take the max dimension
|
|
117
|
+
match = sizes_attr.match(/(\d+)x(\d+)/i)
|
|
118
|
+
return 0 unless match
|
|
119
|
+
|
|
120
|
+
[ match[1].to_i, match[2].to_i ].max
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def resolve_url(href)
|
|
124
|
+
return nil if href.blank?
|
|
125
|
+
|
|
126
|
+
uri = URI.parse(href)
|
|
127
|
+
if uri.absolute?
|
|
128
|
+
href
|
|
129
|
+
else
|
|
130
|
+
URI.join(website_url, href).to_s
|
|
131
|
+
end
|
|
132
|
+
rescue URI::InvalidURIError, URI::BadURIError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def download_favicon(url)
|
|
137
|
+
response = image_client.get(url)
|
|
138
|
+
return unless response.status == 200
|
|
139
|
+
|
|
140
|
+
content_type = response.headers["content-type"]&.split(";")&.first&.strip&.downcase
|
|
141
|
+
return unless content_type && settings.allowed_content_types.include?(content_type)
|
|
142
|
+
|
|
143
|
+
body = response.body
|
|
144
|
+
return unless body && body.bytesize > 0
|
|
145
|
+
return if body.bytesize > settings.max_download_size
|
|
146
|
+
|
|
147
|
+
filename = derive_filename(url, content_type)
|
|
148
|
+
|
|
149
|
+
Result.new(
|
|
150
|
+
io: StringIO.new(body),
|
|
151
|
+
filename: filename,
|
|
152
|
+
content_type: content_type,
|
|
153
|
+
url: url
|
|
154
|
+
)
|
|
155
|
+
rescue Faraday::Error
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def derive_filename(favicon_url, content_type)
|
|
160
|
+
uri = URI.parse(favicon_url)
|
|
161
|
+
basename = File.basename(uri.path) if uri.path.present?
|
|
162
|
+
|
|
163
|
+
if basename.present? && basename.include?(".")
|
|
164
|
+
basename
|
|
165
|
+
else
|
|
166
|
+
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".ico"
|
|
167
|
+
"favicon-#{SecureRandom.hex(8)}#{ext}"
|
|
168
|
+
end
|
|
169
|
+
rescue URI::InvalidURIError
|
|
170
|
+
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".ico"
|
|
171
|
+
"favicon-#{SecureRandom.hex(8)}#{ext}"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def html_client
|
|
175
|
+
build_client("text/html, application/xhtml+xml")
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def image_client
|
|
179
|
+
build_client("image/*")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def build_client(accept_header)
|
|
183
|
+
timeout = settings.fetch_timeout
|
|
184
|
+
|
|
185
|
+
Faraday.new do |f|
|
|
186
|
+
f.response :follow_redirects, limit: 3
|
|
187
|
+
f.options.timeout = timeout
|
|
188
|
+
f.options.open_timeout = [ timeout / 2, 3 ].min
|
|
189
|
+
f.headers["User-Agent"] = SourceMonitor.config.http.user_agent || "SourceMonitor/#{SourceMonitor::VERSION}"
|
|
190
|
+
f.headers["Accept"] = accept_header
|
|
191
|
+
f.adapter Faraday.default_adapter
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -37,6 +37,7 @@ module SourceMonitor
|
|
|
37
37
|
attributes[:metadata] = updated_metadata(feed_signature: feed_signature, entries_digest: entries_digest)
|
|
38
38
|
reset_retry_state!(attributes)
|
|
39
39
|
source.update!(attributes)
|
|
40
|
+
enqueue_favicon_fetch_if_needed
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def update_source_for_not_modified(response, duration_ms)
|
|
@@ -62,6 +63,7 @@ module SourceMonitor
|
|
|
62
63
|
attributes[:metadata] = updated_metadata
|
|
63
64
|
reset_retry_state!(attributes)
|
|
64
65
|
source.update!(attributes)
|
|
66
|
+
enqueue_favicon_fetch_if_needed
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def update_source_for_failure(error, duration_ms)
|
|
@@ -137,6 +139,25 @@ module SourceMonitor
|
|
|
137
139
|
attributes[:fetch_circuit_until] = nil
|
|
138
140
|
end
|
|
139
141
|
|
|
142
|
+
def enqueue_favicon_fetch_if_needed
|
|
143
|
+
return unless defined?(ActiveStorage)
|
|
144
|
+
return unless SourceMonitor.config.favicons.enabled?
|
|
145
|
+
return if source.website_url.blank?
|
|
146
|
+
return if source.respond_to?(:favicon) && source.favicon.attached?
|
|
147
|
+
|
|
148
|
+
last_attempt = source.metadata&.dig("favicon_last_attempted_at")
|
|
149
|
+
if last_attempt.present?
|
|
150
|
+
cooldown_days = SourceMonitor.config.favicons.retry_cooldown_days
|
|
151
|
+
return if Time.parse(last_attempt) > cooldown_days.days.ago
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
SourceMonitor::FaviconFetchJob.perform_later(source.id)
|
|
155
|
+
rescue StandardError => error
|
|
156
|
+
Rails.logger.warn(
|
|
157
|
+
"[SourceMonitor::SourceUpdater] Failed to enqueue favicon fetch for source #{source.id}: #{error.message}"
|
|
158
|
+
) if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
159
|
+
end
|
|
160
|
+
|
|
140
161
|
def apply_retry_strategy!(attributes, error, now)
|
|
141
162
|
decision = SourceMonitor::Fetching::RetryPolicy.new(source:, error:, now:).decision
|
|
142
163
|
|
|
@@ -103,6 +103,7 @@ module SourceMonitor
|
|
|
103
103
|
|
|
104
104
|
def request_headers
|
|
105
105
|
headers = (source.custom_headers || {}).transform_keys { |key| key.to_s }
|
|
106
|
+
headers["Referer"] = source.website_url if source.website_url.present?
|
|
106
107
|
headers["If-None-Match"] = source.etag if source.etag.present?
|
|
107
108
|
if source.last_modified.present?
|
|
108
109
|
headers["If-Modified-Since"] = source.last_modified.httpdate
|
data/lib/source_monitor/http.rb
CHANGED
|
@@ -14,7 +14,7 @@ module SourceMonitor
|
|
|
14
14
|
DEFAULT_TIMEOUT = 15
|
|
15
15
|
DEFAULT_OPEN_TIMEOUT = 5
|
|
16
16
|
DEFAULT_MAX_REDIRECTS = 5
|
|
17
|
-
DEFAULT_USER_AGENT = "SourceMonitor/#{SourceMonitor::VERSION}"
|
|
17
|
+
DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; SourceMonitor/#{SourceMonitor::VERSION})"
|
|
18
18
|
RETRY_STATUSES = [ 429, 500, 502, 503, 504 ].freeze
|
|
19
19
|
|
|
20
20
|
class << self
|
|
@@ -89,8 +89,10 @@ module SourceMonitor
|
|
|
89
89
|
def default_headers(settings)
|
|
90
90
|
base_headers = {
|
|
91
91
|
"User-Agent" => resolve_callable(settings.user_agent).presence || DEFAULT_USER_AGENT,
|
|
92
|
-
"Accept" => "application/rss+xml, application/atom+xml, application/json;q=0.9, text/xml;q=0.8",
|
|
93
|
-
"Accept-Encoding" => "gzip,deflate"
|
|
92
|
+
"Accept" => "text/html, application/rss+xml, application/atom+xml, application/json;q=0.9, text/xml;q=0.8",
|
|
93
|
+
"Accept-Encoding" => "gzip,deflate",
|
|
94
|
+
"Accept-Language" => "en-US,en;q=0.9",
|
|
95
|
+
"DNT" => "1"
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
base_headers.merge(settings.headers || {})
|
data/lib/source_monitor.rb
CHANGED
|
@@ -87,6 +87,10 @@ module SourceMonitor
|
|
|
87
87
|
autoload :HealthCheckBroadcaster, "source_monitor/import_sessions/health_check_broadcaster"
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
module Favicons
|
|
91
|
+
autoload :Discoverer, "source_monitor/favicons/discoverer"
|
|
92
|
+
end
|
|
93
|
+
|
|
90
94
|
module Images
|
|
91
95
|
autoload :ContentRewriter, "source_monitor/images/content_rewriter"
|
|
92
96
|
autoload :Downloader, "source_monitor/images/downloader"
|
data/source_monitor.gemspec
CHANGED
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
24
24
|
tracked_files = `git ls-files -z`.split("\x0")
|
|
25
25
|
tracked_files.reject do |file|
|
|
26
|
-
file.start_with?(".ai/", ".github/", "coverage/", "node_modules/", "pkg/", "spec/", "test/", "tmp/", "vendor/", "examples/", "bin/")
|
|
26
|
+
file.start_with?(".ai/", ".github/", ".vbw-planning/", "coverage/", "node_modules/", "pkg/", "spec/", "test/", "tmp/", "vendor/", "examples/", "bin/")
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
spec.files += [ "CHANGELOG.md" ].select { |path| File.exist?(File.join(__dir__, path)) }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: source_monitor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dchuk
|
|
@@ -342,111 +342,6 @@ files:
|
|
|
342
342
|
- ".gitignore"
|
|
343
343
|
- ".rubocop.yml"
|
|
344
344
|
- ".ruby-version"
|
|
345
|
-
- ".vbw-planning/PROJECT.md"
|
|
346
|
-
- ".vbw-planning/ROADMAP.md"
|
|
347
|
-
- ".vbw-planning/SHIPPED.md"
|
|
348
|
-
- ".vbw-planning/STATE.md"
|
|
349
|
-
- ".vbw-planning/codebase/ARCHITECTURE.md"
|
|
350
|
-
- ".vbw-planning/codebase/CONCERNS.md"
|
|
351
|
-
- ".vbw-planning/codebase/CONVENTIONS.md"
|
|
352
|
-
- ".vbw-planning/codebase/DEPENDENCIES.md"
|
|
353
|
-
- ".vbw-planning/codebase/INDEX.md"
|
|
354
|
-
- ".vbw-planning/codebase/META.md"
|
|
355
|
-
- ".vbw-planning/codebase/PATTERNS.md"
|
|
356
|
-
- ".vbw-planning/codebase/STACK.md"
|
|
357
|
-
- ".vbw-planning/codebase/STRUCTURE.md"
|
|
358
|
-
- ".vbw-planning/codebase/TESTING.md"
|
|
359
|
-
- ".vbw-planning/config.json"
|
|
360
|
-
- ".vbw-planning/discovery.json"
|
|
361
|
-
- ".vbw-planning/milestones/default/ROADMAP.md"
|
|
362
|
-
- ".vbw-planning/milestones/default/STATE.md"
|
|
363
|
-
- ".vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md"
|
|
364
|
-
- ".vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md"
|
|
365
|
-
- ".vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md"
|
|
366
|
-
- ".vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md"
|
|
367
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md"
|
|
368
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md"
|
|
369
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md"
|
|
370
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md"
|
|
371
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md"
|
|
372
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md"
|
|
373
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md"
|
|
374
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md"
|
|
375
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md"
|
|
376
|
-
- ".vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md"
|
|
377
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md"
|
|
378
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md"
|
|
379
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md"
|
|
380
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md"
|
|
381
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md"
|
|
382
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md"
|
|
383
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md"
|
|
384
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md"
|
|
385
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md"
|
|
386
|
-
- ".vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md"
|
|
387
|
-
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md"
|
|
388
|
-
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md"
|
|
389
|
-
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md"
|
|
390
|
-
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md"
|
|
391
|
-
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md"
|
|
392
|
-
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md"
|
|
393
|
-
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md"
|
|
394
|
-
- ".vbw-planning/milestones/generator-enhancements/REQUIREMENTS.md"
|
|
395
|
-
- ".vbw-planning/milestones/generator-enhancements/ROADMAP.md"
|
|
396
|
-
- ".vbw-planning/milestones/generator-enhancements/SHIPPED.md"
|
|
397
|
-
- ".vbw-planning/milestones/generator-enhancements/STATE.md"
|
|
398
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-CONTEXT.md"
|
|
399
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-VERIFICATION.md"
|
|
400
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01-SUMMARY.md"
|
|
401
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01.md"
|
|
402
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/02-verification/02-VERIFICATION.md"
|
|
403
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01-SUMMARY.md"
|
|
404
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01.md"
|
|
405
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/03-VERIFICATION.md"
|
|
406
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01-SUMMARY.md"
|
|
407
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01.md"
|
|
408
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/04-VERIFICATION.md"
|
|
409
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01-SUMMARY.md"
|
|
410
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01.md"
|
|
411
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/05-VERIFICATION.md"
|
|
412
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01-SUMMARY.md"
|
|
413
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01.md"
|
|
414
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02-SUMMARY.md"
|
|
415
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02.md"
|
|
416
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/06-VERIFICATION.md"
|
|
417
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md"
|
|
418
|
-
- ".vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01.md"
|
|
419
|
-
- ".vbw-planning/milestones/upgrade-assurance/REQUIREMENTS.md"
|
|
420
|
-
- ".vbw-planning/milestones/upgrade-assurance/ROADMAP.md"
|
|
421
|
-
- ".vbw-planning/milestones/upgrade-assurance/STATE.md"
|
|
422
|
-
- ".vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/01-VERIFICATION.md"
|
|
423
|
-
- ".vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01-SUMMARY.md"
|
|
424
|
-
- ".vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01.md"
|
|
425
|
-
- ".vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01-SUMMARY.md"
|
|
426
|
-
- ".vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01.md"
|
|
427
|
-
- ".vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/03-VERIFICATION.md"
|
|
428
|
-
- ".vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01-SUMMARY.md"
|
|
429
|
-
- ".vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01.md"
|
|
430
|
-
- ".vbw-planning/phases/01-aia-certificate-resolution/.context-dev.md"
|
|
431
|
-
- ".vbw-planning/phases/01-aia-certificate-resolution/PLAN-01-SUMMARY.md"
|
|
432
|
-
- ".vbw-planning/phases/01-aia-certificate-resolution/PLAN-01.md"
|
|
433
|
-
- ".vbw-planning/phases/01-aia-certificate-resolution/PLAN-02-SUMMARY.md"
|
|
434
|
-
- ".vbw-planning/phases/01-aia-certificate-resolution/PLAN-02.md"
|
|
435
|
-
- ".vbw-planning/phases/01-aia-certificate-resolution/PLAN-03-SUMMARY.md"
|
|
436
|
-
- ".vbw-planning/phases/01-aia-certificate-resolution/PLAN-03.md"
|
|
437
|
-
- ".vbw-planning/phases/02-test-performance/.context-dev.md"
|
|
438
|
-
- ".vbw-planning/phases/02-test-performance/.context-lead.md"
|
|
439
|
-
- ".vbw-planning/phases/02-test-performance/.context-qa.md"
|
|
440
|
-
- ".vbw-planning/phases/02-test-performance/02-RESEARCH.md"
|
|
441
|
-
- ".vbw-planning/phases/02-test-performance/02-VERIFICATION.md"
|
|
442
|
-
- ".vbw-planning/phases/02-test-performance/PLAN-01-SUMMARY.md"
|
|
443
|
-
- ".vbw-planning/phases/02-test-performance/PLAN-01.md"
|
|
444
|
-
- ".vbw-planning/phases/02-test-performance/PLAN-02-SUMMARY.md"
|
|
445
|
-
- ".vbw-planning/phases/02-test-performance/PLAN-02.md"
|
|
446
|
-
- ".vbw-planning/phases/02-test-performance/PLAN-03-SUMMARY.md"
|
|
447
|
-
- ".vbw-planning/phases/02-test-performance/PLAN-03.md"
|
|
448
|
-
- ".vbw-planning/phases/02-test-performance/PLAN-04-SUMMARY.md"
|
|
449
|
-
- ".vbw-planning/phases/02-test-performance/PLAN-04.md"
|
|
450
345
|
- AGENTS.md
|
|
451
346
|
- CHANGELOG.md
|
|
452
347
|
- CLAUDE.md
|
|
@@ -468,6 +363,7 @@ files:
|
|
|
468
363
|
- app/assets/javascripts/source_monitor/controllers/confirm_navigation_controller.js
|
|
469
364
|
- app/assets/javascripts/source_monitor/controllers/dropdown_controller.js
|
|
470
365
|
- app/assets/javascripts/source_monitor/controllers/modal_controller.js
|
|
366
|
+
- app/assets/javascripts/source_monitor/controllers/notification_container_controller.js
|
|
471
367
|
- app/assets/javascripts/source_monitor/controllers/notification_controller.js
|
|
472
368
|
- app/assets/javascripts/source_monitor/controllers/select_all_controller.js
|
|
473
369
|
- app/assets/javascripts/source_monitor/turbo_actions.js
|
|
@@ -488,6 +384,7 @@ files:
|
|
|
488
384
|
- app/controllers/source_monitor/logs_controller.rb
|
|
489
385
|
- app/controllers/source_monitor/scrape_logs_controller.rb
|
|
490
386
|
- app/controllers/source_monitor/source_bulk_scrapes_controller.rb
|
|
387
|
+
- app/controllers/source_monitor/source_favicon_fetches_controller.rb
|
|
491
388
|
- app/controllers/source_monitor/source_fetches_controller.rb
|
|
492
389
|
- app/controllers/source_monitor/source_health_checks_controller.rb
|
|
493
390
|
- app/controllers/source_monitor/source_health_resets_controller.rb
|
|
@@ -499,6 +396,7 @@ files:
|
|
|
499
396
|
- app/helpers/source_monitor/table_sort_helper.rb
|
|
500
397
|
- app/jobs/source_monitor/application_job.rb
|
|
501
398
|
- app/jobs/source_monitor/download_content_images_job.rb
|
|
399
|
+
- app/jobs/source_monitor/favicon_fetch_job.rb
|
|
502
400
|
- app/jobs/source_monitor/fetch_feed_job.rb
|
|
503
401
|
- app/jobs/source_monitor/import_opml_job.rb
|
|
504
402
|
- app/jobs/source_monitor/import_session_health_check_job.rb
|
|
@@ -612,6 +510,7 @@ files:
|
|
|
612
510
|
- lib/source_monitor/configuration/authentication_settings.rb
|
|
613
511
|
- lib/source_monitor/configuration/deprecation_registry.rb
|
|
614
512
|
- lib/source_monitor/configuration/events.rb
|
|
513
|
+
- lib/source_monitor/configuration/favicons_settings.rb
|
|
615
514
|
- lib/source_monitor/configuration/fetching_settings.rb
|
|
616
515
|
- lib/source_monitor/configuration/health_settings.rb
|
|
617
516
|
- lib/source_monitor/configuration/http_settings.rb
|
|
@@ -634,6 +533,7 @@ files:
|
|
|
634
533
|
- lib/source_monitor/dashboard/upcoming_fetch_schedule.rb
|
|
635
534
|
- lib/source_monitor/engine.rb
|
|
636
535
|
- lib/source_monitor/events.rb
|
|
536
|
+
- lib/source_monitor/favicons/discoverer.rb
|
|
637
537
|
- lib/source_monitor/feedjira_extensions.rb
|
|
638
538
|
- lib/source_monitor/fetching/advisory_lock.rb
|
|
639
539
|
- lib/source_monitor/fetching/completion/event_publisher.rb
|
data/.vbw-planning/PROJECT.md
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
<!-- VBW PROJECT TEMPLATE (ARTF-04) -- Human-facing project definition -->
|
|
2
|
-
<!-- Created by /vbw init, maintained by Architect agent -->
|
|
3
|
-
|
|
4
|
-
# SourceMonitor
|
|
5
|
-
|
|
6
|
-
## What This Is
|
|
7
|
-
|
|
8
|
-
SourceMonitor is a mountable Rails 8 engine for ingesting RSS/Atom/JSON feeds, scraping article content via pluggable adapters, and providing Solid Queue-powered dashboards for monitoring and remediation. It is distributed as a RubyGem and integrates with host Rails applications.
|
|
9
|
-
|
|
10
|
-
## Core Value
|
|
11
|
-
|
|
12
|
-
A drop-in Rails engine that gives any Rails application feed monitoring, content scraping, and operational dashboards without building the plumbing from scratch.
|
|
13
|
-
|
|
14
|
-
## Requirements
|
|
15
|
-
|
|
16
|
-
### Validated
|
|
17
|
-
|
|
18
|
-
None yet.
|
|
19
|
-
|
|
20
|
-
### Active
|
|
21
|
-
|
|
22
|
-
- [ ] Close test coverage gaps identified in the coverage baseline
|
|
23
|
-
- [ ] Refactor large files for maintainability and single-responsibility
|
|
24
|
-
- [ ] Ensure codebase follows Rails best practices and conventions throughout
|
|
25
|
-
|
|
26
|
-
### Out of Scope
|
|
27
|
-
|
|
28
|
-
- Multi-database support (MySQL/SQLite) -- Keep PostgreSQL-only for now
|
|
29
|
-
- Built-in authentication -- Continue relying on host app for auth
|
|
30
|
-
|
|
31
|
-
## Context
|
|
32
|
-
|
|
33
|
-
This is a brownfield Rails engine at v0.2.1 with 530 source files (325 Ruby, 48 ERB). The codebase has 130 test files, CI/CD via GitHub Actions, and a coverage baseline tracking 2329 lines of uncovered code. Key technical debt includes large files (FeedFetcher 627 lines, Configuration 655 lines, ImportSessionsController 792 lines) and coverage gaps in critical paths.
|
|
34
|
-
|
|
35
|
-
## Constraints
|
|
36
|
-
|
|
37
|
-
- **Ruby**: >= 3.4.0
|
|
38
|
-
- **Rails**: >= 8.0.3, < 9.0
|
|
39
|
-
- **Database**: PostgreSQL only
|
|
40
|
-
- **Testing**: Minitest (not RSpec), branch coverage via SimpleCov
|
|
41
|
-
|
|
42
|
-
## Key Decisions
|
|
43
|
-
|
|
44
|
-
| Decision | Rationale | Outcome |
|
|
45
|
-
|----------|-----------|---------|
|
|
46
|
-
| Focus on coverage + refactoring before new features | Stabilize existing code before adding complexity | Pending |
|
|
47
|
-
| Keep PostgreSQL-only | Not worth the complexity of multi-DB support at this stage | Confirmed |
|
|
48
|
-
| Keep host-app auth | Engine should be composable, not opinionated about auth | Confirmed |
|
|
49
|
-
|
|
50
|
-
---
|
|
51
|
-
*Last updated: 2026-02-09 after VBW bootstrap*
|
data/.vbw-planning/ROADMAP.md
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Roadmap
|
|
2
|
-
|
|
3
|
-
## Milestone: aia-ssl-fix
|
|
4
|
-
|
|
5
|
-
### Phases
|
|
6
|
-
|
|
7
|
-
1. [x] **AIA Certificate Resolution** -- Fix SSL failures for feeds with missing intermediate certificates by implementing AIA (Authority Information Access) resolution
|
|
8
|
-
2. [x] **Test Performance** -- Reduce test suite runtime from ~133s to ~50s by splitting monolithic test classes, enabling parallelism, reducing log IO, and adopting before_all
|
|
9
|
-
|
|
10
|
-
### Phase Details
|
|
11
|
-
|
|
12
|
-
#### Phase 1: AIA Certificate Resolution
|
|
13
|
-
|
|
14
|
-
**Goal:** Implement automatic AIA intermediate certificate fetching so feeds like netflixtechblog.com (served via Medium/AWS with wrong intermediates) succeed without manual cert configuration.
|
|
15
|
-
|
|
16
|
-
**Requirements:**
|
|
17
|
-
- REQ-AIA-01: Create AIAResolver module with thread-safe cache and 1-hour TTL
|
|
18
|
-
- REQ-AIA-02: Add cert_store: parameter to HTTP.client for custom cert stores
|
|
19
|
-
- REQ-AIA-03: On Faraday::SSLError, attempt AIA resolution before failing
|
|
20
|
-
- REQ-AIA-04: Best-effort only -- never make things worse (rescue StandardError -> nil)
|
|
21
|
-
|
|
22
|
-
**Success Criteria:**
|
|
23
|
-
- [ ] AIAResolver.resolve(hostname) fetches leaf cert, extracts AIA URL, downloads intermediate
|
|
24
|
-
- [ ] HTTP.client(cert_store:) accepts and uses custom cert stores
|
|
25
|
-
- [ ] FeedFetcher retries once with AIA-resolved cert store on SSL failure
|
|
26
|
-
- [ ] All existing tests pass (1003+), new tests cover AIA paths
|
|
27
|
-
- [ ] RuboCop zero offenses, Brakeman zero warnings
|
|
28
|
-
|
|
29
|
-
#### Phase 2: Test Performance
|
|
30
|
-
|
|
31
|
-
**Goal:** Reduce test suite wall-clock time from ~133s to ~50s through structural optimizations. The 3-agent investigation identified that FeedFetcherTest (71 tests, 84.8s, 64% of total) is a monolithic class that cannot be parallelized, integration tests add 31s, and 95MB of debug logging adds 5-15s.
|
|
32
|
-
|
|
33
|
-
**Requirements:**
|
|
34
|
-
- REQ-PERF-01: Split FeedFetcherTest into 5+ smaller classes by concern (success paths, error handling, adaptive interval, dirty-check, content fingerprint, utilities)
|
|
35
|
-
- REQ-PERF-02: Set test log level to :warn in test/dummy/config/environments/test.rb (eliminates 95MB log IO)
|
|
36
|
-
- REQ-PERF-03: Tag integration tests (host_install_flow, release_packaging) so they can be excluded during dev with --exclude-pattern
|
|
37
|
-
- REQ-PERF-04: Switch default parallelism from forks to threads (avoids PG segfault, enables splitting benefit)
|
|
38
|
-
- REQ-PERF-05: Adopt before_all/setup_once in top DB-heavy test files (dashboard/queries_test.rb, etc.)
|
|
39
|
-
|
|
40
|
-
**Success Criteria:**
|
|
41
|
-
- [ ] FeedFetcherTest split into 5+ files, each independently runnable
|
|
42
|
-
- [ ] All 1031+ tests pass with PARALLEL_WORKERS=1 and default workers
|
|
43
|
-
- [ ] Test suite completes in <70s locally (down from 133s)
|
|
44
|
-
- [ ] `bin/rails test --exclude-pattern="**/integration/**"` runs <50s
|
|
45
|
-
- [ ] RuboCop zero offenses, Brakeman zero warnings
|
|
46
|
-
- [ ] No test isolation regressions (parallel runs still green)
|
|
47
|
-
|
|
48
|
-
### Progress
|
|
49
|
-
|
|
50
|
-
| Phase | Status | Plans | Completed |
|
|
51
|
-
|-------|--------|-------|-----------|
|
|
52
|
-
| 1. AIA Certificate Resolution | Complete | 3 | 3 |
|
|
53
|
-
| 2. Test Performance | Complete | 4 | 4 |
|