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,296 @@
1
+ ---
2
+ name: action-cable-patterns
3
+ description: Implements real-time features with Action Cable and WebSockets. Use when adding live updates, chat features, notifications, real-time dashboards, or when user mentions Action Cable, WebSockets, channels, or real-time.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Action Cable Patterns for Rails 8
8
+
9
+ ## Overview
10
+
11
+ Action Cable integrates WebSockets with Rails:
12
+ - Real-time updates without polling
13
+ - Server-to-client push notifications
14
+ - Chat and messaging features
15
+ - Live dashboards and feeds
16
+
17
+ ## Quick Start
18
+
19
+ ```yaml
20
+ # config/cable.yml
21
+ development:
22
+ adapter: async
23
+
24
+ test:
25
+ adapter: test
26
+
27
+ production:
28
+ adapter: solid_cable # Rails 8 default
29
+ ```
30
+
31
+ ## Connection Authentication
32
+
33
+ ```ruby
34
+ # app/channels/application_cable/connection.rb
35
+ module ApplicationCable
36
+ class Connection < ActionCable::Connection::Base
37
+ identified_by :current_user
38
+
39
+ def connect
40
+ self.current_user = find_verified_user
41
+ end
42
+
43
+ private
44
+
45
+ def find_verified_user
46
+ if session_token = cookies.signed[:session_token]
47
+ if session = Session.find_by(token: session_token)
48
+ session.user
49
+ else
50
+ reject_unauthorized_connection
51
+ end
52
+ else
53
+ reject_unauthorized_connection
54
+ end
55
+ end
56
+ end
57
+ end
58
+ ```
59
+
60
+ ## Channel Patterns
61
+
62
+ ### Pattern 1: Notifications Channel
63
+
64
+ ```ruby
65
+ # app/channels/notifications_channel.rb
66
+ class NotificationsChannel < ApplicationCable::Channel
67
+ def subscribed
68
+ stream_for current_user
69
+ end
70
+
71
+ def self.notify(user, notification)
72
+ broadcast_to(user, {
73
+ type: "notification",
74
+ id: notification.id,
75
+ title: notification.title,
76
+ body: notification.body
77
+ })
78
+ end
79
+ end
80
+ ```
81
+
82
+ ### Pattern 2: Resource Updates Channel
83
+
84
+ ```ruby
85
+ # app/channels/events_channel.rb
86
+ class EventsChannel < ApplicationCable::Channel
87
+ def subscribed
88
+ @event = Event.find(params[:event_id])
89
+
90
+ if authorized?
91
+ stream_for @event
92
+ else
93
+ reject
94
+ end
95
+ end
96
+
97
+ def self.broadcast_update(event)
98
+ broadcast_to(event, {
99
+ type: "update",
100
+ html: ApplicationController.renderer.render(
101
+ partial: "events/event", locals: { event: event }
102
+ )
103
+ })
104
+ end
105
+
106
+ private
107
+
108
+ def authorized?
109
+ EventPolicy.new(current_user, @event).show?
110
+ end
111
+ end
112
+ ```
113
+
114
+ ### Pattern 3: Integration with Turbo Streams
115
+
116
+ ```ruby
117
+ # app/models/comment.rb
118
+ class Comment < ApplicationRecord
119
+ after_create_commit -> {
120
+ broadcast_append_to(
121
+ [event, "comments"],
122
+ target: "comments",
123
+ partial: "comments/comment"
124
+ )
125
+ }
126
+
127
+ after_destroy_commit -> {
128
+ broadcast_remove_to([event, "comments"])
129
+ }
130
+ end
131
+ ```
132
+
133
+ ```erb
134
+ <%# app/views/events/show.html.erb %>
135
+ <%= turbo_stream_from @event, "comments" %>
136
+
137
+ <div id="comments">
138
+ <%= render @event.comments %>
139
+ </div>
140
+ ```
141
+
142
+ ## Broadcasting from Services
143
+
144
+ ```ruby
145
+ module Events
146
+ class UpdateService
147
+ def call(event, params)
148
+ event.update!(params)
149
+ EventsChannel.broadcast_update(event)
150
+ DashboardChannel.broadcast_stats(event.account)
151
+ success(event)
152
+ end
153
+ end
154
+ end
155
+ ```
156
+
157
+ ## Testing Channels
158
+
159
+ ### Channel Test (Minitest)
160
+
161
+ ```ruby
162
+ # test/channels/notifications_channel_test.rb
163
+ require "test_helper"
164
+
165
+ class NotificationsChannelTest < ActionCable::Channel::TestCase
166
+ setup do
167
+ @user = users(:one)
168
+ stub_connection(current_user: @user)
169
+ end
170
+
171
+ test "subscribes successfully" do
172
+ subscribe
173
+ assert subscription.confirmed?
174
+ end
175
+
176
+ test "streams for the current user" do
177
+ subscribe
178
+ assert_has_stream_for @user
179
+ end
180
+
181
+ test "broadcasts notification to user" do
182
+ subscribe
183
+ notification = notifications(:one)
184
+
185
+ assert_broadcast_on(
186
+ NotificationsChannel.broadcasting_for(@user),
187
+ hash_including(type: "notification")
188
+ ) do
189
+ NotificationsChannel.notify(@user, notification)
190
+ end
191
+ end
192
+ end
193
+ ```
194
+
195
+ ### Channel with Authorization Test
196
+
197
+ ```ruby
198
+ # test/channels/events_channel_test.rb
199
+ require "test_helper"
200
+
201
+ class EventsChannelTest < ActionCable::Channel::TestCase
202
+ setup do
203
+ @user = users(:one)
204
+ @event = events(:one) # belongs to @user's account
205
+ @other_event = events(:other_account)
206
+ stub_connection(current_user: @user)
207
+ end
208
+
209
+ test "subscribes to authorized event" do
210
+ subscribe(event_id: @event.id)
211
+ assert subscription.confirmed?
212
+ assert_has_stream_for @event
213
+ end
214
+
215
+ test "rejects unauthorized event" do
216
+ subscribe(event_id: @other_event.id)
217
+ assert subscription.rejected?
218
+ end
219
+ end
220
+ ```
221
+
222
+ ### Connection Test
223
+
224
+ ```ruby
225
+ # test/channels/connection_test.rb
226
+ require "test_helper"
227
+
228
+ class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
229
+ test "connects with valid session token" do
230
+ user = users(:one)
231
+ session = user.sessions.create!
232
+
233
+ connect cookies: { session_token: session.token }
234
+
235
+ assert_equal user, connection.current_user
236
+ end
237
+
238
+ test "rejects without session token" do
239
+ assert_reject_connection do
240
+ connect
241
+ end
242
+ end
243
+ end
244
+ ```
245
+
246
+ ## Stimulus Controller for Channels
247
+
248
+ ```javascript
249
+ // app/javascript/controllers/chat_controller.js
250
+ import { Controller } from "@hotwired/stimulus"
251
+ import consumer from "../channels/consumer"
252
+
253
+ export default class extends Controller {
254
+ static targets = ["messages", "input"]
255
+ static values = { roomId: Number }
256
+
257
+ connect() {
258
+ this.channel = consumer.subscriptions.create(
259
+ { channel: "ChatChannel", room_id: this.roomIdValue },
260
+ {
261
+ received: this.received.bind(this),
262
+ }
263
+ )
264
+ }
265
+
266
+ disconnect() {
267
+ this.channel?.unsubscribe()
268
+ }
269
+
270
+ received(data) {
271
+ if (data.type === "message") {
272
+ this.messagesTarget.insertAdjacentHTML("beforeend", data.html)
273
+ this.messagesTarget.scrollTop = this.messagesTarget.scrollHeight
274
+ }
275
+ }
276
+
277
+ send(event) {
278
+ event.preventDefault()
279
+ const body = this.inputTarget.value.trim()
280
+ if (body) {
281
+ this.channel.perform("speak", { body })
282
+ this.inputTarget.value = ""
283
+ }
284
+ }
285
+ }
286
+ ```
287
+
288
+ ## Checklist
289
+
290
+ - [ ] Connection authentication configured
291
+ - [ ] Channel authorization implemented
292
+ - [ ] Channel tests written
293
+ - [ ] Broadcasting from services/models
294
+ - [ ] Client-side subscription set up
295
+ - [ ] Turbo Stream integration (if applicable)
296
+ - [ ] All tests GREEN
@@ -0,0 +1,295 @@
1
+ ---
2
+ name: action-mailer-patterns
3
+ description: Implements transactional emails with Action Mailer and TDD. Use when creating email templates, notification emails, password resets, email previews, or when user mentions mailer, email, notifications, or transactional emails.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Action Mailer Patterns for Rails 8
8
+
9
+ ## Overview
10
+
11
+ Action Mailer handles transactional emails:
12
+ - HTML and text email templates
13
+ - Layouts for consistent styling
14
+ - Previews for development
15
+ - Background delivery via Active Job (Solid Queue)
16
+ - Internationalized emails
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ bin/rails generate mailer User welcome password_reset
22
+ ```
23
+
24
+ ## Configuration
25
+
26
+ ```ruby
27
+ # config/environments/development.rb
28
+ config.action_mailer.delivery_method = :letter_opener
29
+ config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
30
+
31
+ # config/environments/production.rb
32
+ config.action_mailer.delivery_method = :smtp
33
+ config.action_mailer.default_url_options = { host: "example.com" }
34
+ ```
35
+
36
+ ### Application Mailer
37
+
38
+ ```ruby
39
+ # app/mailers/application_mailer.rb
40
+ class ApplicationMailer < ActionMailer::Base
41
+ default from: "noreply@example.com"
42
+ layout "mailer"
43
+
44
+ helper_method :app_name
45
+
46
+ private
47
+
48
+ def app_name
49
+ Rails.application.class.module_parent_name
50
+ end
51
+ end
52
+ ```
53
+
54
+ ## TDD Workflow
55
+
56
+ ```
57
+ Mailer Progress:
58
+ - [ ] Step 1: Write mailer test (RED)
59
+ - [ ] Step 2: Run test (fails)
60
+ - [ ] Step 3: Create mailer method
61
+ - [ ] Step 4: Create email templates
62
+ - [ ] Step 5: Run test (GREEN)
63
+ - [ ] Step 6: Create preview
64
+ ```
65
+
66
+ ## Testing Mailers (Minitest)
67
+
68
+ ### Mailer Test
69
+
70
+ ```ruby
71
+ # test/mailers/user_mailer_test.rb
72
+ require "test_helper"
73
+
74
+ class UserMailerTest < ActionMailer::TestCase
75
+ setup do
76
+ @user = users(:one)
77
+ end
78
+
79
+ test "welcome email renders headers" do
80
+ mail = UserMailer.welcome(@user)
81
+
82
+ assert_equal I18n.t("user_mailer.welcome.subject"), mail.subject
83
+ assert_equal [@user.email_address], mail.to
84
+ assert_equal ["noreply@example.com"], mail.from
85
+ end
86
+
87
+ test "welcome email renders HTML body" do
88
+ mail = UserMailer.welcome(@user)
89
+
90
+ assert_includes mail.html_part.body.to_s, @user.name
91
+ assert_includes mail.html_part.body.to_s, "Welcome"
92
+ end
93
+
94
+ test "welcome email renders text body" do
95
+ mail = UserMailer.welcome(@user)
96
+
97
+ assert_includes mail.text_part.body.to_s, @user.name
98
+ end
99
+
100
+ test "welcome email includes login link" do
101
+ mail = UserMailer.welcome(@user)
102
+
103
+ assert_includes mail.html_part.body.to_s, new_session_url
104
+ end
105
+
106
+ test "password_reset email includes token" do
107
+ token = "reset-token-123"
108
+ mail = UserMailer.password_reset(@user, token)
109
+
110
+ assert_equal [@user.email_address], mail.to
111
+ assert_includes mail.html_part.body.to_s, token
112
+ end
113
+ end
114
+ ```
115
+
116
+ ### Testing Delivery
117
+
118
+ ```ruby
119
+ # test/integration/registration_test.rb
120
+ require "test_helper"
121
+
122
+ class RegistrationTest < ActionDispatch::IntegrationTest
123
+ test "registration sends welcome email" do
124
+ assert_enqueued_email_with UserMailer, :welcome do
125
+ post registrations_path, params: {
126
+ registration: { email: "new@example.com", name: "Test", password: "password123" }
127
+ }
128
+ end
129
+ end
130
+ end
131
+ ```
132
+
133
+ ### Testing with perform_enqueued_jobs
134
+
135
+ ```ruby
136
+ # test/integration/notification_test.rb
137
+ require "test_helper"
138
+
139
+ class NotificationTest < ActionDispatch::IntegrationTest
140
+ test "sends notification email" do
141
+ assert_emails 1 do
142
+ perform_enqueued_jobs do
143
+ NotificationMailer.daily_digest(users(:one)).deliver_later
144
+ end
145
+ end
146
+ end
147
+ end
148
+ ```
149
+
150
+ ## Mailer Implementation
151
+
152
+ ### Basic Mailer
153
+
154
+ ```ruby
155
+ # app/mailers/user_mailer.rb
156
+ class UserMailer < ApplicationMailer
157
+ def welcome(user)
158
+ @user = user
159
+ @login_url = new_session_url
160
+
161
+ mail(to: @user.email_address, subject: t(".subject"))
162
+ end
163
+
164
+ def password_reset(user, token)
165
+ @user = user
166
+ @token = token
167
+ @reset_url = edit_password_url(token: token)
168
+ @expires_in = "24 hours"
169
+
170
+ mail(to: @user.email_address, subject: t(".subject"))
171
+ end
172
+ end
173
+ ```
174
+
175
+ ### Mailer with Attachments
176
+
177
+ ```ruby
178
+ class ReportMailer < ApplicationMailer
179
+ def monthly_report(user, report)
180
+ @user = user
181
+ @report = report
182
+
183
+ attachments["report-#{Date.current}.pdf"] = report.to_pdf
184
+
185
+ mail(to: @user.email_address, subject: t(".subject"))
186
+ end
187
+ end
188
+ ```
189
+
190
+ ### Bundled Notification Pattern
191
+
192
+ Send one email with multiple notifications instead of many emails:
193
+
194
+ ```ruby
195
+ class NotificationMailer < ApplicationMailer
196
+ def daily_digest(user)
197
+ @user = user
198
+ @notifications = user.notifications.unread.today
199
+
200
+ return if @notifications.empty?
201
+
202
+ mail(to: @user.email_address, subject: t(".subject", count: @notifications.count))
203
+ end
204
+ end
205
+ ```
206
+
207
+ ## Email Templates
208
+
209
+ ```erb
210
+ <%# app/views/user_mailer/welcome.html.erb %>
211
+ <h1><%= t(".greeting", name: @user.name) %></h1>
212
+ <p><%= t(".intro") %></p>
213
+ <p><%= link_to t(".login_button"), @login_url, class: "button" %></p>
214
+ ```
215
+
216
+ ```erb
217
+ <%# app/views/user_mailer/welcome.text.erb %>
218
+ <%= t(".greeting", name: @user.name) %>
219
+
220
+ <%= t(".intro") %>
221
+
222
+ <%= t(".login_prompt") %>: <%= @login_url %>
223
+ ```
224
+
225
+ ## Delivery Methods
226
+
227
+ ```ruby
228
+ # Background delivery (preferred)
229
+ UserMailer.welcome(user).deliver_later
230
+
231
+ # With delay
232
+ UserMailer.welcome(user).deliver_later(wait: 5.minutes)
233
+
234
+ # Immediate (avoid in production)
235
+ UserMailer.welcome(user).deliver_now
236
+ ```
237
+
238
+ ## Previews
239
+
240
+ ```ruby
241
+ # test/mailers/previews/user_mailer_preview.rb
242
+ class UserMailerPreview < ActionMailer::Preview
243
+ def welcome
244
+ user = User.first
245
+ UserMailer.welcome(user)
246
+ end
247
+
248
+ def password_reset
249
+ user = User.first
250
+ UserMailer.password_reset(user, "preview-token-123")
251
+ end
252
+ end
253
+ ```
254
+
255
+ Access at: `http://localhost:3000/rails/mailers`
256
+
257
+ ## I18n for Emails
258
+
259
+ ```yaml
260
+ # config/locales/mailers/en.yml
261
+ en:
262
+ user_mailer:
263
+ welcome:
264
+ subject: "Welcome to Our App!"
265
+ greeting: "Hello %{name}!"
266
+ intro: "Thanks for signing up."
267
+ login_button: "Log In Now"
268
+ login_prompt: "Log in here"
269
+ password_reset:
270
+ subject: "Reset Your Password"
271
+ ```
272
+
273
+ ### Localized Delivery
274
+
275
+ ```ruby
276
+ class UserMailer < ApplicationMailer
277
+ def welcome(user)
278
+ @user = user
279
+ I18n.with_locale(user.locale || I18n.default_locale) do
280
+ mail(to: @user.email_address, subject: t(".subject"))
281
+ end
282
+ end
283
+ end
284
+ ```
285
+
286
+ ## Checklist
287
+
288
+ - [ ] Mailer test written first (RED)
289
+ - [ ] Mailer method created
290
+ - [ ] HTML template created
291
+ - [ ] Text template created
292
+ - [ ] Uses I18n for all text
293
+ - [ ] Preview created
294
+ - [ ] Uses `deliver_later` (not `deliver_now`)
295
+ - [ ] All tests GREEN