source_monitor 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +1 -0
- data/.claude/skills/sm-configure/SKILL.md +8 -1
- data/.claude/skills/sm-configure/reference/configuration-reference.md +11 -0
- data/.claude/skills/sm-event-handler/SKILL.md +1 -1
- data/.claude/skills/sm-event-handler/reference/events-api.md +1 -1
- data/.claude/skills/sm-host-setup/SKILL.md +13 -3
- data/.claude/skills/sm-host-setup/reference/initializer-template.md +11 -0
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +9 -1
- data/.claude/skills/sm-upgrade/reference/version-history.md +12 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +1 -1
- data/README.md +3 -3
- data/VERSION +1 -1
- data/app/assets/builds/source_monitor/application.css +4 -0
- data/app/controllers/source_monitor/application_controller.rb +73 -14
- data/app/controllers/source_monitor/bulk_scrape_enablements_controller.rb +1 -1
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +3 -1
- data/app/controllers/source_monitor/import_sessions_controller.rb +118 -72
- data/app/controllers/source_monitor/sources_controller.rb +4 -18
- data/app/models/source_monitor/source.rb +1 -1
- data/app/views/layouts/source_monitor/application.html.erb +6 -0
- data/docs/configuration.md +18 -1
- data/docs/deployment.md +1 -1
- data/docs/goals/engine-hardening/.goalbuddy-board/app.js +543 -0
- data/docs/goals/engine-hardening/.goalbuddy-board/goalbuddy-mark.png +0 -0
- data/docs/goals/engine-hardening/.goalbuddy-board/index.html +111 -0
- data/docs/goals/engine-hardening/.goalbuddy-board/styles.css +991 -0
- data/docs/goals/engine-hardening/goal.md +97 -0
- data/docs/goals/engine-hardening/notes/T001-spec-validation.md +37 -0
- data/docs/goals/engine-hardening/state.yaml +324 -0
- data/docs/setup.md +3 -3
- data/docs/upgrade.md +41 -0
- data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +10 -0
- data/lib/source_monitor/analytics/scrape_recommendations.rb +21 -2
- data/lib/source_monitor/configuration/authentication_settings.rb +5 -1
- data/lib/source_monitor/fetching/feed_fetcher/failure_outcome.rb +85 -0
- data/lib/source_monitor/fetching/feed_fetcher/success_outcome.rb +85 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +27 -88
- data/lib/source_monitor/fetching/fetch_runner.rb +12 -5
- data/lib/source_monitor/import_sessions/wizard.rb +612 -0
- data/lib/source_monitor/items/batch_item_creator.rb +7 -6
- data/lib/source_monitor/items/item_creator.rb +7 -14
- data/lib/source_monitor/items/normalized_entry.rb +61 -0
- data/lib/source_monitor/security/authentication.rb +10 -0
- data/lib/source_monitor/version.rb +1 -1
- data/lib/source_monitor.rb +2 -0
- data/source_monitor.gemspec +7 -2
- metadata +12 -68
- data/.claude/agent-memory/vbw-vbw-debugger/MEMORY.md +0 -15
- data/.claude/agent-memory/vbw-vbw-dev/MEMORY.md +0 -34
- data/.claude/agent-memory/vbw-vbw-lead/MEMORY.md +0 -49
- data/.claude/agents/rails-concern.md +0 -464
- data/.claude/agents/rails-controller.md +0 -424
- data/.claude/agents/rails-hotwire.md +0 -446
- data/.claude/agents/rails-implement.md +0 -374
- data/.claude/agents/rails-job.md +0 -334
- data/.claude/agents/rails-lint.md +0 -294
- data/.claude/agents/rails-mailer.md +0 -371
- data/.claude/agents/rails-migration.md +0 -449
- data/.claude/agents/rails-model.md +0 -420
- data/.claude/agents/rails-policy.md +0 -443
- data/.claude/agents/rails-presenter.md +0 -427
- data/.claude/agents/rails-query.md +0 -412
- data/.claude/agents/rails-review.md +0 -490
- data/.claude/agents/rails-service.md +0 -458
- data/.claude/agents/rails-state-records.md +0 -465
- data/.claude/agents/rails-tdd.md +0 -314
- data/.claude/agents/rails-test.md +0 -441
- data/.claude/agents/rails-view-component.md +0 -418
- data/.claude/commands/rails-audit.md +0 -77
- data/.claude/commands/release.md +0 -366
- data/.claude/hooks/block-secrets.sh +0 -52
- data/.claude/settings.json +0 -85
- data/.claude/skills/action-cable-patterns/SKILL.md +0 -296
- data/.claude/skills/action-mailer-patterns/SKILL.md +0 -295
- data/.claude/skills/active-storage-setup/SKILL.md +0 -311
- data/.claude/skills/api-versioning/SKILL.md +0 -294
- data/.claude/skills/authentication-flow/SKILL.md +0 -335
- data/.claude/skills/authentication-flow/reference/current.md +0 -248
- data/.claude/skills/authentication-flow/reference/passwordless.md +0 -253
- data/.claude/skills/authentication-flow/reference/sessions.md +0 -201
- data/.claude/skills/authorization-pundit/SKILL.md +0 -462
- data/.claude/skills/caching-strategies/SKILL.md +0 -350
- data/.claude/skills/database-migrations/SKILL.md +0 -354
- data/.claude/skills/form-object-patterns/SKILL.md +0 -399
- data/.claude/skills/hotwire-patterns/SKILL.md +0 -247
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +0 -307
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +0 -112
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +0 -158
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +0 -218
- data/.claude/skills/i18n-patterns/SKILL.md +0 -320
- data/.claude/skills/install/SKILL.md +0 -367
- data/.claude/skills/performance-optimization/SKILL.md +0 -311
- data/.claude/skills/rails-architecture/SKILL.md +0 -259
- data/.claude/skills/rails-architecture/reference/error-handling.md +0 -333
- data/.claude/skills/rails-architecture/reference/event-tracking.md +0 -142
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +0 -417
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +0 -152
- data/.claude/skills/rails-architecture/reference/query-patterns.md +0 -342
- data/.claude/skills/rails-architecture/reference/service-patterns.md +0 -286
- data/.claude/skills/rails-architecture/reference/state-records.md +0 -250
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +0 -326
- data/.claude/skills/rails-concern/SKILL.md +0 -399
- data/.claude/skills/rails-controller/SKILL.md +0 -336
- data/.claude/skills/rails-model-generator/SKILL.md +0 -321
- data/.claude/skills/rails-model-generator/reference/validations.md +0 -298
- data/.claude/skills/rails-presenter/SKILL.md +0 -274
- data/.claude/skills/rails-query-object/SKILL.md +0 -289
- data/.claude/skills/rails-service-object/SKILL.md +0 -349
- data/.claude/skills/solid-queue-setup/SKILL.md +0 -307
- data/.claude/skills/tdd-cycle/SKILL.md +0 -359
- data/.claude/skills/viewcomponent-patterns/SKILL.md +0 -333
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +0 -187
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +0 -112
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +0 -130
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "uri"
|
|
5
|
-
require "source_monitor/import_sessions/entry_normalizer"
|
|
3
|
+
require "source_monitor/import_sessions/wizard"
|
|
6
4
|
require "source_monitor/sources/params"
|
|
7
5
|
|
|
8
6
|
module SourceMonitor
|
|
9
7
|
class ImportSessionsController < ApplicationController
|
|
10
|
-
include SourceMonitor::ImportSessions::OpmlParser
|
|
11
|
-
include SourceMonitor::ImportSessions::EntryAnnotation
|
|
12
|
-
include SourceMonitor::ImportSessions::HealthCheckManagement
|
|
13
8
|
include SourceMonitor::ImportSessions::BulkConfiguration
|
|
14
9
|
|
|
15
10
|
STEP_HANDLERS = {
|
|
@@ -86,92 +81,60 @@ module SourceMonitor
|
|
|
86
81
|
def persist_step!
|
|
87
82
|
return if @import_session.current_step == @current_step
|
|
88
83
|
|
|
89
|
-
deactivate_health_checks
|
|
84
|
+
import_session_wizard.deactivate_health_checks if @current_step != "health_check"
|
|
90
85
|
@import_session.update_column(:current_step, @current_step)
|
|
91
86
|
end
|
|
92
87
|
|
|
93
88
|
def handle_health_check_step
|
|
94
|
-
|
|
95
|
-
@
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
result = import_session_wizard.handle_health_check
|
|
90
|
+
@selected_source_ids = result.selected_source_ids
|
|
91
|
+
|
|
92
|
+
if result.blocked?
|
|
93
|
+
@selection_error = result.selection_error
|
|
94
|
+
apply_health_check_context(result.health_check_context)
|
|
99
95
|
render :show, status: :unprocessable_entity
|
|
100
96
|
return
|
|
101
97
|
end
|
|
102
98
|
|
|
103
|
-
@current_step =
|
|
104
|
-
|
|
105
|
-
@import_session.update_column(:current_step, @current_step) if @import_session.current_step != @current_step
|
|
106
|
-
prepare_health_check_context if @current_step == "health_check"
|
|
99
|
+
@current_step = result.current_step
|
|
100
|
+
apply_health_check_context(result.health_check_context) if @current_step == "health_check"
|
|
107
101
|
redirect_to source_monitor.step_import_session_path(@import_session, step: @current_step), allow_other_host: false
|
|
108
102
|
end
|
|
109
103
|
|
|
110
104
|
def handle_upload_step
|
|
111
|
-
|
|
105
|
+
result = import_session_wizard.handle_upload
|
|
106
|
+
@upload_errors = result.errors
|
|
112
107
|
if @upload_errors.any?
|
|
113
108
|
render :show, status: :unprocessable_entity
|
|
114
109
|
return
|
|
115
110
|
end
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if valid_entries.empty?
|
|
120
|
-
@upload_errors = [ "We couldn't find any valid feeds in that OPML file. Check the file and try again." ]
|
|
121
|
-
@import_session.update!(opml_file_metadata: build_file_metadata, parsed_sources: parsed_entries, current_step: "upload")
|
|
122
|
-
render :show, status: :unprocessable_entity
|
|
123
|
-
return
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
@import_session.update!(
|
|
127
|
-
opml_file_metadata: build_file_metadata.merge("uploaded_at" => Time.current),
|
|
128
|
-
parsed_sources: parsed_entries,
|
|
129
|
-
current_step: target_step
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
@current_step = target_step
|
|
133
|
-
prepare_preview_context(skip_default: true) if @current_step == "preview"
|
|
112
|
+
@current_step = result.current_step
|
|
113
|
+
apply_preview_context(result.preview_context) if @current_step == "preview"
|
|
134
114
|
|
|
135
115
|
respond_to do |format|
|
|
136
116
|
format.turbo_stream { render :show }
|
|
137
117
|
format.html { redirect_to source_monitor.step_import_session_path(@import_session, step: @current_step) }
|
|
138
118
|
end
|
|
139
|
-
rescue UploadError => error
|
|
140
|
-
@upload_errors = [ error.message ]
|
|
141
|
-
render :show, status: :unprocessable_entity
|
|
142
119
|
end
|
|
143
120
|
|
|
144
121
|
def handle_preview_step
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if params.dig(:import_session, :select_all).present?
|
|
148
|
-
@selected_source_ids = selectable_entries.map { |entry| entry[:id] }
|
|
149
|
-
@import_session.update_column(:selected_source_ids, @selected_source_ids)
|
|
150
|
-
valid_ids = @selected_source_ids
|
|
151
|
-
elsif params.dig(:import_session, :select_none).present?
|
|
152
|
-
@selected_source_ids = []
|
|
153
|
-
@import_session.update_column(:selected_source_ids, @selected_source_ids)
|
|
154
|
-
valid_ids = []
|
|
155
|
-
else
|
|
156
|
-
@selected_source_ids = build_selection_from_params
|
|
157
|
-
valid_ids = selectable_entries.index_by { |entry| entry[:id] }.slice(*@selected_source_ids).keys
|
|
158
|
-
@import_session.update!(selected_source_ids: valid_ids)
|
|
159
|
-
end
|
|
122
|
+
result = import_session_wizard.handle_preview
|
|
123
|
+
@selected_source_ids = result.selected_source_ids
|
|
160
124
|
|
|
161
|
-
if
|
|
162
|
-
@selection_error =
|
|
163
|
-
|
|
125
|
+
if result.blocked?
|
|
126
|
+
@selection_error = result.selection_error
|
|
127
|
+
apply_preview_context(result.preview_context)
|
|
164
128
|
render :show, status: :unprocessable_entity
|
|
165
129
|
return
|
|
166
130
|
end
|
|
167
131
|
|
|
168
|
-
@current_step =
|
|
169
|
-
@import_session.update_column(:current_step, @current_step) if @import_session.current_step != @current_step
|
|
132
|
+
@current_step = result.current_step
|
|
170
133
|
|
|
171
134
|
if @current_step == "health_check"
|
|
172
135
|
prepare_health_check_context
|
|
173
136
|
else
|
|
174
|
-
|
|
137
|
+
apply_preview_context(result.preview_context)
|
|
175
138
|
end
|
|
176
139
|
|
|
177
140
|
respond_to do |format|
|
|
@@ -200,31 +163,25 @@ module SourceMonitor
|
|
|
200
163
|
end
|
|
201
164
|
|
|
202
165
|
def handle_confirm_step
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
166
|
+
result = import_session_wizard.handle_confirm
|
|
167
|
+
apply_confirm_context(result)
|
|
168
|
+
|
|
169
|
+
if result.blocked?
|
|
170
|
+
@selection_error = result.selection_error
|
|
208
171
|
render :show, status: :unprocessable_entity
|
|
209
172
|
return
|
|
210
173
|
end
|
|
211
|
-
|
|
212
|
-
user_id: @import_session.user_id,
|
|
213
|
-
bulk_settings: @import_session.bulk_settings
|
|
214
|
-
)
|
|
215
|
-
SourceMonitor::ImportOpmlJob.perform_later(@import_session.id, history.id)
|
|
216
|
-
@import_session.update_column(:current_step, "confirm") if @import_session.current_step != "confirm"
|
|
217
|
-
message = "Import started for #{@selected_entries.size} sources."
|
|
174
|
+
|
|
218
175
|
respond_to do |format|
|
|
219
176
|
format.turbo_stream do
|
|
220
177
|
responder = SourceMonitor::TurboStreams::StreamResponder.new
|
|
221
|
-
responder.toast(message
|
|
178
|
+
responder.toast(message: result.message, level: :success)
|
|
222
179
|
responder.redirect(source_monitor.sources_path)
|
|
223
180
|
render turbo_stream: responder.render(view_context)
|
|
224
181
|
end
|
|
225
182
|
|
|
226
183
|
format.html do
|
|
227
|
-
redirect_to source_monitor.sources_path, notice: message
|
|
184
|
+
redirect_to source_monitor.sources_path, notice: result.message
|
|
228
185
|
end
|
|
229
186
|
end
|
|
230
187
|
end
|
|
@@ -302,6 +259,95 @@ module SourceMonitor
|
|
|
302
259
|
end
|
|
303
260
|
# :nocov:
|
|
304
261
|
|
|
262
|
+
def import_session_wizard
|
|
263
|
+
SourceMonitor::ImportSessions::Wizard.new(
|
|
264
|
+
import_session: @import_session,
|
|
265
|
+
params: params,
|
|
266
|
+
current_step: @current_step
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def permitted_step(value)
|
|
271
|
+
step = value.to_s.presence
|
|
272
|
+
return unless step
|
|
273
|
+
|
|
274
|
+
ImportSession::STEP_ORDER.find { |candidate| candidate == step }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def target_step
|
|
278
|
+
permitted_step(import_session_state_params[:next_step]) || @current_step || ImportSession.default_step
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def session_attributes
|
|
282
|
+
attrs = import_session_state_params.except(:next_step, :current_step, "next_step", "current_step")
|
|
283
|
+
attrs[:current_step] = target_step
|
|
284
|
+
attrs
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def import_session_state_params
|
|
288
|
+
@import_session_state_params ||= begin
|
|
289
|
+
raw = params[:import_session] || params["import_session"] || {}
|
|
290
|
+
permitted = if raw.respond_to?(:permit)
|
|
291
|
+
raw.permit(
|
|
292
|
+
:current_step,
|
|
293
|
+
:next_step,
|
|
294
|
+
:select_all,
|
|
295
|
+
:select_none,
|
|
296
|
+
parsed_sources: [],
|
|
297
|
+
selected_source_ids: [],
|
|
298
|
+
bulk_settings: {},
|
|
299
|
+
opml_file_metadata: {}
|
|
300
|
+
)
|
|
301
|
+
else
|
|
302
|
+
raw.to_h
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
SourceMonitor::Security::ParameterSanitizer.sanitize(permitted.to_h).with_indifferent_access
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def prepare_preview_context(skip_default: false)
|
|
310
|
+
context = if skip_default
|
|
311
|
+
import_session_wizard.preview_context
|
|
312
|
+
else
|
|
313
|
+
import_session_wizard.preview_context_with_default_selection
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
apply_preview_context(context)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def prepare_health_check_context
|
|
320
|
+
apply_health_check_context(import_session_wizard.health_check_context)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def prepare_confirm_context
|
|
324
|
+
apply_confirm_context(import_session_wizard.confirm_context)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def apply_preview_context(context)
|
|
328
|
+
@filter = context.filter
|
|
329
|
+
@page = context.page
|
|
330
|
+
@selected_source_ids = context.selected_source_ids
|
|
331
|
+
@preview_entries = context.preview_entries
|
|
332
|
+
@filtered_entries = context.filtered_entries
|
|
333
|
+
@paginated_entries = context.paginated_entries
|
|
334
|
+
@has_next_page = context.has_next_page
|
|
335
|
+
@has_previous_page = context.has_previous_page
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def apply_health_check_context(context)
|
|
339
|
+
@selected_source_ids = context.selected_source_ids
|
|
340
|
+
@health_check_entries = context.health_check_entries
|
|
341
|
+
@health_check_target_ids = context.health_check_target_ids
|
|
342
|
+
@health_progress = context.health_progress
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def apply_confirm_context(context)
|
|
346
|
+
@selected_source_ids = context.selected_source_ids
|
|
347
|
+
@selected_entries = context.selected_entries
|
|
348
|
+
@bulk_settings = context.bulk_settings
|
|
349
|
+
end
|
|
350
|
+
|
|
305
351
|
def authorize_import_session!
|
|
306
352
|
return if !SourceMonitor::Security::Authentication.authentication_configured?
|
|
307
353
|
|
|
@@ -52,8 +52,9 @@ module SourceMonitor
|
|
|
52
52
|
@avg_feed_word_counts = word_counts[:feed]
|
|
53
53
|
@avg_scraped_word_counts = word_counts[:scraped]
|
|
54
54
|
|
|
55
|
-
@
|
|
56
|
-
@
|
|
55
|
+
@scrape_recommendations = SourceMonitor::Analytics::ScrapeRecommendations.new
|
|
56
|
+
@scrape_candidate_ids = Set.new(@scrape_recommendations.candidate_ids_for(source_ids))
|
|
57
|
+
@total_scrape_candidate_count = @scrape_recommendations.candidates_count
|
|
57
58
|
|
|
58
59
|
# Row partial preload requirements (V3): item_activity_rates,
|
|
59
60
|
# avg_feed_word_counts, avg_scraped_word_counts are pre-computed above
|
|
@@ -188,23 +189,8 @@ module SourceMonitor
|
|
|
188
189
|
def expand_scrape_recommendation_filter
|
|
189
190
|
return unless @search_params["scraping_enabled_eq"] == "recommend"
|
|
190
191
|
|
|
191
|
-
threshold = SourceMonitor.config.scraping.scrape_recommendation_threshold
|
|
192
192
|
@search_params.delete("scraping_enabled_eq")
|
|
193
|
-
@search_params
|
|
194
|
-
@search_params["active_eq"] = "true"
|
|
195
|
-
@search_params["avg_feed_words_lt"] = threshold.to_s
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def compute_scrape_candidate_ids
|
|
199
|
-
threshold = SourceMonitor.config.scraping.scrape_recommendation_threshold
|
|
200
|
-
return Set.new if threshold.nil? || threshold <= 0
|
|
201
|
-
|
|
202
|
-
candidate_ids = @sources.select do |source|
|
|
203
|
-
avg = @avg_feed_word_counts[source.id]
|
|
204
|
-
avg.present? && avg < threshold && !source.scraping_enabled?
|
|
205
|
-
end.map(&:id)
|
|
206
|
-
|
|
207
|
-
Set.new(candidate_ids)
|
|
193
|
+
@search_params.merge!(SourceMonitor::Analytics::ScrapeRecommendations.new.filter_params)
|
|
208
194
|
end
|
|
209
195
|
|
|
210
196
|
def enqueue_unscraped_items(source)
|
|
@@ -66,7 +66,7 @@ module SourceMonitor
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def scrape_candidates(threshold: SourceMonitor.config.scraping.scrape_recommendation_threshold)
|
|
69
|
-
SourceMonitor::
|
|
69
|
+
SourceMonitor::Analytics::ScrapeRecommendations.new(threshold:).relation
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
# Bulk-enable scraping for sources that don't already have it enabled.
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
<div id="source_monitor_notifications"
|
|
20
20
|
data-notification-container-target="list"
|
|
21
21
|
class="flex w-full flex-col gap-3">
|
|
22
|
+
<%# Request flashes render response-local here (never broadcast to all tabs). %>
|
|
23
|
+
<% source_monitor_flash_toasts.each do |toast| %>
|
|
24
|
+
<%= render "source_monitor/shared/toast",
|
|
25
|
+
message: toast[:message],
|
|
26
|
+
level: toast[:level] %>
|
|
27
|
+
<% end %>
|
|
22
28
|
</div>
|
|
23
29
|
<div data-notification-container-target="badge"
|
|
24
30
|
class="pointer-events-auto hidden">
|
data/docs/configuration.md
CHANGED
|
@@ -137,6 +137,11 @@ Call `config.realtime.action_cable_config` if you need a full hash for environme
|
|
|
137
137
|
|
|
138
138
|
## Authentication Helpers
|
|
139
139
|
|
|
140
|
+
**Fail-closed by default.** SourceMonitor denies access to every engine route
|
|
141
|
+
(returning `403 Forbidden`) unless you configure an authentication or
|
|
142
|
+
authorization handler. This prevents the engine's create/update/delete/enqueue
|
|
143
|
+
routes from being public by accident.
|
|
144
|
+
|
|
140
145
|
Protect the dashboard with host-specific auth in one place:
|
|
141
146
|
|
|
142
147
|
```ruby
|
|
@@ -148,7 +153,19 @@ config.authentication.current_user_method = :current_user
|
|
|
148
153
|
config.authentication.user_signed_in_method = :user_signed_in?
|
|
149
154
|
```
|
|
150
155
|
|
|
151
|
-
Handlers can be symbols (invoked on the controller) or callables. Return `false` or raise to deny access.
|
|
156
|
+
Handlers can be symbols (invoked on the controller) or callables. Return `false` or raise to deny access. As soon as either handler is configured, the handler decides access and the fail-closed guard no longer applies.
|
|
157
|
+
|
|
158
|
+
### Open access opt-in (non-production)
|
|
159
|
+
|
|
160
|
+
For local demos or sandboxes where engine routes are deliberately public, you
|
|
161
|
+
can explicitly opt out of the fail-closed guard:
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
config.authentication.open_access = true # default: false
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This is intended for non-production/demo environments only. Configuring a
|
|
168
|
+
handler always takes precedence over this flag.
|
|
152
169
|
|
|
153
170
|
## Health Model
|
|
154
171
|
|
data/docs/deployment.md
CHANGED
|
@@ -33,7 +33,7 @@ SourceMonitor assumes the standard Rails 8 process split:
|
|
|
33
33
|
|
|
34
34
|
## Security & Authentication
|
|
35
35
|
|
|
36
|
-
- Lock down the
|
|
36
|
+
- SourceMonitor is **fail-closed by default**: without a configured handler every engine route returns `403 Forbidden`. Lock down the routes with authentication hooks (`config.authentication.authenticate_with` / `authorize_with`). Only set `config.authentication.open_access = true` for non-production demos where public access is intentional.
|
|
37
37
|
- Configure HTTPS for Action Cable if you expose Solid Cable over the public internet.
|
|
38
38
|
- Store API keys for authenticated feeds in encrypted credentials and inject them via per-source custom headers.
|
|
39
39
|
|