source_monitor 0.11.1 → 0.12.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/commands/rails-audit.md +77 -0
- data/CHANGELOG.md +50 -0
- data/CLAUDE.md +2 -2
- data/Gemfile.lock +7 -20
- data/RAILS_AUDIT.md +424 -0
- data/VERSION +1 -1
- data/app/assets/builds/source_monitor/application.css +4 -24
- data/app/assets/builds/source_monitor/application.js +57 -89
- data/app/assets/builds/source_monitor/application.js.map +4 -4
- data/app/assets/javascripts/source_monitor/application.js +3 -6
- data/app/assets/javascripts/source_monitor/controllers/dropdown_controller.js +6 -86
- data/app/assets/javascripts/source_monitor/controllers/filter_submit_controller.js +13 -0
- data/app/assets/javascripts/source_monitor/controllers/modal_controller.js +56 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +3 -13
- data/app/components/source_monitor/application_component.rb +10 -0
- data/app/components/source_monitor/filter_dropdown_component.rb +62 -0
- data/app/components/source_monitor/icon_component.rb +140 -0
- data/app/components/source_monitor/status_badge_component.html.erb +8 -0
- data/app/components/source_monitor/status_badge_component.rb +96 -0
- data/app/controllers/concerns/source_monitor/sanitizes_search_params.rb +4 -0
- data/app/controllers/concerns/source_monitor/set_source.rb +13 -0
- data/app/controllers/source_monitor/application_controller.rb +17 -0
- data/app/controllers/source_monitor/bulk_scrape_enablements_controller.rb +6 -10
- data/app/controllers/source_monitor/dashboard_controller.rb +5 -1
- data/app/controllers/source_monitor/import_history_dismissals_controller.rb +1 -1
- data/app/controllers/source_monitor/import_sessions_controller.rb +30 -9
- data/app/controllers/source_monitor/item_scrapes_controller.rb +70 -0
- data/app/controllers/source_monitor/items_controller.rb +2 -69
- data/app/controllers/source_monitor/source_bulk_scrapes_controller.rb +1 -4
- data/app/controllers/source_monitor/source_favicon_fetches_controller.rb +2 -12
- data/app/controllers/source_monitor/source_fetches_controller.rb +1 -6
- data/app/controllers/source_monitor/source_health_checks_controller.rb +9 -16
- data/app/controllers/source_monitor/source_health_resets_controller.rb +1 -6
- data/app/controllers/source_monitor/source_retries_controller.rb +1 -6
- data/app/controllers/source_monitor/source_scrape_tests_controller.rb +2 -4
- data/app/controllers/source_monitor/source_turbo_responses.rb +1 -3
- data/app/controllers/source_monitor/sources_controller.rb +15 -20
- data/app/helpers/source_monitor/application_helper.rb +15 -31
- data/app/helpers/source_monitor/health_badge_helper.rb +8 -0
- data/app/jobs/source_monitor/download_content_images_job.rb +1 -59
- data/app/jobs/source_monitor/favicon_fetch_job.rb +1 -58
- data/app/jobs/source_monitor/fetch_feed_job.rb +2 -52
- data/app/jobs/source_monitor/import_opml_job.rb +6 -145
- data/app/jobs/source_monitor/import_session_health_check_job.rb +15 -76
- data/app/jobs/source_monitor/item_cleanup_job.rb +5 -0
- data/app/jobs/source_monitor/log_cleanup_job.rb +13 -2
- data/app/jobs/source_monitor/schedule_fetches_job.rb +8 -0
- data/app/jobs/source_monitor/scrape_item_job.rb +6 -52
- data/app/jobs/source_monitor/source_health_check_job.rb +1 -72
- data/app/models/concerns/source_monitor/loggable.rb +12 -0
- data/app/models/source_monitor/fetch_log.rb +0 -8
- data/app/models/source_monitor/health_check_log.rb +0 -8
- data/app/models/source_monitor/import_history.rb +14 -0
- data/app/models/source_monitor/import_session.rb +2 -0
- data/app/models/source_monitor/item.rb +15 -0
- data/app/models/source_monitor/item_content.rb +4 -3
- data/app/models/source_monitor/scrape_log.rb +4 -6
- data/app/models/source_monitor/source.rb +28 -19
- data/app/presenters/source_monitor/base_presenter.rb +19 -0
- data/app/presenters/source_monitor/source_details_presenter.rb +61 -0
- data/app/presenters/source_monitor/sources_filter_presenter.rb +61 -0
- data/app/views/source_monitor/dashboard/_recent_activity.html.erb +3 -3
- data/app/views/source_monitor/dashboard/_stat_card.html.erb +2 -1
- data/app/views/source_monitor/dashboard/_stats.html.erb +5 -7
- data/app/views/source_monitor/items/_details.html.erb +11 -14
- data/app/views/source_monitor/items/index.html.erb +10 -35
- data/app/views/source_monitor/logs/index.html.erb +20 -41
- data/app/views/source_monitor/shared/_form_errors.html.erb +14 -0
- data/app/views/source_monitor/source_scrape_tests/_result.html.erb +1 -29
- data/app/views/source_monitor/source_scrape_tests/_result_content.html.erb +33 -0
- data/app/views/source_monitor/source_scrape_tests/show.html.erb +1 -29
- data/app/views/source_monitor/sources/_bulk_scrape_enable_modal.html.erb +2 -2
- data/app/views/source_monitor/sources/_bulk_scrape_modal.html.erb +7 -5
- data/app/views/source_monitor/sources/_details.html.erb +24 -52
- data/app/views/source_monitor/sources/_health_status_badge.html.erb +4 -6
- data/app/views/source_monitor/sources/_row.html.erb +7 -18
- data/app/views/source_monitor/sources/edit.html.erb +1 -10
- data/app/views/source_monitor/sources/index.html.erb +26 -46
- data/app/views/source_monitor/sources/new.html.erb +1 -10
- data/config/routes.rb +1 -1
- data/db/migrate/20260313120000_add_composite_indexes_to_log_tables.rb +14 -0
- data/db/migrate/20260314120000_align_health_status_default.rb +11 -0
- data/lib/source_monitor/analytics/sources_index_metrics.rb +15 -0
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +10 -4
- data/lib/source_monitor/dashboard/turbo_broadcaster.rb +21 -5
- data/lib/source_monitor/favicons/fetcher.rb +86 -0
- data/lib/source_monitor/fetching/cloudflare_bypass.rb +14 -5
- data/lib/source_monitor/fetching/completion/event_publisher.rb +12 -0
- data/lib/source_monitor/fetching/completion/follow_up_handler.rb +15 -2
- data/lib/source_monitor/fetching/completion/retention_handler.rb +11 -3
- data/lib/source_monitor/fetching/feed_fetcher.rb +2 -21
- data/lib/source_monitor/fetching/fetch_runner.rb +12 -3
- data/lib/source_monitor/fetching/retry_orchestrator.rb +102 -0
- data/lib/source_monitor/fetching/stalled_fetch_reconciler.rb +9 -0
- data/lib/source_monitor/health/source_health_check_orchestrator.rb +95 -0
- data/lib/source_monitor/health.rb +1 -0
- data/lib/source_monitor/images/downloader.rb +6 -7
- data/lib/source_monitor/images/processor.rb +98 -0
- data/lib/source_monitor/import_sessions/health_check_updater.rb +95 -0
- data/lib/source_monitor/import_sessions/opml_importer.rb +163 -0
- data/lib/source_monitor/items/item_creator.rb +0 -21
- data/lib/source_monitor/logs/query.rb +20 -0
- data/lib/source_monitor/queries/scrape_candidates_query.rb +30 -0
- data/lib/source_monitor/queries.rb +7 -0
- data/lib/source_monitor/scheduler.rb +5 -0
- data/lib/source_monitor/scraping/bulk_result_presenter.rb +11 -8
- data/lib/source_monitor/scraping/runner.rb +52 -0
- data/lib/source_monitor/scraping/scheduler.rb +5 -0
- data/lib/source_monitor/scraping/state.rb +4 -2
- data/lib/source_monitor/security/parameter_sanitizer.rb +7 -0
- data/lib/source_monitor/version.rb +1 -1
- data/lib/source_monitor.rb +7 -0
- data/source_monitor.gemspec +1 -0
- metadata +47 -1
|
@@ -3,20 +3,14 @@
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class BulkScrapeEnablementsController < ApplicationController
|
|
5
5
|
def create
|
|
6
|
-
source_ids =
|
|
6
|
+
source_ids = enablement_params[:source_ids]
|
|
7
7
|
|
|
8
8
|
if source_ids.empty?
|
|
9
9
|
handle_empty_selection
|
|
10
10
|
return
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
updated_count = sources.update_all(
|
|
15
|
-
scraping_enabled: true,
|
|
16
|
-
auto_scrape: true,
|
|
17
|
-
scraper_adapter: default_adapter,
|
|
18
|
-
updated_at: Time.current
|
|
19
|
-
)
|
|
13
|
+
updated_count = Source.enable_scraping!(source_ids)
|
|
20
14
|
|
|
21
15
|
respond_to do |format|
|
|
22
16
|
format.turbo_stream do
|
|
@@ -37,8 +31,10 @@ module SourceMonitor
|
|
|
37
31
|
|
|
38
32
|
private
|
|
39
33
|
|
|
40
|
-
def
|
|
41
|
-
|
|
34
|
+
def enablement_params
|
|
35
|
+
raw_ids = Array(params.dig(:bulk_scrape_enablement, :source_ids))
|
|
36
|
+
ids = raw_ids.map(&:to_i).reject(&:zero?)
|
|
37
|
+
{ source_ids: ids }
|
|
42
38
|
end
|
|
43
39
|
|
|
44
40
|
def handle_empty_selection
|
|
@@ -30,7 +30,11 @@ module SourceMonitor
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
def schedule_pages_params
|
|
33
|
-
params.fetch(:schedule_pages, {})
|
|
33
|
+
raw = params.fetch(:schedule_pages, {})
|
|
34
|
+
return {} unless raw.respond_to?(:permit)
|
|
35
|
+
|
|
36
|
+
permitted_keys = raw.keys.select { |k| k.to_s.match?(/\Apage_\d+\z/) }
|
|
37
|
+
raw.permit(*permitted_keys).to_h
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class ImportHistoryDismissalsController < ApplicationController
|
|
5
5
|
def create
|
|
6
|
-
import_history = ImportHistory.find(params[:import_history_id])
|
|
6
|
+
import_history = ImportHistory.where(user_id: source_monitor_current_user&.id).find(params[:import_history_id])
|
|
7
7
|
import_history.update!(dismissed_at: Time.current)
|
|
8
8
|
|
|
9
9
|
respond_to do |format|
|
|
@@ -12,11 +12,30 @@ module SourceMonitor
|
|
|
12
12
|
include SourceMonitor::ImportSessions::HealthCheckManagement
|
|
13
13
|
include SourceMonitor::ImportSessions::BulkConfiguration
|
|
14
14
|
|
|
15
|
+
STEP_HANDLERS = {
|
|
16
|
+
"upload" => :handle_upload_step,
|
|
17
|
+
"preview" => :handle_preview_step,
|
|
18
|
+
"health_check" => :handle_health_check_step,
|
|
19
|
+
"configure" => :handle_configure_step,
|
|
20
|
+
"confirm" => :handle_confirm_step
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
STEP_CONTEXTS = {
|
|
24
|
+
"preview" => :prepare_preview_context,
|
|
25
|
+
"health_check" => :prepare_health_check_context,
|
|
26
|
+
"configure" => :prepare_configure_context,
|
|
27
|
+
"confirm" => :prepare_confirm_context
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
15
30
|
before_action :ensure_current_user!
|
|
16
31
|
before_action :set_import_session, only: %i[show update destroy]
|
|
17
32
|
before_action :authorize_import_session!, only: %i[show update destroy]
|
|
18
33
|
before_action :set_wizard_step, only: %i[show update]
|
|
19
34
|
|
|
35
|
+
# The OPML import wizard requires a persisted ImportSession record to track
|
|
36
|
+
# state across steps (file upload, preview, health check, configure, confirm).
|
|
37
|
+
# Visiting "new" immediately creates a session and redirects to the first step,
|
|
38
|
+
# so there is no separate form -- the wizard IS the form.
|
|
20
39
|
def new
|
|
21
40
|
create
|
|
22
41
|
end
|
|
@@ -31,20 +50,15 @@ module SourceMonitor
|
|
|
31
50
|
end
|
|
32
51
|
|
|
33
52
|
def show
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
prepare_configure_context if @current_step == "configure"
|
|
37
|
-
prepare_confirm_context if @current_step == "confirm"
|
|
53
|
+
context_method = STEP_CONTEXTS[@current_step]
|
|
54
|
+
send(context_method) if context_method
|
|
38
55
|
persist_step!
|
|
39
56
|
render :show
|
|
40
57
|
end
|
|
41
58
|
|
|
42
59
|
def update
|
|
43
|
-
|
|
44
|
-
return
|
|
45
|
-
return handle_health_check_step if @current_step == "health_check"
|
|
46
|
-
return handle_configure_step if @current_step == "configure"
|
|
47
|
-
return handle_confirm_step if @current_step == "confirm"
|
|
60
|
+
handler = STEP_HANDLERS[@current_step]
|
|
61
|
+
return send(handler) if handler
|
|
48
62
|
|
|
49
63
|
@import_session.update!(session_attributes)
|
|
50
64
|
@current_step = target_step
|
|
@@ -245,6 +259,13 @@ module SourceMonitor
|
|
|
245
259
|
return @fallback_user_id
|
|
246
260
|
end
|
|
247
261
|
|
|
262
|
+
# Only create guest users in development/test. An engine should never
|
|
263
|
+
# create records in host-app tables in production.
|
|
264
|
+
unless Rails.env.local?
|
|
265
|
+
@fallback_user_id = nil
|
|
266
|
+
return @fallback_user_id
|
|
267
|
+
end
|
|
268
|
+
|
|
248
269
|
@fallback_user_id = create_guest_user&.id
|
|
249
270
|
rescue StandardError
|
|
250
271
|
@fallback_user_id = nil
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SourceMonitor
|
|
4
|
+
class ItemScrapesController < ApplicationController
|
|
5
|
+
include ActionView::RecordIdentifier
|
|
6
|
+
|
|
7
|
+
before_action :set_item
|
|
8
|
+
|
|
9
|
+
def create
|
|
10
|
+
log_manual_scrape("controller:start", extra: { format: request.format })
|
|
11
|
+
|
|
12
|
+
enqueue_result = SourceMonitor::Scraping::Enqueuer.enqueue(item: @item, reason: :manual)
|
|
13
|
+
log_manual_scrape("controller:enqueue_result", extra: { status: enqueue_result.status, message: enqueue_result.message })
|
|
14
|
+
flash_key, flash_message = scrape_flash_payload(enqueue_result)
|
|
15
|
+
status = enqueue_result.failure? ? :unprocessable_entity : :ok
|
|
16
|
+
|
|
17
|
+
respond_to do |format|
|
|
18
|
+
format.turbo_stream do
|
|
19
|
+
responder = SourceMonitor::TurboStreams::StreamResponder.new
|
|
20
|
+
|
|
21
|
+
if enqueue_result.enqueued? || enqueue_result.already_enqueued?
|
|
22
|
+
@item.reload
|
|
23
|
+
responder.replace_details(
|
|
24
|
+
@item,
|
|
25
|
+
partial: "source_monitor/items/details_wrapper",
|
|
26
|
+
locals: { item: @item }
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if flash_message
|
|
31
|
+
level = flash_key == :notice ? :info : :error
|
|
32
|
+
responder.toast(message: flash_message, level:, delay_ms: toast_delay_for(level))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
render turbo_stream: responder.render(view_context), status: status
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
format.html do
|
|
39
|
+
if flash_key && flash_message
|
|
40
|
+
redirect_to source_monitor.item_path(@item), flash: { flash_key => flash_message }
|
|
41
|
+
else
|
|
42
|
+
redirect_to source_monitor.item_path(@item)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def set_item
|
|
51
|
+
@item = Item.active.includes(:source, :item_content).find(params[:item_id])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def scrape_flash_payload(result)
|
|
55
|
+
case result.status
|
|
56
|
+
when :enqueued
|
|
57
|
+
[ :notice, "Scrape has been enqueued and will run shortly." ]
|
|
58
|
+
when :already_enqueued
|
|
59
|
+
[ :notice, result.message ]
|
|
60
|
+
else
|
|
61
|
+
[ :alert, result.message || "Unable to enqueue scrape for this item." ]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def log_manual_scrape(stage, extra: {})
|
|
66
|
+
payload = { stage:, item_id: @item&.id }.merge(extra.compact)
|
|
67
|
+
Rails.logger.info("[SourceMonitor::ManualScrape] #{payload.to_json}")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -10,7 +10,7 @@ module SourceMonitor
|
|
|
10
10
|
PER_PAGE = 25
|
|
11
11
|
SEARCH_FIELD = :title_or_summary_or_url_or_source_name_cont
|
|
12
12
|
|
|
13
|
-
before_action :set_item, only:
|
|
13
|
+
before_action :set_item, only: :show
|
|
14
14
|
before_action :load_scrape_context, only: :show
|
|
15
15
|
|
|
16
16
|
def index
|
|
@@ -24,6 +24,7 @@ module SourceMonitor
|
|
|
24
24
|
per_page: PER_PAGE
|
|
25
25
|
).paginate
|
|
26
26
|
|
|
27
|
+
@paginator = paginator
|
|
27
28
|
@items = paginator.records
|
|
28
29
|
@page = paginator.page
|
|
29
30
|
@has_next_page = paginator.has_next_page
|
|
@@ -36,54 +37,6 @@ module SourceMonitor
|
|
|
36
37
|
def show
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
# TODO: Extract to ItemScrapesController (CRUD-only convention).
|
|
40
|
-
# Deferred to avoid view/route churn in a cleanup phase.
|
|
41
|
-
def scrape
|
|
42
|
-
log_manual_scrape("controller:start", item: @item, extra: { format: request.format })
|
|
43
|
-
|
|
44
|
-
enqueue_result = SourceMonitor::Scraping::Enqueuer.enqueue(item: @item, reason: :manual)
|
|
45
|
-
log_manual_scrape(
|
|
46
|
-
"controller:enqueue_result",
|
|
47
|
-
item: @item,
|
|
48
|
-
extra: { status: enqueue_result.status, message: enqueue_result.message }
|
|
49
|
-
)
|
|
50
|
-
flash_key, flash_message = scrape_flash_payload(enqueue_result)
|
|
51
|
-
status = enqueue_result.failure? ? :unprocessable_entity : :ok
|
|
52
|
-
|
|
53
|
-
respond_to do |format|
|
|
54
|
-
format.turbo_stream do
|
|
55
|
-
log_manual_scrape("controller:respond_turbo", item: @item, extra: { status: status })
|
|
56
|
-
|
|
57
|
-
responder = SourceMonitor::TurboStreams::StreamResponder.new
|
|
58
|
-
|
|
59
|
-
if enqueue_result.enqueued? || enqueue_result.already_enqueued?
|
|
60
|
-
@item.reload
|
|
61
|
-
responder.replace_details(
|
|
62
|
-
@item,
|
|
63
|
-
partial: "source_monitor/items/details_wrapper",
|
|
64
|
-
locals: { item: @item }
|
|
65
|
-
)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
if flash_message
|
|
69
|
-
level = flash_key == :notice ? :info : :error
|
|
70
|
-
responder.toast(message: flash_message, level:, delay_ms: toast_delay_for(level))
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
render turbo_stream: responder.render(view_context), status: status
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
format.html do
|
|
77
|
-
log_manual_scrape("controller:respond_html", item: @item)
|
|
78
|
-
if flash_key && flash_message
|
|
79
|
-
redirect_to source_monitor.item_path(@item), flash: { flash_key => flash_message }
|
|
80
|
-
else
|
|
81
|
-
redirect_to source_monitor.item_path(@item)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
40
|
private
|
|
88
41
|
|
|
89
42
|
def set_item
|
|
@@ -94,25 +47,5 @@ module SourceMonitor
|
|
|
94
47
|
@recent_scrape_logs = @item.scrape_logs.order(started_at: :desc).limit(5)
|
|
95
48
|
@latest_scrape_log = @recent_scrape_logs.first
|
|
96
49
|
end
|
|
97
|
-
|
|
98
|
-
def scrape_flash_payload(result)
|
|
99
|
-
case result.status
|
|
100
|
-
when :enqueued
|
|
101
|
-
[ :notice, "Scrape has been enqueued and will run shortly." ]
|
|
102
|
-
when :already_enqueued
|
|
103
|
-
[ :notice, result.message ]
|
|
104
|
-
else
|
|
105
|
-
[ :alert, result.message || "Unable to enqueue scrape for this item." ]
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def log_manual_scrape(stage, item:, extra: {})
|
|
110
|
-
return unless defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
111
|
-
|
|
112
|
-
payload = { stage:, item_id: item&.id }.merge(extra.compact)
|
|
113
|
-
Rails.logger.info("[SourceMonitor::ManualScrape] #{payload.to_json}")
|
|
114
|
-
rescue StandardError
|
|
115
|
-
nil
|
|
116
|
-
end
|
|
117
50
|
end
|
|
118
51
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class SourceBulkScrapesController < ApplicationController
|
|
5
5
|
include SourceMonitor::SourceTurboResponses
|
|
6
|
+
include SourceMonitor::SetSource
|
|
6
7
|
|
|
7
8
|
ITEMS_PREVIEW_LIMIT = SourceMonitor::Scraping::BulkSourceScraper::DEFAULT_PREVIEW_LIMIT
|
|
8
9
|
|
|
@@ -24,10 +25,6 @@ module SourceMonitor
|
|
|
24
25
|
|
|
25
26
|
private
|
|
26
27
|
|
|
27
|
-
def set_source
|
|
28
|
-
@source = Source.find(params[:source_id])
|
|
29
|
-
end
|
|
30
|
-
|
|
31
28
|
def bulk_scrape_params
|
|
32
29
|
params.fetch(:bulk_scrape, {}).permit(:selection)
|
|
33
30
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class SourceFaviconFetchesController < ApplicationController
|
|
5
5
|
include SourceMonitor::SourceTurboResponses
|
|
6
|
+
include SourceMonitor::SetSource
|
|
6
7
|
|
|
7
8
|
before_action :set_source
|
|
8
9
|
|
|
@@ -16,23 +17,12 @@ module SourceMonitor
|
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
# Clear cooldown so the job doesn't skip this attempt
|
|
19
|
-
|
|
20
|
+
@source.clear_favicon_cooldown!
|
|
20
21
|
|
|
21
22
|
SourceMonitor::FaviconFetchJob.perform_later(@source.id)
|
|
22
23
|
render_fetch_enqueue_response("Favicon fetch has been enqueued.")
|
|
23
24
|
rescue StandardError => error
|
|
24
25
|
handle_fetch_failure(error, prefix: "Favicon fetch")
|
|
25
26
|
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def set_source
|
|
30
|
-
@source = Source.find(params[:source_id])
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def clear_favicon_cooldown(source)
|
|
34
|
-
metadata = (source.metadata || {}).except("favicon_last_attempted_at")
|
|
35
|
-
source.update_column(:metadata, metadata)
|
|
36
|
-
end
|
|
37
27
|
end
|
|
38
28
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class SourceFetchesController < ApplicationController
|
|
5
5
|
include SourceMonitor::SourceTurboResponses
|
|
6
|
+
include SourceMonitor::SetSource
|
|
6
7
|
|
|
7
8
|
before_action :set_source
|
|
8
9
|
|
|
@@ -12,11 +13,5 @@ module SourceMonitor
|
|
|
12
13
|
rescue StandardError => error
|
|
13
14
|
handle_fetch_failure(error)
|
|
14
15
|
end
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
def set_source
|
|
19
|
-
@source = Source.find(params[:source_id])
|
|
20
|
-
end
|
|
21
16
|
end
|
|
22
17
|
end
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class SourceHealthChecksController < ApplicationController
|
|
5
5
|
include SourceMonitor::SourceTurboResponses
|
|
6
|
+
include SourceMonitor::SetSource
|
|
7
|
+
|
|
8
|
+
PROCESSING_BADGE = {
|
|
9
|
+
label: "Processing",
|
|
10
|
+
classes: "bg-blue-100 text-blue-700",
|
|
11
|
+
show_spinner: true,
|
|
12
|
+
status: "processing"
|
|
13
|
+
}.freeze
|
|
6
14
|
|
|
7
15
|
before_action :set_source
|
|
8
16
|
|
|
@@ -10,25 +18,10 @@ module SourceMonitor
|
|
|
10
18
|
SourceMonitor::SourceHealthCheckJob.perform_later(@source.id)
|
|
11
19
|
render_fetch_enqueue_response(
|
|
12
20
|
"Health check enqueued",
|
|
13
|
-
health_status_override:
|
|
21
|
+
health_status_override: PROCESSING_BADGE
|
|
14
22
|
)
|
|
15
23
|
rescue StandardError => error
|
|
16
24
|
handle_fetch_failure(error, prefix: "Health check")
|
|
17
25
|
end
|
|
18
|
-
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
def set_source
|
|
22
|
-
@source = Source.find(params[:source_id])
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def processing_badge
|
|
26
|
-
{
|
|
27
|
-
label: "Processing",
|
|
28
|
-
classes: "bg-blue-100 text-blue-700",
|
|
29
|
-
show_spinner: true,
|
|
30
|
-
status: "processing"
|
|
31
|
-
}
|
|
32
|
-
end
|
|
33
26
|
end
|
|
34
27
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class SourceHealthResetsController < ApplicationController
|
|
5
5
|
include SourceMonitor::SourceTurboResponses
|
|
6
|
+
include SourceMonitor::SetSource
|
|
6
7
|
|
|
7
8
|
before_action :set_source
|
|
8
9
|
|
|
@@ -17,11 +18,5 @@ module SourceMonitor
|
|
|
17
18
|
rescue StandardError => error
|
|
18
19
|
handle_fetch_failure(error, prefix: "Health reset")
|
|
19
20
|
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def set_source
|
|
24
|
-
@source = Source.find(params[:source_id])
|
|
25
|
-
end
|
|
26
21
|
end
|
|
27
22
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class SourceRetriesController < ApplicationController
|
|
5
5
|
include SourceMonitor::SourceTurboResponses
|
|
6
|
+
include SourceMonitor::SetSource
|
|
6
7
|
|
|
7
8
|
before_action :set_source
|
|
8
9
|
|
|
@@ -20,11 +21,5 @@ module SourceMonitor
|
|
|
20
21
|
rescue StandardError => error
|
|
21
22
|
handle_fetch_failure(error)
|
|
22
23
|
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
def set_source
|
|
27
|
-
@source = Source.find(params[:source_id])
|
|
28
|
-
end
|
|
29
24
|
end
|
|
30
25
|
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module SourceMonitor
|
|
4
4
|
class SourceScrapeTestsController < ApplicationController
|
|
5
|
+
include SourceMonitor::SetSource
|
|
6
|
+
|
|
5
7
|
before_action :set_source
|
|
6
8
|
|
|
7
9
|
def create
|
|
@@ -38,10 +40,6 @@ module SourceMonitor
|
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
41
|
-
def set_source
|
|
42
|
-
@source = Source.find(params[:source_id])
|
|
43
|
-
end
|
|
44
|
-
|
|
45
43
|
def pick_test_item
|
|
46
44
|
@source.items
|
|
47
45
|
.joins(:item_content)
|
|
@@ -107,9 +107,7 @@ module SourceMonitor
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def bulk_scrape_flash_payload(result)
|
|
110
|
-
|
|
111
|
-
presenter = SourceMonitor::Scraping::BulkResultPresenter.new(result:, pluralizer:)
|
|
112
|
-
presenter.to_flash_payload
|
|
110
|
+
SourceMonitor::Scraping::BulkResultPresenter.new(result:).to_flash_payload
|
|
113
111
|
end
|
|
114
112
|
end
|
|
115
113
|
end
|
|
@@ -48,20 +48,22 @@ module SourceMonitor
|
|
|
48
48
|
@item_activity_rates = metrics.item_activity_rates
|
|
49
49
|
|
|
50
50
|
source_ids = @sources.map(&:id)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.group("sourcemon_items.source_id")
|
|
55
|
-
.average(:feed_word_count)
|
|
56
|
-
@avg_scraped_word_counts = base.where.not(scraped_word_count: nil)
|
|
57
|
-
.group("sourcemon_items.source_id")
|
|
58
|
-
.average(:scraped_word_count)
|
|
59
|
-
else
|
|
60
|
-
@avg_feed_word_counts = {}
|
|
61
|
-
@avg_scraped_word_counts = {}
|
|
62
|
-
end
|
|
51
|
+
word_counts = metrics.word_count_averages(source_ids)
|
|
52
|
+
@avg_feed_word_counts = word_counts[:feed]
|
|
53
|
+
@avg_scraped_word_counts = word_counts[:scraped]
|
|
63
54
|
|
|
64
55
|
@scrape_candidate_ids = compute_scrape_candidate_ids
|
|
56
|
+
|
|
57
|
+
# Row partial preload requirements (V3): item_activity_rates,
|
|
58
|
+
# avg_feed_word_counts, avg_scraped_word_counts are pre-computed above
|
|
59
|
+
# and passed as locals to avoid N+1 queries in _row.html.erb.
|
|
60
|
+
adapter_options = Source.distinct.where.not(scraper_adapter: [ nil, "" ]).order(:scraper_adapter).pluck(:scraper_adapter)
|
|
61
|
+
@filter_presenter = SourceMonitor::SourcesFilterPresenter.new(
|
|
62
|
+
search_params: @search_params,
|
|
63
|
+
search_term: @search_term,
|
|
64
|
+
fetch_interval_filter: @fetch_interval_filter,
|
|
65
|
+
adapter_options: adapter_options
|
|
66
|
+
)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def show
|
|
@@ -131,7 +133,7 @@ module SourceMonitor
|
|
|
131
133
|
search_params:
|
|
132
134
|
)
|
|
133
135
|
|
|
134
|
-
redirect_location = safe_redirect_path(params[:redirect_to])
|
|
136
|
+
redirect_location = SourceMonitor::Security::ParameterSanitizer.safe_redirect_path(params[:redirect_to])
|
|
135
137
|
|
|
136
138
|
responder = SourceMonitor::TurboStreams::StreamResponder.new
|
|
137
139
|
presenter = SourceMonitor::Sources::TurboStreamPresenter.new(source: @source, responder:)
|
|
@@ -168,13 +170,6 @@ module SourceMonitor
|
|
|
168
170
|
SourceMonitor::Sources::Params.sanitize(params)
|
|
169
171
|
end
|
|
170
172
|
|
|
171
|
-
def safe_redirect_path(raw_value)
|
|
172
|
-
return if raw_value.blank?
|
|
173
|
-
|
|
174
|
-
sanitized = SourceMonitor::Security::ParameterSanitizer.sanitize(raw_value.to_s)
|
|
175
|
-
sanitized.start_with?("/") ? sanitized : nil
|
|
176
|
-
end
|
|
177
|
-
|
|
178
173
|
def handle_destroy_failure(search_params, error_message)
|
|
179
174
|
respond_to do |format|
|
|
180
175
|
format.turbo_stream do
|
|
@@ -35,6 +35,16 @@ module SourceMonitor
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
def compact_blank_hash(hash)
|
|
39
|
+
return {} if hash.blank?
|
|
40
|
+
|
|
41
|
+
if hash.respond_to?(:compact_blank)
|
|
42
|
+
hash.compact_blank
|
|
43
|
+
else
|
|
44
|
+
hash.reject { |_key, value| value.respond_to?(:blank?) ? value.blank? : value.nil? }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
38
48
|
def fetch_interval_bucket_path(bucket, search_params, selected: false)
|
|
39
49
|
query = fetch_interval_bucket_query(bucket, search_params, selected: selected)
|
|
40
50
|
route_helpers = SourceMonitor::Engine.routes.url_helpers
|
|
@@ -62,11 +72,7 @@ module SourceMonitor
|
|
|
62
72
|
updated
|
|
63
73
|
end
|
|
64
74
|
|
|
65
|
-
|
|
66
|
-
query.compact_blank
|
|
67
|
-
else
|
|
68
|
-
query.reject { |_key, value| value.respond_to?(:blank?) ? value.blank? : value.nil? }
|
|
69
|
-
end
|
|
75
|
+
compact_blank_hash(query)
|
|
70
76
|
end
|
|
71
77
|
|
|
72
78
|
def fetch_interval_filter_label(bucket, filter)
|
|
@@ -181,18 +187,10 @@ module SourceMonitor
|
|
|
181
187
|
async_status_badge(status)
|
|
182
188
|
end
|
|
183
189
|
|
|
184
|
-
# Helper to render the loading spinner SVG
|
|
190
|
+
# Helper to render the loading spinner SVG via IconComponent.
|
|
191
|
+
# Accepts a custom css_class to override the default spinner styling.
|
|
185
192
|
def loading_spinner_svg(css_class: "mr-1 h-4 w-4 animate-spin text-blue-500")
|
|
186
|
-
|
|
187
|
-
class: css_class,
|
|
188
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
189
|
-
fill: "none",
|
|
190
|
-
viewBox: "0 0 24 24",
|
|
191
|
-
aria: { hidden: "true" }
|
|
192
|
-
) do
|
|
193
|
-
concat tag.circle(class: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", stroke_width: "4")
|
|
194
|
-
concat tag.path(class: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 0 1 8-8v4a4 4 0 0 0-4 4H4z")
|
|
195
|
-
end
|
|
193
|
+
render SourceMonitor::IconComponent.new(:spinner, size: nil, css_class: css_class)
|
|
196
194
|
end
|
|
197
195
|
|
|
198
196
|
def formatted_setting_value(value)
|
|
@@ -276,21 +274,7 @@ module SourceMonitor
|
|
|
276
274
|
private
|
|
277
275
|
|
|
278
276
|
def external_link_icon
|
|
279
|
-
|
|
280
|
-
class: "inline-block h-3 w-3 text-slate-400",
|
|
281
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
282
|
-
fill: "none",
|
|
283
|
-
viewBox: "0 0 24 24",
|
|
284
|
-
stroke_width: "2",
|
|
285
|
-
stroke: "currentColor",
|
|
286
|
-
aria: { hidden: "true" }
|
|
287
|
-
) do
|
|
288
|
-
tag.path(
|
|
289
|
-
stroke_linecap: "round",
|
|
290
|
-
stroke_linejoin: "round",
|
|
291
|
-
d: "M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
|
292
|
-
)
|
|
293
|
-
end
|
|
277
|
+
render SourceMonitor::IconComponent.new(:external_link, size: nil, css_class: "inline-block h-3 w-3 text-slate-400")
|
|
294
278
|
end
|
|
295
279
|
|
|
296
280
|
def derive_item_scrape_status(item:, source: nil)
|
|
@@ -40,6 +40,14 @@ module SourceMonitor
|
|
|
40
40
|
path: helpers.source_health_check_path(source),
|
|
41
41
|
method: :post,
|
|
42
42
|
data: { testid: "source-health-action-health_check" }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: :reset,
|
|
46
|
+
label: "Reset to Active Status",
|
|
47
|
+
description: "Clears failure count, backoff timers, and auto-pause state so the source resumes normal fetching.",
|
|
48
|
+
path: helpers.source_health_reset_path(source),
|
|
49
|
+
method: :post,
|
|
50
|
+
data: { testid: "source-health-action-reset" }
|
|
43
51
|
}
|
|
44
52
|
]
|
|
45
53
|
else
|