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,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
|