source_monitor 0.2.0 → 0.3.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/agents/rails-concern.md +464 -0
- data/.claude/agents/rails-controller.md +424 -0
- data/.claude/agents/rails-hotwire.md +446 -0
- data/.claude/agents/rails-implement.md +374 -0
- data/.claude/agents/rails-job.md +334 -0
- data/.claude/agents/rails-lint.md +294 -0
- data/.claude/agents/rails-mailer.md +371 -0
- data/.claude/agents/rails-migration.md +449 -0
- data/.claude/agents/rails-model.md +420 -0
- data/.claude/agents/rails-policy.md +443 -0
- data/.claude/agents/rails-presenter.md +427 -0
- data/.claude/agents/rails-query.md +412 -0
- data/.claude/agents/rails-review.md +490 -0
- data/.claude/agents/rails-service.md +458 -0
- data/.claude/agents/rails-state-records.md +465 -0
- data/.claude/agents/rails-tdd.md +314 -0
- data/.claude/agents/rails-test.md +441 -0
- data/.claude/agents/rails-view-component.md +418 -0
- data/.claude/hooks/block-secrets.sh +52 -0
- data/.claude/settings.json +85 -0
- data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
- data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
- data/.claude/skills/active-storage-setup/SKILL.md +311 -0
- data/.claude/skills/api-versioning/SKILL.md +294 -0
- data/.claude/skills/authentication-flow/SKILL.md +335 -0
- data/.claude/skills/authentication-flow/reference/current.md +248 -0
- data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
- data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
- data/.claude/skills/authorization-pundit/SKILL.md +462 -0
- data/.claude/skills/caching-strategies/SKILL.md +350 -0
- data/.claude/skills/database-migrations/SKILL.md +354 -0
- data/.claude/skills/form-object-patterns/SKILL.md +399 -0
- data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
- data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
- data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
- data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
- data/.claude/skills/i18n-patterns/SKILL.md +320 -0
- data/.claude/skills/install/SKILL.md +367 -0
- data/.claude/skills/performance-optimization/SKILL.md +311 -0
- data/.claude/skills/rails-architecture/SKILL.md +259 -0
- data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
- data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
- data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
- data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
- data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
- data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
- data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
- data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
- data/.claude/skills/rails-concern/SKILL.md +399 -0
- data/.claude/skills/rails-controller/SKILL.md +336 -0
- data/.claude/skills/rails-model-generator/SKILL.md +321 -0
- data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
- data/.claude/skills/rails-presenter/SKILL.md +274 -0
- data/.claude/skills/rails-query-object/SKILL.md +289 -0
- data/.claude/skills/rails-service-object/SKILL.md +349 -0
- data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
- data/.claude/skills/tdd-cycle/SKILL.md +359 -0
- data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/.vbw-planning/.notification-log.jsonl +192 -0
- data/.vbw-planning/.session-log.jsonl +871 -0
- data/.vbw-planning/PROJECT.md +51 -0
- data/.vbw-planning/REQUIREMENTS.md +50 -0
- data/.vbw-planning/SHIPPED.md +28 -0
- data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
- data/.vbw-planning/codebase/CONCERNS.md +99 -0
- data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
- data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
- data/.vbw-planning/codebase/INDEX.md +86 -0
- data/.vbw-planning/codebase/META.md +42 -0
- data/.vbw-planning/codebase/PATTERNS.md +262 -0
- data/.vbw-planning/codebase/STACK.md +101 -0
- data/.vbw-planning/codebase/STRUCTURE.md +324 -0
- data/.vbw-planning/codebase/TESTING.md +154 -0
- data/.vbw-planning/config.json +12 -0
- data/.vbw-planning/discovery.json +24 -0
- data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
- data/.vbw-planning/milestones/default/STATE.md +83 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
- data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
- data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
- data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +179 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +114 -101
- data/Rakefile +2 -0
- data/app/assets/builds/source_monitor/application.css +2076 -0
- data/app/assets/builds/source_monitor/application.js +2758 -0
- data/app/assets/builds/source_monitor/application.js.map +7 -0
- data/app/controllers/source_monitor/application_controller.rb +2 -0
- data/app/controllers/source_monitor/health_controller.rb +2 -0
- data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
- data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
- data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
- data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
- data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
- data/app/controllers/source_monitor/items_controller.rb +2 -0
- data/app/controllers/source_monitor/sources_controller.rb +0 -14
- data/app/helpers/source_monitor/application_helper.rb +4 -112
- data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
- data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
- data/app/jobs/source_monitor/application_job.rb +2 -0
- data/app/models/source_monitor/application_record.rb +2 -0
- data/app/models/source_monitor/log_entry.rb +0 -2
- data/config/coverage_baseline.json +217 -1862
- data/config/routes.rb +2 -0
- data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
- data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
- data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
- data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
- data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
- data/lib/source_monitor/assets/bundler.rb +2 -0
- data/lib/source_monitor/assets.rb +2 -0
- data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
- data/lib/source_monitor/configuration/events.rb +60 -0
- data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
- data/lib/source_monitor/configuration/health_settings.rb +27 -0
- data/lib/source_monitor/configuration/http_settings.rb +43 -0
- data/lib/source_monitor/configuration/model_definition.rb +108 -0
- data/lib/source_monitor/configuration/models.rb +36 -0
- data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
- data/lib/source_monitor/configuration/retention_settings.rb +45 -0
- data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
- data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
- data/lib/source_monitor/configuration/validation_definition.rb +32 -0
- data/lib/source_monitor/configuration.rb +12 -579
- data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
- data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
- data/lib/source_monitor/dashboard/queries.rb +2 -195
- data/lib/source_monitor/engine.rb +2 -0
- data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
- data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
- data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
- data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
- data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
- data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
- data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
- data/lib/source_monitor/items/item_creator.rb +28 -455
- data/lib/source_monitor/setup/bundle_installer.rb +2 -0
- data/lib/source_monitor/setup/cli.rb +2 -0
- data/lib/source_monitor/setup/dependency_checker.rb +2 -0
- data/lib/source_monitor/setup/detectors.rb +2 -0
- data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
- data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
- data/lib/source_monitor/setup/install_generator.rb +2 -0
- data/lib/source_monitor/setup/migration_installer.rb +2 -0
- data/lib/source_monitor/setup/node_installer.rb +2 -0
- data/lib/source_monitor/setup/prompter.rb +2 -0
- data/lib/source_monitor/setup/requirements.rb +2 -0
- data/lib/source_monitor/setup/shell_runner.rb +2 -0
- data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/printer.rb +2 -0
- data/lib/source_monitor/setup/verification/result.rb +2 -0
- data/lib/source_monitor/setup/verification/runner.rb +2 -0
- data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
- data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
- data/lib/source_monitor/setup/workflow.rb +2 -0
- data/lib/source_monitor/version.rb +3 -1
- data/lib/source_monitor.rb +140 -58
- data/lib/tasks/source_monitor_assets.rake +2 -0
- data/lib/tasks/source_monitor_setup.rake +2 -0
- data/lib/tasks/source_monitor_tasks.rake +2 -0
- data/source_monitor.gemspec +3 -1
- metadata +144 -4
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-lint
|
|
3
|
+
description: Runs RuboCop style fixes and Brakeman security scanning with auto-correction. Use when the user mentions linting, rubocop, brakeman, style fixes, code formatting, security scanning, or wants to clean up code quality issues.
|
|
4
|
+
tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# RuboCop Style Fixes and Brakeman Security
|
|
8
|
+
|
|
9
|
+
## Project Conventions
|
|
10
|
+
- **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
|
|
11
|
+
- **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
|
|
12
|
+
- **Authorization:** Pundit policies (deny by default)
|
|
13
|
+
- **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
|
|
14
|
+
- **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
|
|
15
|
+
- **State:** State-as-records for business state (booleans only for technical flags)
|
|
16
|
+
- **Architecture:** Rich models first, service objects for multi-model orchestration
|
|
17
|
+
- **Routing:** Everything-is-CRUD (new resource over new action)
|
|
18
|
+
- **Quality:** RuboCop (omakase) + Brakeman
|
|
19
|
+
|
|
20
|
+
## Lint Workflow
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
1. bin/rubocop -a → Auto-fix safe style issues
|
|
24
|
+
2. bin/rubocop → Review remaining issues
|
|
25
|
+
3. bin/brakeman -q → Security vulnerability scan
|
|
26
|
+
4. Fix manually → What auto-correct cannot handle
|
|
27
|
+
5. Re-run both → Verify clean
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## RuboCop Omakase Configuration
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# Gemfile
|
|
34
|
+
group :development do
|
|
35
|
+
gem "rubocop-rails-omakase", require: false
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```yaml
|
|
40
|
+
# .rubocop.yml
|
|
41
|
+
inherit_gem:
|
|
42
|
+
rubocop-rails-omakase: rubocop.yml
|
|
43
|
+
|
|
44
|
+
AllCops:
|
|
45
|
+
TargetRubyVersion: 3.3
|
|
46
|
+
NewCops: enable
|
|
47
|
+
Exclude:
|
|
48
|
+
- "db/schema.rb"
|
|
49
|
+
- "bin/**/*"
|
|
50
|
+
- "vendor/**/*"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Running RuboCop
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bin/rubocop # Check all files
|
|
57
|
+
bin/rubocop app/models/ # Check directory
|
|
58
|
+
bin/rubocop -a # Auto-correct safe fixes
|
|
59
|
+
bin/rubocop -A # Auto-correct all (including unsafe)
|
|
60
|
+
bin/rubocop --display-cop-names # Show cop names
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Common Auto-Fixable Offenses
|
|
64
|
+
|
|
65
|
+
| Offense | Before | After |
|
|
66
|
+
|---------|--------|-------|
|
|
67
|
+
| Frozen string literal | (missing) | `# frozen_string_literal: true` |
|
|
68
|
+
| String quotes | `'single'` | `"double"` |
|
|
69
|
+
| Trailing whitespace | `code ` | `code` |
|
|
70
|
+
| Hash syntax | `:key => value` | `key: value` |
|
|
71
|
+
| Redundant return | `return value` | `value` |
|
|
72
|
+
| Redundant self | `self.name` | `name` |
|
|
73
|
+
|
|
74
|
+
### Common Manual Fixes
|
|
75
|
+
|
|
76
|
+
#### Line Too Long
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# Before:
|
|
80
|
+
validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
81
|
+
|
|
82
|
+
# After:
|
|
83
|
+
validates :email,
|
|
84
|
+
presence: true,
|
|
85
|
+
uniqueness: { case_sensitive: false },
|
|
86
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Method Too Long
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Extract private methods to keep actions concise
|
|
93
|
+
def create
|
|
94
|
+
authorize Event
|
|
95
|
+
@event = build_event
|
|
96
|
+
if @event.save
|
|
97
|
+
redirect_to @event, notice: t(".success")
|
|
98
|
+
else
|
|
99
|
+
render :new, status: :unprocessable_entity
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def build_event
|
|
106
|
+
current_account.events.new(event_params)
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Class Too Long
|
|
111
|
+
|
|
112
|
+
Extract concerns when models exceed the line limit:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
class User < ApplicationRecord
|
|
116
|
+
include Authenticatable
|
|
117
|
+
include HasProfile
|
|
118
|
+
include Notifiable
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Disabling Cops Inline
|
|
123
|
+
|
|
124
|
+
Use sparingly with justification:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
order(Arel.sql(sort_column)) # rubocop:disable Rails/ReflectionClassName
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Project-Specific Overrides
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
# .rubocop.yml
|
|
134
|
+
Metrics/MethodLength:
|
|
135
|
+
Max: 20
|
|
136
|
+
Exclude: ["test/**/*", "db/migrate/*"]
|
|
137
|
+
|
|
138
|
+
Metrics/ClassLength:
|
|
139
|
+
Max: 200
|
|
140
|
+
Exclude: ["test/**/*"]
|
|
141
|
+
|
|
142
|
+
Metrics/BlockLength:
|
|
143
|
+
Exclude: ["test/**/*", "config/routes.rb"]
|
|
144
|
+
|
|
145
|
+
Rails/HasManyOrHasOneDependent:
|
|
146
|
+
Enabled: true
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Brakeman Security Scanning
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# Gemfile
|
|
153
|
+
group :development do
|
|
154
|
+
gem "brakeman", require: false
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Running Brakeman
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
bin/brakeman # Full scan
|
|
162
|
+
bin/brakeman -q # Quiet (warnings only)
|
|
163
|
+
bin/brakeman -f json -o brakeman.json # JSON for CI
|
|
164
|
+
bin/brakeman -I # Generate ignore file interactively
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Common Warnings and Fixes
|
|
168
|
+
|
|
169
|
+
#### SQL Injection
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
# DANGEROUS:
|
|
173
|
+
Event.where("name LIKE '%#{params[:q]}%'")
|
|
174
|
+
# FIX:
|
|
175
|
+
Event.where("name LIKE ?", "%#{params[:q]}%")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### Cross-Site Scripting
|
|
179
|
+
|
|
180
|
+
```ruby
|
|
181
|
+
# DANGEROUS:
|
|
182
|
+
raw(@event.description)
|
|
183
|
+
# FIX:
|
|
184
|
+
sanitize(@event.description, tags: %w[p br strong em])
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
#### Mass Assignment
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# DANGEROUS:
|
|
191
|
+
Event.new(params[:event])
|
|
192
|
+
# FIX:
|
|
193
|
+
Event.new(event_params) # Use strong parameters
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Open Redirect
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
# DANGEROUS:
|
|
200
|
+
redirect_to(params[:return_to])
|
|
201
|
+
# FIX:
|
|
202
|
+
redirect_to(params[:return_to] || root_path, allow_other_host: false)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### File Access
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# DANGEROUS:
|
|
209
|
+
send_file(params[:path])
|
|
210
|
+
# FIX:
|
|
211
|
+
filename = File.basename(params[:filename])
|
|
212
|
+
path = Rails.root.join("storage", "reports", filename)
|
|
213
|
+
send_file(path) if File.exist?(path)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Dynamic Render
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# DANGEROUS:
|
|
220
|
+
render params[:template]
|
|
221
|
+
# FIX:
|
|
222
|
+
ALLOWED = %w[about contact faq].freeze
|
|
223
|
+
render template if ALLOWED.include?(template)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Ignoring False Positives
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
// config/brakeman.ignore
|
|
230
|
+
{
|
|
231
|
+
"ignored_warnings": [
|
|
232
|
+
{
|
|
233
|
+
"warning_type": "SQL Injection",
|
|
234
|
+
"fingerprint": "abc123...",
|
|
235
|
+
"note": "Arel.sql used with constant string, not user input"
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## CI Configuration
|
|
242
|
+
|
|
243
|
+
```yaml
|
|
244
|
+
# .github/workflows/lint.yml
|
|
245
|
+
name: Lint & Security
|
|
246
|
+
on: [push, pull_request]
|
|
247
|
+
jobs:
|
|
248
|
+
rubocop:
|
|
249
|
+
runs-on: ubuntu-latest
|
|
250
|
+
steps:
|
|
251
|
+
- uses: actions/checkout@v4
|
|
252
|
+
- uses: ruby/setup-ruby@v1
|
|
253
|
+
with: { bundler-cache: true }
|
|
254
|
+
- run: bin/rubocop
|
|
255
|
+
brakeman:
|
|
256
|
+
runs-on: ubuntu-latest
|
|
257
|
+
steps:
|
|
258
|
+
- uses: actions/checkout@v4
|
|
259
|
+
- uses: ruby/setup-ruby@v1
|
|
260
|
+
with: { bundler-cache: true }
|
|
261
|
+
- run: bin/brakeman -q --no-pager
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Test File Linting
|
|
265
|
+
|
|
266
|
+
Test files should follow Minitest conventions. Check for:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
# CORRECT:
|
|
270
|
+
require "test_helper"
|
|
271
|
+
class EventTest < ActiveSupport::TestCase
|
|
272
|
+
test "requires name" do
|
|
273
|
+
# ...
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# WRONG (RSpec patterns should never appear):
|
|
278
|
+
# describe Event do
|
|
279
|
+
# it "requires name" do
|
|
280
|
+
# expect(event).to be_invalid
|
|
281
|
+
# end
|
|
282
|
+
# end
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Lint Checklist
|
|
286
|
+
|
|
287
|
+
- [ ] `bin/rubocop -a` to auto-fix safe issues
|
|
288
|
+
- [ ] `bin/rubocop` to check remaining issues
|
|
289
|
+
- [ ] Fix remaining issues manually
|
|
290
|
+
- [ ] `bin/brakeman -q` to scan for security issues
|
|
291
|
+
- [ ] Fix all CRITICAL and HIGH Brakeman warnings
|
|
292
|
+
- [ ] Document ignored warnings with justification
|
|
293
|
+
- [ ] Re-run both tools to verify clean
|
|
294
|
+
- [ ] Run tests to ensure fixes don't break anything: `bin/rails test`
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-mailer
|
|
3
|
+
description: Generates ActionMailer classes with previews, parameterized mailers, and bundled notification patterns. Use when creating email notifications, mailer previews, digest emails, or when the user mentions mailers, emails, deliver_later, notifications, or email templates.
|
|
4
|
+
tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ActionMailer with Previews and Bundled Notifications
|
|
8
|
+
|
|
9
|
+
## Project Conventions
|
|
10
|
+
- **Testing:** Minitest + fixtures (NEVER RSpec or FactoryBot)
|
|
11
|
+
- **Components:** ViewComponents for reusable UI (partials OK for simple one-offs)
|
|
12
|
+
- **Authorization:** Pundit policies (deny by default)
|
|
13
|
+
- **Jobs:** Solid Queue, shallow jobs, `_later`/`_now` naming
|
|
14
|
+
- **Frontend:** Hotwire (Turbo + Stimulus) + Tailwind CSS
|
|
15
|
+
- **State:** State-as-records for business state (booleans only for technical flags)
|
|
16
|
+
- **Architecture:** Rich models first, service objects for multi-model orchestration
|
|
17
|
+
- **Routing:** Everything-is-CRUD (new resource over new action)
|
|
18
|
+
- **Quality:** RuboCop (omakase) + Brakeman
|
|
19
|
+
|
|
20
|
+
## Mailer File Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
app/mailers/ # Mailer classes
|
|
24
|
+
app/views/layouts/mailer.html.erb # Shared layout
|
|
25
|
+
app/views/user_mailer/ # Templates per mailer (HTML + text)
|
|
26
|
+
test/mailers/ # Mailer tests
|
|
27
|
+
test/mailers/previews/ # Browser previews (/rails/mailers)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Basic Mailer Structure
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
# app/mailers/application_mailer.rb
|
|
34
|
+
class ApplicationMailer < ActionMailer::Base
|
|
35
|
+
default from: "notifications@example.com"
|
|
36
|
+
layout "mailer"
|
|
37
|
+
self.deliver_later_queue_name = :mailers
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# app/mailers/user_mailer.rb
|
|
41
|
+
class UserMailer < ApplicationMailer
|
|
42
|
+
def welcome(user)
|
|
43
|
+
@user = user
|
|
44
|
+
@login_url = new_session_url
|
|
45
|
+
mail(to: @user.email_address, subject: t(".subject", name: @user.name))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def password_reset(user)
|
|
49
|
+
@user = user
|
|
50
|
+
@reset_url = edit_password_url(token: @user.password_reset_token)
|
|
51
|
+
mail(to: @user.email_address, subject: t(".subject"))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Templates (HTML + Text)
|
|
57
|
+
|
|
58
|
+
```erb
|
|
59
|
+
<%# app/views/user_mailer/welcome.html.erb %>
|
|
60
|
+
<h1><%= t(".greeting", name: @user.name) %></h1>
|
|
61
|
+
<p><%= t(".body") %></p>
|
|
62
|
+
<%= link_to t(".login_button"), @login_url %>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
<%# app/views/user_mailer/welcome.text.erb %>
|
|
67
|
+
<%= t(".greeting", name: @user.name) %>
|
|
68
|
+
<%= t(".body") %>
|
|
69
|
+
<%= t(".login_prompt") %>: <%= @login_url %>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Always provide both `.html.erb` and `.text.erb` templates. HTML-only emails trigger spam filters.
|
|
73
|
+
|
|
74
|
+
## Parameterized Mailers
|
|
75
|
+
|
|
76
|
+
Share setup logic across actions with `params`:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
# app/mailers/order_mailer.rb
|
|
80
|
+
class OrderMailer < ApplicationMailer
|
|
81
|
+
before_action :set_order
|
|
82
|
+
before_action :set_user
|
|
83
|
+
|
|
84
|
+
def confirmation
|
|
85
|
+
mail(to: @user.email_address, subject: t(".subject", number: @order.number))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def shipped
|
|
89
|
+
@tracking_url = @order.tracking_url
|
|
90
|
+
mail(to: @user.email_address, subject: t(".subject", number: @order.number))
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def cancelled
|
|
94
|
+
mail(to: @user.email_address, subject: t(".subject", number: @order.number))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def set_order = @order = params[:order]
|
|
100
|
+
def set_user = @user = @order.user
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Usage:
|
|
104
|
+
OrderMailer.with(order: order).confirmation.deliver_later
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Mailer Previews
|
|
108
|
+
|
|
109
|
+
Previews render emails in the browser at `/rails/mailers`:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
# test/mailers/previews/user_mailer_preview.rb
|
|
113
|
+
class UserMailerPreview < ActionMailer::Preview
|
|
114
|
+
def welcome
|
|
115
|
+
UserMailer.welcome(User.first)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def password_reset
|
|
119
|
+
user = User.first
|
|
120
|
+
user.password_reset_token ||= SecureRandom.urlsafe_base64(20)
|
|
121
|
+
UserMailer.password_reset(user)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# test/mailers/previews/order_mailer_preview.rb
|
|
126
|
+
class OrderMailerPreview < ActionMailer::Preview
|
|
127
|
+
def confirmation
|
|
128
|
+
OrderMailer.with(order: Order.first).confirmation
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def shipped
|
|
132
|
+
OrderMailer.with(order: Order.where.not(tracking_url: nil).first || Order.first).shipped
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Bundled Notification Pattern (Digest Emails)
|
|
138
|
+
|
|
139
|
+
Instead of one email per event, collect notifications and send in a batch.
|
|
140
|
+
|
|
141
|
+
### Notification Model
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
# app/models/notification.rb
|
|
145
|
+
class Notification < ApplicationRecord
|
|
146
|
+
belongs_to :user
|
|
147
|
+
belongs_to :notifiable, polymorphic: true
|
|
148
|
+
|
|
149
|
+
scope :undelivered, -> { where(delivered_at: nil) }
|
|
150
|
+
scope :for_digest, -> { undelivered.where("created_at <= ?", Time.current) }
|
|
151
|
+
|
|
152
|
+
def mark_delivered!
|
|
153
|
+
update!(delivered_at: Time.current)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Digest Mailer
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# app/mailers/notification_mailer.rb
|
|
162
|
+
class NotificationMailer < ApplicationMailer
|
|
163
|
+
def digest(user, notifications)
|
|
164
|
+
@user = user
|
|
165
|
+
@notifications = notifications
|
|
166
|
+
@grouped = notifications.group_by(&:notifiable_type)
|
|
167
|
+
mail(to: @user.email_address, subject: t(".subject", count: notifications.size))
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Recurring Digest Job
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# app/jobs/send_digest_emails_job.rb
|
|
176
|
+
class SendDigestEmailsJob < ApplicationJob
|
|
177
|
+
queue_as :mailers
|
|
178
|
+
|
|
179
|
+
def perform
|
|
180
|
+
users_with_notifications.find_each do |user|
|
|
181
|
+
notifications = user.notifications.for_digest.to_a
|
|
182
|
+
next if notifications.empty?
|
|
183
|
+
|
|
184
|
+
NotificationMailer.digest(user, notifications).deliver_now
|
|
185
|
+
notifications.each(&:mark_delivered!)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def users_with_notifications
|
|
192
|
+
User.where(id: Notification.undelivered.select(:user_id).distinct)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
# config/recurring.yml
|
|
199
|
+
production:
|
|
200
|
+
send_digest_emails:
|
|
201
|
+
class: SendDigestEmailsJob
|
|
202
|
+
schedule: every day at 8am
|
|
203
|
+
queue: mailers
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Collecting Notifications
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# app/models/concerns/notifiable.rb
|
|
210
|
+
module Notifiable
|
|
211
|
+
extend ActiveSupport::Concern
|
|
212
|
+
|
|
213
|
+
included do
|
|
214
|
+
has_many :notifications, as: :notifiable, dependent: :destroy
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def notify_users(users, type: self.class.name.underscore)
|
|
218
|
+
users.each do |user|
|
|
219
|
+
Notification.create!(user: user, notifiable: self, notification_type: type)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Inline Attachments
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class ReportMailer < ApplicationMailer
|
|
229
|
+
def monthly_report(account, report_data)
|
|
230
|
+
@account = account
|
|
231
|
+
attachments.inline["logo.png"] = File.read(Rails.root.join("app/assets/images/logo.png"))
|
|
232
|
+
attachments["report.pdf"] = { mime_type: "application/pdf", content: generate_pdf(report_data) }
|
|
233
|
+
mail(to: account_admin_email(account), subject: t(".subject"))
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Integration with Solid Queue
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
# In controllers/services — always deliver_later
|
|
242
|
+
UserMailer.welcome(user).deliver_later
|
|
243
|
+
OrderMailer.with(order: order).confirmation.deliver_later(queue: :critical)
|
|
244
|
+
NotificationMailer.digest(user, notifications).deliver_later(wait_until: Date.tomorrow.beginning_of_day)
|
|
245
|
+
|
|
246
|
+
# Inside background jobs — deliver_now is fine (already async)
|
|
247
|
+
class SendDigestEmailsJob < ApplicationJob
|
|
248
|
+
def perform
|
|
249
|
+
NotificationMailer.digest(user, notifications).deliver_now
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## I18n for Mailers
|
|
255
|
+
|
|
256
|
+
```yaml
|
|
257
|
+
# config/locales/mailers.en.yml
|
|
258
|
+
en:
|
|
259
|
+
user_mailer:
|
|
260
|
+
welcome:
|
|
261
|
+
subject: "Welcome to %{name}!"
|
|
262
|
+
greeting: "Hi %{name},"
|
|
263
|
+
body: "Thanks for signing up."
|
|
264
|
+
password_reset:
|
|
265
|
+
subject: "Reset your password"
|
|
266
|
+
order_mailer:
|
|
267
|
+
confirmation:
|
|
268
|
+
subject: "Order %{number} confirmed"
|
|
269
|
+
notification_mailer:
|
|
270
|
+
digest:
|
|
271
|
+
subject:
|
|
272
|
+
one: "You have 1 new notification"
|
|
273
|
+
other: "You have %{count} new notifications"
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Testing Mailers with Minitest
|
|
277
|
+
|
|
278
|
+
```ruby
|
|
279
|
+
# test/mailers/user_mailer_test.rb
|
|
280
|
+
require "test_helper"
|
|
281
|
+
|
|
282
|
+
class UserMailerTest < ActionMailer::TestCase
|
|
283
|
+
test "welcome email" do
|
|
284
|
+
user = users(:regular)
|
|
285
|
+
email = UserMailer.welcome(user)
|
|
286
|
+
|
|
287
|
+
assert_emails 1 do
|
|
288
|
+
email.deliver_now
|
|
289
|
+
end
|
|
290
|
+
assert_equal [user.email_address], email.to
|
|
291
|
+
assert_match "Welcome", email.subject
|
|
292
|
+
assert_match user.name, email.body.encoded
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# test/mailers/order_mailer_test.rb
|
|
297
|
+
require "test_helper"
|
|
298
|
+
|
|
299
|
+
class OrderMailerTest < ActionMailer::TestCase
|
|
300
|
+
test "confirmation email" do
|
|
301
|
+
order = orders(:confirmed)
|
|
302
|
+
email = OrderMailer.with(order: order).confirmation
|
|
303
|
+
assert_equal [order.user.email_address], email.to
|
|
304
|
+
assert_match order.number, email.subject
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
test "shipped email includes tracking URL" do
|
|
308
|
+
order = orders(:shipped)
|
|
309
|
+
email = OrderMailer.with(order: order).shipped
|
|
310
|
+
assert_match order.tracking_url, email.body.encoded
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# test/controllers/registrations_controller_test.rb — integration
|
|
315
|
+
require "test_helper"
|
|
316
|
+
|
|
317
|
+
class RegistrationsControllerTest < ActionDispatch::IntegrationTest
|
|
318
|
+
test "sends welcome email on registration" do
|
|
319
|
+
assert_enqueued_emails 1 do
|
|
320
|
+
post registrations_url, params: {
|
|
321
|
+
user: { name: "Test", email_address: "new@example.com", password: "password123" }
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# test/jobs/send_digest_emails_job_test.rb
|
|
328
|
+
require "test_helper"
|
|
329
|
+
|
|
330
|
+
class SendDigestEmailsJobTest < ActiveJob::TestCase
|
|
331
|
+
test "sends digest to users with notifications" do
|
|
332
|
+
assert_emails 1 do
|
|
333
|
+
SendDigestEmailsJob.perform_now
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
test "marks notifications as delivered" do
|
|
338
|
+
SendDigestEmailsJob.perform_now
|
|
339
|
+
assert_equal 0, Notification.undelivered.count
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
test "skips users with no notifications" do
|
|
343
|
+
Notification.update_all(delivered_at: Time.current)
|
|
344
|
+
assert_no_emails do
|
|
345
|
+
SendDigestEmailsJob.perform_now
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Mailer Generation Checklist
|
|
352
|
+
|
|
353
|
+
- [ ] Mailer class inherits from `ApplicationMailer`
|
|
354
|
+
- [ ] Both HTML and text templates created
|
|
355
|
+
- [ ] I18n keys for subjects and body text
|
|
356
|
+
- [ ] Preview class in `test/mailers/previews/`
|
|
357
|
+
- [ ] Test covers recipients, subject, and body content
|
|
358
|
+
- [ ] `deliver_later` used in controllers (not `deliver_now`)
|
|
359
|
+
- [ ] Queue configured (defaults to `:mailers`)
|
|
360
|
+
- [ ] Digest pattern for high-frequency notifications
|
|
361
|
+
|
|
362
|
+
## Anti-Patterns
|
|
363
|
+
|
|
364
|
+
| Anti-Pattern | Problem | Solution |
|
|
365
|
+
|--------------|---------|----------|
|
|
366
|
+
| `deliver_now` in controllers | Blocks HTTP request | Use `deliver_later` |
|
|
367
|
+
| No text template | Spam filters flag HTML-only | Always provide `.text.erb` |
|
|
368
|
+
| No preview | Can't visually verify emails | Create preview class |
|
|
369
|
+
| Hardcoded strings | Can't translate | Use I18n |
|
|
370
|
+
| One email per event | Inbox flood | Use digest/bundled pattern |
|
|
371
|
+
| Business logic in mailer | Wrong layer | Keep in model/service |
|