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,367 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: install
|
|
3
|
+
description: Sets up a new Rails 8 application with recommended defaults for TDD, authentication, multi-tenancy, and Hotwire. Use when creating a new Rails project, bootstrapping an app, or when user mentions new app, rails new, project setup, or initial setup.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails 8 Application Setup
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Bootstrap a new Rails 8 application with production-ready defaults:
|
|
12
|
+
- Rails 8 with Solid Queue, Solid Cache, Solid Cable
|
|
13
|
+
- Built-in authentication (has_secure_password)
|
|
14
|
+
- Multi-tenancy via Account scoping
|
|
15
|
+
- Hotwire (Turbo + Stimulus)
|
|
16
|
+
- Minitest + fixtures for testing
|
|
17
|
+
- Tailwind CSS for styling
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
rails new myapp --css tailwind --database sqlite3
|
|
23
|
+
cd myapp
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Post-Generation Setup
|
|
27
|
+
|
|
28
|
+
### 1. Generate Authentication
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
bin/rails generate authentication
|
|
32
|
+
bin/rails db:migrate
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This creates:
|
|
36
|
+
- `User` model with `email_address` and `password_digest`
|
|
37
|
+
- `Session` model with token-based sessions
|
|
38
|
+
- `Current` model (CurrentAttributes)
|
|
39
|
+
- `Authentication` concern
|
|
40
|
+
- `SessionsController`
|
|
41
|
+
|
|
42
|
+
### 2. Add Account Model (Multi-Tenancy)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
bin/rails generate model Account name:string
|
|
46
|
+
bin/rails generate migration AddAccountToUsers account:references
|
|
47
|
+
bin/rails db:migrate
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# app/models/account.rb
|
|
52
|
+
class Account < ApplicationRecord
|
|
53
|
+
has_many :users, dependent: :destroy
|
|
54
|
+
|
|
55
|
+
validates :name, presence: true
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# app/models/user.rb
|
|
61
|
+
class User < ApplicationRecord
|
|
62
|
+
has_secure_password
|
|
63
|
+
has_many :sessions, dependent: :destroy
|
|
64
|
+
belongs_to :account
|
|
65
|
+
|
|
66
|
+
normalizes :email_address, with: ->(e) { e.strip.downcase }
|
|
67
|
+
|
|
68
|
+
validates :email_address, presence: true,
|
|
69
|
+
uniqueness: true,
|
|
70
|
+
format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
71
|
+
end
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. Add Current Account
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# app/models/current.rb
|
|
78
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
79
|
+
attribute :session
|
|
80
|
+
attribute :user_agent, :ip_address
|
|
81
|
+
|
|
82
|
+
delegate :user, to: :session, allow_nil: true
|
|
83
|
+
|
|
84
|
+
def account
|
|
85
|
+
user&.account
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 4. Configure Test Helper
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
# test/test_helper.rb
|
|
94
|
+
ENV["RAILS_ENV"] ||= "test"
|
|
95
|
+
require_relative "../config/environment"
|
|
96
|
+
require "rails/test_help"
|
|
97
|
+
|
|
98
|
+
module ActiveSupport
|
|
99
|
+
class TestCase
|
|
100
|
+
parallelize(workers: :number_of_processors)
|
|
101
|
+
fixtures :all
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def sign_in(user)
|
|
106
|
+
session = user.sessions.create!
|
|
107
|
+
cookies.signed.permanent[:session_token] = {
|
|
108
|
+
value: session.token,
|
|
109
|
+
httponly: true
|
|
110
|
+
}
|
|
111
|
+
Current.session = session
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def sign_out
|
|
115
|
+
Current.session&.destroy
|
|
116
|
+
cookies.delete(:session_token)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 5. Create Fixtures
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
# test/fixtures/accounts.yml
|
|
126
|
+
one:
|
|
127
|
+
name: Acme Corp
|
|
128
|
+
|
|
129
|
+
other:
|
|
130
|
+
name: Other Corp
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
# test/fixtures/users.yml
|
|
135
|
+
one:
|
|
136
|
+
email_address: user@example.com
|
|
137
|
+
password_digest: <%= BCrypt::Password.create("password123") %>
|
|
138
|
+
account: one
|
|
139
|
+
|
|
140
|
+
admin:
|
|
141
|
+
email_address: admin@example.com
|
|
142
|
+
password_digest: <%= BCrypt::Password.create("password123") %>
|
|
143
|
+
account: one
|
|
144
|
+
|
|
145
|
+
other_account:
|
|
146
|
+
email_address: other@example.com
|
|
147
|
+
password_digest: <%= BCrypt::Password.create("password123") %>
|
|
148
|
+
account: other
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
# test/fixtures/sessions.yml
|
|
153
|
+
one:
|
|
154
|
+
user: one
|
|
155
|
+
token: <%= SecureRandom.urlsafe_base64(32) %>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### 6. Configure Solid Queue
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
# config/queue.yml (already generated by Rails 8)
|
|
162
|
+
default: &default
|
|
163
|
+
dispatchers:
|
|
164
|
+
- polling_interval: 1
|
|
165
|
+
batch_size: 500
|
|
166
|
+
workers:
|
|
167
|
+
- queues: "*"
|
|
168
|
+
threads: 3
|
|
169
|
+
processes: 1
|
|
170
|
+
polling_interval: 0.1
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# config/environments/production.rb
|
|
175
|
+
config.active_job.queue_adapter = :solid_queue
|
|
176
|
+
config.solid_queue.connects_to = { database: { writing: :queue } }
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 7. Configure Solid Cache
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# config/environments/production.rb
|
|
183
|
+
config.cache_store = :solid_cache_store
|
|
184
|
+
config.solid_cache.connects_to = { database: { writing: :cache } }
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 8. Add Development Gems
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
# Gemfile
|
|
191
|
+
group :development, :test do
|
|
192
|
+
gem "debug", platforms: %i[mri windows], require: "debug/prelude"
|
|
193
|
+
gem "brakeman", require: false
|
|
194
|
+
gem "rubocop-rails-omakase", require: false
|
|
195
|
+
gem "bullet"
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
bundle install
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 9. Configure Bullet (N+1 Detection)
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
# config/environments/development.rb
|
|
207
|
+
config.after_initialize do
|
|
208
|
+
Bullet.enable = true
|
|
209
|
+
Bullet.alert = true
|
|
210
|
+
Bullet.bullet_logger = true
|
|
211
|
+
Bullet.console = true
|
|
212
|
+
Bullet.rails_logger = true
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# config/environments/test.rb
|
|
216
|
+
config.after_initialize do
|
|
217
|
+
Bullet.enable = true
|
|
218
|
+
Bullet.raise = true
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### 10. Add Application Layout Defaults
|
|
223
|
+
|
|
224
|
+
```erb
|
|
225
|
+
<%# app/views/layouts/application.html.erb %>
|
|
226
|
+
<!DOCTYPE html>
|
|
227
|
+
<html>
|
|
228
|
+
<head>
|
|
229
|
+
<title><%= content_for(:title) || "MyApp" %></title>
|
|
230
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
231
|
+
<%= csrf_meta_tags %>
|
|
232
|
+
<%= csp_meta_tag %>
|
|
233
|
+
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
|
234
|
+
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
|
235
|
+
<%= javascript_importmap_tags %>
|
|
236
|
+
</head>
|
|
237
|
+
<body class="min-h-screen bg-white">
|
|
238
|
+
<div id="flash">
|
|
239
|
+
<% flash.each do |type, message| %>
|
|
240
|
+
<div class="flash flash-<%= type %>"><%= message %></div>
|
|
241
|
+
<% end %>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<main class="container mx-auto px-4 py-8">
|
|
245
|
+
<%= yield %>
|
|
246
|
+
</main>
|
|
247
|
+
</body>
|
|
248
|
+
</html>
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Verification
|
|
252
|
+
|
|
253
|
+
Run the test suite to confirm everything is wired up:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
bin/rails test
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Smoke Test
|
|
260
|
+
|
|
261
|
+
```ruby
|
|
262
|
+
# test/models/user_test.rb
|
|
263
|
+
require "test_helper"
|
|
264
|
+
|
|
265
|
+
class UserTest < ActiveSupport::TestCase
|
|
266
|
+
test "fixture user is valid" do
|
|
267
|
+
user = users(:one)
|
|
268
|
+
assert user.valid?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
test "requires email_address" do
|
|
272
|
+
user = User.new(password: "password123", account: accounts(:one))
|
|
273
|
+
assert_not user.valid?
|
|
274
|
+
assert user.errors[:email_address].any?
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
test "requires unique email_address" do
|
|
278
|
+
existing = users(:one)
|
|
279
|
+
user = User.new(
|
|
280
|
+
email_address: existing.email_address,
|
|
281
|
+
password: "password123",
|
|
282
|
+
account: accounts(:one)
|
|
283
|
+
)
|
|
284
|
+
assert_not user.valid?
|
|
285
|
+
assert user.errors[:email_address].any?
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
# test/models/account_test.rb
|
|
292
|
+
require "test_helper"
|
|
293
|
+
|
|
294
|
+
class AccountTest < ActiveSupport::TestCase
|
|
295
|
+
test "fixture account is valid" do
|
|
296
|
+
account = accounts(:one)
|
|
297
|
+
assert account.valid?
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
test "requires name" do
|
|
301
|
+
account = Account.new
|
|
302
|
+
assert_not account.valid?
|
|
303
|
+
assert account.errors[:name].any?
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Directory Structure After Setup
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
app/
|
|
312
|
+
├── controllers/
|
|
313
|
+
│ ├── application_controller.rb
|
|
314
|
+
│ ├── concerns/
|
|
315
|
+
│ │ └── authentication.rb
|
|
316
|
+
│ └── sessions_controller.rb
|
|
317
|
+
├── models/
|
|
318
|
+
│ ├── account.rb
|
|
319
|
+
│ ├── current.rb
|
|
320
|
+
│ ├── session.rb
|
|
321
|
+
│ └── user.rb
|
|
322
|
+
├── views/
|
|
323
|
+
│ └── layouts/
|
|
324
|
+
│ └── application.html.erb
|
|
325
|
+
config/
|
|
326
|
+
├── database.yml
|
|
327
|
+
├── queue.yml
|
|
328
|
+
├── cable.yml
|
|
329
|
+
└── environments/
|
|
330
|
+
├── development.rb
|
|
331
|
+
├── production.rb
|
|
332
|
+
└── test.rb
|
|
333
|
+
test/
|
|
334
|
+
├── test_helper.rb
|
|
335
|
+
├── fixtures/
|
|
336
|
+
│ ├── accounts.yml
|
|
337
|
+
│ ├── users.yml
|
|
338
|
+
│ └── sessions.yml
|
|
339
|
+
└── models/
|
|
340
|
+
├── user_test.rb
|
|
341
|
+
└── account_test.rb
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Rails 8 Defaults
|
|
345
|
+
|
|
346
|
+
Rails 8 ships with these defaults already configured:
|
|
347
|
+
- **Solid Queue** - Database-backed Active Job backend (replaces Redis/Sidekiq)
|
|
348
|
+
- **Solid Cache** - Database-backed cache store (replaces Redis/Memcached)
|
|
349
|
+
- **Solid Cable** - Database-backed Action Cable adapter
|
|
350
|
+
- **Propshaft** - Asset pipeline (replaces Sprockets)
|
|
351
|
+
- **Import Maps** - JavaScript without bundling
|
|
352
|
+
- **Kamal** - Deployment
|
|
353
|
+
- **Thruster** - HTTP/2 proxy with asset caching
|
|
354
|
+
|
|
355
|
+
## Checklist
|
|
356
|
+
|
|
357
|
+
- [ ] Rails app generated
|
|
358
|
+
- [ ] Authentication generated and migrated
|
|
359
|
+
- [ ] Account model created (multi-tenancy)
|
|
360
|
+
- [ ] Current model configured with account
|
|
361
|
+
- [ ] Test helper with sign_in/sign_out
|
|
362
|
+
- [ ] Fixtures created (accounts, users, sessions)
|
|
363
|
+
- [ ] Solid Queue configured
|
|
364
|
+
- [ ] Solid Cache configured
|
|
365
|
+
- [ ] Bullet configured for N+1 detection
|
|
366
|
+
- [ ] Smoke tests pass
|
|
367
|
+
- [ ] All tests GREEN
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: performance-optimization
|
|
3
|
+
description: Identifies and fixes Rails performance issues including N+1 queries, slow queries, and memory problems. Use when optimizing queries, fixing N+1 issues, improving response times, or when user mentions performance, slow, optimization, or Bullet gem.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, Glob, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Performance Optimization for Rails 8
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
Performance optimization focuses on:
|
|
12
|
+
- N+1 query detection and prevention
|
|
13
|
+
- Query optimization with eager loading
|
|
14
|
+
- Database indexing
|
|
15
|
+
- Memory management
|
|
16
|
+
- Batch processing
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
# Gemfile
|
|
22
|
+
group :development, :test do
|
|
23
|
+
gem "bullet" # N+1 detection
|
|
24
|
+
gem "rack-mini-profiler" # Request profiling
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Bullet Configuration
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# config/environments/development.rb
|
|
32
|
+
config.after_initialize do
|
|
33
|
+
Bullet.enable = true
|
|
34
|
+
Bullet.alert = true
|
|
35
|
+
Bullet.bullet_logger = true
|
|
36
|
+
Bullet.console = true
|
|
37
|
+
Bullet.rails_logger = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# config/environments/test.rb
|
|
41
|
+
config.after_initialize do
|
|
42
|
+
Bullet.enable = true
|
|
43
|
+
Bullet.raise = true # Fail tests on N+1
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## N+1 Query Problems
|
|
48
|
+
|
|
49
|
+
### The Problem
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# BAD: N+1 - 1 query for events, N queries for venues
|
|
53
|
+
@events = Event.all
|
|
54
|
+
@events.each { |e| e.venue.name } # Query per event!
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### The Solution
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# GOOD: 2 queries total
|
|
61
|
+
@events = Event.includes(:venue)
|
|
62
|
+
@events.each { |e| e.venue.name } # No additional query
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Eager Loading Methods
|
|
66
|
+
|
|
67
|
+
| Method | Use When |
|
|
68
|
+
|--------|----------|
|
|
69
|
+
| `includes` | Most cases (Rails chooses strategy) |
|
|
70
|
+
| `preload` | Force separate queries, large datasets |
|
|
71
|
+
| `eager_load` | Filtering on association, need LEFT JOIN |
|
|
72
|
+
| `joins` | Only filtering, don't need association data |
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# Single association
|
|
76
|
+
Event.includes(:venue)
|
|
77
|
+
|
|
78
|
+
# Multiple
|
|
79
|
+
Event.includes(:venue, :organizer)
|
|
80
|
+
|
|
81
|
+
# Nested
|
|
82
|
+
Event.includes(venue: :address)
|
|
83
|
+
|
|
84
|
+
# Deep nesting
|
|
85
|
+
Event.includes(
|
|
86
|
+
:venue, :organizer,
|
|
87
|
+
vendors: [:category, :reviews],
|
|
88
|
+
comments: :user
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Query Optimization Patterns
|
|
93
|
+
|
|
94
|
+
### Pattern 1: Scoped Eager Loading
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class Event < ApplicationRecord
|
|
98
|
+
scope :with_details, -> {
|
|
99
|
+
includes(:venue, :organizer, vendors: :category)
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Controller
|
|
104
|
+
@events = Event.with_details.where(account: current_account)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Pattern 2: Counter Caches
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# Migration
|
|
111
|
+
add_column :events, :comments_count, :integer, default: 0, null: false
|
|
112
|
+
|
|
113
|
+
# Model
|
|
114
|
+
class Comment < ApplicationRecord
|
|
115
|
+
belongs_to :event, counter_cache: true
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Usage (no query)
|
|
119
|
+
event.comments_count
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Pattern 3: Select Only Needed Columns
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# BAD
|
|
126
|
+
User.all.map(&:name)
|
|
127
|
+
|
|
128
|
+
# GOOD
|
|
129
|
+
User.pluck(:name)
|
|
130
|
+
|
|
131
|
+
# For objects with limited columns
|
|
132
|
+
User.select(:id, :name, :email)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Pattern 4: Batch Processing
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
# BAD: Loads all records
|
|
139
|
+
Event.all.each { |e| process(e) }
|
|
140
|
+
|
|
141
|
+
# GOOD: Processes in batches
|
|
142
|
+
Event.find_each(batch_size: 500) { |e| process(e) }
|
|
143
|
+
|
|
144
|
+
# For updates
|
|
145
|
+
Event.in_batches(of: 1000) do |batch|
|
|
146
|
+
batch.update_all(status: :archived)
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Pattern 5: Exists? vs Present?
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
# BAD: Loads all records
|
|
154
|
+
if Event.where(status: :active).any?
|
|
155
|
+
if Event.where(status: :active).present?
|
|
156
|
+
|
|
157
|
+
# GOOD: SELECT 1 LIMIT 1
|
|
158
|
+
if Event.where(status: :active).exists?
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Pattern 6: Size vs Count vs Length
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
# count: Always queries database
|
|
165
|
+
# size: Uses counter cache if available, else count
|
|
166
|
+
# length: Loads collection if not loaded
|
|
167
|
+
|
|
168
|
+
# Use size (handles both cases)
|
|
169
|
+
events.size
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Database Indexing
|
|
173
|
+
|
|
174
|
+
### When to Add Indexes
|
|
175
|
+
|
|
176
|
+
| Add Index For | Example |
|
|
177
|
+
|--------------|---------|
|
|
178
|
+
| Foreign keys | `account_id`, `user_id` |
|
|
179
|
+
| WHERE columns | `WHERE status = 'active'` |
|
|
180
|
+
| ORDER BY columns | `ORDER BY created_at DESC` |
|
|
181
|
+
| JOIN columns | `JOIN ON events.venue_id` |
|
|
182
|
+
| Unique constraints | `email`, `uuid` |
|
|
183
|
+
|
|
184
|
+
### Index Types
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
add_index :events, :status # Single
|
|
188
|
+
add_index :events, [:account_id, :status] # Composite
|
|
189
|
+
add_index :users, :email, unique: true # Unique
|
|
190
|
+
add_index :events, :event_date, where: "status = 0" # Partial
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Testing for Performance
|
|
194
|
+
|
|
195
|
+
### N+1 Detection in Tests
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
# test/test_helper.rb
|
|
199
|
+
class ActiveSupport::TestCase
|
|
200
|
+
setup do
|
|
201
|
+
Bullet.start_request if Bullet.enable?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
teardown do
|
|
205
|
+
if Bullet.enable?
|
|
206
|
+
Bullet.perform_out_of_channel_notifications if Bullet.notification?
|
|
207
|
+
Bullet.end_request
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Query Count Assertions
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# test/support/query_counter.rb
|
|
217
|
+
module QueryCounter
|
|
218
|
+
def count_queries(&block)
|
|
219
|
+
count = 0
|
|
220
|
+
counter = ->(*, _) { count += 1 }
|
|
221
|
+
ActiveSupport::Notifications.subscribed(counter, "sql.active_record", &block)
|
|
222
|
+
count
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# test/test_helper.rb
|
|
227
|
+
class ActiveSupport::TestCase
|
|
228
|
+
include QueryCounter
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
# test/models/event_test.rb
|
|
234
|
+
require "test_helper"
|
|
235
|
+
|
|
236
|
+
class EventPerformanceTest < ActiveSupport::TestCase
|
|
237
|
+
test "with_details makes minimal queries" do
|
|
238
|
+
query_count = count_queries do
|
|
239
|
+
Event.with_details.where(account: accounts(:one)).each do |e|
|
|
240
|
+
e.venue&.name
|
|
241
|
+
e.organizer&.name
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# events + venues + organizers = 3 queries max
|
|
246
|
+
assert query_count <= 3, "Expected <= 3 queries, got #{query_count}"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Missing Index Detection
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
# test/db/schema_test.rb
|
|
255
|
+
require "test_helper"
|
|
256
|
+
|
|
257
|
+
class SchemaPerformanceTest < ActiveSupport::TestCase
|
|
258
|
+
test "all foreign keys have indexes" do
|
|
259
|
+
connection = ActiveRecord::Base.connection
|
|
260
|
+
|
|
261
|
+
connection.tables.each do |table|
|
|
262
|
+
columns = connection.columns(table)
|
|
263
|
+
fk_columns = columns.select { |c| c.name.end_with?("_id") }
|
|
264
|
+
indexes = connection.indexes(table)
|
|
265
|
+
|
|
266
|
+
fk_columns.each do |col|
|
|
267
|
+
indexed = indexes.any? { |idx| idx.columns.include?(col.name) }
|
|
268
|
+
assert indexed, "Missing index: #{table}.#{col.name}"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Memory Optimization
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
# BAD: Builds large array
|
|
279
|
+
Event.all.map(&:name).join(", ")
|
|
280
|
+
|
|
281
|
+
# GOOD: Streams results
|
|
282
|
+
Event.pluck(:name).join(", ")
|
|
283
|
+
|
|
284
|
+
# BAD: Instantiates all AR objects
|
|
285
|
+
Event.all.each { |e| e.update!(processed: true) }
|
|
286
|
+
|
|
287
|
+
# GOOD: Direct SQL update in batches
|
|
288
|
+
Event.in_batches.update_all(processed: true)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Quick Fixes Reference
|
|
292
|
+
|
|
293
|
+
| Problem | Solution |
|
|
294
|
+
|---------|----------|
|
|
295
|
+
| N+1 on belongs_to | `includes(:association)` |
|
|
296
|
+
| N+1 on has_many | `includes(:association)` |
|
|
297
|
+
| Slow COUNT | Add counter_cache |
|
|
298
|
+
| Loading all columns | Use `select` or `pluck` |
|
|
299
|
+
| Large dataset iteration | Use `find_each` |
|
|
300
|
+
| Missing index on FK | Add index on `*_id` columns |
|
|
301
|
+
| Slow WHERE clause | Add index on filtered column |
|
|
302
|
+
|
|
303
|
+
## Checklist
|
|
304
|
+
|
|
305
|
+
- [ ] Bullet enabled in development/test
|
|
306
|
+
- [ ] No N+1 queries in critical paths
|
|
307
|
+
- [ ] Foreign keys have indexes
|
|
308
|
+
- [ ] Counter caches for frequent counts
|
|
309
|
+
- [ ] Eager loading in controllers
|
|
310
|
+
- [ ] Batch processing for large datasets
|
|
311
|
+
- [ ] All tests GREEN
|