source_monitor 0.3.2 → 0.4.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/agent-memory/vbw-vbw-dev/MEMORY.md +34 -0
- data/.claude/agent-memory/vbw-vbw-lead/MEMORY.md +49 -0
- data/.claude/commands/release.md +255 -0
- data/.claude/skills/sm-configure/SKILL.md +13 -2
- data/.claude/skills/sm-configure/reference/configuration-reference.md +33 -0
- data/.claude/skills/sm-host-setup/SKILL.md +21 -3
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +36 -0
- data/.claude/skills/sm-job/SKILL.md +10 -9
- data/.gitignore +4 -0
- data/.vbw-planning/REQUIREMENTS.md +22 -0
- data/.vbw-planning/ROADMAP.md +125 -0
- data/.vbw-planning/STATE.md +43 -0
- data/.vbw-planning/config.json +3 -1
- data/.vbw-planning/discovery.json +3 -1
- data/.vbw-planning/phases/01-generator-steps/01-CONTEXT.md +33 -0
- data/.vbw-planning/phases/01-generator-steps/01-VERIFICATION.md +86 -0
- data/.vbw-planning/phases/01-generator-steps/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/phases/01-generator-steps/PLAN-01.md +380 -0
- data/.vbw-planning/phases/02-verification/02-VERIFICATION.md +78 -0
- data/.vbw-planning/phases/02-verification/PLAN-01-SUMMARY.md +46 -0
- data/.vbw-planning/phases/02-verification/PLAN-01.md +500 -0
- data/.vbw-planning/phases/03-docs-alignment/03-VERIFICATION.md +89 -0
- data/.vbw-planning/phases/03-docs-alignment/PLAN-01-SUMMARY.md +48 -0
- data/.vbw-planning/phases/03-docs-alignment/PLAN-01.md +456 -0
- data/.vbw-planning/phases/04-dashboard-ux/04-VERIFICATION.md +129 -0
- data/.vbw-planning/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +70 -0
- data/.vbw-planning/phases/04-dashboard-ux/PLAN-01.md +747 -0
- data/.vbw-planning/phases/05-active-storage-images/05-VERIFICATION.md +156 -0
- data/.vbw-planning/phases/05-active-storage-images/PLAN-01-SUMMARY.md +69 -0
- data/.vbw-planning/phases/05-active-storage-images/PLAN-01.md +455 -0
- data/.vbw-planning/phases/05-active-storage-images/PLAN-02-SUMMARY.md +39 -0
- data/.vbw-planning/phases/05-active-storage-images/PLAN-02.md +488 -0
- data/.vbw-planning/phases/06-netflix-feed-fix/06-VERIFICATION.md +100 -0
- data/.vbw-planning/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +37 -0
- data/.vbw-planning/phases/06-netflix-feed-fix/PLAN-01.md +345 -0
- data/CHANGELOG.md +43 -0
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/app/assets/builds/source_monitor/application.css +9 -0
- data/app/helpers/source_monitor/application_helper.rb +38 -0
- data/app/jobs/source_monitor/download_content_images_job.rb +72 -0
- data/app/models/source_monitor/item_content.rb +2 -0
- data/app/views/source_monitor/dashboard/_recent_activity.html.erb +9 -0
- data/app/views/source_monitor/items/_details.html.erb +2 -2
- data/app/views/source_monitor/logs/index.html.erb +9 -0
- data/app/views/source_monitor/sources/_details.html.erb +2 -2
- data/app/views/source_monitor/sources/_row.html.erb +1 -1
- data/docs/setup.md +13 -4
- data/docs/troubleshooting.md +38 -7
- data/lib/generators/source_monitor/install/install_generator.rb +201 -0
- data/lib/source_monitor/configuration/http_settings.rb +7 -1
- data/lib/source_monitor/configuration/images_settings.rb +37 -0
- data/lib/source_monitor/configuration.rb +3 -1
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +16 -7
- data/lib/source_monitor/dashboard/recent_activity.rb +1 -0
- data/lib/source_monitor/dashboard/recent_activity_presenter.rb +15 -2
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +13 -0
- data/lib/source_monitor/http.rb +23 -0
- data/lib/source_monitor/images/content_rewriter.rb +81 -0
- data/lib/source_monitor/images/downloader.rb +82 -0
- data/lib/source_monitor/logs/table_presenter.rb +25 -0
- data/lib/source_monitor/setup/procfile_patcher.rb +31 -0
- data/lib/source_monitor/setup/queue_config_patcher.rb +84 -0
- data/lib/source_monitor/setup/verification/recurring_schedule_verifier.rb +102 -0
- data/lib/source_monitor/setup/verification/runner.rb +1 -1
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +1 -1
- data/lib/source_monitor/setup/workflow.rb +10 -0
- data/lib/source_monitor/version.rb +1 -1
- data/lib/source_monitor.rb +8 -0
- metadata +34 -3
- data/.vbw-planning/.notification-log.jsonl +0 -294
- data/.vbw-planning/.session-log.jsonl +0 -1376
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokolexbor"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module SourceMonitor
|
|
7
|
+
module Images
|
|
8
|
+
class ContentRewriter
|
|
9
|
+
attr_reader :html, :base_url
|
|
10
|
+
|
|
11
|
+
def initialize(html, base_url: nil)
|
|
12
|
+
@html = html.to_s
|
|
13
|
+
@base_url = base_url
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns an array of absolute image URLs found in <img> tags.
|
|
17
|
+
# Skips data: URIs, blank src, and invalid URLs.
|
|
18
|
+
def image_urls
|
|
19
|
+
return [] if html.blank?
|
|
20
|
+
|
|
21
|
+
doc = parse_fragment
|
|
22
|
+
urls = []
|
|
23
|
+
|
|
24
|
+
doc.css("img[src]").each do |img|
|
|
25
|
+
url = resolve_url(img["src"])
|
|
26
|
+
urls << url if url && downloadable_url?(url)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
urls.uniq
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Rewrites <img src="..."> attributes by yielding each original URL
|
|
33
|
+
# to the block and replacing with the block's return value.
|
|
34
|
+
# Returns the rewritten HTML string.
|
|
35
|
+
# If the block returns nil, the original URL is preserved (graceful fallback).
|
|
36
|
+
def rewrite
|
|
37
|
+
return html if html.blank?
|
|
38
|
+
|
|
39
|
+
doc = parse_fragment
|
|
40
|
+
|
|
41
|
+
doc.css("img[src]").each do |img|
|
|
42
|
+
original_url = resolve_url(img["src"])
|
|
43
|
+
next unless original_url && downloadable_url?(original_url)
|
|
44
|
+
|
|
45
|
+
new_url = yield(original_url)
|
|
46
|
+
img["src"] = new_url if new_url.present?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
doc.to_html
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def parse_fragment
|
|
55
|
+
Nokolexbor::DocumentFragment.parse(html)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve_url(src)
|
|
59
|
+
src = src.to_s.strip
|
|
60
|
+
return nil if src.blank?
|
|
61
|
+
return nil if src.start_with?("data:")
|
|
62
|
+
|
|
63
|
+
uri = URI.parse(src)
|
|
64
|
+
if uri.relative? && base_url.present?
|
|
65
|
+
URI.join(base_url, src).to_s
|
|
66
|
+
elsif uri.absolute?
|
|
67
|
+
src
|
|
68
|
+
end
|
|
69
|
+
rescue URI::InvalidURIError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def downloadable_url?(url)
|
|
74
|
+
uri = URI.parse(url)
|
|
75
|
+
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
|
|
76
|
+
rescue URI::InvalidURIError
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module SourceMonitor
|
|
7
|
+
module Images
|
|
8
|
+
class Downloader
|
|
9
|
+
Result = Struct.new(:io, :filename, :content_type, :byte_size, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
attr_reader :url, :settings
|
|
12
|
+
|
|
13
|
+
def initialize(url, settings: nil)
|
|
14
|
+
@url = url
|
|
15
|
+
@settings = settings || SourceMonitor.config.images
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Downloads the image and returns a Result, or nil if download fails
|
|
19
|
+
# or the image does not meet validation criteria.
|
|
20
|
+
def call
|
|
21
|
+
response = fetch_image
|
|
22
|
+
return unless response
|
|
23
|
+
|
|
24
|
+
content_type = response.headers["content-type"]&.split(";")&.first&.strip&.downcase
|
|
25
|
+
return unless allowed_content_type?(content_type)
|
|
26
|
+
|
|
27
|
+
body = response.body
|
|
28
|
+
return unless body && body.bytesize > 0
|
|
29
|
+
return if body.bytesize > settings.max_download_size
|
|
30
|
+
|
|
31
|
+
filename = derive_filename(url, content_type)
|
|
32
|
+
|
|
33
|
+
Result.new(
|
|
34
|
+
io: StringIO.new(body),
|
|
35
|
+
filename: filename,
|
|
36
|
+
content_type: content_type,
|
|
37
|
+
byte_size: body.bytesize
|
|
38
|
+
)
|
|
39
|
+
rescue Faraday::Error, URI::InvalidURIError, Timeout::Error
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def fetch_image
|
|
46
|
+
connection = Faraday.new do |f|
|
|
47
|
+
f.options.timeout = settings.download_timeout
|
|
48
|
+
f.options.open_timeout = [ settings.download_timeout / 2, 5 ].min
|
|
49
|
+
f.headers["User-Agent"] = SourceMonitor.config.http.user_agent || "SourceMonitor/#{SourceMonitor::VERSION}"
|
|
50
|
+
f.headers["Accept"] = "image/*"
|
|
51
|
+
f.adapter Faraday.default_adapter
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
response = connection.get(url)
|
|
55
|
+
return response if response.status == 200
|
|
56
|
+
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def allowed_content_type?(content_type)
|
|
61
|
+
return false if content_type.blank?
|
|
62
|
+
|
|
63
|
+
settings.allowed_content_types.include?(content_type)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def derive_filename(image_url, content_type)
|
|
67
|
+
uri = URI.parse(image_url)
|
|
68
|
+
basename = File.basename(uri.path) if uri.path.present?
|
|
69
|
+
|
|
70
|
+
if basename.present? && basename.include?(".")
|
|
71
|
+
basename
|
|
72
|
+
else
|
|
73
|
+
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".bin"
|
|
74
|
+
"image-#{SecureRandom.hex(8)}#{ext}"
|
|
75
|
+
end
|
|
76
|
+
rescue URI::InvalidURIError
|
|
77
|
+
ext = Rack::Mime::MIME_TYPES.invert[content_type] || ".bin"
|
|
78
|
+
"image-#{SecureRandom.hex(8)}#{ext}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -61,6 +61,22 @@ module SourceMonitor
|
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def url_label
|
|
65
|
+
if fetch?
|
|
66
|
+
domain_from_feed_url
|
|
67
|
+
elsif scrape?
|
|
68
|
+
entry.item&.url
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def url_href
|
|
73
|
+
if fetch?
|
|
74
|
+
entry.source&.feed_url
|
|
75
|
+
elsif scrape?
|
|
76
|
+
entry.item&.url
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
64
80
|
def source_label
|
|
65
81
|
entry.source&.name
|
|
66
82
|
end
|
|
@@ -142,6 +158,15 @@ module SourceMonitor
|
|
|
142
158
|
private
|
|
143
159
|
|
|
144
160
|
attr_reader :entry, :url_helpers
|
|
161
|
+
|
|
162
|
+
def domain_from_feed_url
|
|
163
|
+
feed_url = entry.source&.feed_url
|
|
164
|
+
return nil if feed_url.blank?
|
|
165
|
+
|
|
166
|
+
URI.parse(feed_url.to_s).host
|
|
167
|
+
rescue URI::InvalidURIError
|
|
168
|
+
nil
|
|
169
|
+
end
|
|
145
170
|
end
|
|
146
171
|
|
|
147
172
|
def initialize(entries:, url_helpers:)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module SourceMonitor
|
|
6
|
+
module Setup
|
|
7
|
+
class ProcfilePatcher
|
|
8
|
+
JOBS_ENTRY = "jobs: bundle exec rake solid_queue:start"
|
|
9
|
+
|
|
10
|
+
def initialize(path: "Procfile.dev")
|
|
11
|
+
@path = Pathname.new(path)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def patch
|
|
15
|
+
if path.exist?
|
|
16
|
+
content = path.read
|
|
17
|
+
return false if content.match?(/^jobs:/)
|
|
18
|
+
|
|
19
|
+
path.open("a") { |f| f.puts("", JOBS_ENTRY) }
|
|
20
|
+
else
|
|
21
|
+
path.write("web: bin/rails server -p 3000\n#{JOBS_ENTRY}\n")
|
|
22
|
+
end
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :path
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module SourceMonitor
|
|
7
|
+
module Setup
|
|
8
|
+
class QueueConfigPatcher
|
|
9
|
+
RECURRING_SCHEDULE_VALUE = "config/recurring.yml"
|
|
10
|
+
|
|
11
|
+
DEFAULT_DISPATCHER = {
|
|
12
|
+
"polling_interval" => 1,
|
|
13
|
+
"batch_size" => 500,
|
|
14
|
+
"recurring_schedule" => RECURRING_SCHEDULE_VALUE
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(path: "config/queue.yml")
|
|
18
|
+
@path = Pathname.new(path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def patch
|
|
22
|
+
return false unless path.exist?
|
|
23
|
+
|
|
24
|
+
parsed = YAML.safe_load(path.read, aliases: true) || {}
|
|
25
|
+
return false if has_recurring_schedule?(parsed)
|
|
26
|
+
|
|
27
|
+
add_recurring_schedule!(parsed)
|
|
28
|
+
path.write(YAML.dump(parsed))
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :path
|
|
35
|
+
|
|
36
|
+
def has_recurring_schedule?(parsed)
|
|
37
|
+
parsed.each_value do |value|
|
|
38
|
+
next unless value.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
dispatchers = value["dispatchers"]
|
|
41
|
+
if dispatchers.is_a?(Array)
|
|
42
|
+
return true if dispatchers.any? { |d| d.is_a?(Hash) && d.key?("recurring_schedule") }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
return true if has_recurring_schedule?(value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if parsed.key?("dispatchers") && parsed["dispatchers"].is_a?(Array)
|
|
49
|
+
return true if parsed["dispatchers"].any? { |d| d.is_a?(Hash) && d.key?("recurring_schedule") }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add_recurring_schedule!(parsed)
|
|
56
|
+
found_dispatchers = false
|
|
57
|
+
|
|
58
|
+
parsed.each_value do |value|
|
|
59
|
+
next unless value.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
if value.key?("dispatchers") && value["dispatchers"].is_a?(Array)
|
|
62
|
+
value["dispatchers"].each do |dispatcher|
|
|
63
|
+
next unless dispatcher.is_a?(Hash)
|
|
64
|
+
dispatcher["recurring_schedule"] ||= RECURRING_SCHEDULE_VALUE
|
|
65
|
+
end
|
|
66
|
+
found_dispatchers = true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if parsed.key?("dispatchers") && parsed["dispatchers"].is_a?(Array)
|
|
71
|
+
parsed["dispatchers"].each do |dispatcher|
|
|
72
|
+
next unless dispatcher.is_a?(Hash)
|
|
73
|
+
dispatcher["recurring_schedule"] ||= RECURRING_SCHEDULE_VALUE
|
|
74
|
+
end
|
|
75
|
+
found_dispatchers = true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless found_dispatchers
|
|
79
|
+
parsed["dispatchers"] = [ DEFAULT_DISPATCHER.dup ]
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SourceMonitor
|
|
4
|
+
module Setup
|
|
5
|
+
module Verification
|
|
6
|
+
class RecurringScheduleVerifier
|
|
7
|
+
SOURCE_MONITOR_KEY_PREFIX = "source_monitor_"
|
|
8
|
+
SOURCE_MONITOR_NAMESPACE = "SourceMonitor::"
|
|
9
|
+
|
|
10
|
+
def initialize(task_relation: default_task_relation, connection: default_connection)
|
|
11
|
+
@task_relation = task_relation
|
|
12
|
+
@connection = connection
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
return missing_gem_result unless task_relation
|
|
17
|
+
return missing_tables_result unless tables_present?
|
|
18
|
+
|
|
19
|
+
tasks = all_tasks
|
|
20
|
+
sm_tasks = source_monitor_tasks(tasks)
|
|
21
|
+
|
|
22
|
+
if sm_tasks.any?
|
|
23
|
+
ok_result("#{sm_tasks.size} SourceMonitor recurring task(s) registered")
|
|
24
|
+
elsif tasks.any?
|
|
25
|
+
warning_result(
|
|
26
|
+
"Recurring tasks exist but none belong to SourceMonitor",
|
|
27
|
+
"Add SourceMonitor entries to config/recurring.yml and ensure the dispatcher has `recurring_schedule: config/recurring.yml`"
|
|
28
|
+
)
|
|
29
|
+
else
|
|
30
|
+
warning_result(
|
|
31
|
+
"No recurring tasks are registered with Solid Queue",
|
|
32
|
+
"Configure a dispatcher with `recurring_schedule: config/recurring.yml` in config/queue.yml and ensure recurring.yml contains SourceMonitor task entries"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
error_result(
|
|
37
|
+
"Recurring schedule verification failed: #{e.message}",
|
|
38
|
+
"Verify Solid Queue migrations are up to date and the dispatcher is configured with recurring_schedule"
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :task_relation, :connection
|
|
45
|
+
|
|
46
|
+
def default_task_relation
|
|
47
|
+
SolidQueue::RecurringTask if defined?(SolidQueue::RecurringTask)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def default_connection
|
|
51
|
+
SolidQueue::RecurringTask.connection if defined?(SolidQueue::RecurringTask)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def tables_present?
|
|
57
|
+
return false unless connection
|
|
58
|
+
|
|
59
|
+
connection.table_exists?(task_relation.table_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def all_tasks
|
|
63
|
+
task_relation.all.to_a
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def source_monitor_tasks(tasks)
|
|
67
|
+
tasks.select do |task|
|
|
68
|
+
task.key.start_with?(SOURCE_MONITOR_KEY_PREFIX) ||
|
|
69
|
+
task.class_name.to_s.start_with?(SOURCE_MONITOR_NAMESPACE) ||
|
|
70
|
+
task.command.to_s.include?(SOURCE_MONITOR_NAMESPACE)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def missing_gem_result
|
|
75
|
+
error_result(
|
|
76
|
+
"Solid Queue gem is not available",
|
|
77
|
+
"Add `solid_queue` to your Gemfile and bundle install"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def missing_tables_result
|
|
82
|
+
error_result(
|
|
83
|
+
"Solid Queue recurring tasks table is missing",
|
|
84
|
+
"Run `rails solid_queue:install` or copy the engine's Solid Queue migration"
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def ok_result(details)
|
|
89
|
+
Result.new(key: :recurring_schedule, name: "Recurring Schedule", status: :ok, details: details)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def warning_result(details, remediation)
|
|
93
|
+
Result.new(key: :recurring_schedule, name: "Recurring Schedule", status: :warning, details: details, remediation: remediation)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def error_result(details, remediation)
|
|
97
|
+
Result.new(key: :recurring_schedule, name: "Recurring Schedule", status: :error, details: details, remediation: remediation)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -21,7 +21,7 @@ module SourceMonitor
|
|
|
21
21
|
if recent
|
|
22
22
|
ok_result("Solid Queue workers are reporting heartbeats")
|
|
23
23
|
else
|
|
24
|
-
warning_result("No Solid Queue workers have reported in the last #{DEFAULT_HEARTBEAT_THRESHOLD.inspect}", "Start a Solid Queue worker with `bin/rails solid_queue:start`
|
|
24
|
+
warning_result("No Solid Queue workers have reported in the last #{DEFAULT_HEARTBEAT_THRESHOLD.inspect}", "Start a Solid Queue worker with `bin/rails solid_queue:start` or add `jobs: bundle exec rake solid_queue:start` to Procfile.dev and run `bin/dev`")
|
|
25
25
|
end
|
|
26
26
|
rescue StandardError => e
|
|
27
27
|
error_result("Solid Queue verification failed: #{e.message}", "Verify Solid Queue migrations are up to date and workers can access the database")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "pathname"
|
|
4
|
+
require_relative "procfile_patcher"
|
|
5
|
+
require_relative "queue_config_patcher"
|
|
4
6
|
require_relative "skills_installer"
|
|
5
7
|
|
|
6
8
|
module SourceMonitor
|
|
@@ -35,6 +37,8 @@ module SourceMonitor
|
|
|
35
37
|
install_generator: InstallGenerator.new,
|
|
36
38
|
migration_installer: MigrationInstaller.new,
|
|
37
39
|
initializer_patcher: InitializerPatcher.new,
|
|
40
|
+
procfile_patcher: ProcfilePatcher.new,
|
|
41
|
+
queue_config_patcher: QueueConfigPatcher.new,
|
|
38
42
|
devise_detector: method(:default_devise_detector),
|
|
39
43
|
verifier: Verification::Runner.new,
|
|
40
44
|
skills_installer: SkillsInstaller.new
|
|
@@ -47,6 +51,8 @@ module SourceMonitor
|
|
|
47
51
|
@install_generator = install_generator
|
|
48
52
|
@migration_installer = migration_installer
|
|
49
53
|
@initializer_patcher = initializer_patcher
|
|
54
|
+
@procfile_patcher = procfile_patcher
|
|
55
|
+
@queue_config_patcher = queue_config_patcher
|
|
50
56
|
@devise_detector = devise_detector
|
|
51
57
|
@verifier = verifier
|
|
52
58
|
@skills_installer = skills_installer
|
|
@@ -64,6 +70,8 @@ module SourceMonitor
|
|
|
64
70
|
install_generator.run(mount_path: mount_path)
|
|
65
71
|
migration_installer.install
|
|
66
72
|
initializer_patcher.ensure_navigation_hint(mount_path: mount_path)
|
|
73
|
+
procfile_patcher.patch
|
|
74
|
+
queue_config_patcher.patch
|
|
67
75
|
|
|
68
76
|
if devise_available? && prompter.yes?("Wire Devise authentication hooks into SourceMonitor?", default: true)
|
|
69
77
|
initializer_patcher.ensure_devise_hooks
|
|
@@ -89,6 +97,8 @@ module SourceMonitor
|
|
|
89
97
|
:install_generator,
|
|
90
98
|
:migration_installer,
|
|
91
99
|
:initializer_patcher,
|
|
100
|
+
:procfile_patcher,
|
|
101
|
+
:queue_config_patcher,
|
|
92
102
|
:devise_detector,
|
|
93
103
|
:verifier,
|
|
94
104
|
:skills_installer
|
data/lib/source_monitor.rb
CHANGED
|
@@ -87,6 +87,11 @@ module SourceMonitor
|
|
|
87
87
|
autoload :HealthCheckBroadcaster, "source_monitor/import_sessions/health_check_broadcaster"
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
module Images
|
|
91
|
+
autoload :ContentRewriter, "source_monitor/images/content_rewriter"
|
|
92
|
+
autoload :Downloader, "source_monitor/images/downloader"
|
|
93
|
+
end
|
|
94
|
+
|
|
90
95
|
module Items
|
|
91
96
|
autoload :ItemCreator, "source_monitor/items/item_creator"
|
|
92
97
|
autoload :RetentionPruner, "source_monitor/items/retention_pruner"
|
|
@@ -161,6 +166,8 @@ module SourceMonitor
|
|
|
161
166
|
autoload :InstallGenerator, "source_monitor/setup/install_generator"
|
|
162
167
|
autoload :MigrationInstaller, "source_monitor/setup/migration_installer"
|
|
163
168
|
autoload :InitializerPatcher, "source_monitor/setup/initializer_patcher"
|
|
169
|
+
autoload :ProcfilePatcher, "source_monitor/setup/procfile_patcher"
|
|
170
|
+
autoload :QueueConfigPatcher, "source_monitor/setup/queue_config_patcher"
|
|
164
171
|
autoload :Workflow, "source_monitor/setup/workflow"
|
|
165
172
|
autoload :CLI, "source_monitor/setup/cli"
|
|
166
173
|
|
|
@@ -169,6 +176,7 @@ module SourceMonitor
|
|
|
169
176
|
autoload :Summary, "source_monitor/setup/verification/result"
|
|
170
177
|
autoload :SolidQueueVerifier, "source_monitor/setup/verification/solid_queue_verifier"
|
|
171
178
|
autoload :ActionCableVerifier, "source_monitor/setup/verification/action_cable_verifier"
|
|
179
|
+
autoload :RecurringScheduleVerifier, "source_monitor/setup/verification/recurring_schedule_verifier"
|
|
172
180
|
autoload :Runner, "source_monitor/setup/verification/runner"
|
|
173
181
|
autoload :Printer, "source_monitor/setup/verification/printer"
|
|
174
182
|
autoload :TelemetryLogger, "source_monitor/setup/verification/telemetry_logger"
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- dchuk
|
|
@@ -238,6 +238,8 @@ executables: []
|
|
|
238
238
|
extensions: []
|
|
239
239
|
extra_rdoc_files: []
|
|
240
240
|
files:
|
|
241
|
+
- ".claude/agent-memory/vbw-vbw-dev/MEMORY.md"
|
|
242
|
+
- ".claude/agent-memory/vbw-vbw-lead/MEMORY.md"
|
|
241
243
|
- ".claude/agents/rails-concern.md"
|
|
242
244
|
- ".claude/agents/rails-controller.md"
|
|
243
245
|
- ".claude/agents/rails-hotwire.md"
|
|
@@ -256,6 +258,7 @@ files:
|
|
|
256
258
|
- ".claude/agents/rails-tdd.md"
|
|
257
259
|
- ".claude/agents/rails-test.md"
|
|
258
260
|
- ".claude/agents/rails-view-component.md"
|
|
261
|
+
- ".claude/commands/release.md"
|
|
259
262
|
- ".claude/hooks/block-secrets.sh"
|
|
260
263
|
- ".claude/settings.json"
|
|
261
264
|
- ".claude/skills/action-cable-patterns/SKILL.md"
|
|
@@ -336,11 +339,11 @@ files:
|
|
|
336
339
|
- ".gitignore"
|
|
337
340
|
- ".rubocop.yml"
|
|
338
341
|
- ".ruby-version"
|
|
339
|
-
- ".vbw-planning/.notification-log.jsonl"
|
|
340
|
-
- ".vbw-planning/.session-log.jsonl"
|
|
341
342
|
- ".vbw-planning/PROJECT.md"
|
|
342
343
|
- ".vbw-planning/REQUIREMENTS.md"
|
|
344
|
+
- ".vbw-planning/ROADMAP.md"
|
|
343
345
|
- ".vbw-planning/SHIPPED.md"
|
|
346
|
+
- ".vbw-planning/STATE.md"
|
|
344
347
|
- ".vbw-planning/codebase/ARCHITECTURE.md"
|
|
345
348
|
- ".vbw-planning/codebase/CONCERNS.md"
|
|
346
349
|
- ".vbw-planning/codebase/CONVENTIONS.md"
|
|
@@ -386,6 +389,27 @@ files:
|
|
|
386
389
|
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md"
|
|
387
390
|
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md"
|
|
388
391
|
- ".vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md"
|
|
392
|
+
- ".vbw-planning/phases/01-generator-steps/01-CONTEXT.md"
|
|
393
|
+
- ".vbw-planning/phases/01-generator-steps/01-VERIFICATION.md"
|
|
394
|
+
- ".vbw-planning/phases/01-generator-steps/PLAN-01-SUMMARY.md"
|
|
395
|
+
- ".vbw-planning/phases/01-generator-steps/PLAN-01.md"
|
|
396
|
+
- ".vbw-planning/phases/02-verification/02-VERIFICATION.md"
|
|
397
|
+
- ".vbw-planning/phases/02-verification/PLAN-01-SUMMARY.md"
|
|
398
|
+
- ".vbw-planning/phases/02-verification/PLAN-01.md"
|
|
399
|
+
- ".vbw-planning/phases/03-docs-alignment/03-VERIFICATION.md"
|
|
400
|
+
- ".vbw-planning/phases/03-docs-alignment/PLAN-01-SUMMARY.md"
|
|
401
|
+
- ".vbw-planning/phases/03-docs-alignment/PLAN-01.md"
|
|
402
|
+
- ".vbw-planning/phases/04-dashboard-ux/04-VERIFICATION.md"
|
|
403
|
+
- ".vbw-planning/phases/04-dashboard-ux/PLAN-01-SUMMARY.md"
|
|
404
|
+
- ".vbw-planning/phases/04-dashboard-ux/PLAN-01.md"
|
|
405
|
+
- ".vbw-planning/phases/05-active-storage-images/05-VERIFICATION.md"
|
|
406
|
+
- ".vbw-planning/phases/05-active-storage-images/PLAN-01-SUMMARY.md"
|
|
407
|
+
- ".vbw-planning/phases/05-active-storage-images/PLAN-01.md"
|
|
408
|
+
- ".vbw-planning/phases/05-active-storage-images/PLAN-02-SUMMARY.md"
|
|
409
|
+
- ".vbw-planning/phases/05-active-storage-images/PLAN-02.md"
|
|
410
|
+
- ".vbw-planning/phases/06-netflix-feed-fix/06-VERIFICATION.md"
|
|
411
|
+
- ".vbw-planning/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md"
|
|
412
|
+
- ".vbw-planning/phases/06-netflix-feed-fix/PLAN-01.md"
|
|
389
413
|
- AGENTS.md
|
|
390
414
|
- CHANGELOG.md
|
|
391
415
|
- CLAUDE.md
|
|
@@ -437,6 +461,7 @@ files:
|
|
|
437
461
|
- app/helpers/source_monitor/health_badge_helper.rb
|
|
438
462
|
- app/helpers/source_monitor/table_sort_helper.rb
|
|
439
463
|
- app/jobs/source_monitor/application_job.rb
|
|
464
|
+
- app/jobs/source_monitor/download_content_images_job.rb
|
|
440
465
|
- app/jobs/source_monitor/fetch_feed_job.rb
|
|
441
466
|
- app/jobs/source_monitor/import_opml_job.rb
|
|
442
467
|
- app/jobs/source_monitor/import_session_health_check_job.rb
|
|
@@ -550,6 +575,7 @@ files:
|
|
|
550
575
|
- lib/source_monitor/configuration/fetching_settings.rb
|
|
551
576
|
- lib/source_monitor/configuration/health_settings.rb
|
|
552
577
|
- lib/source_monitor/configuration/http_settings.rb
|
|
578
|
+
- lib/source_monitor/configuration/images_settings.rb
|
|
553
579
|
- lib/source_monitor/configuration/model_definition.rb
|
|
554
580
|
- lib/source_monitor/configuration/models.rb
|
|
555
581
|
- lib/source_monitor/configuration/realtime_settings.rb
|
|
@@ -587,6 +613,8 @@ files:
|
|
|
587
613
|
- lib/source_monitor/health/source_health_monitor.rb
|
|
588
614
|
- lib/source_monitor/health/source_health_reset.rb
|
|
589
615
|
- lib/source_monitor/http.rb
|
|
616
|
+
- lib/source_monitor/images/content_rewriter.rb
|
|
617
|
+
- lib/source_monitor/images/downloader.rb
|
|
590
618
|
- lib/source_monitor/import_sessions/entry_normalizer.rb
|
|
591
619
|
- lib/source_monitor/import_sessions/health_check_broadcaster.rb
|
|
592
620
|
- lib/source_monitor/instrumentation.rb
|
|
@@ -640,12 +668,15 @@ files:
|
|
|
640
668
|
- lib/source_monitor/setup/install_generator.rb
|
|
641
669
|
- lib/source_monitor/setup/migration_installer.rb
|
|
642
670
|
- lib/source_monitor/setup/node_installer.rb
|
|
671
|
+
- lib/source_monitor/setup/procfile_patcher.rb
|
|
643
672
|
- lib/source_monitor/setup/prompter.rb
|
|
673
|
+
- lib/source_monitor/setup/queue_config_patcher.rb
|
|
644
674
|
- lib/source_monitor/setup/requirements.rb
|
|
645
675
|
- lib/source_monitor/setup/shell_runner.rb
|
|
646
676
|
- lib/source_monitor/setup/skills_installer.rb
|
|
647
677
|
- lib/source_monitor/setup/verification/action_cable_verifier.rb
|
|
648
678
|
- lib/source_monitor/setup/verification/printer.rb
|
|
679
|
+
- lib/source_monitor/setup/verification/recurring_schedule_verifier.rb
|
|
649
680
|
- lib/source_monitor/setup/verification/result.rb
|
|
650
681
|
- lib/source_monitor/setup/verification/runner.rb
|
|
651
682
|
- lib/source_monitor/setup/verification/solid_queue_verifier.rb
|