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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agent-memory/vbw-vbw-dev/MEMORY.md +34 -0
  3. data/.claude/agent-memory/vbw-vbw-lead/MEMORY.md +49 -0
  4. data/.claude/commands/release.md +255 -0
  5. data/.claude/skills/sm-configure/SKILL.md +13 -2
  6. data/.claude/skills/sm-configure/reference/configuration-reference.md +33 -0
  7. data/.claude/skills/sm-host-setup/SKILL.md +21 -3
  8. data/.claude/skills/sm-host-setup/reference/setup-checklist.md +36 -0
  9. data/.claude/skills/sm-job/SKILL.md +10 -9
  10. data/.gitignore +4 -0
  11. data/.vbw-planning/REQUIREMENTS.md +22 -0
  12. data/.vbw-planning/ROADMAP.md +125 -0
  13. data/.vbw-planning/STATE.md +43 -0
  14. data/.vbw-planning/config.json +3 -1
  15. data/.vbw-planning/discovery.json +3 -1
  16. data/.vbw-planning/phases/01-generator-steps/01-CONTEXT.md +33 -0
  17. data/.vbw-planning/phases/01-generator-steps/01-VERIFICATION.md +86 -0
  18. data/.vbw-planning/phases/01-generator-steps/PLAN-01-SUMMARY.md +61 -0
  19. data/.vbw-planning/phases/01-generator-steps/PLAN-01.md +380 -0
  20. data/.vbw-planning/phases/02-verification/02-VERIFICATION.md +78 -0
  21. data/.vbw-planning/phases/02-verification/PLAN-01-SUMMARY.md +46 -0
  22. data/.vbw-planning/phases/02-verification/PLAN-01.md +500 -0
  23. data/.vbw-planning/phases/03-docs-alignment/03-VERIFICATION.md +89 -0
  24. data/.vbw-planning/phases/03-docs-alignment/PLAN-01-SUMMARY.md +48 -0
  25. data/.vbw-planning/phases/03-docs-alignment/PLAN-01.md +456 -0
  26. data/.vbw-planning/phases/04-dashboard-ux/04-VERIFICATION.md +129 -0
  27. data/.vbw-planning/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +70 -0
  28. data/.vbw-planning/phases/04-dashboard-ux/PLAN-01.md +747 -0
  29. data/.vbw-planning/phases/05-active-storage-images/05-VERIFICATION.md +156 -0
  30. data/.vbw-planning/phases/05-active-storage-images/PLAN-01-SUMMARY.md +69 -0
  31. data/.vbw-planning/phases/05-active-storage-images/PLAN-01.md +455 -0
  32. data/.vbw-planning/phases/05-active-storage-images/PLAN-02-SUMMARY.md +39 -0
  33. data/.vbw-planning/phases/05-active-storage-images/PLAN-02.md +488 -0
  34. data/.vbw-planning/phases/06-netflix-feed-fix/06-VERIFICATION.md +100 -0
  35. data/.vbw-planning/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +37 -0
  36. data/.vbw-planning/phases/06-netflix-feed-fix/PLAN-01.md +345 -0
  37. data/CHANGELOG.md +43 -0
  38. data/Gemfile.lock +1 -1
  39. data/VERSION +1 -1
  40. data/app/assets/builds/source_monitor/application.css +9 -0
  41. data/app/helpers/source_monitor/application_helper.rb +38 -0
  42. data/app/jobs/source_monitor/download_content_images_job.rb +72 -0
  43. data/app/models/source_monitor/item_content.rb +2 -0
  44. data/app/views/source_monitor/dashboard/_recent_activity.html.erb +9 -0
  45. data/app/views/source_monitor/items/_details.html.erb +2 -2
  46. data/app/views/source_monitor/logs/index.html.erb +9 -0
  47. data/app/views/source_monitor/sources/_details.html.erb +2 -2
  48. data/app/views/source_monitor/sources/_row.html.erb +1 -1
  49. data/docs/setup.md +13 -4
  50. data/docs/troubleshooting.md +38 -7
  51. data/lib/generators/source_monitor/install/install_generator.rb +201 -0
  52. data/lib/source_monitor/configuration/http_settings.rb +7 -1
  53. data/lib/source_monitor/configuration/images_settings.rb +37 -0
  54. data/lib/source_monitor/configuration.rb +3 -1
  55. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +16 -7
  56. data/lib/source_monitor/dashboard/recent_activity.rb +1 -0
  57. data/lib/source_monitor/dashboard/recent_activity_presenter.rb +15 -2
  58. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +13 -0
  59. data/lib/source_monitor/http.rb +23 -0
  60. data/lib/source_monitor/images/content_rewriter.rb +81 -0
  61. data/lib/source_monitor/images/downloader.rb +82 -0
  62. data/lib/source_monitor/logs/table_presenter.rb +25 -0
  63. data/lib/source_monitor/setup/procfile_patcher.rb +31 -0
  64. data/lib/source_monitor/setup/queue_config_patcher.rb +84 -0
  65. data/lib/source_monitor/setup/verification/recurring_schedule_verifier.rb +102 -0
  66. data/lib/source_monitor/setup/verification/runner.rb +1 -1
  67. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +1 -1
  68. data/lib/source_monitor/setup/workflow.rb +10 -0
  69. data/lib/source_monitor/version.rb +1 -1
  70. data/lib/source_monitor.rb +8 -0
  71. metadata +34 -3
  72. data/.vbw-planning/.notification-log.jsonl +0 -294
  73. 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
@@ -18,7 +18,7 @@ module SourceMonitor
18
18
  attr_reader :verifiers
19
19
 
20
20
  def default_verifiers
21
- [ SolidQueueVerifier.new, ActionCableVerifier.new ]
21
+ [ SolidQueueVerifier.new, RecurringScheduleVerifier.new, ActionCableVerifier.new ]
22
22
  end
23
23
  end
24
24
  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` and ensure it stays running")
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SourceMonitor
4
- VERSION = "0.3.2"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -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.3.2
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