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,399 @@
1
+ ---
2
+ name: rails-concern
3
+ description: Creates Rails concerns for shared behavior across models or controllers with TDD. Use when extracting shared code, creating reusable modules, DRYing up models/controllers, or when user mentions concerns, modules, mixins, or shared behavior.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Concern Generator (TDD)
8
+
9
+ Creates concerns (ActiveSupport::Concern modules) for shared behavior with tests first.
10
+
11
+ ## Quick Start
12
+
13
+ 1. Write failing test for the concern behavior
14
+ 2. Run test to confirm RED
15
+ 3. Implement concern in `app/models/concerns/` or `app/controllers/concerns/`
16
+ 4. Run test to confirm GREEN
17
+
18
+ ## When to Use Concerns
19
+
20
+ **Good use cases:**
21
+ - Shared validations across multiple models
22
+ - Common scopes used by several models
23
+ - Shared callbacks (e.g., UUID generation, slug creation)
24
+ - Controller authentication/authorization helpers
25
+ - Pagination or filtering logic
26
+ - Auditing and tracking behavior
27
+
28
+ **Avoid concerns when:**
29
+ - Logic is only used in one place (YAGNI)
30
+ - Creating "god" concerns with unrelated methods
31
+ - Logic should be a service object instead
32
+ - Concern would need its own state/config (use a class)
33
+
34
+ ## TDD Workflow
35
+
36
+ ### Step 1: Create Concern Test (RED)
37
+
38
+ For **Model Concerns**, test via a model that includes it:
39
+
40
+ ```ruby
41
+ # test/models/concerns/has_uuid_test.rb
42
+ require "test_helper"
43
+
44
+ class HasUuidTest < ActiveSupport::TestCase
45
+ test "generates uuid before validation on create" do
46
+ event = Event.new(name: "Test", account: accounts(:one))
47
+ event.valid?
48
+ assert_present event.uuid
49
+ end
50
+
51
+ test "does not overwrite existing uuid" do
52
+ event = Event.new(name: "Test", uuid: "custom-uuid", account: accounts(:one))
53
+ event.valid?
54
+ assert_equal "custom-uuid", event.uuid
55
+ end
56
+
57
+ test "validates uuid uniqueness" do
58
+ existing = events(:one)
59
+ event = Event.new(uuid: existing.uuid)
60
+ assert_not event.valid?
61
+ assert_includes event.errors[:uuid], "has already been taken"
62
+ end
63
+
64
+ test "find_by_uuid! finds record" do
65
+ event = events(:one)
66
+ assert_equal event, Event.find_by_uuid!(event.uuid)
67
+ end
68
+
69
+ test "find_by_uuid! raises for missing uuid" do
70
+ assert_raises(ActiveRecord::RecordNotFound) do
71
+ Event.find_by_uuid!("nonexistent")
72
+ end
73
+ end
74
+ end
75
+ ```
76
+
77
+ Alternative: Use a shared test module for concerns used by many models:
78
+
79
+ ```ruby
80
+ # test/support/shared_tests/has_uuid_tests.rb
81
+ module HasUuidTests
82
+ extend ActiveSupport::Concern
83
+
84
+ included do
85
+ test "generates uuid on create" do
86
+ record = build_record_for_concern
87
+ record.valid?
88
+ assert_present record.uuid
89
+ end
90
+ end
91
+ end
92
+
93
+ # test/models/event_test.rb
94
+ class EventTest < ActiveSupport::TestCase
95
+ include HasUuidTests
96
+
97
+ private
98
+
99
+ def build_record_for_concern
100
+ Event.new(name: "Test", account: accounts(:one))
101
+ end
102
+ end
103
+ ```
104
+
105
+ For **Controller Concerns**, test via integration tests:
106
+
107
+ ```ruby
108
+ # test/controllers/concerns/filterable_test.rb
109
+ require "test_helper"
110
+
111
+ class FilterableTest < ActionDispatch::IntegrationTest
112
+ setup do
113
+ sign_in users(:one)
114
+ end
115
+
116
+ test "filters resources by status" do
117
+ get resources_path(status: "active")
118
+ assert_response :success
119
+ assert_includes response.body, resources(:active).name
120
+ assert_not_includes response.body, resources(:inactive).name
121
+ end
122
+ end
123
+ ```
124
+
125
+ ### Step 2: Run Test (Confirm RED)
126
+
127
+ ```bash
128
+ bin/rails test test/models/concerns/has_uuid_test.rb
129
+ ```
130
+
131
+ ### Step 3: Implement Concern (GREEN)
132
+
133
+ **Model Concern:**
134
+
135
+ ```ruby
136
+ # app/models/concerns/has_uuid.rb
137
+ module HasUuid
138
+ extend ActiveSupport::Concern
139
+
140
+ included do
141
+ before_validation :generate_uuid, on: :create
142
+ validates :uuid, presence: true, uniqueness: true
143
+ end
144
+
145
+ class_methods do
146
+ def find_by_uuid!(uuid)
147
+ find_by!(uuid: uuid)
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ def generate_uuid
154
+ self.uuid ||= SecureRandom.uuid
155
+ end
156
+ end
157
+ ```
158
+
159
+ **Controller Concern:**
160
+
161
+ ```ruby
162
+ # app/controllers/concerns/filterable.rb
163
+ module Filterable
164
+ extend ActiveSupport::Concern
165
+
166
+ private
167
+
168
+ def apply_filters(scope, allowed_filters)
169
+ allowed_filters.each do |filter|
170
+ if params[filter].present?
171
+ scope = scope.where(filter => params[filter])
172
+ end
173
+ end
174
+ scope
175
+ end
176
+ end
177
+ ```
178
+
179
+ ### Step 4: Run Test (Confirm GREEN)
180
+
181
+ ```bash
182
+ bin/rails test test/models/concerns/has_uuid_test.rb
183
+ ```
184
+
185
+ ## Common Concern Patterns
186
+
187
+ ### Pattern 1: UUID Generation
188
+
189
+ ```ruby
190
+ # app/models/concerns/has_uuid.rb
191
+ module HasUuid
192
+ extend ActiveSupport::Concern
193
+
194
+ included do
195
+ before_validation :generate_uuid, on: :create
196
+ validates :uuid, presence: true, uniqueness: true
197
+ end
198
+
199
+ private
200
+
201
+ def generate_uuid
202
+ self.uuid ||= SecureRandom.uuid
203
+ end
204
+ end
205
+ ```
206
+
207
+ ### Pattern 2: Soft Delete
208
+
209
+ ```ruby
210
+ # app/models/concerns/soft_deletable.rb
211
+ module SoftDeletable
212
+ extend ActiveSupport::Concern
213
+
214
+ included do
215
+ scope :kept, -> { where(deleted_at: nil) }
216
+ scope :discarded, -> { where.not(deleted_at: nil) }
217
+ end
218
+
219
+ def discard
220
+ update(deleted_at: Time.current)
221
+ end
222
+
223
+ def undiscard
224
+ update(deleted_at: nil)
225
+ end
226
+
227
+ def discarded?
228
+ deleted_at.present?
229
+ end
230
+ end
231
+ ```
232
+
233
+ ### Pattern 3: Searchable
234
+
235
+ ```ruby
236
+ # app/models/concerns/searchable.rb
237
+ module Searchable
238
+ extend ActiveSupport::Concern
239
+
240
+ class_methods do
241
+ def search(query)
242
+ return all if query.blank?
243
+
244
+ columns = searchable_columns.map { |c| "#{table_name}.#{c}" }
245
+ conditions = columns.map { |c| "#{c} LIKE :q" }.join(" OR ")
246
+ where(conditions, q: "%#{sanitize_sql_like(query)}%")
247
+ end
248
+
249
+ def searchable_columns
250
+ %w[name]
251
+ end
252
+ end
253
+ end
254
+ ```
255
+
256
+ ### Pattern 4: Auditable
257
+
258
+ ```ruby
259
+ # app/models/concerns/auditable.rb
260
+ module Auditable
261
+ extend ActiveSupport::Concern
262
+
263
+ included do
264
+ has_many :audit_logs, as: :auditable, dependent: :destroy
265
+ after_create :log_creation
266
+ after_update :log_update
267
+ end
268
+
269
+ private
270
+
271
+ def log_creation
272
+ audit_logs.create(action: "created", changes_data: attributes)
273
+ end
274
+
275
+ def log_update
276
+ return unless saved_changes.any?
277
+ audit_logs.create(action: "updated", changes_data: saved_changes)
278
+ end
279
+ end
280
+ ```
281
+
282
+ ### Pattern 5: Sluggable
283
+
284
+ ```ruby
285
+ # app/models/concerns/sluggable.rb
286
+ module Sluggable
287
+ extend ActiveSupport::Concern
288
+
289
+ included do
290
+ before_validation :generate_slug, on: :create
291
+ validates :slug, presence: true, uniqueness: { scope: slug_scope }
292
+ end
293
+
294
+ class_methods do
295
+ def slug_scope
296
+ nil
297
+ end
298
+
299
+ def find_by_slug!(slug)
300
+ find_by!(slug: slug)
301
+ end
302
+ end
303
+
304
+ def to_param
305
+ slug
306
+ end
307
+
308
+ private
309
+
310
+ def generate_slug
311
+ return if slug.present?
312
+ base_slug = slug_source.parameterize
313
+ self.slug = base_slug
314
+ counter = 1
315
+ while self.class.exists?(slug: self.slug)
316
+ self.slug = "#{base_slug}-#{counter}"
317
+ counter += 1
318
+ end
319
+ end
320
+
321
+ def slug_source
322
+ respond_to?(:name) ? name : to_s
323
+ end
324
+ end
325
+ ```
326
+
327
+ ### Pattern 6: Accountable (Multi-Tenancy)
328
+
329
+ ```ruby
330
+ # app/models/concerns/accountable.rb
331
+ module Accountable
332
+ extend ActiveSupport::Concern
333
+
334
+ included do
335
+ belongs_to :account
336
+ validates :account, presence: true
337
+
338
+ scope :for_account, ->(account) { where(account: account) }
339
+ end
340
+ end
341
+ ```
342
+
343
+ ### Pattern 7: Tokenizable
344
+
345
+ ```ruby
346
+ # app/models/concerns/tokenizable.rb
347
+ module Tokenizable
348
+ extend ActiveSupport::Concern
349
+
350
+ included do
351
+ has_secure_token :api_token
352
+ end
353
+
354
+ class_methods do
355
+ def find_by_api_token!(token)
356
+ find_by!(api_token: token)
357
+ end
358
+ end
359
+
360
+ def regenerate_api_token!
361
+ regenerate_api_token
362
+ save!
363
+ end
364
+ end
365
+ ```
366
+
367
+ ## Usage
368
+
369
+ **In Models:**
370
+
371
+ ```ruby
372
+ class Event < ApplicationRecord
373
+ include HasUuid
374
+ include SoftDeletable
375
+ include Searchable
376
+ include Accountable
377
+ end
378
+ ```
379
+
380
+ **In Controllers:**
381
+
382
+ ```ruby
383
+ class ApplicationController < ActionController::Base
384
+ include Authentication
385
+ include Filterable
386
+ end
387
+ ```
388
+
389
+ ## Checklist
390
+
391
+ - [ ] Test written first (RED)
392
+ - [ ] Uses `extend ActiveSupport::Concern`
393
+ - [ ] `included` block for callbacks/validations/scopes
394
+ - [ ] `class_methods` block for class-level methods
395
+ - [ ] Instance methods outside blocks
396
+ - [ ] Single responsibility (one purpose per concern)
397
+ - [ ] Well-named (describes what it adds: `HasUuid`, `SoftDeletable`, `Searchable`)
398
+ - [ ] Database-agnostic (no PostgreSQL-specific SQL like ILIKE)
399
+ - [ ] All tests GREEN
@@ -0,0 +1,336 @@
1
+ ---
2
+ name: rails-controller
3
+ description: Creates Rails controllers with TDD approach - integration test first, then implementation. Use when creating new controllers, adding controller actions, implementing CRUD operations, or when user mentions controllers, routes, or API endpoints.
4
+ allowed-tools: Read, Write, Edit, Bash, Glob, Grep
5
+ ---
6
+
7
+ # Rails Controller Generator (TDD)
8
+
9
+ Creates RESTful controllers following project conventions with integration tests first.
10
+
11
+ ## Quick Start
12
+
13
+ 1. Write failing integration test in `test/controllers/` or `test/integration/`
14
+ 2. Run test to confirm RED
15
+ 3. Implement controller action
16
+ 4. Run test to confirm GREEN
17
+ 5. Refactor if needed
18
+
19
+ ## Project Conventions
20
+
21
+ This project uses:
22
+ - **Pundit** for authorization (`authorize @resource`, `policy_scope(Model)`)
23
+ - **Pagy** for pagination
24
+ - **Presenters** for view formatting
25
+ - **Multi-tenancy** via `current_account`
26
+ - **Turbo Stream** responses for dynamic updates
27
+
28
+ ## TDD Workflow
29
+
30
+ ### Step 1: Create Integration Test (RED)
31
+
32
+ ```ruby
33
+ # test/controllers/[resources]_controller_test.rb
34
+ require "test_helper"
35
+
36
+ class ResourcesControllerTest < ActionDispatch::IntegrationTest
37
+ setup do
38
+ @user = users(:one)
39
+ @resource = resources(:one)
40
+ sign_in @user
41
+ end
42
+
43
+ # === INDEX ===
44
+ test "GET /resources returns success" do
45
+ get resources_path
46
+ assert_response :success
47
+ end
48
+
49
+ test "GET /resources shows only current_account resources (multi-tenant)" do
50
+ other_resource = resources(:other_account)
51
+
52
+ get resources_path
53
+
54
+ assert_includes response.body, @resource.name
55
+ assert_not_includes response.body, other_resource.name
56
+ end
57
+
58
+ test "GET /resources paginates results" do
59
+ get resources_path
60
+ assert_response :success
61
+ end
62
+
63
+ # === SHOW ===
64
+ test "GET /resources/:id returns success" do
65
+ get resource_path(@resource)
66
+ assert_response :success
67
+ end
68
+
69
+ test "GET /resources/:id returns 404 for other account" do
70
+ other_resource = resources(:other_account)
71
+
72
+ assert_raises(ActiveRecord::RecordNotFound) do
73
+ get resource_path(other_resource)
74
+ end
75
+ end
76
+
77
+ # === NEW ===
78
+ test "GET /resources/new returns success" do
79
+ get new_resource_path
80
+ assert_response :success
81
+ end
82
+
83
+ # === CREATE ===
84
+ test "POST /resources creates with valid params" do
85
+ assert_difference("Resource.count", 1) do
86
+ post resources_path, params: {
87
+ resource: { name: "New Resource", field1: "value" }
88
+ }
89
+ end
90
+
91
+ assert_redirected_to resources_path
92
+ assert_equal @user.account, Resource.last.account
93
+ end
94
+
95
+ test "POST /resources rejects invalid params" do
96
+ assert_no_difference("Resource.count") do
97
+ post resources_path, params: {
98
+ resource: { name: "" }
99
+ }
100
+ end
101
+
102
+ assert_response :unprocessable_entity
103
+ end
104
+
105
+ # === EDIT ===
106
+ test "GET /resources/:id/edit returns success" do
107
+ get edit_resource_path(@resource)
108
+ assert_response :success
109
+ end
110
+
111
+ # === UPDATE ===
112
+ test "PATCH /resources/:id updates with valid params" do
113
+ patch resource_path(@resource), params: {
114
+ resource: { name: "Updated Name" }
115
+ }
116
+
117
+ assert_redirected_to resource_path(@resource)
118
+ assert_equal "Updated Name", @resource.reload.name
119
+ end
120
+
121
+ test "PATCH /resources/:id rejects invalid params" do
122
+ patch resource_path(@resource), params: {
123
+ resource: { name: "" }
124
+ }
125
+
126
+ assert_response :unprocessable_entity
127
+ end
128
+
129
+ # === DESTROY ===
130
+ test "DELETE /resources/:id destroys resource" do
131
+ assert_difference("Resource.count", -1) do
132
+ delete resource_path(@resource)
133
+ end
134
+
135
+ assert_redirected_to resources_path
136
+ end
137
+
138
+ # === AUTHORIZATION ===
139
+ test "unauthenticated user is redirected" do
140
+ sign_out
141
+
142
+ get resources_path
143
+ assert_redirected_to new_session_path
144
+ end
145
+ end
146
+ ```
147
+
148
+ ### Step 2: Run Test (Confirm RED)
149
+
150
+ ```bash
151
+ bin/rails test test/controllers/resources_controller_test.rb
152
+ ```
153
+
154
+ ### Step 3: Implement Controller (GREEN)
155
+
156
+ ```ruby
157
+ # app/controllers/[resources]_controller.rb
158
+ class ResourcesController < ApplicationController
159
+ before_action :set_resource, only: [:show, :edit, :update, :destroy]
160
+
161
+ def index
162
+ authorize Resource, :index?
163
+ @pagy, resources = pagy(policy_scope(Resource).order(created_at: :desc))
164
+ @resources = resources.map { |r| ResourcePresenter.new(r) }
165
+ end
166
+
167
+ def show
168
+ authorize @resource
169
+ @resource = ResourcePresenter.new(@resource)
170
+ end
171
+
172
+ def new
173
+ @resource = current_account.resources.build
174
+ authorize @resource
175
+ end
176
+
177
+ def create
178
+ @resource = current_account.resources.build(resource_params)
179
+ authorize @resource
180
+
181
+ if @resource.save
182
+ redirect_to resources_path, notice: "Resource created successfully"
183
+ else
184
+ render :new, status: :unprocessable_entity
185
+ end
186
+ end
187
+
188
+ def edit
189
+ authorize @resource
190
+ end
191
+
192
+ def update
193
+ authorize @resource
194
+
195
+ if @resource.update(resource_params)
196
+ redirect_to @resource, notice: "Resource updated successfully"
197
+ else
198
+ render :edit, status: :unprocessable_entity
199
+ end
200
+ end
201
+
202
+ def destroy
203
+ authorize @resource
204
+ @resource.destroy
205
+ redirect_to resources_path, notice: "Resource deleted successfully"
206
+ end
207
+
208
+ private
209
+
210
+ def set_resource
211
+ @resource = policy_scope(Resource).find(params[:id])
212
+ end
213
+
214
+ def resource_params
215
+ params.require(:resource).permit(:name, :field1, :field2)
216
+ end
217
+ end
218
+ ```
219
+
220
+ ### Step 4: Run Test (Confirm GREEN)
221
+
222
+ ```bash
223
+ bin/rails test test/controllers/resources_controller_test.rb
224
+ ```
225
+
226
+ ## Test Helpers
227
+
228
+ Add to `test/test_helper.rb`:
229
+
230
+ ```ruby
231
+ class ActionDispatch::IntegrationTest
232
+ def sign_in(user)
233
+ session = user.identity.sessions.create!
234
+ cookies.signed[:session_token] = session.token
235
+ end
236
+
237
+ def sign_out
238
+ cookies.delete(:session_token)
239
+ end
240
+ end
241
+ ```
242
+
243
+ ## Namespaced Controllers
244
+
245
+ For nested routes like `settings/accounts`:
246
+
247
+ ```ruby
248
+ # app/controllers/settings/accounts_controller.rb
249
+ module Settings
250
+ class AccountsController < ApplicationController
251
+ before_action :set_account
252
+
253
+ def show
254
+ authorize @account
255
+ end
256
+
257
+ private
258
+
259
+ def set_account
260
+ @account = current_account
261
+ end
262
+ end
263
+ end
264
+ ```
265
+
266
+ ## Turbo Stream Response Pattern
267
+
268
+ ```ruby
269
+ def create
270
+ @resource = current_account.resources.build(resource_params)
271
+ authorize @resource
272
+
273
+ if @resource.save
274
+ respond_to do |format|
275
+ format.html { redirect_to resources_path, notice: "Created" }
276
+ format.turbo_stream do
277
+ flash.now[:notice] = "Created"
278
+ @pagy, @resources = pagy(policy_scope(Resource).order(created_at: :desc))
279
+ render turbo_stream: [
280
+ turbo_stream.replace("resources-list", partial: "resources/list"),
281
+ turbo_stream.update("modal", "")
282
+ ]
283
+ end
284
+ end
285
+ else
286
+ render :new, status: :unprocessable_entity
287
+ end
288
+ end
289
+ ```
290
+
291
+ ### Testing Turbo Streams
292
+
293
+ ```ruby
294
+ test "POST /resources with turbo_stream format" do
295
+ post resources_path, params: {
296
+ resource: { name: "Turbo Resource" }
297
+ }, as: :turbo_stream
298
+
299
+ assert_response :success
300
+ assert_includes response.body, "turbo-stream"
301
+ end
302
+ ```
303
+
304
+ ## Full CRUD Fixture Setup
305
+
306
+ ```yaml
307
+ # test/fixtures/resources.yml
308
+ one:
309
+ name: "My Resource"
310
+ field1: "Value 1"
311
+ account: one
312
+
313
+ two:
314
+ name: "Another Resource"
315
+ field1: "Value 2"
316
+ account: one
317
+
318
+ other_account:
319
+ name: "Other Account Resource"
320
+ field1: "Value 3"
321
+ account: two
322
+ ```
323
+
324
+ ## Checklist
325
+
326
+ - [ ] Integration test written first (RED)
327
+ - [ ] Multi-tenant isolation tested
328
+ - [ ] Authorization tested (redirect/404 for unauthorized)
329
+ - [ ] Controller uses `authorize` on every action
330
+ - [ ] Controller uses `policy_scope` for queries
331
+ - [ ] Presenter wraps models for views
332
+ - [ ] Strong parameters defined
333
+ - [ ] All 7 CRUD actions tested (index, show, new, create, edit, update, destroy)
334
+ - [ ] Invalid params tested (422 response)
335
+ - [ ] Turbo Stream responses tested (if applicable)
336
+ - [ ] All tests GREEN