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.
Files changed (196) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/agents/rails-concern.md +464 -0
  3. data/.claude/agents/rails-controller.md +424 -0
  4. data/.claude/agents/rails-hotwire.md +446 -0
  5. data/.claude/agents/rails-implement.md +374 -0
  6. data/.claude/agents/rails-job.md +334 -0
  7. data/.claude/agents/rails-lint.md +294 -0
  8. data/.claude/agents/rails-mailer.md +371 -0
  9. data/.claude/agents/rails-migration.md +449 -0
  10. data/.claude/agents/rails-model.md +420 -0
  11. data/.claude/agents/rails-policy.md +443 -0
  12. data/.claude/agents/rails-presenter.md +427 -0
  13. data/.claude/agents/rails-query.md +412 -0
  14. data/.claude/agents/rails-review.md +490 -0
  15. data/.claude/agents/rails-service.md +458 -0
  16. data/.claude/agents/rails-state-records.md +465 -0
  17. data/.claude/agents/rails-tdd.md +314 -0
  18. data/.claude/agents/rails-test.md +441 -0
  19. data/.claude/agents/rails-view-component.md +418 -0
  20. data/.claude/hooks/block-secrets.sh +52 -0
  21. data/.claude/settings.json +85 -0
  22. data/.claude/skills/action-cable-patterns/SKILL.md +296 -0
  23. data/.claude/skills/action-mailer-patterns/SKILL.md +295 -0
  24. data/.claude/skills/active-storage-setup/SKILL.md +311 -0
  25. data/.claude/skills/api-versioning/SKILL.md +294 -0
  26. data/.claude/skills/authentication-flow/SKILL.md +335 -0
  27. data/.claude/skills/authentication-flow/reference/current.md +248 -0
  28. data/.claude/skills/authentication-flow/reference/passwordless.md +253 -0
  29. data/.claude/skills/authentication-flow/reference/sessions.md +201 -0
  30. data/.claude/skills/authorization-pundit/SKILL.md +462 -0
  31. data/.claude/skills/caching-strategies/SKILL.md +350 -0
  32. data/.claude/skills/database-migrations/SKILL.md +354 -0
  33. data/.claude/skills/form-object-patterns/SKILL.md +399 -0
  34. data/.claude/skills/hotwire-patterns/SKILL.md +247 -0
  35. data/.claude/skills/hotwire-patterns/reference/stimulus.md +307 -0
  36. data/.claude/skills/hotwire-patterns/reference/tailwind-integration.md +112 -0
  37. data/.claude/skills/hotwire-patterns/reference/turbo-frames.md +158 -0
  38. data/.claude/skills/hotwire-patterns/reference/turbo-streams.md +218 -0
  39. data/.claude/skills/i18n-patterns/SKILL.md +320 -0
  40. data/.claude/skills/install/SKILL.md +367 -0
  41. data/.claude/skills/performance-optimization/SKILL.md +311 -0
  42. data/.claude/skills/rails-architecture/SKILL.md +259 -0
  43. data/.claude/skills/rails-architecture/reference/error-handling.md +333 -0
  44. data/.claude/skills/rails-architecture/reference/event-tracking.md +142 -0
  45. data/.claude/skills/rails-architecture/reference/layer-interactions.md +417 -0
  46. data/.claude/skills/rails-architecture/reference/multi-tenancy.md +152 -0
  47. data/.claude/skills/rails-architecture/reference/query-patterns.md +342 -0
  48. data/.claude/skills/rails-architecture/reference/service-patterns.md +286 -0
  49. data/.claude/skills/rails-architecture/reference/state-records.md +250 -0
  50. data/.claude/skills/rails-architecture/reference/testing-strategy.md +326 -0
  51. data/.claude/skills/rails-concern/SKILL.md +399 -0
  52. data/.claude/skills/rails-controller/SKILL.md +336 -0
  53. data/.claude/skills/rails-model-generator/SKILL.md +321 -0
  54. data/.claude/skills/rails-model-generator/reference/validations.md +298 -0
  55. data/.claude/skills/rails-presenter/SKILL.md +274 -0
  56. data/.claude/skills/rails-query-object/SKILL.md +289 -0
  57. data/.claude/skills/rails-service-object/SKILL.md +349 -0
  58. data/.claude/skills/solid-queue-setup/SKILL.md +307 -0
  59. data/.claude/skills/tdd-cycle/SKILL.md +359 -0
  60. data/.claude/skills/viewcomponent-patterns/SKILL.md +333 -0
  61. data/.gitignore +1 -0
  62. data/.rubocop.yml +2 -0
  63. data/.ruby-version +1 -1
  64. data/.vbw-planning/.notification-log.jsonl +192 -0
  65. data/.vbw-planning/.session-log.jsonl +871 -0
  66. data/.vbw-planning/PROJECT.md +51 -0
  67. data/.vbw-planning/REQUIREMENTS.md +50 -0
  68. data/.vbw-planning/SHIPPED.md +28 -0
  69. data/.vbw-planning/codebase/ARCHITECTURE.md +147 -0
  70. data/.vbw-planning/codebase/CONCERNS.md +99 -0
  71. data/.vbw-planning/codebase/CONVENTIONS.md +97 -0
  72. data/.vbw-planning/codebase/DEPENDENCIES.md +100 -0
  73. data/.vbw-planning/codebase/INDEX.md +86 -0
  74. data/.vbw-planning/codebase/META.md +42 -0
  75. data/.vbw-planning/codebase/PATTERNS.md +262 -0
  76. data/.vbw-planning/codebase/STACK.md +101 -0
  77. data/.vbw-planning/codebase/STRUCTURE.md +324 -0
  78. data/.vbw-planning/codebase/TESTING.md +154 -0
  79. data/.vbw-planning/config.json +12 -0
  80. data/.vbw-planning/discovery.json +24 -0
  81. data/.vbw-planning/milestones/default/ROADMAP.md +115 -0
  82. data/.vbw-planning/milestones/default/STATE.md +83 -0
  83. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01-SUMMARY.md +56 -0
  84. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-01.md +187 -0
  85. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02-SUMMARY.md +64 -0
  86. data/.vbw-planning/milestones/default/phases/01-coverage-analysis-quick-wins/PLAN-02.md +137 -0
  87. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01-SUMMARY.md +67 -0
  88. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-01.md +142 -0
  89. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02-SUMMARY.md +64 -0
  90. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-02.md +138 -0
  91. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03-SUMMARY.md +85 -0
  92. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-03.md +147 -0
  93. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04-SUMMARY.md +63 -0
  94. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-04.md +129 -0
  95. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05-SUMMARY.md +74 -0
  96. data/.vbw-planning/milestones/default/phases/02-critical-path-test-coverage/PLAN-05.md +154 -0
  97. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION-wave1.md +303 -0
  98. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/03-VERIFICATION.md +510 -0
  99. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01-SUMMARY.md +61 -0
  100. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-01.md +161 -0
  101. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02-SUMMARY.md +66 -0
  102. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-02.md +132 -0
  103. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03-SUMMARY.md +59 -0
  104. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-03.md +171 -0
  105. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04-SUMMARY.md +56 -0
  106. data/.vbw-planning/milestones/default/phases/03-large-file-refactoring/PLAN-04.md +152 -0
  107. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/04-CONTEXT.md +33 -0
  108. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01-SUMMARY.md +42 -0
  109. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-01.md +119 -0
  110. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02-SUMMARY.md +52 -0
  111. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-02.md +195 -0
  112. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03-SUMMARY.md +79 -0
  113. data/.vbw-planning/milestones/default/phases/04-code-quality-conventions-cleanup/PLAN-03.md +130 -0
  114. data/CHANGELOG.md +28 -0
  115. data/CLAUDE.md +179 -0
  116. data/Gemfile +8 -0
  117. data/Gemfile.lock +114 -101
  118. data/Rakefile +2 -0
  119. data/app/assets/builds/source_monitor/application.css +2076 -0
  120. data/app/assets/builds/source_monitor/application.js +2758 -0
  121. data/app/assets/builds/source_monitor/application.js.map +7 -0
  122. data/app/controllers/source_monitor/application_controller.rb +2 -0
  123. data/app/controllers/source_monitor/health_controller.rb +2 -0
  124. data/app/controllers/source_monitor/import_sessions/bulk_configuration.rb +106 -0
  125. data/app/controllers/source_monitor/import_sessions/entry_annotation.rb +187 -0
  126. data/app/controllers/source_monitor/import_sessions/health_check_management.rb +112 -0
  127. data/app/controllers/source_monitor/import_sessions/opml_parser.rb +130 -0
  128. data/app/controllers/source_monitor/import_sessions_controller.rb +6 -507
  129. data/app/controllers/source_monitor/items_controller.rb +2 -0
  130. data/app/controllers/source_monitor/sources_controller.rb +0 -14
  131. data/app/helpers/source_monitor/application_helper.rb +4 -112
  132. data/app/helpers/source_monitor/health_badge_helper.rb +69 -0
  133. data/app/helpers/source_monitor/table_sort_helper.rb +53 -0
  134. data/app/jobs/source_monitor/application_job.rb +2 -0
  135. data/app/models/source_monitor/application_record.rb +2 -0
  136. data/app/models/source_monitor/log_entry.rb +0 -2
  137. data/config/coverage_baseline.json +217 -1862
  138. data/config/routes.rb +2 -0
  139. data/db/migrate/20251009103000_add_feed_content_readability_to_sources.rb +2 -0
  140. data/db/migrate/20251014171659_add_performance_indexes.rb +2 -0
  141. data/db/migrate/20251014172525_add_fetch_status_check_constraint.rb +2 -0
  142. data/db/migrate/20251108120116_refresh_fetch_status_constraint.rb +2 -0
  143. data/db/migrate/20260210204022_add_composite_index_to_log_entries.rb +17 -0
  144. data/lib/source_monitor/assets/bundler.rb +2 -0
  145. data/lib/source_monitor/assets.rb +2 -0
  146. data/lib/source_monitor/configuration/authentication_settings.rb +62 -0
  147. data/lib/source_monitor/configuration/events.rb +60 -0
  148. data/lib/source_monitor/configuration/fetching_settings.rb +27 -0
  149. data/lib/source_monitor/configuration/health_settings.rb +27 -0
  150. data/lib/source_monitor/configuration/http_settings.rb +43 -0
  151. data/lib/source_monitor/configuration/model_definition.rb +108 -0
  152. data/lib/source_monitor/configuration/models.rb +36 -0
  153. data/lib/source_monitor/configuration/realtime_settings.rb +95 -0
  154. data/lib/source_monitor/configuration/retention_settings.rb +45 -0
  155. data/lib/source_monitor/configuration/scraper_registry.rb +67 -0
  156. data/lib/source_monitor/configuration/scraping_settings.rb +39 -0
  157. data/lib/source_monitor/configuration/validation_definition.rb +32 -0
  158. data/lib/source_monitor/configuration.rb +12 -579
  159. data/lib/source_monitor/dashboard/queries/recent_activity_query.rb +138 -0
  160. data/lib/source_monitor/dashboard/queries/stats_query.rb +71 -0
  161. data/lib/source_monitor/dashboard/queries.rb +2 -195
  162. data/lib/source_monitor/engine.rb +2 -0
  163. data/lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb +141 -0
  164. data/lib/source_monitor/fetching/feed_fetcher/entry_processor.rb +89 -0
  165. data/lib/source_monitor/fetching/feed_fetcher/source_updater.rb +200 -0
  166. data/lib/source_monitor/fetching/feed_fetcher.rb +37 -379
  167. data/lib/source_monitor/items/item_creator/content_extractor.rb +113 -0
  168. data/lib/source_monitor/items/item_creator/entry_parser/media_extraction.rb +96 -0
  169. data/lib/source_monitor/items/item_creator/entry_parser.rb +294 -0
  170. data/lib/source_monitor/items/item_creator.rb +28 -455
  171. data/lib/source_monitor/setup/bundle_installer.rb +2 -0
  172. data/lib/source_monitor/setup/cli.rb +2 -0
  173. data/lib/source_monitor/setup/dependency_checker.rb +2 -0
  174. data/lib/source_monitor/setup/detectors.rb +2 -0
  175. data/lib/source_monitor/setup/gemfile_editor.rb +2 -0
  176. data/lib/source_monitor/setup/initializer_patcher.rb +2 -0
  177. data/lib/source_monitor/setup/install_generator.rb +2 -0
  178. data/lib/source_monitor/setup/migration_installer.rb +2 -0
  179. data/lib/source_monitor/setup/node_installer.rb +2 -0
  180. data/lib/source_monitor/setup/prompter.rb +2 -0
  181. data/lib/source_monitor/setup/requirements.rb +2 -0
  182. data/lib/source_monitor/setup/shell_runner.rb +2 -0
  183. data/lib/source_monitor/setup/verification/action_cable_verifier.rb +2 -0
  184. data/lib/source_monitor/setup/verification/printer.rb +2 -0
  185. data/lib/source_monitor/setup/verification/result.rb +2 -0
  186. data/lib/source_monitor/setup/verification/runner.rb +2 -0
  187. data/lib/source_monitor/setup/verification/solid_queue_verifier.rb +2 -0
  188. data/lib/source_monitor/setup/verification/telemetry_logger.rb +2 -0
  189. data/lib/source_monitor/setup/workflow.rb +2 -0
  190. data/lib/source_monitor/version.rb +3 -1
  191. data/lib/source_monitor.rb +140 -58
  192. data/lib/tasks/source_monitor_assets.rake +2 -0
  193. data/lib/tasks/source_monitor_setup.rake +2 -0
  194. data/lib/tasks/source_monitor_tasks.rake +2 -0
  195. data/source_monitor.gemspec +3 -1
  196. 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 |