source_monitor 0.7.1 → 0.8.1
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 +18 -6
- 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 +45 -0
- data/CLAUDE.md +24 -3
- data/Gemfile.lock +1 -1
- data/README.md +6 -4
- data/Rakefile +0 -2
- 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/import_sessions/steps/_preview.html.erb +7 -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
|
@@ -70,7 +70,7 @@ SourceMonitor.configure do |config|
|
|
|
70
70
|
# config.http.retry_interval = 0.5
|
|
71
71
|
# config.http.retry_backoff_factor = 2
|
|
72
72
|
# config.http.retry_statuses = [429, 500, 502, 503, 504]
|
|
73
|
-
# Merge extra default headers (User-Agent defaults to
|
|
73
|
+
# Merge extra default headers (User-Agent defaults to a browser-like string).
|
|
74
74
|
# config.http.headers = { "X-Request-ID" => -> { SecureRandom.uuid } }
|
|
75
75
|
|
|
76
76
|
# ---- Adaptive fetch scheduling ----------------------------------------
|
|
@@ -141,6 +141,20 @@ SourceMonitor.configure do |config|
|
|
|
141
141
|
# config.models.source.validate :enforce_custom_rules
|
|
142
142
|
# config.models.source.validate ->(record) { record.errors.add(:base, "custom error") }
|
|
143
143
|
|
|
144
|
+
# ---- Favicons ----------------------------------------------------------
|
|
145
|
+
# Automatically fetch and store source favicons via Active Storage.
|
|
146
|
+
# Requires Active Storage in the host app (rails active_storage:install).
|
|
147
|
+
# Without Active Storage, favicons are silently disabled and colored
|
|
148
|
+
# initials placeholders are shown instead.
|
|
149
|
+
# config.favicons.enabled = true # default: true
|
|
150
|
+
# config.favicons.fetch_timeout = 5 # seconds
|
|
151
|
+
# config.favicons.max_download_size = 1_048_576 # 1 MB
|
|
152
|
+
# config.favicons.retry_cooldown_days = 7
|
|
153
|
+
# config.favicons.allowed_content_types = %w[
|
|
154
|
+
# image/x-icon image/vnd.microsoft.icon image/png
|
|
155
|
+
# image/jpeg image/gif image/svg+xml image/webp
|
|
156
|
+
# ]
|
|
157
|
+
|
|
144
158
|
# ---- Realtime adapter -------------------------------------------------
|
|
145
159
|
# Choose the Action Cable backend powering Turbo Streams. Solid Cable keeps
|
|
146
160
|
# everything in the primary database so Redis is no longer required. Switch
|
|
@@ -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.1
|
|
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*
|