source_monitor 0.7.1 → 0.8.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-configure/SKILL.md +10 -1
- data/.claude/skills/sm-configure/reference/configuration-reference.md +44 -0
- data/.claude/skills/sm-host-setup/reference/initializer-template.md +17 -0
- data/.claude/skills/sm-host-setup/reference/setup-checklist.md +2 -0
- data/.claude/skills/sm-job/reference/job-conventions.md +26 -0
- data/.claude/skills/sm-upgrade/reference/version-history.md +22 -0
- data/.gitignore +10 -0
- data/AGENTS.md +1 -1
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +11 -5
- data/Gemfile.lock +1 -1
- data/README.md +6 -4
- data/VERSION +1 -1
- data/app/assets/builds/source_monitor/application.css +43 -0
- data/app/assets/builds/source_monitor/application.js +127 -0
- data/app/assets/builds/source_monitor/application.js.map +3 -3
- data/app/assets/javascripts/source_monitor/application.js +2 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_container_controller.js +138 -0
- data/app/assets/javascripts/source_monitor/controllers/notification_controller.js +11 -0
- data/app/controllers/source_monitor/source_favicon_fetches_controller.rb +38 -0
- data/app/controllers/source_monitor/sources_controller.rb +11 -0
- data/app/helpers/source_monitor/application_helper.rb +51 -0
- data/app/jobs/source_monitor/favicon_fetch_job.rb +71 -0
- data/app/jobs/source_monitor/import_opml_job.rb +9 -0
- data/app/jobs/source_monitor/source_health_check_job.rb +10 -0
- data/app/models/source_monitor/source.rb +2 -0
- data/app/views/layouts/source_monitor/application.html.erb +23 -2
- data/app/views/source_monitor/shared/_toast.html.erb +1 -0
- data/app/views/source_monitor/sources/_details.html.erb +34 -5
- data/app/views/source_monitor/sources/_row.html.erb +11 -6
- data/config/routes.rb +1 -0
- data/docs/configuration.md +1 -1
- data/docs/upgrade.md +22 -0
- data/lib/generators/source_monitor/install/templates/source_monitor.rb.tt +15 -1
- data/lib/source_monitor/configuration/favicons_settings.rb +42 -0
- data/lib/source_monitor/configuration/http_settings.rb +1 -1
- data/lib/source_monitor/configuration/scraping_settings.rb +1 -1
- data/lib/source_monitor/configuration.rb +3 -1
- data/lib/source_monitor/favicons/discoverer.rb +196 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +21 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +1 -0
- data/lib/source_monitor/http.rb +5 -3
- data/lib/source_monitor/version.rb +1 -1
- data/lib/source_monitor.rb +4 -0
- data/source_monitor.gemspec +1 -1
- metadata +6 -106
- data/.vbw-planning/PROJECT.md +0 -51
- data/.vbw-planning/ROADMAP.md +0 -53
- data/.vbw-planning/SHIPPED.md +0 -63
- data/.vbw-planning/STATE.md +0 -27
- data/.vbw-planning/codebase/ARCHITECTURE.md +0 -147
- data/.vbw-planning/codebase/CONCERNS.md +0 -99
- data/.vbw-planning/codebase/CONVENTIONS.md +0 -97
- data/.vbw-planning/codebase/DEPENDENCIES.md +0 -100
- data/.vbw-planning/codebase/INDEX.md +0 -86
- data/.vbw-planning/codebase/META.md +0 -42
- data/.vbw-planning/codebase/PATTERNS.md +0 -262
- data/.vbw-planning/codebase/STACK.md +0 -101
- data/.vbw-planning/codebase/STRUCTURE.md +0 -324
- data/.vbw-planning/codebase/TESTING.md +0 -154
- data/.vbw-planning/config.json +0 -53
- data/.vbw-planning/discovery.json +0 -26
- data/.vbw-planning/milestones/default/ROADMAP.md +0 -115
- data/.vbw-planning/milestones/default/STATE.md +0 -82
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +0 -56
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +0 -187
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +0 -64
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +0 -137
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +0 -67
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +0 -142
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +0 -64
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +0 -138
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +0 -85
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +0 -147
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +0 -63
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +0 -129
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +0 -74
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +0 -154
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +0 -303
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +0 -510
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +0 -61
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +0 -161
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +0 -66
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +0 -132
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +0 -59
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +0 -171
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +0 -56
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +0 -152
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +0 -33
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +0 -42
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +0 -119
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +0 -52
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +0 -195
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +0 -79
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +0 -130
- data/.vbw-planning/milestones/generator-enhancements/REQUIREMENTS.md +0 -72
- data/.vbw-planning/milestones/generator-enhancements/ROADMAP.md +0 -125
- data/.vbw-planning/milestones/generator-enhancements/SHIPPED.md +0 -40
- data/.vbw-planning/milestones/generator-enhancements/STATE.md +0 -43
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-CONTEXT.md +0 -33
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/01-VERIFICATION.md +0 -86
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01-SUMMARY.md +0 -61
- data/.vbw-planning/milestones/generator-enhancements/phases/01-generator-steps/PLAN-01.md +0 -380
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/02-VERIFICATION.md +0 -78
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01-SUMMARY.md +0 -46
- data/.vbw-planning/milestones/generator-enhancements/phases/02-verification/PLAN-01.md +0 -500
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/03-VERIFICATION.md +0 -89
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01-SUMMARY.md +0 -48
- data/.vbw-planning/milestones/generator-enhancements/phases/03-docs-alignment/PLAN-01.md +0 -456
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/04-VERIFICATION.md +0 -129
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01-SUMMARY.md +0 -70
- data/.vbw-planning/milestones/generator-enhancements/phases/04-dashboard-ux/PLAN-01.md +0 -747
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/05-VERIFICATION.md +0 -156
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01-SUMMARY.md +0 -69
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-01.md +0 -455
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02-SUMMARY.md +0 -39
- data/.vbw-planning/milestones/generator-enhancements/phases/05-active-storage-images/PLAN-02.md +0 -488
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/06-VERIFICATION.md +0 -100
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01-SUMMARY.md +0 -37
- data/.vbw-planning/milestones/generator-enhancements/phases/06-netflix-feed-fix/PLAN-01.md +0 -345
- data/.vbw-planning/milestones/upgrade-assurance/REQUIREMENTS.md +0 -80
- data/.vbw-planning/milestones/upgrade-assurance/ROADMAP.md +0 -75
- data/.vbw-planning/milestones/upgrade-assurance/STATE.md +0 -29
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/01-VERIFICATION.md +0 -144
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01-SUMMARY.md +0 -43
- data/.vbw-planning/milestones/upgrade-assurance/phases/01-upgrade-command/PLAN-01.md +0 -405
- data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01-SUMMARY.md +0 -27
- data/.vbw-planning/milestones/upgrade-assurance/phases/02-config-deprecation/PLAN-01.md +0 -303
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/03-VERIFICATION.md +0 -380
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01-SUMMARY.md +0 -36
- data/.vbw-planning/milestones/upgrade-assurance/phases/03-upgrade-skill-docs/PLAN-01.md +0 -652
- data/.vbw-planning/phases/01-aia-certificate-resolution/.context-dev.md +0 -17
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01-SUMMARY.md +0 -26
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-01.md +0 -71
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02-SUMMARY.md +0 -16
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-02.md +0 -56
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03-SUMMARY.md +0 -17
- data/.vbw-planning/phases/01-aia-certificate-resolution/PLAN-03.md +0 -98
- data/.vbw-planning/phases/02-test-performance/.context-dev.md +0 -75
- data/.vbw-planning/phases/02-test-performance/.context-lead.md +0 -89
- data/.vbw-planning/phases/02-test-performance/.context-qa.md +0 -23
- data/.vbw-planning/phases/02-test-performance/02-RESEARCH.md +0 -56
- data/.vbw-planning/phases/02-test-performance/02-VERIFICATION.md +0 -51
- data/.vbw-planning/phases/02-test-performance/PLAN-01-SUMMARY.md +0 -37
- data/.vbw-planning/phases/02-test-performance/PLAN-01.md +0 -156
- data/.vbw-planning/phases/02-test-performance/PLAN-02-SUMMARY.md +0 -33
- data/.vbw-planning/phases/02-test-performance/PLAN-02.md +0 -120
- data/.vbw-planning/phases/02-test-performance/PLAN-03-SUMMARY.md +0 -30
- data/.vbw-planning/phases/02-test-performance/PLAN-03.md +0 -154
- data/.vbw-planning/phases/02-test-performance/PLAN-04-SUMMARY.md +0 -28
- data/.vbw-planning/phases/02-test-performance/PLAN-04.md +0 -133
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 932dca7d7b8f754262dd37aac3cf722aee017ec53e662116dd97527ec2a8a1f3
|
|
4
|
+
data.tar.gz: 7d6ff568a5e3eb1cf5269736771474f19da9e82b18759232b03855735f284819
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1318387b90d5811d52fc5d852a2be9578515a0d69f77baa90ecc1fbab930a3d2067a1e7056f8d2bd15940de2c0799e147203b31e4d91784e3078fb0fa8edc6e5
|
|
7
|
+
data.tar.gz: b35f81be14c230f1865642b5aa9f8fd462791c8fe3db293bf0f6326f2fc9fc1295eaef5f9899488d298e01e6eb3e6b316c00ed53595341a17973edd50cbb7b10
|
|
@@ -29,7 +29,7 @@ After the block executes, `ModelExtensions.reload!` runs automatically to apply
|
|
|
29
29
|
|
|
30
30
|
## Configuration Sections
|
|
31
31
|
|
|
32
|
-
The `config` object (`SourceMonitor::Configuration`) has
|
|
32
|
+
The `config` object (`SourceMonitor::Configuration`) has 13 sub-sections plus top-level queue/job settings:
|
|
33
33
|
|
|
34
34
|
| Section | Accessor | Class |
|
|
35
35
|
|---|---|---|
|
|
@@ -45,6 +45,7 @@ The `config` object (`SourceMonitor::Configuration`) has 11 sub-sections plus to
|
|
|
45
45
|
| Realtime | `config.realtime` | `RealtimeSettings` |
|
|
46
46
|
| Authentication | `config.authentication` | `AuthenticationSettings` |
|
|
47
47
|
| Images | `config.images` | `ImagesSettings` |
|
|
48
|
+
| Favicons | `config.favicons` | `FaviconsSettings` |
|
|
48
49
|
|
|
49
50
|
See `reference/configuration-reference.md` for every setting with types, defaults, and examples.
|
|
50
51
|
|
|
@@ -91,6 +92,14 @@ config.models.source.include_concern "MyApp::SourceExtension"
|
|
|
91
92
|
config.models.item.validate :custom_check
|
|
92
93
|
```
|
|
93
94
|
|
|
95
|
+
### Favicons (Active Storage)
|
|
96
|
+
```ruby
|
|
97
|
+
config.favicons.enabled = true
|
|
98
|
+
config.favicons.fetch_timeout = 10
|
|
99
|
+
config.favicons.max_download_size = 512 * 1024 # 512 KB
|
|
100
|
+
config.favicons.retry_cooldown_days = 14
|
|
101
|
+
```
|
|
102
|
+
|
|
94
103
|
### Realtime
|
|
95
104
|
```ruby
|
|
96
105
|
config.realtime.adapter = :redis
|
|
@@ -342,6 +342,50 @@ When enabled, `DownloadContentImagesJob` is automatically enqueued after new ite
|
|
|
342
342
|
|
|
343
343
|
---
|
|
344
344
|
|
|
345
|
+
## Favicons Settings (`config.favicons`)
|
|
346
|
+
|
|
347
|
+
Class: `SourceMonitor::Configuration::FaviconsSettings`
|
|
348
|
+
|
|
349
|
+
Controls automatic favicon fetching and storage for sources via Active Storage.
|
|
350
|
+
|
|
351
|
+
**Prerequisite:** The host app must have Active Storage installed (`rails active_storage:install` + migrations). Without Active Storage, favicons are silently disabled and colored initials placeholders are shown instead.
|
|
352
|
+
|
|
353
|
+
| Setting | Type | Default | Description |
|
|
354
|
+
|---|---|---|---|
|
|
355
|
+
| `enabled` | Boolean | `true` | Enable automatic favicon fetching |
|
|
356
|
+
| `fetch_timeout` | Integer | `5` | HTTP timeout for favicon requests (seconds) |
|
|
357
|
+
| `max_download_size` | Integer | `1048576` (1 MB) | Maximum favicon file size in bytes; larger files are skipped |
|
|
358
|
+
| `retry_cooldown_days` | Integer | `7` | Days to wait before retrying a failed favicon fetch |
|
|
359
|
+
| `allowed_content_types` | Array | `["image/x-icon", "image/vnd.microsoft.icon", "image/png", "image/jpeg", "image/gif", "image/svg+xml", "image/webp"]` | Permitted MIME types for downloaded favicons |
|
|
360
|
+
|
|
361
|
+
### Helper Method
|
|
362
|
+
|
|
363
|
+
| Method | Returns | Description |
|
|
364
|
+
|---|---|---|
|
|
365
|
+
| `enabled?` | Boolean | Returns `true` when `enabled` is truthy AND `ActiveStorage` is defined |
|
|
366
|
+
|
|
367
|
+
```ruby
|
|
368
|
+
# Customize favicon settings
|
|
369
|
+
config.favicons.enabled = true
|
|
370
|
+
config.favicons.fetch_timeout = 10
|
|
371
|
+
config.favicons.max_download_size = 512 * 1024 # 512 KB
|
|
372
|
+
config.favicons.retry_cooldown_days = 14
|
|
373
|
+
config.favicons.allowed_content_types = %w[image/png image/x-icon image/svg+xml]
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
When enabled, `FaviconFetchJob` is automatically enqueued:
|
|
377
|
+
1. After a new source is created (via UI or OPML import) with a `website_url`
|
|
378
|
+
2. After a successful feed fetch when the source has no favicon attached and is outside the retry cooldown
|
|
379
|
+
|
|
380
|
+
The job uses `Favicons::Discoverer` which tries three strategies in order:
|
|
381
|
+
1. Direct `/favicon.ico` fetch from the source's domain
|
|
382
|
+
2. HTML page parsing for `<link rel="icon">`, `<link rel="apple-touch-icon">`, and similar tags (prefers largest by `sizes` attribute)
|
|
383
|
+
3. Google Favicon API as a last resort
|
|
384
|
+
|
|
385
|
+
Failed attempts are tracked in the source's `metadata` JSONB column (`favicon_last_attempted_at`) to respect the cooldown period.
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
345
389
|
## Environment Variables
|
|
346
390
|
|
|
347
391
|
| Variable | Purpose |
|
|
@@ -176,6 +176,23 @@ SourceMonitor.configure do |config|
|
|
|
176
176
|
# record.errors.add(:base, "custom error") unless record.valid_for_my_app?
|
|
177
177
|
# }
|
|
178
178
|
|
|
179
|
+
# ===========================================================================
|
|
180
|
+
# Favicons (Active Storage)
|
|
181
|
+
# ===========================================================================
|
|
182
|
+
# Automatically fetch and store source favicons via Active Storage.
|
|
183
|
+
# Requires Active Storage in the host app (rails active_storage:install).
|
|
184
|
+
# Without Active Storage, favicons are silently disabled -- colored
|
|
185
|
+
# initials placeholders are shown instead.
|
|
186
|
+
|
|
187
|
+
# config.favicons.enabled = true # default: true
|
|
188
|
+
# config.favicons.fetch_timeout = 5 # seconds
|
|
189
|
+
# config.favicons.max_download_size = 1_048_576 # 1 MB
|
|
190
|
+
# config.favicons.retry_cooldown_days = 7
|
|
191
|
+
# config.favicons.allowed_content_types = %w[
|
|
192
|
+
# image/x-icon image/vnd.microsoft.icon image/png
|
|
193
|
+
# image/jpeg image/gif image/svg+xml image/webp
|
|
194
|
+
# ]
|
|
195
|
+
|
|
179
196
|
# ===========================================================================
|
|
180
197
|
# Realtime (Action Cable) Adapter
|
|
181
198
|
# ===========================================================================
|
|
@@ -155,6 +155,8 @@ bin/source_monitor verify
|
|
|
155
155
|
- [ ] Event callbacks wired for host integration
|
|
156
156
|
- [ ] Realtime adapter confirmed (Solid Cable or Redis)
|
|
157
157
|
- [ ] Mission Control integration enabled (if desired)
|
|
158
|
+
- [ ] Active Storage installed (required for favicons and image downloads)
|
|
159
|
+
- [ ] Favicon settings configured (`config.favicons.*`) if customization needed
|
|
158
160
|
|
|
159
161
|
## Troubleshooting
|
|
160
162
|
|
|
@@ -164,6 +164,32 @@ class ScheduleFetchesJob < ApplicationJob
|
|
|
164
164
|
end
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
### Lightweight Fetch Job (FaviconFetchJob)
|
|
168
|
+
|
|
169
|
+
Demonstrates multi-strategy cascade with guard clauses:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
class FaviconFetchJob < ApplicationJob
|
|
173
|
+
source_monitor_queue :fetch
|
|
174
|
+
discard_on ActiveJob::DeserializationError
|
|
175
|
+
|
|
176
|
+
def perform(source_id)
|
|
177
|
+
source = Source.find_by(id: source_id)
|
|
178
|
+
return unless source
|
|
179
|
+
return unless should_fetch?(source)
|
|
180
|
+
|
|
181
|
+
result = Favicons::Discoverer.new(source: source).call
|
|
182
|
+
attach_favicon(source, result) if result.success?
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Notable patterns:
|
|
188
|
+
- Multiple guard clauses: source exists, Active Storage defined, no existing favicon, outside cooldown period
|
|
189
|
+
- Uses `Favicons::Discoverer` service with 3-strategy cascade (direct `/favicon.ico`, HTML parsing, Google API)
|
|
190
|
+
- Failed attempts tracked in source `metadata` JSONB (`favicon_last_attempted_at`) for retry cooldown
|
|
191
|
+
- Graceful degradation: host apps without Active Storage never enqueue this job
|
|
192
|
+
|
|
167
193
|
### Broadcast Job (SourceHealthCheckJob)
|
|
168
194
|
|
|
169
195
|
Demonstrates result broadcasting:
|
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Version-specific migration notes for each major/minor version transition. Agents should reference this file when guiding users through multi-version upgrades.
|
|
4
4
|
|
|
5
|
+
## 0.7.x to 0.8.0
|
|
6
|
+
|
|
7
|
+
**Key changes:**
|
|
8
|
+
- Default HTTP User-Agent changed from `SourceMonitor/<version>` to `Mozilla/5.0 (compatible; SourceMonitor/<version>)` with browser-like headers (Accept-Language, DNT, Referer). Prevents bot-blocking by feed servers.
|
|
9
|
+
- Default `max_in_flight_per_source` changed from `25` to `nil` (unlimited). If you relied on the previous default for per-source rate limiting, set it explicitly.
|
|
10
|
+
- Successful manual health checks on degraded sources now trigger a feed fetch for faster recovery.
|
|
11
|
+
- Automatic source favicons via Active Storage with multi-strategy discovery (direct `/favicon.ico`, HTML `<link>` parsing, Google Favicon API fallback)
|
|
12
|
+
- New configuration section: `config.favicons` with `enabled`, `fetch_timeout`, `max_download_size`, `retry_cooldown_days`, and `allowed_content_types` settings
|
|
13
|
+
- Colored initials placeholder shown when no favicon is available or Active Storage is not installed
|
|
14
|
+
- OPML imports trigger favicon fetches for each imported source with a `website_url`
|
|
15
|
+
- Toast notifications capped at 3 visible with "+N more" badge, click-to-expand, and "Clear all" button
|
|
16
|
+
- Error-level toasts auto-dismiss after 10 seconds (vs 5 seconds for info/success)
|
|
17
|
+
|
|
18
|
+
**Action items:**
|
|
19
|
+
1. Re-run `bin/rails source_monitor:upgrade` to get updated initializer template
|
|
20
|
+
2. If you explicitly set `config.http.user_agent`, your value is preserved. Otherwise the new browser-like default applies automatically.
|
|
21
|
+
3. If you need per-source scrape rate limiting, add `config.scraping.max_in_flight_per_source = 25` (or your preferred value) to your initializer
|
|
22
|
+
4. If using Active Storage, favicons are enabled by default -- no action needed
|
|
23
|
+
5. If NOT using Active Storage, favicons are silently disabled -- no action needed
|
|
24
|
+
6. Toast stacking is automatic -- no configuration needed
|
|
25
|
+
7. No breaking changes -- all existing configuration remains valid
|
|
26
|
+
|
|
5
27
|
## 0.3.x to 0.4.0
|
|
6
28
|
|
|
7
29
|
**Released:** 2026-02-12
|
data/.gitignore
CHANGED
|
@@ -27,5 +27,15 @@
|
|
|
27
27
|
.vbw-planning/.active-agent
|
|
28
28
|
.vbw-planning/.active-agent-count
|
|
29
29
|
.vbw-planning/.todo-flat-migrated
|
|
30
|
+
.vbw-planning/.agent-worktrees/
|
|
31
|
+
.vbw-planning/.cache/
|
|
32
|
+
.vbw-planning/.context-usage
|
|
33
|
+
.vbw-planning/.contracts/
|
|
34
|
+
.vbw-planning/.events/
|
|
35
|
+
.vbw-planning/.execution-state.json
|
|
30
36
|
/codebase_analysis.md
|
|
37
|
+
/VERIFICATION.md
|
|
38
|
+
/test/dummy/public/assets/
|
|
39
|
+
/test/lib/tmp/
|
|
31
40
|
*.gem
|
|
41
|
+
.vbw-worktrees/
|
data/AGENTS.md
CHANGED
|
@@ -83,7 +83,7 @@ Store secrets (API keys, webhook tokens) in `config/credentials/` and never comm
|
|
|
83
83
|
|
|
84
84
|
## Claude Code Skills
|
|
85
85
|
|
|
86
|
-
SourceMonitor ships
|
|
86
|
+
SourceMonitor ships 15 engine-specific Claude Code skills (`sm-*` prefix) covering the domain model, configuration DSL, pipeline stages, testing conventions, and more. Skills are distributed with the gem and installed into `.claude/skills/` via rake tasks:
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
89
|
bin/rails source_monitor:skills:install # Consumer skills (host app integration)
|
data/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,41 @@ All notable changes to this project are documented below. The format follows [Ke
|
|
|
15
15
|
|
|
16
16
|
- No unreleased changes yet.
|
|
17
17
|
|
|
18
|
+
## [0.8.0] - 2026-02-21
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Automatic source favicons.** Sources now display favicons next to their names in list and detail views. Favicons are fetched automatically via background job on source creation and successful feed fetches using a multi-strategy cascade: `/favicon.ico` direct fetch, HTML `<link>` tag parsing (preferring largest available), and Google Favicon API fallback. Requires Active Storage in the host app.
|
|
23
|
+
- New configuration section: `config.favicons` with `enabled` (default: `true`), `fetch_timeout` (5s), `max_download_size` (1MB), `retry_cooldown_days` (7), and `allowed_content_types` settings.
|
|
24
|
+
- Colored initials placeholder shown when no favicon is available (consistent HSL color derived from source name).
|
|
25
|
+
- Graceful degradation: host apps without Active Storage see placeholders only, no errors.
|
|
26
|
+
- OPML imports also trigger favicon fetches for each imported source with a `website_url`.
|
|
27
|
+
- Manual "Fetch Favicon" button on source detail pages; favicon fetch also triggered on 304 Not Modified responses when missing.
|
|
28
|
+
- Redirect-following in favicon discoverer for domains that redirect (e.g., `reddit.com` -> `www.reddit.com`).
|
|
29
|
+
- **Toast notification stacking.** Bulk operations no longer flood the screen with overlapping toasts. At most 3 toasts are visible at a time; overflow is shown as a "+N more" badge that expands the full stack on click. "Clear all" button dismisses every toast at once.
|
|
30
|
+
- Error-level toasts persist for 10 seconds (vs 5 seconds for info/success).
|
|
31
|
+
- Hidden toasts promote into visible slots as earlier toasts auto-dismiss.
|
|
32
|
+
- Container controller tracks DOM changes via MutationObserver and properly cleans up event listeners on disconnect.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **Browser-like default User-Agent.** Default HTTP User-Agent changed from `SourceMonitor/<version>` to `Mozilla/5.0 (compatible; SourceMonitor/<version>)` with full browser-like headers (Accept, Accept-Language, DNT, Referer from source `website_url`). This prevents bot-blocking by feed servers.
|
|
37
|
+
- **Smarter scrape rate limiting.** Default `max_in_flight_per_source` changed from `25` to `nil` (unlimited). The previous default unnecessarily throttled scraping for sources with many items. Set an explicit value in your initializer if you need per-source caps.
|
|
38
|
+
- **Health check triggers status re-evaluation.** A successful manual health check on a degraded (declining/critical/warning) source now triggers a feed fetch, allowing the health monitor to transition the source back to "improving" status instead of requiring the source to recover on its own schedule.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- Favicon discoverer properly follows HTTP redirects (e.g., `reddit.com` -> `www.reddit.com`).
|
|
43
|
+
- Favicon fetch uses `rails_blob_path` for correct routing within the engine context.
|
|
44
|
+
- Favicon display prefers PNG format (via Google Favicon API) over raw ICO for better browser compatibility.
|
|
45
|
+
- Gemspec excludes `.vbw-planning/` from gem package to reduce gem size.
|
|
46
|
+
|
|
47
|
+
### Testing
|
|
48
|
+
|
|
49
|
+
- 1,125 tests, 0 failures.
|
|
50
|
+
- RuboCop: 0 offenses.
|
|
51
|
+
- Brakeman: 0 warnings.
|
|
52
|
+
|
|
18
53
|
## [0.7.1] - 2026-02-18
|
|
19
54
|
|
|
20
55
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -4,17 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
## Active Context
|
|
6
6
|
|
|
7
|
-
**Milestone:**
|
|
8
|
-
**
|
|
9
|
-
**
|
|
10
|
-
**
|
|
7
|
+
**Milestone:** polish-and-reliability
|
|
8
|
+
**Phase:** 1 -- Backend Fixes (pending planning)
|
|
9
|
+
**Last shipped:** aia-ssl-fix (2026-02-20) -- 2 phases, 7 plans, 8 commits
|
|
10
|
+
**Previous:** upgrade-assurance (2026-02-13), generator-enhancements (2026-02-12)
|
|
11
11
|
|
|
12
12
|
## Key Decisions
|
|
13
13
|
|
|
14
14
|
- Keep PostgreSQL-only for now
|
|
15
15
|
- Keep host-app auth model
|
|
16
16
|
- Ruby autoload for lib/ modules (not Zeitwerk)
|
|
17
|
-
- PG parallel fork segfault
|
|
17
|
+
- PG parallel fork segfault resolved: switched to thread-based parallelism in aia-ssl-fix milestone
|
|
18
18
|
|
|
19
19
|
## Installed Skills
|
|
20
20
|
|
|
@@ -100,6 +100,12 @@ Run /vbw:help for all commands.
|
|
|
100
100
|
- No N+1 queries (use `includes`/`preload`).
|
|
101
101
|
- No hardcoded credentials (use Rails credentials or ENV).
|
|
102
102
|
|
|
103
|
+
## QA and UAT Rules
|
|
104
|
+
|
|
105
|
+
- **Browser-first verification:** During VBW QA (`/vbw:qa`) and UAT (`/vbw:verify`), ALWAYS start by using `agent-browser` to test UI scenarios yourself before presenting checkpoints to the user. Navigate to the dummy app (port 3002), take snapshots/screenshots, and verify visual and functional behavior with agents first.
|
|
106
|
+
- **Automate what you can:** Any test that can be verified programmatically (config defaults, job enqueue behavior, controller responses) should be automated -- only present truly visual/interactive tests to the user.
|
|
107
|
+
- **Dummy app port:** The SourceMonitor dummy app runs on port 3002 (`cd test/dummy && bin/rails server -p 3002`).
|
|
108
|
+
|
|
103
109
|
## Security Rules
|
|
104
110
|
|
|
105
111
|
### Protected Files (NEVER read or output)
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -9,8 +9,8 @@ SourceMonitor is a production-ready Rails 8 mountable engine for ingesting, norm
|
|
|
9
9
|
In your host Rails app:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
bundle add source_monitor --version "~> 0.
|
|
13
|
-
# or add `gem "source_monitor", "~> 0.
|
|
12
|
+
bundle add source_monitor --version "~> 0.7.1"
|
|
13
|
+
# or add `gem "source_monitor", "~> 0.7.1"` manually, then run:
|
|
14
14
|
bundle install
|
|
15
15
|
```
|
|
16
16
|
|
|
@@ -19,7 +19,9 @@ This exposes `bin/source_monitor` (via Bundler binstubs) so you can run the guid
|
|
|
19
19
|
## Highlights
|
|
20
20
|
- Full-featured source and item administration backed by Turbo Streams and Tailwind UI components
|
|
21
21
|
- Adaptive fetch pipeline (Feedjira + Faraday) with conditional GETs, retention pruning, and scrape orchestration
|
|
22
|
+
- Automatic source favicons via Active Storage with multi-strategy discovery and graceful fallback
|
|
22
23
|
- Realtime dashboard metrics, batching/caching query layer, and Mission Control integration hooks
|
|
24
|
+
- Smart toast notification stacking (max 3 visible, "+N more" overflow badge, click-to-expand)
|
|
23
25
|
- Extensible scraper adapters (Readability included) with per-source settings and structured result metadata
|
|
24
26
|
- Declarative configuration DSL covering queues, HTTP, retention, events, model extensions, authentication, and realtime transports
|
|
25
27
|
- First-class observability through ActiveSupport notifications and `SourceMonitor::Metrics` counters/gauges
|
|
@@ -41,7 +43,7 @@ This exposes `bin/source_monitor` (via Bundler binstubs) so you can run the guid
|
|
|
41
43
|
Before running any SourceMonitor commands inside your host app, add the gem and install dependencies:
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
|
-
bundle add source_monitor --version "~> 0.
|
|
46
|
+
bundle add source_monitor --version "~> 0.7.1"
|
|
45
47
|
# or edit your Gemfile, then run
|
|
46
48
|
bundle install
|
|
47
49
|
```
|
|
@@ -113,7 +115,7 @@ See [docs/configuration.md](docs/configuration.md) for exhaustive coverage and e
|
|
|
113
115
|
|
|
114
116
|
## Claude Code Skills
|
|
115
117
|
|
|
116
|
-
SourceMonitor ships
|
|
118
|
+
SourceMonitor ships 15 engine-specific Claude Code skills (`sm-*` prefix) that give AI agents deep context about the engine's domain model, configuration DSL, pipeline stages, and testing conventions. Skills are bundled with the gem and installed into your host app's `.claude/skills/` directory.
|
|
117
119
|
|
|
118
120
|
```bash
|
|
119
121
|
bin/rails source_monitor:skills:install # Consumer skills (host app integration)
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.8.0
|
|
@@ -651,6 +651,14 @@ video {
|
|
|
651
651
|
right: 0px;
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
+
.fm-admin .-bottom-1 {
|
|
655
|
+
bottom: -0.25rem;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.fm-admin .-right-1 {
|
|
659
|
+
right: -0.25rem;
|
|
660
|
+
}
|
|
661
|
+
|
|
654
662
|
.fm-admin .left-4 {
|
|
655
663
|
left: 1rem;
|
|
656
664
|
}
|
|
@@ -957,6 +965,10 @@ video {
|
|
|
957
965
|
align-items: flex-start;
|
|
958
966
|
}
|
|
959
967
|
|
|
968
|
+
.fm-admin .items-end {
|
|
969
|
+
align-items: flex-end;
|
|
970
|
+
}
|
|
971
|
+
|
|
960
972
|
.fm-admin .items-center {
|
|
961
973
|
align-items: center;
|
|
962
974
|
}
|
|
@@ -977,6 +989,10 @@ video {
|
|
|
977
989
|
gap: 0.25rem;
|
|
978
990
|
}
|
|
979
991
|
|
|
992
|
+
.fm-admin .gap-1\.5 {
|
|
993
|
+
gap: 0.375rem;
|
|
994
|
+
}
|
|
995
|
+
|
|
980
996
|
.fm-admin .gap-2 {
|
|
981
997
|
gap: 0.5rem;
|
|
982
998
|
}
|
|
@@ -1348,6 +1364,11 @@ video {
|
|
|
1348
1364
|
background-color: rgb(248 250 252 / var(--tw-bg-opacity, 1));
|
|
1349
1365
|
}
|
|
1350
1366
|
|
|
1367
|
+
.fm-admin .bg-slate-700 {
|
|
1368
|
+
--tw-bg-opacity: 1;
|
|
1369
|
+
background-color: rgb(51 65 85 / var(--tw-bg-opacity, 1));
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1351
1372
|
.fm-admin .bg-slate-800 {
|
|
1352
1373
|
--tw-bg-opacity: 1;
|
|
1353
1374
|
background-color: rgb(30 41 59 / var(--tw-bg-opacity, 1));
|
|
@@ -1367,6 +1388,15 @@ video {
|
|
|
1367
1388
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
|
1368
1389
|
}
|
|
1369
1390
|
|
|
1391
|
+
.fm-admin .object-contain {
|
|
1392
|
+
-o-object-fit: contain;
|
|
1393
|
+
object-fit: contain;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
.fm-admin .p-0\.5 {
|
|
1397
|
+
padding: 0.125rem;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1370
1400
|
.fm-admin .p-3 {
|
|
1371
1401
|
padding: 0.75rem;
|
|
1372
1402
|
}
|
|
@@ -1726,6 +1756,10 @@ video {
|
|
|
1726
1756
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
|
1727
1757
|
}
|
|
1728
1758
|
|
|
1759
|
+
.fm-admin .underline {
|
|
1760
|
+
text-decoration-line: underline;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1729
1763
|
.fm-admin .opacity-0 {
|
|
1730
1764
|
opacity: 0;
|
|
1731
1765
|
}
|
|
@@ -1848,6 +1882,11 @@ video {
|
|
|
1848
1882
|
background-color: rgb(248 250 252 / var(--tw-bg-opacity, 1));
|
|
1849
1883
|
}
|
|
1850
1884
|
|
|
1885
|
+
.fm-admin .hover\:bg-slate-600:hover {
|
|
1886
|
+
--tw-bg-opacity: 1;
|
|
1887
|
+
background-color: rgb(71 85 105 / var(--tw-bg-opacity, 1));
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1851
1890
|
.fm-admin .hover\:bg-slate-700:hover {
|
|
1852
1891
|
--tw-bg-opacity: 1;
|
|
1853
1892
|
background-color: rgb(51 65 85 / var(--tw-bg-opacity, 1));
|
|
@@ -1962,6 +2001,10 @@ video {
|
|
|
1962
2001
|
background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1));
|
|
1963
2002
|
}
|
|
1964
2003
|
|
|
2004
|
+
.fm-admin :is(.group:hover .group-hover\:inline-flex) {
|
|
2005
|
+
display: inline-flex;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1965
2008
|
.fm-admin :is(.peer:checked ~ .peer-checked\:border-blue-500) {
|
|
1966
2009
|
--tw-border-opacity: 1;
|
|
1967
2010
|
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
|
@@ -2488,6 +2488,7 @@ var notification_controller_default = class extends Controller {
|
|
|
2488
2488
|
}
|
|
2489
2489
|
this.clearTimeout();
|
|
2490
2490
|
this.registerController();
|
|
2491
|
+
this.applyLevelDelay();
|
|
2491
2492
|
this.startTimer();
|
|
2492
2493
|
}
|
|
2493
2494
|
disconnect() {
|
|
@@ -2505,8 +2506,17 @@ var notification_controller_default = class extends Controller {
|
|
|
2505
2506
|
if (this.delayValue <= 0) return;
|
|
2506
2507
|
this.timeoutId = window.setTimeout(() => this.dismiss(), this.delayValue);
|
|
2507
2508
|
}
|
|
2509
|
+
applyLevelDelay() {
|
|
2510
|
+
const level = this.element.dataset.level;
|
|
2511
|
+
if (level === "error" && this.delayValue === 5e3) {
|
|
2512
|
+
this.delayValue = 1e4;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2508
2515
|
dismiss() {
|
|
2509
2516
|
if (!this.element) return;
|
|
2517
|
+
this.element.dispatchEvent(
|
|
2518
|
+
new CustomEvent("notification:dismissed", { bubbles: true })
|
|
2519
|
+
);
|
|
2510
2520
|
this.element.classList.add("opacity-0", "translate-y-2");
|
|
2511
2521
|
window.setTimeout(() => {
|
|
2512
2522
|
if (this.element && this.element.remove) {
|
|
@@ -2521,6 +2531,122 @@ var notification_controller_default = class extends Controller {
|
|
|
2521
2531
|
}
|
|
2522
2532
|
};
|
|
2523
2533
|
|
|
2534
|
+
// app/assets/javascripts/source_monitor/controllers/notification_container_controller.js
|
|
2535
|
+
var notification_container_controller_default = class extends Controller {
|
|
2536
|
+
static values = {
|
|
2537
|
+
maxVisible: { default: 3, type: Number },
|
|
2538
|
+
expanded: { default: false, type: Boolean }
|
|
2539
|
+
};
|
|
2540
|
+
static targets = ["list", "badge", "badgeCount", "clearAll"];
|
|
2541
|
+
connect() {
|
|
2542
|
+
this.rafId = null;
|
|
2543
|
+
this.boundHandleClickOutside = this.handleClickOutside.bind(this);
|
|
2544
|
+
this.boundScheduleRecalculate = () => this.scheduleRecalculate();
|
|
2545
|
+
this.observer = new MutationObserver(this.boundScheduleRecalculate);
|
|
2546
|
+
this.observer.observe(this.listTarget, { childList: true });
|
|
2547
|
+
this.listTarget.addEventListener(
|
|
2548
|
+
"notification:dismissed",
|
|
2549
|
+
this.boundScheduleRecalculate
|
|
2550
|
+
);
|
|
2551
|
+
this.recalculateVisibility();
|
|
2552
|
+
}
|
|
2553
|
+
disconnect() {
|
|
2554
|
+
if (this.observer) {
|
|
2555
|
+
this.observer.disconnect();
|
|
2556
|
+
this.observer = null;
|
|
2557
|
+
}
|
|
2558
|
+
if (this.rafId) {
|
|
2559
|
+
cancelAnimationFrame(this.rafId);
|
|
2560
|
+
this.rafId = null;
|
|
2561
|
+
}
|
|
2562
|
+
this.listTarget.removeEventListener(
|
|
2563
|
+
"notification:dismissed",
|
|
2564
|
+
this.boundScheduleRecalculate
|
|
2565
|
+
);
|
|
2566
|
+
document.removeEventListener("click", this.boundHandleClickOutside);
|
|
2567
|
+
}
|
|
2568
|
+
scheduleRecalculate() {
|
|
2569
|
+
if (this.rafId) {
|
|
2570
|
+
cancelAnimationFrame(this.rafId);
|
|
2571
|
+
}
|
|
2572
|
+
this.rafId = requestAnimationFrame(() => {
|
|
2573
|
+
this.rafId = null;
|
|
2574
|
+
this.recalculateVisibility();
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
recalculateVisibility() {
|
|
2578
|
+
const toasts = Array.from(this.listTarget.children);
|
|
2579
|
+
const total = toasts.length;
|
|
2580
|
+
if (this.expandedValue) {
|
|
2581
|
+
toasts.forEach((toast) => {
|
|
2582
|
+
toast.classList.remove("hidden");
|
|
2583
|
+
toast.removeAttribute("aria-hidden");
|
|
2584
|
+
toast.removeAttribute("inert");
|
|
2585
|
+
});
|
|
2586
|
+
} else {
|
|
2587
|
+
toasts.forEach((toast, index) => {
|
|
2588
|
+
if (index < this.maxVisibleValue) {
|
|
2589
|
+
toast.classList.remove("hidden");
|
|
2590
|
+
toast.removeAttribute("aria-hidden");
|
|
2591
|
+
toast.removeAttribute("inert");
|
|
2592
|
+
} else {
|
|
2593
|
+
toast.classList.add("hidden");
|
|
2594
|
+
toast.setAttribute("aria-hidden", "true");
|
|
2595
|
+
toast.setAttribute("inert", "");
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
const hiddenCount = this.expandedValue ? 0 : Math.max(0, total - this.maxVisibleValue);
|
|
2600
|
+
if (this.hasBadgeTarget) {
|
|
2601
|
+
if (this.hasBadgeCountTarget) {
|
|
2602
|
+
this.badgeCountTarget.textContent = `+${hiddenCount} more`;
|
|
2603
|
+
}
|
|
2604
|
+
if (hiddenCount > 0) {
|
|
2605
|
+
this.badgeTarget.classList.remove("hidden");
|
|
2606
|
+
} else {
|
|
2607
|
+
this.badgeTarget.classList.add("hidden");
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
if (this.hasClearAllTarget) {
|
|
2611
|
+
const showClearAll = total > 0 && (hiddenCount > 0 || this.expandedValue);
|
|
2612
|
+
if (showClearAll) {
|
|
2613
|
+
this.clearAllTarget.classList.remove("hidden");
|
|
2614
|
+
} else {
|
|
2615
|
+
this.clearAllTarget.classList.add("hidden");
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
toggleExpand(event) {
|
|
2620
|
+
event.preventDefault();
|
|
2621
|
+
if (this.expandedValue) {
|
|
2622
|
+
this.collapseStack();
|
|
2623
|
+
} else {
|
|
2624
|
+
this.expandStack();
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
expandStack() {
|
|
2628
|
+
this.expandedValue = true;
|
|
2629
|
+
this.recalculateVisibility();
|
|
2630
|
+
document.addEventListener("click", this.boundHandleClickOutside);
|
|
2631
|
+
}
|
|
2632
|
+
collapseStack() {
|
|
2633
|
+
this.expandedValue = false;
|
|
2634
|
+
document.removeEventListener("click", this.boundHandleClickOutside);
|
|
2635
|
+
this.recalculateVisibility();
|
|
2636
|
+
}
|
|
2637
|
+
handleClickOutside(event) {
|
|
2638
|
+
if (!this.element.contains(event.target)) {
|
|
2639
|
+
this.collapseStack();
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
clearAll(event) {
|
|
2643
|
+
event.preventDefault();
|
|
2644
|
+
const toasts = Array.from(this.listTarget.children);
|
|
2645
|
+
toasts.forEach((toast) => toast.remove());
|
|
2646
|
+
this.collapseStack();
|
|
2647
|
+
}
|
|
2648
|
+
};
|
|
2649
|
+
|
|
2524
2650
|
// app/assets/javascripts/source_monitor/controllers/dropdown_controller.js
|
|
2525
2651
|
var dropdown_controller_default = class extends Controller {
|
|
2526
2652
|
static targets = ["menu"];
|
|
@@ -2746,6 +2872,7 @@ if (!existingApplication) {
|
|
|
2746
2872
|
window.SourceMonitorStimulus = application;
|
|
2747
2873
|
}
|
|
2748
2874
|
application.register("notification", notification_controller_default);
|
|
2875
|
+
application.register("notification-container", notification_container_controller_default);
|
|
2749
2876
|
application.register("async-submit", async_submit_controller_default);
|
|
2750
2877
|
application.register("dropdown", dropdown_controller_default);
|
|
2751
2878
|
application.register("modal", modal_controller_default);
|