source_monitor 0.3.0 → 0.3.2
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-architecture/SKILL.md +233 -0
- data/.claude/skills/sm-architecture/reference/extraction-patterns.md +192 -0
- data/.claude/skills/sm-architecture/reference/module-map.md +194 -0
- data/.claude/skills/sm-configuration-setting/SKILL.md +264 -0
- data/.claude/skills/sm-configuration-setting/reference/settings-catalog.md +248 -0
- data/.claude/skills/sm-configuration-setting/reference/settings-pattern.md +297 -0
- data/.claude/skills/sm-configure/SKILL.md +153 -0
- data/.claude/skills/sm-configure/reference/configuration-reference.md +321 -0
- data/.claude/skills/sm-dashboard-widget/SKILL.md +344 -0
- data/.claude/skills/sm-dashboard-widget/reference/dashboard-patterns.md +304 -0
- data/.claude/skills/sm-domain-model/SKILL.md +188 -0
- data/.claude/skills/sm-domain-model/reference/model-graph.md +114 -0
- data/.claude/skills/sm-domain-model/reference/table-structure.md +348 -0
- data/.claude/skills/sm-engine-migration/SKILL.md +395 -0
- data/.claude/skills/sm-engine-migration/reference/migration-conventions.md +255 -0
- data/.claude/skills/sm-engine-test/SKILL.md +302 -0
- data/.claude/skills/sm-engine-test/reference/test-helpers.md +259 -0
- data/.claude/skills/sm-engine-test/reference/test-patterns.md +411 -0
- data/.claude/skills/sm-event-handler/SKILL.md +265 -0
- data/.claude/skills/sm-event-handler/reference/events-api.md +229 -0
- data/.claude/skills/sm-health-rule/SKILL.md +327 -0
- data/.claude/skills/sm-health-rule/reference/health-system.md +269 -0
- data/.claude/skills/sm-host-setup/SKILL.md +223 -0
- data/.claude/skills/sm-host-setup/reference/initializer-template.md +195 -0
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +134 -0
- data/.claude/skills/sm-job/SKILL.md +263 -0
- data/.claude/skills/sm-job/reference/job-conventions.md +245 -0
- data/.claude/skills/sm-model-extension/SKILL.md +287 -0
- data/.claude/skills/sm-model-extension/reference/extension-api.md +317 -0
- data/.claude/skills/sm-pipeline-stage/SKILL.md +254 -0
- data/.claude/skills/sm-pipeline-stage/reference/completion-handlers.md +152 -0
- data/.claude/skills/sm-pipeline-stage/reference/entry-processing.md +191 -0
- data/.claude/skills/sm-pipeline-stage/reference/feed-fetcher-architecture.md +198 -0
- data/.claude/skills/sm-scraper-adapter/SKILL.md +284 -0
- data/.claude/skills/sm-scraper-adapter/reference/adapter-contract.md +167 -0
- data/.claude/skills/sm-scraper-adapter/reference/example-adapter.md +274 -0
- data/.vbw-planning/.notification-log.jsonl +102 -0
- data/.vbw-planning/.session-log.jsonl +505 -0
- data/AGENTS.md +20 -57
- data/CHANGELOG.md +19 -0
- data/CLAUDE.md +44 -1
- data/CONTRIBUTING.md +5 -5
- data/Gemfile.lock +20 -21
- data/README.md +18 -5
- data/VERSION +1 -0
- data/docs/deployment.md +1 -1
- data/docs/setup.md +4 -4
- data/lib/source_monitor/setup/skills_installer.rb +94 -0
- data/lib/source_monitor/setup/workflow.rb +17 -2
- data/lib/source_monitor/version.rb +1 -1
- data/lib/tasks/source_monitor_setup.rake +58 -0
- data/source_monitor.gemspec +1 -0
- metadata +39 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Health System Reference
|
|
2
|
+
|
|
3
|
+
## System Registration
|
|
4
|
+
|
|
5
|
+
The health system registers itself during engine initialization via `Health.setup!`:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
module SourceMonitor
|
|
9
|
+
module Health
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def setup!
|
|
13
|
+
register_fetch_callback
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fetch_callback
|
|
17
|
+
@fetch_callback ||= lambda do |event|
|
|
18
|
+
source = event&.source
|
|
19
|
+
next unless source
|
|
20
|
+
SourceHealthMonitor.new(source: source).call
|
|
21
|
+
rescue StandardError => error
|
|
22
|
+
log_error(source, error)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def register_fetch_callback
|
|
27
|
+
callbacks = SourceMonitor.config.events.callbacks_for(:after_fetch_completed)
|
|
28
|
+
return if callbacks.include?(fetch_callback)
|
|
29
|
+
SourceMonitor.config.events.after_fetch_completed(fetch_callback)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Key behaviors:
|
|
36
|
+
- Uses a memoized lambda to prevent duplicate registration
|
|
37
|
+
- Checks existing callbacks before adding
|
|
38
|
+
- Errors in the health callback are logged but don't crash the fetch pipeline
|
|
39
|
+
|
|
40
|
+
## SourceHealthMonitor Details
|
|
41
|
+
|
|
42
|
+
### Rolling Success Rate Calculation
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
def calculate_success_rate(logs)
|
|
46
|
+
successes = logs.count { |log| log.success? }
|
|
47
|
+
total = logs.size
|
|
48
|
+
return 0.0 if total.zero?
|
|
49
|
+
(successes.to_f / total).round(4)
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The rate is stored as a float (0.0 to 1.0) on `source.rolling_success_rate`.
|
|
54
|
+
|
|
55
|
+
### Minimum Sample Size
|
|
56
|
+
|
|
57
|
+
Thresholds only apply when the number of logs equals the configured `window_size`. This prevents premature auto-pausing of new sources:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
def thresholds_applicable?(sample_size)
|
|
61
|
+
sample_size >= minimum_sample_size
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def minimum_sample_size
|
|
65
|
+
[config.window_size.to_i, 1].max
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Auto-Pause Algorithm
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
IF rate < auto_pause_threshold AND thresholds_active:
|
|
73
|
+
new_until = now + auto_pause_cooldown_minutes
|
|
74
|
+
IF existing_until > new_until:
|
|
75
|
+
keep existing_until (don't shorten pause)
|
|
76
|
+
ELSE:
|
|
77
|
+
use new_until
|
|
78
|
+
|
|
79
|
+
SET auto_paused_until = new_until
|
|
80
|
+
SET auto_paused_at = now (or keep existing)
|
|
81
|
+
PUSH next_fetch_at past pause window
|
|
82
|
+
PUSH backoff_until past pause window
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Auto-Resume Algorithm
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
IF auto_paused_until IS NOT NULL AND rate >= auto_resume_threshold:
|
|
89
|
+
CLEAR auto_paused_until
|
|
90
|
+
CLEAR auto_paused_at
|
|
91
|
+
CLEAR backoff_until
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The resume threshold defaults to `max(auto_resume_threshold, auto_pause_threshold)` to prevent flapping.
|
|
95
|
+
|
|
96
|
+
### Consecutive Failure Detection
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
def consecutive_failures(logs)
|
|
100
|
+
logs.take_while { |log| !log_success?(log) }.size
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Logs are ordered by `started_at DESC`, so `take_while` counts the most recent consecutive failures.
|
|
105
|
+
|
|
106
|
+
### Improving Streak Detection
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
def improving_streak?(logs)
|
|
110
|
+
success_streak = 0
|
|
111
|
+
failure_seen = false
|
|
112
|
+
logs.each do |log|
|
|
113
|
+
if log_success?(log)
|
|
114
|
+
success_streak += 1
|
|
115
|
+
else
|
|
116
|
+
failure_seen = true
|
|
117
|
+
break
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
success_streak >= 2 && failure_seen
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A source is "improving" when it has >= 2 consecutive recent successes AND at least one failure exists in the window.
|
|
125
|
+
|
|
126
|
+
### Fixed Interval Enforcement
|
|
127
|
+
|
|
128
|
+
For non-adaptive sources, the monitor ensures `backoff_until` doesn't persist beyond what's needed:
|
|
129
|
+
|
|
130
|
+
```ruby
|
|
131
|
+
def enforce_fixed_interval(attrs, auto_paused_until)
|
|
132
|
+
return if source.adaptive_fetching_enabled?
|
|
133
|
+
return if auto_paused_active?(auto_paused_until)
|
|
134
|
+
|
|
135
|
+
backoff_value = attrs.key?(:backoff_until) ? attrs[:backoff_until] : source.backoff_until
|
|
136
|
+
return if backoff_value.blank?
|
|
137
|
+
|
|
138
|
+
fixed_minutes = [source.fetch_interval_minutes.to_i, 1].max
|
|
139
|
+
attrs[:next_fetch_at] = now + fixed_minutes.minutes
|
|
140
|
+
attrs[:backoff_until] = nil
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Circuit Breaker vs Auto-Pause
|
|
145
|
+
|
|
146
|
+
These are two separate protection mechanisms:
|
|
147
|
+
|
|
148
|
+
| Feature | Circuit Breaker | Auto-Pause |
|
|
149
|
+
|---------|----------------|------------|
|
|
150
|
+
| **Scope** | Single fetch attempt | Rolling window |
|
|
151
|
+
| **Trigger** | RetryPolicy exhaustion | Success rate threshold |
|
|
152
|
+
| **Duration** | Error-type specific (1-2 hours) | Configurable cooldown (60 min default) |
|
|
153
|
+
| **Fields** | `fetch_circuit_*` | `auto_paused_*` |
|
|
154
|
+
| **Managed by** | RetryPolicy + SourceUpdater | SourceHealthMonitor |
|
|
155
|
+
| **Resets on** | Successful fetch | Success rate recovery |
|
|
156
|
+
|
|
157
|
+
### Circuit Breaker Flow
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
Error occurs -> RetryPolicy.decision
|
|
161
|
+
-> retry? (attempts remaining)
|
|
162
|
+
-> schedule retry with wait
|
|
163
|
+
-> open_circuit? (attempts exhausted)
|
|
164
|
+
-> set fetch_circuit_until
|
|
165
|
+
-> FetchRunner skips fetch while circuit open
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Auto-Pause Flow
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
After every fetch -> SourceHealthMonitor.call
|
|
172
|
+
-> calculate rolling success rate
|
|
173
|
+
-> IF rate < threshold AND window full
|
|
174
|
+
-> set auto_paused_until
|
|
175
|
+
-> push next_fetch_at past pause window
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## HealthCheckLog Model
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
class HealthCheckLog < ApplicationRecord
|
|
182
|
+
include SourceMonitor::Loggable
|
|
183
|
+
|
|
184
|
+
belongs_to :source
|
|
185
|
+
has_one :log_entry, as: :loggable, dependent: :destroy
|
|
186
|
+
|
|
187
|
+
attribute :http_response_headers, default: -> { {} }
|
|
188
|
+
validates :source, presence: true
|
|
189
|
+
|
|
190
|
+
after_save :sync_log_entry # Creates unified LogEntry record
|
|
191
|
+
end
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Fields:
|
|
195
|
+
- `source_id` -- associated source
|
|
196
|
+
- `success` -- boolean
|
|
197
|
+
- `started_at`, `completed_at` -- timing
|
|
198
|
+
- `duration_ms` -- request duration
|
|
199
|
+
- `http_status` -- response status code
|
|
200
|
+
- `http_response_headers` -- response headers hash
|
|
201
|
+
- `error_class`, `error_message` -- error details
|
|
202
|
+
|
|
203
|
+
## Source Model Health Fields
|
|
204
|
+
|
|
205
|
+
From migration `20251012090000_add_health_fields_to_sources`:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# Health status tracking
|
|
209
|
+
:health_status # string, default: "healthy"
|
|
210
|
+
:health_status_changed_at # datetime
|
|
211
|
+
:rolling_success_rate # float
|
|
212
|
+
:health_auto_pause_threshold # float (per-source override)
|
|
213
|
+
|
|
214
|
+
# Auto-pause state
|
|
215
|
+
:auto_paused_at # datetime
|
|
216
|
+
:auto_paused_until # datetime
|
|
217
|
+
|
|
218
|
+
# Circuit breaker state (fetch-level)
|
|
219
|
+
:fetch_retry_attempt # integer, default: 0
|
|
220
|
+
:fetch_circuit_opened_at # datetime
|
|
221
|
+
:fetch_circuit_until # datetime
|
|
222
|
+
|
|
223
|
+
# Existing fields used by health system
|
|
224
|
+
:failure_count # integer
|
|
225
|
+
:last_error # string
|
|
226
|
+
:last_error_at # datetime
|
|
227
|
+
:backoff_until # datetime
|
|
228
|
+
:next_fetch_at # datetime
|
|
229
|
+
:fetch_status # string
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Source Model Health Methods
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
def fetch_circuit_open?
|
|
236
|
+
fetch_circuit_until.present? && fetch_circuit_until.future?
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def auto_paused?
|
|
240
|
+
auto_paused_until.present? && auto_paused_until.future?
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def fetch_retry_attempt
|
|
244
|
+
value = super
|
|
245
|
+
value.present? ? value : 0
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Health Check Controllers
|
|
250
|
+
|
|
251
|
+
| Controller | Actions | Purpose |
|
|
252
|
+
|------------|---------|---------|
|
|
253
|
+
| `SourceHealthChecksController` | `create` | Trigger on-demand health check |
|
|
254
|
+
| `SourceHealthResetsController` | `create` | Reset all health state |
|
|
255
|
+
| `HealthController` | `show` | Engine health endpoint |
|
|
256
|
+
|
|
257
|
+
## Import Source Health Check
|
|
258
|
+
|
|
259
|
+
Used during OPML import to validate feed URLs before importing:
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
result = Health::ImportSourceHealthCheck.new(feed_url: "https://example.com/feed.xml").call
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
- No Source record needed (works with raw URL)
|
|
266
|
+
- No conditional headers (no prior state)
|
|
267
|
+
- Returns simple Result: `status`, `error_message`, `http_status`
|
|
268
|
+
- Used by `ImportSessionHealthCheckJob` for batch validation
|
|
269
|
+
- Results stored in `import_session.parsed_sources[n]["health_status"]`
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sm-host-setup
|
|
3
|
+
description: Use when setting up SourceMonitor in a host Rails application, including gem installation, engine mounting, migration copying, initializer creation, and verifying the install works.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# sm-host-setup: Host Application Setup
|
|
8
|
+
|
|
9
|
+
Guides integration of the SourceMonitor engine into a host Rails application.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
- Adding SourceMonitor to a new or existing Rails 8 host app
|
|
14
|
+
- Troubleshooting a broken installation
|
|
15
|
+
- Re-running setup after upgrading the gem
|
|
16
|
+
- Rolling back the engine from a host app
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
| Requirement | Minimum | How to Check |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Ruby | 3.4+ | `ruby -v` |
|
|
23
|
+
| Rails | 8.0+ | `bin/rails about` |
|
|
24
|
+
| PostgreSQL | 14+ | `psql --version` |
|
|
25
|
+
| Node.js | 18+ | `node -v` |
|
|
26
|
+
| Solid Queue | >= 0.3, < 3.0 | Check host Gemfile |
|
|
27
|
+
| Solid Cable or Redis | Solid Cable >= 3.0 | Check host Gemfile |
|
|
28
|
+
|
|
29
|
+
## Installation Methods
|
|
30
|
+
|
|
31
|
+
### Method 1: Guided CLI (Recommended)
|
|
32
|
+
|
|
33
|
+
The engine ships a guided installer that handles every step interactively.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# From the host app root:
|
|
37
|
+
bundle add source_monitor --version "~> 0.3.0"
|
|
38
|
+
bundle install
|
|
39
|
+
bin/source_monitor install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The guided workflow:
|
|
43
|
+
1. Checks prerequisites via `DependencyChecker`
|
|
44
|
+
2. Prompts for mount path (default: `/source_monitor`)
|
|
45
|
+
3. Ensures `gem "source_monitor"` in Gemfile, runs `bundle install`
|
|
46
|
+
4. Runs `npm install` if `package.json` exists
|
|
47
|
+
5. Executes `bin/rails generate source_monitor:install --mount-path=...`
|
|
48
|
+
6. Copies and deduplicates migrations, runs `bin/rails db:migrate`
|
|
49
|
+
7. Patches the initializer with navigation hint and optional Devise hooks
|
|
50
|
+
8. Runs verification and prints a report
|
|
51
|
+
|
|
52
|
+
Non-interactive mode:
|
|
53
|
+
```bash
|
|
54
|
+
bin/source_monitor install --yes
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Method 2: Manual Step-by-Step
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 1. Add the gem
|
|
61
|
+
gem "source_monitor", "~> 0.3.0" # in Gemfile
|
|
62
|
+
bundle install
|
|
63
|
+
|
|
64
|
+
# 2. Run the install generator
|
|
65
|
+
bin/rails generate source_monitor:install --mount-path=/source_monitor
|
|
66
|
+
|
|
67
|
+
# 3. Copy engine migrations
|
|
68
|
+
bin/rails railties:install:migrations FROM=source_monitor
|
|
69
|
+
|
|
70
|
+
# 4. Apply migrations
|
|
71
|
+
bin/rails db:migrate
|
|
72
|
+
|
|
73
|
+
# 5. Start background workers
|
|
74
|
+
bin/rails solid_queue:start
|
|
75
|
+
|
|
76
|
+
# 6. Verify
|
|
77
|
+
bin/source_monitor verify
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Method 3: GitHub Edge
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
# Gemfile
|
|
84
|
+
gem "source_monitor", github: "dchuk/source_monitor"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## What the Install Generator Does
|
|
88
|
+
|
|
89
|
+
The generator (`SourceMonitor::Generators::InstallGenerator`) performs two actions:
|
|
90
|
+
|
|
91
|
+
1. **Mounts the engine** in `config/routes.rb`:
|
|
92
|
+
```ruby
|
|
93
|
+
mount SourceMonitor::Engine, at: "/source_monitor"
|
|
94
|
+
```
|
|
95
|
+
Skips if already mounted. The mount path is configurable via `--mount-path`.
|
|
96
|
+
|
|
97
|
+
2. **Creates the initializer** at `config/initializers/source_monitor.rb`:
|
|
98
|
+
Uses the template at `lib/generators/source_monitor/install/templates/source_monitor.rb.tt`. Skips if the file already exists.
|
|
99
|
+
|
|
100
|
+
Re-running the generator is safe and idempotent.
|
|
101
|
+
|
|
102
|
+
## Post-Install Configuration
|
|
103
|
+
|
|
104
|
+
After installation, review and customize the initializer. Key areas:
|
|
105
|
+
|
|
106
|
+
| Section | Purpose |
|
|
107
|
+
|---|---|
|
|
108
|
+
| Queue settings | Queue names, concurrency, namespace |
|
|
109
|
+
| Authentication | `authenticate_with`, `authorize_with` hooks |
|
|
110
|
+
| HTTP client | Timeouts, proxy, retries |
|
|
111
|
+
| Fetching | Adaptive scheduling intervals and factors |
|
|
112
|
+
| Health | Auto-pause/resume thresholds |
|
|
113
|
+
| Scrapers | Register custom scraper adapters |
|
|
114
|
+
| Retention | Item age limits and pruning strategy |
|
|
115
|
+
| Events | Lifecycle callbacks for integration |
|
|
116
|
+
| Models | Table prefix, concerns, validations |
|
|
117
|
+
| Realtime | Action Cable adapter (Solid Cable/Redis) |
|
|
118
|
+
|
|
119
|
+
See the `sm-configure` skill for full configuration reference.
|
|
120
|
+
|
|
121
|
+
## Verification
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# CLI verification
|
|
125
|
+
bin/source_monitor verify
|
|
126
|
+
|
|
127
|
+
# Rake task verification
|
|
128
|
+
bin/rails source_monitor:setup:verify
|
|
129
|
+
|
|
130
|
+
# Prerequisite check only
|
|
131
|
+
bin/rails source_monitor:setup:check
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Verification checks Solid Queue workers and Action Cable health. Non-zero exit on failure.
|
|
135
|
+
|
|
136
|
+
Enable telemetry logging:
|
|
137
|
+
```bash
|
|
138
|
+
SOURCE_MONITOR_SETUP_TELEMETRY=true bin/source_monitor verify
|
|
139
|
+
# Logs to log/source_monitor_setup.log
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Devise Integration
|
|
143
|
+
|
|
144
|
+
When Devise is detected, the guided installer offers to wire authentication hooks:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
config.authentication.authenticate_with :authenticate_user!
|
|
148
|
+
config.authentication.authorize_with ->(controller) {
|
|
149
|
+
controller.current_user&.respond_to?(:admin?) ? controller.current_user.admin? : true
|
|
150
|
+
}
|
|
151
|
+
config.authentication.current_user_method = :current_user
|
|
152
|
+
config.authentication.user_signed_in_method = :user_signed_in?
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Rollback
|
|
156
|
+
|
|
157
|
+
1. Remove `gem "source_monitor"` from Gemfile, `bundle install`
|
|
158
|
+
2. Delete `config/initializers/source_monitor.rb`
|
|
159
|
+
3. Remove `mount SourceMonitor::Engine` from `config/routes.rb`
|
|
160
|
+
4. Drop tables: `bin/rails db:migrate:down VERSION=<timestamp>` for each engine migration
|
|
161
|
+
5. Remove Solid Queue/Cable migrations only if no other components need them
|
|
162
|
+
|
|
163
|
+
## Host Compatibility
|
|
164
|
+
|
|
165
|
+
| Scenario | Status |
|
|
166
|
+
|---|---|
|
|
167
|
+
| Rails 8 full-stack app | Supported |
|
|
168
|
+
| Rails 8 API-only app | Supported |
|
|
169
|
+
| Dedicated Solid Queue database | Supported |
|
|
170
|
+
| Redis-backed Action Cable | Supported |
|
|
171
|
+
|
|
172
|
+
## Key Source Files
|
|
173
|
+
|
|
174
|
+
| File | Purpose |
|
|
175
|
+
|---|---|
|
|
176
|
+
| `lib/source_monitor/setup/workflow.rb` | Guided installer orchestration |
|
|
177
|
+
| `lib/generators/source_monitor/install/install_generator.rb` | Rails generator |
|
|
178
|
+
| `lib/generators/source_monitor/install/templates/source_monitor.rb.tt` | Initializer template |
|
|
179
|
+
| `lib/source_monitor/setup/initializer_patcher.rb` | Post-install patching |
|
|
180
|
+
| `lib/source_monitor/setup/verification/runner.rb` | Verification runner |
|
|
181
|
+
| `lib/source_monitor/engine.rb` | Engine configuration and initializers |
|
|
182
|
+
| `docs/setup.md` | Full setup documentation |
|
|
183
|
+
| `docs/troubleshooting.md` | Common fixes |
|
|
184
|
+
|
|
185
|
+
## References
|
|
186
|
+
|
|
187
|
+
- `docs/setup.md` -- Complete setup workflow documentation
|
|
188
|
+
- `docs/configuration.md` -- Configuration reference
|
|
189
|
+
- `docs/troubleshooting.md` -- Common issues and fixes
|
|
190
|
+
|
|
191
|
+
## Testing
|
|
192
|
+
|
|
193
|
+
After setup, verify with:
|
|
194
|
+
1. `bin/source_monitor verify` -- Checks Solid Queue and Action Cable
|
|
195
|
+
2. Visit the mount path in browser -- Dashboard should load
|
|
196
|
+
3. Create a source and trigger "Fetch Now" -- Validates end-to-end
|
|
197
|
+
|
|
198
|
+
Optional system test for host apps using Devise:
|
|
199
|
+
```ruby
|
|
200
|
+
# test/system/source_monitor_setup_test.rb
|
|
201
|
+
require "application_system_test_case"
|
|
202
|
+
|
|
203
|
+
class SourceMonitorSetupTest < ApplicationSystemTestCase
|
|
204
|
+
test "signed in admin can reach SourceMonitor" do
|
|
205
|
+
user = users(:admin)
|
|
206
|
+
sign_in user
|
|
207
|
+
visit "/source_monitor"
|
|
208
|
+
assert_text "SourceMonitor Dashboard"
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Checklist
|
|
214
|
+
|
|
215
|
+
- [ ] Ruby 3.4+, Rails 8.0+, PostgreSQL 14+ available
|
|
216
|
+
- [ ] `gem "source_monitor"` added to Gemfile
|
|
217
|
+
- [ ] `bundle install` completed
|
|
218
|
+
- [ ] Install generator ran (`bin/rails generate source_monitor:install`)
|
|
219
|
+
- [ ] Engine migrations copied and applied
|
|
220
|
+
- [ ] Solid Queue workers started
|
|
221
|
+
- [ ] Authentication hooks configured in initializer
|
|
222
|
+
- [ ] `bin/source_monitor verify` passes
|
|
223
|
+
- [ ] Dashboard accessible at mount path
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Initializer Template Reference
|
|
2
|
+
|
|
3
|
+
Complete annotated initializer for `config/initializers/source_monitor.rb`.
|
|
4
|
+
|
|
5
|
+
The install generator creates this file from `lib/generators/source_monitor/install/templates/source_monitor.rb.tt`. This reference expands on every option.
|
|
6
|
+
|
|
7
|
+
## Full Template
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# frozen_string_literal: true
|
|
11
|
+
|
|
12
|
+
# SourceMonitor engine configuration.
|
|
13
|
+
#
|
|
14
|
+
# These values default to conservative settings that work for most hosts.
|
|
15
|
+
# Tweak them here instead of monkey-patching the engine so upgrades remain easy.
|
|
16
|
+
# Restart the application after changes.
|
|
17
|
+
SourceMonitor.configure do |config|
|
|
18
|
+
|
|
19
|
+
# ===========================================================================
|
|
20
|
+
# Queue & Worker Settings
|
|
21
|
+
# ===========================================================================
|
|
22
|
+
|
|
23
|
+
# Namespace prefix for queue names and instrumentation keys.
|
|
24
|
+
# Combined with ActiveJob.queue_name_prefix automatically.
|
|
25
|
+
config.queue_namespace = "source_monitor"
|
|
26
|
+
|
|
27
|
+
# Dedicated queue names. Must match entries in config/solid_queue.yml.
|
|
28
|
+
config.fetch_queue_name = "source_monitor_fetch"
|
|
29
|
+
config.scrape_queue_name = "source_monitor_scrape"
|
|
30
|
+
|
|
31
|
+
# Worker concurrency per queue (advisory for Solid Queue).
|
|
32
|
+
config.fetch_queue_concurrency = 2
|
|
33
|
+
config.scrape_queue_concurrency = 2
|
|
34
|
+
|
|
35
|
+
# Override the job class Solid Queue uses for recurring "command" tasks.
|
|
36
|
+
# config.recurring_command_job_class = "MyRecurringCommandJob"
|
|
37
|
+
|
|
38
|
+
# Toggle lightweight queue metrics on the dashboard.
|
|
39
|
+
config.job_metrics_enabled = true
|
|
40
|
+
|
|
41
|
+
# ===========================================================================
|
|
42
|
+
# Mission Control Integration
|
|
43
|
+
# ===========================================================================
|
|
44
|
+
|
|
45
|
+
# Show "Open Mission Control" link on the SourceMonitor dashboard.
|
|
46
|
+
config.mission_control_enabled = false
|
|
47
|
+
|
|
48
|
+
# String path, route helper lambda, or nil.
|
|
49
|
+
# config.mission_control_dashboard_path = "/mission_control"
|
|
50
|
+
# config.mission_control_dashboard_path = -> {
|
|
51
|
+
# Rails.application.routes.url_helpers.mission_control_jobs_path
|
|
52
|
+
# }
|
|
53
|
+
config.mission_control_dashboard_path = nil
|
|
54
|
+
|
|
55
|
+
# ===========================================================================
|
|
56
|
+
# Authentication
|
|
57
|
+
# ===========================================================================
|
|
58
|
+
# Handlers: Symbol (invoked on controller) or callable (receives controller).
|
|
59
|
+
|
|
60
|
+
# Authenticate before accessing any SourceMonitor page.
|
|
61
|
+
# config.authentication.authenticate_with :authenticate_user!
|
|
62
|
+
|
|
63
|
+
# Authorize after authentication (return false or raise to deny).
|
|
64
|
+
# config.authentication.authorize_with ->(controller) {
|
|
65
|
+
# controller.current_user&.admin?
|
|
66
|
+
# }
|
|
67
|
+
|
|
68
|
+
# Method names SourceMonitor uses to access current user info.
|
|
69
|
+
# config.authentication.current_user_method = :current_user
|
|
70
|
+
# config.authentication.user_signed_in_method = :user_signed_in?
|
|
71
|
+
|
|
72
|
+
# ===========================================================================
|
|
73
|
+
# HTTP Client
|
|
74
|
+
# ===========================================================================
|
|
75
|
+
# Maps to Faraday middleware options.
|
|
76
|
+
|
|
77
|
+
config.http.timeout = 15 # Total request timeout (seconds)
|
|
78
|
+
config.http.open_timeout = 5 # Connection open timeout (seconds)
|
|
79
|
+
config.http.max_redirects = 5 # Max redirects to follow
|
|
80
|
+
# config.http.user_agent = "SourceMonitor/#{SourceMonitor::VERSION}"
|
|
81
|
+
# config.http.proxy = ENV["SOURCE_MONITOR_HTTP_PROXY"]
|
|
82
|
+
# config.http.headers = { "X-Request-ID" => -> { SecureRandom.uuid } }
|
|
83
|
+
|
|
84
|
+
# Retry settings (mapped to faraday-retry)
|
|
85
|
+
# config.http.retry_max = 4
|
|
86
|
+
# config.http.retry_interval = 0.5
|
|
87
|
+
# config.http.retry_interval_randomness = 0.5
|
|
88
|
+
# config.http.retry_backoff_factor = 2
|
|
89
|
+
# config.http.retry_statuses = [429, 500, 502, 503, 504]
|
|
90
|
+
|
|
91
|
+
# ===========================================================================
|
|
92
|
+
# Adaptive Fetch Scheduling
|
|
93
|
+
# ===========================================================================
|
|
94
|
+
|
|
95
|
+
# config.fetching.min_interval_minutes = 5 # Floor (default: 5 min)
|
|
96
|
+
# config.fetching.max_interval_minutes = 1440 # Ceiling (default: 24 hrs)
|
|
97
|
+
# config.fetching.increase_factor = 1.25 # Multiplier when no new items
|
|
98
|
+
# config.fetching.decrease_factor = 0.75 # Multiplier when items arrive
|
|
99
|
+
# config.fetching.failure_increase_factor = 1.5 # Multiplier on errors
|
|
100
|
+
# config.fetching.jitter_percent = 0.1 # Random jitter (+/-10%)
|
|
101
|
+
|
|
102
|
+
# ===========================================================================
|
|
103
|
+
# Source Health Monitoring
|
|
104
|
+
# ===========================================================================
|
|
105
|
+
|
|
106
|
+
config.health.window_size = 20 # Fetch attempts to evaluate
|
|
107
|
+
config.health.healthy_threshold = 0.8 # Ratio for "healthy" badge
|
|
108
|
+
config.health.warning_threshold = 0.5 # Ratio for "warning" badge
|
|
109
|
+
config.health.auto_pause_threshold = 0.2 # Auto-pause below this
|
|
110
|
+
config.health.auto_resume_threshold = 0.6 # Auto-resume above this
|
|
111
|
+
config.health.auto_pause_cooldown_minutes = 60 # Grace period before re-enable
|
|
112
|
+
|
|
113
|
+
# ===========================================================================
|
|
114
|
+
# Scraper Adapters
|
|
115
|
+
# ===========================================================================
|
|
116
|
+
# Must inherit from SourceMonitor::Scrapers::Base.
|
|
117
|
+
|
|
118
|
+
# config.scrapers.register(:custom, "MyApp::Scrapers::CustomAdapter")
|
|
119
|
+
# config.scrapers.unregister(:readability) # Remove built-in
|
|
120
|
+
|
|
121
|
+
# ===========================================================================
|
|
122
|
+
# Retention Defaults
|
|
123
|
+
# ===========================================================================
|
|
124
|
+
# Sources inherit these when their retention fields are blank.
|
|
125
|
+
|
|
126
|
+
config.retention.items_retention_days = nil # nil = retain forever
|
|
127
|
+
config.retention.max_items = nil # nil = unlimited
|
|
128
|
+
# config.retention.strategy = :destroy # or :soft_delete
|
|
129
|
+
|
|
130
|
+
# ===========================================================================
|
|
131
|
+
# Scraping Controls
|
|
132
|
+
# ===========================================================================
|
|
133
|
+
|
|
134
|
+
# config.scraping.max_in_flight_per_source = 25 # Concurrent scrapes per source
|
|
135
|
+
# config.scraping.max_bulk_batch_size = 100 # Max bulk enqueue size
|
|
136
|
+
|
|
137
|
+
# ===========================================================================
|
|
138
|
+
# Event Callbacks
|
|
139
|
+
# ===========================================================================
|
|
140
|
+
# Handlers receive a single event struct.
|
|
141
|
+
|
|
142
|
+
# config.events.after_item_created do |event|
|
|
143
|
+
# # event.item, event.source, event.entry, event.result, event.status
|
|
144
|
+
# NewItemNotifier.publish(event.item, source: event.source)
|
|
145
|
+
# end
|
|
146
|
+
|
|
147
|
+
# config.events.after_item_scraped do |event|
|
|
148
|
+
# # event.item, event.source, event.result, event.log, event.status
|
|
149
|
+
# SearchIndexer.reindex(event.item) if event.success?
|
|
150
|
+
# end
|
|
151
|
+
|
|
152
|
+
# config.events.after_fetch_completed do |event|
|
|
153
|
+
# # event.source, event.result, event.status
|
|
154
|
+
# Rails.logger.info "Fetch for #{event.source.name}: #{event.status}"
|
|
155
|
+
# end
|
|
156
|
+
|
|
157
|
+
# config.events.register_item_processor ->(context) {
|
|
158
|
+
# # context.item, context.source, context.entry, context.result, context.status
|
|
159
|
+
# ItemIndexer.index(context.item)
|
|
160
|
+
# }
|
|
161
|
+
|
|
162
|
+
# ===========================================================================
|
|
163
|
+
# Model Extensions
|
|
164
|
+
# ===========================================================================
|
|
165
|
+
|
|
166
|
+
# Override default table name prefix (default: "sourcemon_").
|
|
167
|
+
# config.models.table_name_prefix = "sourcemon_"
|
|
168
|
+
|
|
169
|
+
# Include concerns to extend engine models.
|
|
170
|
+
# config.models.source.include_concern "MyApp::SourceMonitor::SourceExtensions"
|
|
171
|
+
# config.models.item.include_concern "MyApp::SourceMonitor::ItemExtensions"
|
|
172
|
+
|
|
173
|
+
# Register custom validations.
|
|
174
|
+
# config.models.source.validate :enforce_custom_rules
|
|
175
|
+
# config.models.source.validate ->(record) {
|
|
176
|
+
# record.errors.add(:base, "custom error") unless record.valid_for_my_app?
|
|
177
|
+
# }
|
|
178
|
+
|
|
179
|
+
# ===========================================================================
|
|
180
|
+
# Realtime (Action Cable) Adapter
|
|
181
|
+
# ===========================================================================
|
|
182
|
+
|
|
183
|
+
config.realtime.adapter = :solid_cable
|
|
184
|
+
# config.realtime.adapter = :redis
|
|
185
|
+
# config.realtime.redis_url = ENV.fetch("SOURCE_MONITOR_REDIS_URL", nil)
|
|
186
|
+
|
|
187
|
+
# Solid Cable tuning:
|
|
188
|
+
# config.realtime.solid_cable.polling_interval = "0.1.seconds"
|
|
189
|
+
# config.realtime.solid_cable.message_retention = "1.day"
|
|
190
|
+
# config.realtime.solid_cable.autotrim = true
|
|
191
|
+
# config.realtime.solid_cable.silence_polling = true
|
|
192
|
+
# config.realtime.solid_cable.use_skip_locked = true
|
|
193
|
+
# config.realtime.solid_cable.connects_to = { database: { writing: :cable } }
|
|
194
|
+
end
|
|
195
|
+
```
|