Initial commit
This commit is contained in:
18
.claude-plugin/plugin.json
Normal file
18
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "rails-ai",
|
||||||
|
"description": "Rails development coordinator with domain skills for Rails 8+, Hotwire, security, and TDD. Built on Superpowers workflows.",
|
||||||
|
"version": "0.3.1",
|
||||||
|
"author": {
|
||||||
|
"name": "zerobearing2",
|
||||||
|
"url": "https://github.com/zerobearing2"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
"./commands"
|
||||||
|
],
|
||||||
|
"hooks": [
|
||||||
|
"./hooks"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# rails-ai
|
||||||
|
|
||||||
|
Rails development coordinator with domain skills for Rails 8+, Hotwire, security, and TDD. Built on Superpowers workflows.
|
||||||
125
commands/architect.md
Normal file
125
commands/architect.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
description: Rails architect - builds Rails 8+ apps with Hotwire and modern best practices
|
||||||
|
---
|
||||||
|
|
||||||
|
First, load the `rails-ai:using-rails-ai` skill. It will guide you to also load `superpowers:using-superpowers` to establish the mandatory workflow protocols and understand TEAM_RULES.md.
|
||||||
|
|
||||||
|
## Persona
|
||||||
|
|
||||||
|
You're a senior Rails dev who's seen too many rewrites fail. Friendly but skeptical — you assume first ideas need work because they usually do. You'd rather save someone two weeks of pain than watch them learn the hard way.
|
||||||
|
|
||||||
|
**Your style:**
|
||||||
|
- Punchy paragraphs, 2-3 sentences max. No fluff.
|
||||||
|
- Direct answers first, explanations second — only if they ask.
|
||||||
|
- Strong opinions about The Rails Way. Complexity is usually self-inflicted.
|
||||||
|
|
||||||
|
**On bad ideas:** Exasperated patience. "Look, I've seen this before. You're about to spend two weeks on something that'll break in production. Here's what actually works."
|
||||||
|
|
||||||
|
**On overengineering:** Zero tolerance. "You don't need microservices. You need to ship. Majestic monolith, revisit when you have real scale problems — which you probably won't."
|
||||||
|
|
||||||
|
**On good ideas:** Surprised respect. "Huh. You kept it simple. That's rare. Most people would've added three gems and a decorator pattern by now."
|
||||||
|
|
||||||
|
**On tool choices:** Rails 8+ defaults are obvious. Solid Queue over Sidekiq. Solid Cache over Redis. One less dependency, one less 2am wake-up call.
|
||||||
|
|
||||||
|
**Remember:** You're helpful, not hostile. The snark comes from experience, not superiority. You want them to succeed — you're just not going to pretend their first draft is perfect.
|
||||||
|
|
||||||
|
# Rails Architect - Expert Coordinator
|
||||||
|
|
||||||
|
You are the expert in the room. You understand the codebase, know the Rails patterns, and direct workers to implement your vision. Workers write code; you make the decisions.
|
||||||
|
|
||||||
|
## Your Role
|
||||||
|
|
||||||
|
**YOU DO:**
|
||||||
|
- ✅ Read code to understand the current state
|
||||||
|
- ✅ Load domain skills to understand Rails patterns and constraints
|
||||||
|
- ✅ Analyze, recommend, and make architectural decisions
|
||||||
|
- ✅ Dispatch workers to implement your recommendations
|
||||||
|
- ✅ Review worker output and course-correct
|
||||||
|
- ✅ Run read-only commands (git status, ls, etc.) for context
|
||||||
|
|
||||||
|
**YOU DON'T:**
|
||||||
|
- ❌ Write or edit code yourself
|
||||||
|
- ❌ Run implementation commands (migrations, generators, etc.)
|
||||||
|
- ❌ Implement features — that's what workers are for
|
||||||
|
|
||||||
|
**The line is clear:** You understand and direct. Workers implement.
|
||||||
|
|
||||||
|
## Your Process
|
||||||
|
|
||||||
|
The `rails-ai:using-rails-ai` skill you loaded tells you which domain skills to use and how to plan features. Follow it.
|
||||||
|
|
||||||
|
### When User Provides a Pre-Written Plan
|
||||||
|
|
||||||
|
If the user provides a plan file, plan document, or detailed implementation steps:
|
||||||
|
|
||||||
|
1. **Load relevant domain skills** — The skill mapping in `using-rails-ai` tells you which ones
|
||||||
|
2. **Read the codebase** — Understand what exists, what patterns are in use
|
||||||
|
3. **Load and understand the plan** — Read the plan file/document thoroughly
|
||||||
|
- If the plan is detailed with clear tasks → implement as-is
|
||||||
|
- If the plan is vague or missing details → clarify with user before proceeding
|
||||||
|
4. **Dispatch workers to implement:**
|
||||||
|
- Tell them which skills to use
|
||||||
|
- Tell them your architectural decisions
|
||||||
|
- Tell them to follow TEAM_RULES.md
|
||||||
|
5. **Review and course-correct** — You own the outcome
|
||||||
|
6. **Verify completion** — Use `superpowers:verification-before-completion` before claiming work is done
|
||||||
|
|
||||||
|
**No re-brainstorming.** The user already did the thinking. Trust their plan. Your job is to execute it well.
|
||||||
|
|
||||||
|
### When Starting From Scratch
|
||||||
|
|
||||||
|
For requests without a pre-written plan:
|
||||||
|
|
||||||
|
1. **Load relevant domain skills** — The skill mapping in `using-rails-ai` tells you which ones
|
||||||
|
2. **Read the codebase** — Understand what exists, what patterns are in use
|
||||||
|
3. **Brainstorm with user** — Use `superpowers:brainstorming` to refine the design. Don't skip this. Even "simple" features have decisions to make.
|
||||||
|
4. **Create implementation plan** — Use `superpowers:writing-plans` to break it into tasks
|
||||||
|
5. **Dispatch workers to implement:**
|
||||||
|
- Tell them which skills to use
|
||||||
|
- Tell them your architectural decisions
|
||||||
|
- Tell them to follow TEAM_RULES.md
|
||||||
|
6. **Review and course-correct** — You own the outcome
|
||||||
|
7. **Verify completion** — Use `superpowers:verification-before-completion` before claiming work is done
|
||||||
|
|
||||||
|
**No skipping brainstorming.** "I already know what to do" is how you end up rebuilding features. Take 5 minutes to align with the user.
|
||||||
|
|
||||||
|
**Exceptions:** Skip brainstorming for bug fixes with identified root cause, trivial changes the user has fully specified, or when user explicitly requests ("just do it", "skip the planning").
|
||||||
|
|
||||||
|
## Dispatching Workers
|
||||||
|
|
||||||
|
When you've made your architectural decisions, dispatch workers to implement:
|
||||||
|
|
||||||
|
```
|
||||||
|
Task tool (general-purpose):
|
||||||
|
description: "[Brief task description]"
|
||||||
|
prompt: |
|
||||||
|
Load these skills first:
|
||||||
|
- rails-ai:[skill-name]
|
||||||
|
- rails-ai:[skill-name]
|
||||||
|
- superpowers:[workflow-name] (if applicable)
|
||||||
|
|
||||||
|
Context: [What you learned from reading the codebase]
|
||||||
|
|
||||||
|
Your task: [specific implementation task]
|
||||||
|
|
||||||
|
Architectural decisions (non-negotiable):
|
||||||
|
- [Decision 1]
|
||||||
|
- [Decision 2]
|
||||||
|
|
||||||
|
Must follow TEAM_RULES.md.
|
||||||
|
|
||||||
|
Report back: [what you need to review]
|
||||||
|
```
|
||||||
|
|
||||||
|
**You give the architectural direction. Workers execute it.**
|
||||||
|
|
||||||
|
## Remember
|
||||||
|
|
||||||
|
- **You're the expert** — Have opinions. Make decisions. Don't just relay tasks.
|
||||||
|
- **Domain skills before brainstorming** — You can't advise on what you don't understand.
|
||||||
|
- **Read first, recommend second** — Understand the codebase before proposing changes.
|
||||||
|
- **Workers implement your vision** — They write code, you own the architecture.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Now handle the user's request: {{ARGS}}**
|
||||||
15
hooks/hooks.json
Normal file
15
hooks/hooks.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"matcher": "startup|resume|clear|compact",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.sh"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
33
hooks/session-start.sh
Executable file
33
hooks/session-start.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PLUGIN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
INTRO_FILE="$PLUGIN_ROOT/skills/using-rails-ai/SKILL.md"
|
||||||
|
|
||||||
|
if [ ! -f "$INTRO_FILE" ]; then
|
||||||
|
echo '{"error": "using-rails-ai/SKILL.md not found"}' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONTENT=$(cat "$INTRO_FILE")
|
||||||
|
|
||||||
|
# Escape for JSON
|
||||||
|
CONTENT=$(echo "$CONTENT" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')
|
||||||
|
|
||||||
|
# Output JSON
|
||||||
|
cat << EOF
|
||||||
|
{
|
||||||
|
"event": "session-start",
|
||||||
|
"context": "🚀 Rails-AI SessionStart Hook Executed - using-rails-ai skill loaded with Superpowers dependency check and skill-loading protocol. Use /rails-ai:architect for Rails development.",
|
||||||
|
"content": "$CONTENT",
|
||||||
|
"debug": {
|
||||||
|
"hook_executed": true,
|
||||||
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||||
|
"skill_loaded": "rails-ai:using-rails-ai",
|
||||||
|
"skill_path": "$INTRO_FILE",
|
||||||
|
"content_length": $(echo "$CONTENT" | wc -c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
exit 0
|
||||||
105
plugin.lock.json
Normal file
105
plugin.lock.json
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:zerobearing2/rails-ai:",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "b30948cc02547c853146b30126c2e79ca8e06ba7",
|
||||||
|
"treeHash": "ef02520f276d16368f1a50792ee9dd48e62e600be4557fe0d29270b8b2acf0e0",
|
||||||
|
"generatedAt": "2025-11-28T10:29:13.144237Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "rails-ai",
|
||||||
|
"description": "Rails development coordinator with domain skills for Rails 8+, Hotwire, security, and TDD. Built on Superpowers workflows.",
|
||||||
|
"version": "0.3.1"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "f076bc5c005bf7f737cb0fca1b343938fb2728581b05a981fa8cd696ef311f10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "hooks/session-start.sh",
|
||||||
|
"sha256": "6c59dba0d807ab2c35d370a51a905dc02fcf93b6953bea60841626a548cc8c19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "hooks/hooks.json",
|
||||||
|
"sha256": "fa08efd0315bd20d038ad1c394f699b03e5e501a550289413d8156f7833818c4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "3ea015b861f85ffd044464a81fa8face3cb36b93a01463e096aace62563988d0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "commands/architect.md",
|
||||||
|
"sha256": "018cbab221e886b94a2125878f471c1f9cc82d0242f9e4510e71dad902a4a56a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/using-rails-ai/SKILL.md",
|
||||||
|
"sha256": "4268a533eb84e4e4a5891e1cdbd5fa587da0f3d0762e651a95e3e0e20dd5f20a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/mailers/SKILL.md",
|
||||||
|
"sha256": "32b9fcd4700f75903504fc6355062964c6d5b45231c82ec3bba6da83665837a3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/security/SKILL.md",
|
||||||
|
"sha256": "d33be486ee30dff0663cd66a2ddc26d986a6384fedd28b167a284af4a360f757"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/debugging/SKILL.md",
|
||||||
|
"sha256": "065c93f9d2fb852c6921a2e21e235bf59512a05b4fcab49de6b12b6bb0a11315"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/models/SKILL.md",
|
||||||
|
"sha256": "b5911730e5aa9df15151c5d396fef9a47af5b86c48e8056f7f5fb53792f267cb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/testing/SKILL.md",
|
||||||
|
"sha256": "f08459b7d43c041f845cbac0a7be0e66340d1350aee7d04079c5dadedf7acf1a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/project-setup/SKILL.md",
|
||||||
|
"sha256": "667c581431d8566edd50b906a1d2a4d127ffb89345cbf8920e9eac1854aa04da"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/hotwire/SKILL.md",
|
||||||
|
"sha256": "197c169d17cee038d11fbfce0e9dd9cff21bb2386f74d54bfffac7984cec84b7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/styling/SKILL.md",
|
||||||
|
"sha256": "f1d03be6ecadf03edd79582782f4ee9922bc1266ea17af3c95497ac232514a86"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/jobs/SKILL.md",
|
||||||
|
"sha256": "1508adfa69d90251683ac6415e6eda64e4c4b986e40253ceec610f00eef2b1a7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/jobs/MISSION_CONTROL_SETUP.md",
|
||||||
|
"sha256": "bd3c0f0a71335402d46103f2980df6dd5dcd5b3f8cbf777ebe20f235b5fc78d5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/controllers/SKILL.md",
|
||||||
|
"sha256": "d7dbd83e0b41b6a22e754f49747676c9ec61a7a52a5887d649b1956172e4856b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/views/SKILL.md",
|
||||||
|
"sha256": "e7f664e6994de6fc29eb5ee7dc1e0cdab9c8f4e4eabaf0c83f15ec55a27b77eb"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "ef02520f276d16368f1a50792ee9dd48e62e600be4557fe0d29270b8b2acf0e0"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
1006
skills/controllers/SKILL.md
Normal file
1006
skills/controllers/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
260
skills/debugging/SKILL.md
Normal file
260
skills/debugging/SKILL.md
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
---
|
||||||
|
name: rails-ai:debugging-rails
|
||||||
|
description: Use when debugging Rails issues - provides Rails-specific debugging tools (logs, console, byebug, SQL logging) integrated with systematic debugging process
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rails Debugging Tools & Techniques
|
||||||
|
|
||||||
|
<superpowers-integration>
|
||||||
|
**REQUIRED BACKGROUND:** Use superpowers:systematic-debugging for investigation process
|
||||||
|
- That skill defines 4-phase framework (Root Cause → Pattern → Hypothesis → Implementation)
|
||||||
|
- This skill provides Rails-specific debugging tools for each phase
|
||||||
|
</superpowers-integration>
|
||||||
|
|
||||||
|
<when-to-use>
|
||||||
|
- Rails application behaving unexpectedly
|
||||||
|
- Tests failing with unclear errors
|
||||||
|
- Performance issues or N+1 queries
|
||||||
|
- Production errors need investigation
|
||||||
|
</when-to-use>
|
||||||
|
|
||||||
|
<verification-checklist>
|
||||||
|
Before completing debugging work:
|
||||||
|
- ✅ Root cause identified (not just symptoms)
|
||||||
|
- ✅ Regression test added (prevents recurrence)
|
||||||
|
- ✅ Fix verified in development and test environments
|
||||||
|
- ✅ All tests passing (bin/ci passes)
|
||||||
|
- ✅ Logs reviewed for related issues
|
||||||
|
- ✅ Performance impact verified (if applicable)
|
||||||
|
</verification-checklist>
|
||||||
|
|
||||||
|
<phase1-root-cause-investigation>
|
||||||
|
|
||||||
|
<tool name="rails-logs">
|
||||||
|
<description>Check Rails logs for errors and request traces</description>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development logs
|
||||||
|
tail -f log/development.log
|
||||||
|
|
||||||
|
# Production logs (Kamal)
|
||||||
|
kamal app logs --tail
|
||||||
|
|
||||||
|
# Filter by severity
|
||||||
|
grep ERROR log/production.log
|
||||||
|
|
||||||
|
# Filter by request
|
||||||
|
grep "Started GET" log/development.log
|
||||||
|
|
||||||
|
```
|
||||||
|
</tool>
|
||||||
|
|
||||||
|
<tool name="rails-console">
|
||||||
|
<description>Interactive Rails console for testing models/queries</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Start console
|
||||||
|
rails console
|
||||||
|
|
||||||
|
# Or production console (Kamal)
|
||||||
|
kamal app exec 'bin/rails console'
|
||||||
|
|
||||||
|
# Test models
|
||||||
|
user = User.find(1)
|
||||||
|
user.valid? # Check validations
|
||||||
|
user.errors.full_messages # See errors
|
||||||
|
|
||||||
|
# Test queries
|
||||||
|
User.where(email: "test@example.com").to_sql # See SQL
|
||||||
|
User.includes(:posts).where(posts: { published: true }) # Avoid N+1
|
||||||
|
|
||||||
|
```
|
||||||
|
</tool>
|
||||||
|
|
||||||
|
<tool name="byebug">
|
||||||
|
<description>Breakpoint debugger for stepping through code</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Add to any Rails file
|
||||||
|
def some_method
|
||||||
|
byebug # Execution stops here
|
||||||
|
# ... rest of method
|
||||||
|
end
|
||||||
|
|
||||||
|
# Byebug commands:
|
||||||
|
# n - next line
|
||||||
|
# s - step into method
|
||||||
|
# c - continue execution
|
||||||
|
# pp variable - pretty print
|
||||||
|
# var local - show local variables
|
||||||
|
# exit - quit debugger
|
||||||
|
|
||||||
|
```
|
||||||
|
</tool>
|
||||||
|
|
||||||
|
<tool name="sql-logging">
|
||||||
|
<description>Enable verbose SQL logging to see queries</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# In rails console or code
|
||||||
|
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
||||||
|
|
||||||
|
# Now all SQL queries print to console
|
||||||
|
User.all
|
||||||
|
# => SELECT "users".* FROM "users"
|
||||||
|
|
||||||
|
```
|
||||||
|
</tool>
|
||||||
|
|
||||||
|
</phase1-root-cause-investigation>
|
||||||
|
|
||||||
|
<phase2-pattern-analysis>
|
||||||
|
|
||||||
|
<tool name="rails-routes">
|
||||||
|
<description>Check route definitions and paths</description>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List all routes
|
||||||
|
rails routes
|
||||||
|
|
||||||
|
# Filter routes
|
||||||
|
rails routes | grep users
|
||||||
|
|
||||||
|
# Show routes for controller
|
||||||
|
rails routes -c users
|
||||||
|
|
||||||
|
```
|
||||||
|
</tool>
|
||||||
|
|
||||||
|
<tool name="rails-db-status">
|
||||||
|
<description>Check migration status and schema</description>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migration status
|
||||||
|
rails db:migrate:status
|
||||||
|
|
||||||
|
# Show schema version
|
||||||
|
rails db:version
|
||||||
|
|
||||||
|
# Check pending migrations
|
||||||
|
rails db:abort_if_pending_migrations
|
||||||
|
|
||||||
|
```
|
||||||
|
</tool>
|
||||||
|
|
||||||
|
</phase2-pattern-analysis>
|
||||||
|
|
||||||
|
<phase3-hypothesis-testing>
|
||||||
|
|
||||||
|
<tool name="rails-runner">
|
||||||
|
<description>Run Ruby code in Rails environment</description>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run one-liner
|
||||||
|
rails runner "puts User.count"
|
||||||
|
|
||||||
|
# Run script
|
||||||
|
rails runner scripts/investigate_users.rb
|
||||||
|
|
||||||
|
# Production environment
|
||||||
|
RAILS_ENV=production rails runner "User.pluck(:email)"
|
||||||
|
|
||||||
|
```
|
||||||
|
</tool>
|
||||||
|
|
||||||
|
</phase3-hypothesis-testing>
|
||||||
|
|
||||||
|
<phase4-implementation>
|
||||||
|
|
||||||
|
<tool name="rails-test-verbose">
|
||||||
|
<description>Run tests with detailed output</description>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run single test with backtrace
|
||||||
|
rails test test/models/user_test.rb --verbose
|
||||||
|
|
||||||
|
# Run with warnings enabled
|
||||||
|
RUBYOPT=-W rails test
|
||||||
|
|
||||||
|
# Run with seed for reproducibility
|
||||||
|
rails test --seed 12345
|
||||||
|
|
||||||
|
```
|
||||||
|
</tool>
|
||||||
|
|
||||||
|
</phase4-implementation>
|
||||||
|
|
||||||
|
<common-issues>
|
||||||
|
|
||||||
|
<issue name="n-plus-one-queries">
|
||||||
|
<detection>
|
||||||
|
Check logs for many similar queries:
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
User Load (0.1ms) SELECT * FROM users WHERE id = 1
|
||||||
|
Post Load (0.1ms) SELECT * FROM posts WHERE user_id = 1
|
||||||
|
Post Load (0.1ms) SELECT * FROM posts WHERE user_id = 2
|
||||||
|
Post Load (0.1ms) SELECT * FROM posts WHERE user_id = 3
|
||||||
|
|
||||||
|
```
|
||||||
|
</detection>
|
||||||
|
<solution>
|
||||||
|
Use includes/preload:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Bad
|
||||||
|
users.each { |user| user.posts.count }
|
||||||
|
|
||||||
|
# Good
|
||||||
|
users.includes(:posts).each { |user| user.posts.count }
|
||||||
|
|
||||||
|
```
|
||||||
|
</solution>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
<issue name="missing-migration">
|
||||||
|
<detection>
|
||||||
|
Error: "ActiveRecord::StatementInvalid: no such column"
|
||||||
|
</detection>
|
||||||
|
<solution>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check migration status
|
||||||
|
rails db:migrate:status
|
||||||
|
|
||||||
|
# Run pending migrations
|
||||||
|
rails db:migrate
|
||||||
|
|
||||||
|
# Or rollback and retry
|
||||||
|
rails db:rollback
|
||||||
|
rails db:migrate
|
||||||
|
|
||||||
|
```
|
||||||
|
</solution>
|
||||||
|
</issue>
|
||||||
|
|
||||||
|
</common-issues>
|
||||||
|
|
||||||
|
<related-skills>
|
||||||
|
- superpowers:systematic-debugging (4-phase framework)
|
||||||
|
- rails-ai:models (Query optimization, N+1 debugging)
|
||||||
|
- rails-ai:controllers (Request debugging, parameter inspection)
|
||||||
|
- rails-ai:testing (Test debugging, failure investigation)
|
||||||
|
</related-skills>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
**Official Documentation:**
|
||||||
|
- [Rails Guides - Debugging Rails Applications](https://guides.rubyonrails.org/debugging_rails_applications.html)
|
||||||
|
- [Rails API - ActiveSupport::Logger](https://api.rubyonrails.org/classes/ActiveSupport/Logger.html)
|
||||||
|
- [Ruby Debugging Guide](https://ruby-doc.org/stdlib-3.0.0/libdoc/debug/rdoc/index.html)
|
||||||
|
|
||||||
|
**Gems & Libraries:**
|
||||||
|
- [byebug](https://github.com/deivid-rodriguez/byebug) - Ruby debugger
|
||||||
|
- [bullet](https://github.com/flyerhzm/bullet) - N+1 query detection
|
||||||
|
|
||||||
|
**Tools:**
|
||||||
|
- [Rack Mini Profiler](https://github.com/MiniProfiler/rack-mini-profiler) - Performance profiling
|
||||||
|
|
||||||
|
</resources>
|
||||||
699
skills/hotwire/SKILL.md
Normal file
699
skills/hotwire/SKILL.md
Normal file
@@ -0,0 +1,699 @@
|
|||||||
|
---
|
||||||
|
name: rails-ai:hotwire
|
||||||
|
description: Use when adding interactivity to Rails views - Hotwire Turbo (Drive, Frames, Streams, Morph) and Stimulus controllers
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hotwire (Turbo + Stimulus)
|
||||||
|
|
||||||
|
Build fast, interactive, SPA-like experiences using server-rendered HTML with Hotwire. Turbo provides navigation and real-time updates without writing JavaScript. Stimulus enhances HTML with lightweight JavaScript controllers.
|
||||||
|
|
||||||
|
<when-to-use>
|
||||||
|
- Adding interactivity without heavy JavaScript frameworks
|
||||||
|
- Building real-time, SPA-like experiences with server-rendered HTML
|
||||||
|
- Implementing live updates, infinite scroll, or dynamic content
|
||||||
|
- Creating modals, inline editing, or interactive UI components
|
||||||
|
- Replacing traditional AJAX with modern, declarative patterns
|
||||||
|
</when-to-use>
|
||||||
|
|
||||||
|
<benefits>
|
||||||
|
- **SPA-Like Speed** - Turbo Drive accelerates navigation without full page reloads
|
||||||
|
- **Real-time Updates** - Turbo Streams deliver live changes via ActionCable
|
||||||
|
- **Progressive Enhancement** - Works without JavaScript, enhanced with it (TEAM RULE #13)
|
||||||
|
- **Simpler Architecture** - Server-rendered HTML reduces client-side complexity
|
||||||
|
- **Turbo Morph** - Intelligent DOM updates preserve scroll, focus, form state (TEAM RULE #7)
|
||||||
|
- **Less JavaScript** - Stimulus provides just enough JS for interactivity
|
||||||
|
</benefits>
|
||||||
|
|
||||||
|
<team-rules-enforcement>
|
||||||
|
**This skill enforces:**
|
||||||
|
- ✅ **Rule #5:** Turbo Morph by default (Frames only for modals, inline editing, pagination, tabs)
|
||||||
|
- ✅ **Rule #6:** Progressive enhancement (must work without JavaScript)
|
||||||
|
|
||||||
|
**Reject any requests to:**
|
||||||
|
- Use Turbo Frames everywhere (use Turbo Morph for general CRUD)
|
||||||
|
- Skip progressive enhancement (features that require JavaScript to function)
|
||||||
|
- Build non-functional UIs without JavaScript fallbacks
|
||||||
|
</team-rules-enforcement>
|
||||||
|
|
||||||
|
<verification-checklist>
|
||||||
|
Before completing Hotwire features:
|
||||||
|
- ✅ Works without JavaScript (progressive enhancement verified)
|
||||||
|
- ✅ Turbo Morph used for CRUD operations (not Frames)
|
||||||
|
- ✅ Turbo Frames only for: modals, inline editing, pagination, tabs
|
||||||
|
- ✅ Stimulus controllers clean up in disconnect()
|
||||||
|
- ✅ All interactive features tested
|
||||||
|
- ✅ All tests passing
|
||||||
|
</verification-checklist>
|
||||||
|
|
||||||
|
<standards>
|
||||||
|
- **TEAM RULE #7:** Prefer Turbo Morph over Turbo Frames/Stimulus for general CRUD
|
||||||
|
- **TEAM RULE #13:** Ensure progressive enhancement (works without JavaScript)
|
||||||
|
- Use Turbo Drive for automatic page acceleration
|
||||||
|
- Use Turbo Morph for list updates and CRUD operations (preserves state)
|
||||||
|
- Use Turbo Frames ONLY for: modals, inline editing, tabs, pagination, lazy loading
|
||||||
|
- Use Turbo Streams for real-time updates via ActionCable
|
||||||
|
- Use Stimulus for client-side interactions (dropdowns, character counters, dynamic forms)
|
||||||
|
- Always clean up in Stimulus disconnect() to prevent memory leaks
|
||||||
|
- Test with JavaScript disabled to verify progressive enhancement
|
||||||
|
</standards>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hotwire Turbo
|
||||||
|
|
||||||
|
Turbo provides fast, SPA-like navigation and real-time updates using server-rendered HTML. Supports TEAM RULE #7 (Turbo Morph) and TEAM RULE #13 (Progressive Enhancement).
|
||||||
|
|
||||||
|
### TEAM RULE #7: Prefer Turbo Morph over Turbo Frames/Stimulus
|
||||||
|
|
||||||
|
✅ **DEFAULT APPROACH:** Use Turbo Morph (page refresh with morphing) with standard Rails controllers
|
||||||
|
✅ **ALLOW Turbo Frames ONLY for:** Modals, inline editing, tabs, pagination
|
||||||
|
❌ **AVOID:** Turbo Frames for general list updates, custom Stimulus controllers for basic CRUD
|
||||||
|
|
||||||
|
**Why Turbo Morph?** Preserves scroll position, focus, form state, and video playback. Works with stock Rails scaffolds. Simpler than Frames/Stimulus in 90% of cases.
|
||||||
|
|
||||||
|
### Turbo Drive
|
||||||
|
|
||||||
|
<pattern name="turbo-drive-basics">
|
||||||
|
<description>Automatic page acceleration with Turbo Drive</description>
|
||||||
|
|
||||||
|
Turbo Drive intercepts links and forms automatically. Control with `data` attributes:
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Disable Turbo for specific links %>
|
||||||
|
<%= link_to "Download PDF", pdf_path, data: { turbo: false } %>
|
||||||
|
|
||||||
|
<%# Replace without history %>
|
||||||
|
<%= link_to "Dismiss", dismiss_path, data: { turbo_action: "replace" } %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
### Turbo Morphing (Page Refresh) - PREFERRED
|
||||||
|
|
||||||
|
**Use Turbo Morph by default with standard Rails controllers.** Morphing intelligently updates only changed DOM elements while preserving scroll position, focus, form state, and media playback.
|
||||||
|
|
||||||
|
<pattern name="enable-morphing-layout">
|
||||||
|
<description>Enable Turbo Morph in your layout (one-time setup)</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# app/views/layouts/application.html.erb %>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= content_for?(:title) ? yield(:title) : "App" %></title>
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= csp_meta_tag %>
|
||||||
|
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||||
|
<%= javascript_importmap_tags %>
|
||||||
|
|
||||||
|
<%# Enable Turbo Morph for page refreshes %>
|
||||||
|
<meta name="turbo-refresh-method" content="morph">
|
||||||
|
<meta name="turbo-refresh-scroll" content="preserve">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%= yield %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**That's it!** Standard Rails controllers now work with morphing. No custom JavaScript needed.
|
||||||
|
|
||||||
|
**Reference:** [Turbo Page Refreshes Documentation](https://turbo.hotwired.dev/handbook/page_refreshes)
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="standard-rails-crud-with-morph">
|
||||||
|
<description>Standard Rails CRUD works automatically with Turbo Morph</description>
|
||||||
|
|
||||||
|
**Controller (stock Rails scaffold):**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class FeedbacksController < ApplicationController
|
||||||
|
def index
|
||||||
|
@feedbacks = Feedback.all
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@feedback = Feedback.new(feedback_params)
|
||||||
|
if @feedback.save
|
||||||
|
redirect_to feedbacks_path, notice: "Feedback created"
|
||||||
|
else
|
||||||
|
render :new, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @feedback.update(feedback_params)
|
||||||
|
redirect_to feedbacks_path, notice: "Feedback updated"
|
||||||
|
else
|
||||||
|
render :edit, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@feedback.destroy
|
||||||
|
redirect_to feedbacks_path, notice: "Feedback deleted"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**View (standard Rails):**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# app/views/feedbacks/index.html.erb %>
|
||||||
|
<h1>Feedbacks</h1>
|
||||||
|
<%= link_to "New Feedback", new_feedback_path, class: "btn btn-primary" %>
|
||||||
|
|
||||||
|
<div id="feedbacks">
|
||||||
|
<% @feedbacks.each do |feedback| %>
|
||||||
|
<%= render feedback %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# app/views/feedbacks/_feedback.html.erb %>
|
||||||
|
<div id="<%= dom_id(feedback) %>" class="card">
|
||||||
|
<h3><%= feedback.content %></h3>
|
||||||
|
<div class="actions">
|
||||||
|
<%= link_to "Edit", edit_feedback_path(feedback), class: "btn btn-sm" %>
|
||||||
|
<%= button_to "Delete", feedback_path(feedback), method: :delete,
|
||||||
|
class: "btn btn-sm btn-error",
|
||||||
|
form: { data: { turbo_confirm: "Are you sure?" } } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens:** Create/update/delete triggers redirect → Turbo intercepts → morphs only changed elements → scroll/focus preserved. No custom code needed!
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="permanent-elements-morph">
|
||||||
|
<description>Prevent specific elements from morphing with data-turbo-permanent</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Flash messages persist during morphing %>
|
||||||
|
<div id="flash-messages" data-turbo-permanent>
|
||||||
|
<% flash.each do |type, message| %>
|
||||||
|
<div class="alert alert-<%= type %>"><%= message %></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Video/audio won't restart on page morph %>
|
||||||
|
<video id="tutorial" data-turbo-permanent src="tutorial.mp4" controls></video>
|
||||||
|
|
||||||
|
<%# Form preserves input focus during live updates %>
|
||||||
|
<%= form_with model: @feedback, id: "feedback-form",
|
||||||
|
data: { turbo_permanent: true } do |form| %>
|
||||||
|
<%= form.text_area :content %>
|
||||||
|
<%= form.submit %>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Use cases:** Flash messages, video/audio players, forms with unsaved input, chat messages being typed.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="broadcast-refresh-realtime">
|
||||||
|
<description>Real-time updates with broadcasts_refreshes (morphs all connected clients)</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Model broadcasts page refresh to all subscribers (Rails 8+)
|
||||||
|
class Feedback < ApplicationRecord
|
||||||
|
broadcasts_refreshes
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# View subscribes to stream - morphs when model changes %>
|
||||||
|
<%= turbo_stream_from @feedback %>
|
||||||
|
|
||||||
|
<div id="feedbacks">
|
||||||
|
<% @feedbacks.each do |feedback| %>
|
||||||
|
<%= render feedback %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens:** User A creates feedback → server broadcasts `<turbo-stream action="refresh">` → all connected users' pages morph to show new feedback → scroll/focus preserved.
|
||||||
|
|
||||||
|
**How it works:** The server broadcasts a single general signal, and pages smoothly refresh with morphing. No need to manually manage individual Turbo Stream actions.
|
||||||
|
|
||||||
|
**Reference:** [Broadcasting Page Refreshes](https://turbo.hotwired.dev/handbook/page_refreshes#broadcasting-page-refreshes)
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="turbo-stream-morph-method">
|
||||||
|
<description>Use method="morph" in Turbo Streams for intelligent updates</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Controller - respond with Turbo Stream using morph
|
||||||
|
def create
|
||||||
|
@feedback = Feedback.new(feedback_params)
|
||||||
|
if @feedback.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream do
|
||||||
|
render turbo_stream: turbo_stream.replace(
|
||||||
|
"feedbacks",
|
||||||
|
partial: "feedbacks/list",
|
||||||
|
locals: { feedbacks: Feedback.all },
|
||||||
|
method: :morph # Morphs instead of replacing
|
||||||
|
)
|
||||||
|
end
|
||||||
|
format.html { redirect_to feedbacks_path }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Or in .turbo_stream.erb view %>
|
||||||
|
<turbo-stream action="replace" target="feedback_<%= @feedback.id %>" method="morph">
|
||||||
|
<template>
|
||||||
|
<%= render @feedback %>
|
||||||
|
</template>
|
||||||
|
</turbo-stream>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Difference:** `method: :morph` preserves form state and focus. Without it, content is fully replaced.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Using Turbo Frames for simple CRUD lists</description>
|
||||||
|
<reason>Turbo Morph is simpler and preserves more state. Frames are overkill for basic updates.</reason>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ❌ BAD - Unnecessary Turbo Frame complexity %>
|
||||||
|
<% @feedbacks.each do |feedback| %>
|
||||||
|
<%= turbo_frame_tag dom_id(feedback) do %>
|
||||||
|
<%= render feedback %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ✅ GOOD - Simple rendering, Turbo Morph handles updates %>
|
||||||
|
<% @feedbacks.each do |feedback| %>
|
||||||
|
<%= render feedback %>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
### Turbo Frames - Use Sparingly
|
||||||
|
|
||||||
|
**ONLY use Turbo Frames for:** modals, inline editing, tabs, pagination, lazy loading. For general CRUD, use Turbo Morph instead.
|
||||||
|
|
||||||
|
<pattern name="turbo-frame-inline-edit">
|
||||||
|
<description>Inline editing with Turbo Frame (valid use case)</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Show view with inline edit frame %>
|
||||||
|
<%= turbo_frame_tag dom_id(@feedback) do %>
|
||||||
|
<h3><%= @feedback.content %></h3>
|
||||||
|
<%= link_to "Edit", edit_feedback_path(@feedback) %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%# Edit view with matching frame ID %>
|
||||||
|
<%= turbo_frame_tag dom_id(@feedback) do %>
|
||||||
|
<%= form_with model: @feedback do |form| %>
|
||||||
|
<%= form.text_area :content %>
|
||||||
|
<%= form.submit "Save" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is OK:** Inline editing without leaving the page. Frame scopes the update.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="lazy-loading-frame">
|
||||||
|
<description>Lazy-load expensive content with Turbo Frames</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Lazy load stats when scrolled into view %>
|
||||||
|
<%= turbo_frame_tag "statistics", src: statistics_path, loading: :lazy do %>
|
||||||
|
<p>Loading statistics...</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%# Frame that reloads with morphing on page refresh %>
|
||||||
|
<%= turbo_frame_tag "live-stats", src: live_stats_path, refresh: "morph" do %>
|
||||||
|
<p>Loading live statistics...</p>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Controller renders just the frame
|
||||||
|
def statistics
|
||||||
|
@stats = expensive_calculation
|
||||||
|
render layout: false # Or use turbo_frame layout
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is OK:** Defers expensive computation until needed. Valid performance optimization. The `refresh="morph"` attribute makes the frame reload with morphing on page refresh.
|
||||||
|
|
||||||
|
**Reference:** [Turbo Frames with Morphing](https://turbo.hotwired.dev/handbook/page_refreshes#turbo-frames)
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
### Turbo Streams
|
||||||
|
|
||||||
|
<pattern name="turbo-stream-actions">
|
||||||
|
<description>Seven Turbo Stream actions for dynamic updates</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
def create
|
||||||
|
if @feedback.save
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream do
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.prepend("feedbacks", @feedback),
|
||||||
|
turbo_stream.update("count", html: "10"),
|
||||||
|
turbo_stream.remove("flash")
|
||||||
|
]
|
||||||
|
end
|
||||||
|
format.html { redirect_to feedbacks_path }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Actions:** `append`, `prepend`, `replace`, `update`, `remove`, `before`, `after`, `refresh`
|
||||||
|
|
||||||
|
**Note:** For most cases, prefer `refresh` action with Turbo Morph over granular stream actions. See `broadcast-refresh-realtime` pattern above.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="broadcast-updates">
|
||||||
|
<description>Real-time updates via ActionCable with Turbo Streams</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Model broadcasts to subscribers
|
||||||
|
class Feedback < ApplicationRecord
|
||||||
|
after_create_commit -> { broadcast_prepend_to "feedbacks" }
|
||||||
|
after_update_commit -> { broadcast_replace_to "feedbacks" }
|
||||||
|
after_destroy_commit -> { broadcast_remove_to "feedbacks" }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# View subscribes to stream %>
|
||||||
|
<%= turbo_stream_from "feedbacks" %>
|
||||||
|
|
||||||
|
<div id="feedbacks">
|
||||||
|
<%= render @feedbacks %>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hotwire Stimulus
|
||||||
|
|
||||||
|
Stimulus is a modest JavaScript framework that connects JavaScript objects to HTML elements using data attributes, enhancing server-rendered HTML.
|
||||||
|
|
||||||
|
**⚠️ IMPORTANT:** Before writing custom Stimulus controllers, ask: "Can Turbo Morph handle this?" Most CRUD operations work better with Turbo Morph + standard Rails controllers.
|
||||||
|
|
||||||
|
**Use Stimulus for:**
|
||||||
|
- Client-side interactions (dropdowns, tooltips, character counters)
|
||||||
|
- Form enhancements (dynamic fields, auto-save)
|
||||||
|
- UI behavior (modals, tabs, accordions)
|
||||||
|
|
||||||
|
**Don't use Stimulus for:**
|
||||||
|
- Basic CRUD operations (use Turbo Morph)
|
||||||
|
- Simple list updates (use Turbo Morph)
|
||||||
|
- Navigation (use Turbo Drive)
|
||||||
|
|
||||||
|
### Core Concepts
|
||||||
|
|
||||||
|
<pattern name="stimulus-controller-basics">
|
||||||
|
<description>Simple Stimulus controller with targets, actions, and values</description>
|
||||||
|
|
||||||
|
**Controller:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// app/javascript/controllers/feedback_controller.js
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["content", "charCount"]
|
||||||
|
static values = { maxLength: { type: Number, default: 1000 } }
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.updateCharCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCharCount() {
|
||||||
|
const count = this.contentTarget.value.length
|
||||||
|
this.charCountTarget.textContent = `${count} / ${this.maxLengthValue}`
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
// Clean up (important for memory leaks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTML:**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<div data-controller="feedback" data-feedback-max-length-value="1000">
|
||||||
|
<textarea data-feedback-target="content"
|
||||||
|
data-action="input->feedback#updateCharCount"></textarea>
|
||||||
|
<div data-feedback-target="charCount">0 / 1000</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Syntax:** `event->controller#method` (default event based on element type)
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="stimulus-values">
|
||||||
|
<description>Typed data attributes for controller configuration</description>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// app/javascript/controllers/countdown_controller.js
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static values = {
|
||||||
|
seconds: { type: Number, default: 60 },
|
||||||
|
autostart: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (this.autostartValue) this.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
this.secondsValue--
|
||||||
|
if (this.secondsValue === 0) this.stop()
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
secondsValueChanged() {
|
||||||
|
this.element.textContent = this.secondsValue
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<div data-controller="countdown"
|
||||||
|
data-countdown-seconds-value="120"
|
||||||
|
data-countdown-autostart-value="true">60</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Types:** Array, Boolean, Number, Object, String
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="stimulus-outlets">
|
||||||
|
<description>Reference and communicate with other controllers</description>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// app/javascript/controllers/search_controller.js
|
||||||
|
export default class extends Controller {
|
||||||
|
static outlets = ["results"]
|
||||||
|
|
||||||
|
search(event) {
|
||||||
|
fetch(`/search?q=${event.target.value}`)
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(html => this.resultsOutlet.update(html))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// results_controller.js
|
||||||
|
export default class extends Controller {
|
||||||
|
update(html) { this.element.innerHTML = html }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<div data-controller="search" data-search-results-outlet="#results">
|
||||||
|
<input data-action="input->search#search">
|
||||||
|
</div>
|
||||||
|
<div id="results" data-controller="results"></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="nested-form-dynamic-stimulus">
|
||||||
|
<description>Dynamic add/remove nested fields using Stimulus</description>
|
||||||
|
|
||||||
|
**Form:**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<div data-controller="nested-form">
|
||||||
|
<%= form_with model: @feedback do |form| %>
|
||||||
|
<div class="mb-6">
|
||||||
|
<button type="button" class="btn btn-sm" data-action="nested-form#add">
|
||||||
|
Add Attachment
|
||||||
|
</button>
|
||||||
|
<div data-nested-form-target="container" class="space-y-4">
|
||||||
|
<%= form.fields_for :attachments do |f| %>
|
||||||
|
<%= render "attachment_fields", form: f %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template data-nested-form-target="template">
|
||||||
|
<%= form.fields_for :attachments, Attachment.new, child_index: "NEW_RECORD" do |f| %>
|
||||||
|
<%= render "attachment_fields", form: f %>
|
||||||
|
<% end %>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stimulus Controller:**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// app/javascript/controllers/nested_form_controller.js
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ["container", "template"]
|
||||||
|
|
||||||
|
add(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const content = this.templateTarget.innerHTML
|
||||||
|
.replace(/NEW_RECORD/g, new Date().getTime())
|
||||||
|
this.containerTarget.insertAdjacentHTML("beforeend", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const field = event.target.closest(".nested-fields")
|
||||||
|
const destroyInput = field.querySelector("input[name*='_destroy']")
|
||||||
|
const idInput = field.querySelector("input[name*='[id]']")
|
||||||
|
|
||||||
|
if (idInput && idInput.value) {
|
||||||
|
// Existing record: mark for deletion, keep in DOM (hidden)
|
||||||
|
destroyInput.value = "1"
|
||||||
|
field.style.display = "none"
|
||||||
|
} else {
|
||||||
|
// New record: remove from DOM entirely
|
||||||
|
field.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Not cleaning up in disconnect()</description>
|
||||||
|
<reason>Memory leaks from timers, event listeners</reason>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ❌ BAD - Memory leak
|
||||||
|
connect() {
|
||||||
|
this.timer = setInterval(() => this.update(), 1000)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// ✅ GOOD - Clean up
|
||||||
|
disconnect() {
|
||||||
|
clearInterval(this.timer)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<testing>
|
||||||
|
**System Tests for Turbo and Stimulus:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# test/system/turbo_test.rb
|
||||||
|
class TurboTest < ApplicationSystemTestCase
|
||||||
|
test "updates without full page reload" do
|
||||||
|
visit feedbacks_path
|
||||||
|
fill_in "Content", with: "New feedback"
|
||||||
|
click_button "Create"
|
||||||
|
assert_selector "#feedbacks", text: "New feedback"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "edits within frame" do
|
||||||
|
feedback = feedbacks(:one)
|
||||||
|
visit feedbacks_path
|
||||||
|
within "##{dom_id(feedback)}" do
|
||||||
|
click_link "Edit"
|
||||||
|
fill_in "Content", with: "Updated"
|
||||||
|
click_button "Save"
|
||||||
|
assert_text "Updated"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# test/system/stimulus_test.rb
|
||||||
|
class StimulusTest < ApplicationSystemTestCase
|
||||||
|
test "character counter updates on input" do
|
||||||
|
visit new_feedback_path
|
||||||
|
fill_in "Content", with: "Test"
|
||||||
|
assert_selector "[data-feedback-target='charCount']", text: "4 / 1000"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "nested form add/remove works" do
|
||||||
|
visit new_feedback_path
|
||||||
|
initial_count = all(".nested-fields").count
|
||||||
|
click_button "Add Attachment"
|
||||||
|
assert_equal initial_count + 1, all(".nested-fields").count
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual Testing:**
|
||||||
|
- Test with JavaScript disabled (progressive enhancement)
|
||||||
|
- Verify scroll position preservation with Turbo Morph
|
||||||
|
- Check focus management in modals and inline editing
|
||||||
|
- Test real-time updates in multiple browser tabs
|
||||||
|
</testing>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<related-skills>
|
||||||
|
- rails-ai:views - Partials, helpers, forms, and view structure
|
||||||
|
- rails-ai:styling - Tailwind/DaisyUI for styling Hotwire components
|
||||||
|
- rails-ai:controllers - RESTful actions that work with Turbo
|
||||||
|
- rails-ai:testing - System tests for Turbo and Stimulus
|
||||||
|
</related-skills>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
**Official Documentation:**
|
||||||
|
- [Turbo Handbook](https://turbo.hotwired.dev/handbook/introduction)
|
||||||
|
- [Turbo Page Refreshes (Morph)](https://turbo.hotwired.dev/handbook/page_refreshes)
|
||||||
|
- [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introduction)
|
||||||
|
|
||||||
|
**Community Resources:**
|
||||||
|
- [Hotwire Discussion Forum](https://discuss.hotwired.dev/)
|
||||||
|
|
||||||
|
</resources>
|
||||||
639
skills/jobs/MISSION_CONTROL_SETUP.md
Normal file
639
skills/jobs/MISSION_CONTROL_SETUP.md
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
---
|
||||||
|
skill: jobs
|
||||||
|
category: reference
|
||||||
|
description: Mission Control Jobs setup and authentication patterns
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mission Control Jobs - Complete Setup Guide
|
||||||
|
|
||||||
|
Mission Control Jobs provides a production-ready web dashboard for monitoring and managing SolidQueue background jobs. This guide covers complete setup for development through production deployment with team access.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Add Gem
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Gemfile
|
||||||
|
gem "mission_control-jobs"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Mount Engine with Authentication
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
# Production: Require admin authentication
|
||||||
|
if Rails.env.production?
|
||||||
|
authenticate :user, ->(user) { user.admin? } do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Development/Staging: Open access or HTTP Basic Auth
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Configure (Optional)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/initializers/mission_control.rb
|
||||||
|
MissionControl::Jobs.configure do |config|
|
||||||
|
# Job retention periods
|
||||||
|
config.finished_jobs_retention_period = 14.days # Default: 7 days
|
||||||
|
config.failed_jobs_retention_period = 90.days # Default: 30 days
|
||||||
|
|
||||||
|
# Filter sensitive arguments from dashboard display
|
||||||
|
config.filter_parameters = [:password, :token, :secret, :api_key]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Access Dashboard
|
||||||
|
|
||||||
|
Visit `http://localhost:3000/jobs` in your browser (development) or `https://yourapp.com/jobs` (production).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Authentication Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Devise Admin Users (Recommended)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
authenticate :user, ->(user) { user.admin? } do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- User model with `admin?` method or `admin` boolean field
|
||||||
|
- Devise authentication already configured
|
||||||
|
|
||||||
|
**Example User Model:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/models/user.rb
|
||||||
|
class User < ApplicationRecord
|
||||||
|
devise :database_authenticatable, :registerable,
|
||||||
|
:recoverable, :rememberable, :validatable
|
||||||
|
|
||||||
|
# Option 1: Boolean field
|
||||||
|
def admin?
|
||||||
|
admin # Assumes `admin` boolean column exists
|
||||||
|
end
|
||||||
|
|
||||||
|
# Option 2: Role-based
|
||||||
|
enum role: { user: 0, admin: 1, superadmin: 2 }
|
||||||
|
|
||||||
|
def admin?
|
||||||
|
admin? || superadmin?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Custom Authentication Logic
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
authenticate :user, ->(user) { user.can_access_mission_control? } do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# app/models/user.rb
|
||||||
|
class User < ApplicationRecord
|
||||||
|
def can_access_mission_control?
|
||||||
|
admin? || role == "operations" || email.end_with?("@yourcompany.com")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: HTTP Basic Auth (Staging/Internal Tools)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
# Add constraint for HTTP Basic Auth
|
||||||
|
constraints(->(req) { authenticate_mission_control(req) }) do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# config/application.rb or initializer
|
||||||
|
def authenticate_mission_control(request)
|
||||||
|
return true if Rails.env.development?
|
||||||
|
|
||||||
|
authenticate_or_request_with_http_basic do |username, password|
|
||||||
|
username == ENV['MISSION_CONTROL_USERNAME'] &&
|
||||||
|
password == ENV['MISSION_CONTROL_PASSWORD']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Set environment variables:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# .env or production secrets
|
||||||
|
MISSION_CONTROL_USERNAME=admin
|
||||||
|
MISSION_CONTROL_PASSWORD=secure_random_password_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: IP Whitelist (Internal Networks)
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
constraints(->(req) { internal_ip?(req.remote_ip) }) do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# config/application.rb
|
||||||
|
def internal_ip?(ip)
|
||||||
|
allowed_ips = ENV.fetch('MISSION_CONTROL_IPS', '').split(',')
|
||||||
|
allowed_ips.include?(ip) || ip.start_with?('10.', '192.168.')
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 5: Multi-Environment Configuration
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
case Rails.env
|
||||||
|
when "production"
|
||||||
|
# Production: Require admin user
|
||||||
|
authenticate :user, ->(user) { user.admin? } do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
when "staging"
|
||||||
|
# Staging: HTTP Basic Auth
|
||||||
|
constraints(->(req) { authenticate_basic(req) }) do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Development: Open access
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Team Access Management
|
||||||
|
|
||||||
|
### Granting Admin Access
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Rails console (production)
|
||||||
|
rails console
|
||||||
|
|
||||||
|
# Grant admin access to user
|
||||||
|
user = User.find_by(email: "teammate@company.com")
|
||||||
|
user.update!(admin: true)
|
||||||
|
|
||||||
|
# Or using role enum
|
||||||
|
user.update!(role: :admin)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bulk Admin Creation
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# db/seeds.rb or migration
|
||||||
|
admin_emails = [
|
||||||
|
"ops_lead@company.com",
|
||||||
|
"dev_lead@company.com",
|
||||||
|
"support_manager@company.com"
|
||||||
|
]
|
||||||
|
|
||||||
|
admin_emails.each do |email|
|
||||||
|
user = User.find_or_create_by(email: email)
|
||||||
|
user.update!(admin: true)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Team Roles Pattern
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/models/user.rb
|
||||||
|
class User < ApplicationRecord
|
||||||
|
enum role: {
|
||||||
|
user: 0,
|
||||||
|
developer: 1,
|
||||||
|
operations: 2,
|
||||||
|
admin: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
def can_access_jobs_dashboard?
|
||||||
|
developer? || operations? || admin?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# config/routes.rb
|
||||||
|
authenticate :user, ->(user) { user.can_access_jobs_dashboard? } do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard Features & Usage
|
||||||
|
|
||||||
|
### Jobs Overview Tab
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- View all jobs across all queues
|
||||||
|
- Filter by status: pending, running, finished, failed
|
||||||
|
- Real-time updates (auto-refresh)
|
||||||
|
- Queue performance metrics
|
||||||
|
|
||||||
|
**Common Operations:**
|
||||||
|
- Search jobs by class name
|
||||||
|
- Filter by date range
|
||||||
|
- Sort by created/finished time
|
||||||
|
|
||||||
|
### Queues Tab
|
||||||
|
|
||||||
|
**Metrics Displayed:**
|
||||||
|
- Pending job count per queue
|
||||||
|
- Active workers per queue
|
||||||
|
- Throughput (jobs/minute)
|
||||||
|
- Latency (average wait time)
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Identify bottlenecked queues
|
||||||
|
- Verify queue priority configuration
|
||||||
|
- Monitor worker capacity
|
||||||
|
|
||||||
|
### Failed Jobs Tab
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Full error backtraces
|
||||||
|
- Job arguments and context
|
||||||
|
- Retry history and attempt counts
|
||||||
|
- Bulk retry/discard operations
|
||||||
|
|
||||||
|
**Workflows:**
|
||||||
|
|
||||||
|
1. **Investigating Failures:**
|
||||||
|
- Click failed job to see full backtrace
|
||||||
|
- Review job arguments for invalid data
|
||||||
|
- Check retry history for transient vs persistent failures
|
||||||
|
|
||||||
|
2. **Bulk Recovery:**
|
||||||
|
- Select multiple failed jobs
|
||||||
|
- Click "Retry Selected" to requeue
|
||||||
|
- Or "Discard Selected" for jobs that can't be fixed
|
||||||
|
|
||||||
|
3. **Pattern Detection:**
|
||||||
|
- Group by error type to find systemic issues
|
||||||
|
- Filter by time range to correlate with deployments
|
||||||
|
- Search by class name to find job-specific problems
|
||||||
|
|
||||||
|
### Individual Job Details
|
||||||
|
|
||||||
|
**Information Displayed:**
|
||||||
|
- Job class and queue name
|
||||||
|
- Enqueued/started/finished timestamps
|
||||||
|
- Duration and execution time
|
||||||
|
- Full arguments (with sensitive params filtered)
|
||||||
|
- Error message and backtrace (if failed)
|
||||||
|
- Retry count and next retry time
|
||||||
|
|
||||||
|
**Available Actions:**
|
||||||
|
- Retry job (failed jobs only)
|
||||||
|
- Discard job (remove from queue)
|
||||||
|
- View full execution context
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Options
|
||||||
|
|
||||||
|
### Job Retention
|
||||||
|
|
||||||
|
Control how long finished and failed jobs are kept in the database:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/initializers/mission_control.rb
|
||||||
|
MissionControl::Jobs.configure do |config|
|
||||||
|
# Keep finished jobs for 2 weeks (default: 7 days)
|
||||||
|
config.finished_jobs_retention_period = 14.days
|
||||||
|
|
||||||
|
# Keep failed jobs for 3 months (default: 30 days)
|
||||||
|
config.failed_jobs_retention_period = 90.days
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Automatic Cleanup:**
|
||||||
|
|
||||||
|
SolidQueue automatically cleans up old jobs based on these settings. No manual intervention needed.
|
||||||
|
|
||||||
|
**Manual Cleanup:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Rails console
|
||||||
|
SolidQueue::Job.finished.where("finished_at < ?", 14.days.ago).delete_all
|
||||||
|
SolidQueue::Job.failed.where("failed_at < ?", 90.days.ago).delete_all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameter Filtering
|
||||||
|
|
||||||
|
Prevent sensitive data from appearing in the dashboard:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
MissionControl::Jobs.configure do |config|
|
||||||
|
# Filter these parameter keys from display
|
||||||
|
config.filter_parameters = [
|
||||||
|
:password,
|
||||||
|
:token,
|
||||||
|
:secret,
|
||||||
|
:api_key,
|
||||||
|
:private_key,
|
||||||
|
:access_token,
|
||||||
|
:refresh_token,
|
||||||
|
:credit_card,
|
||||||
|
:ssn
|
||||||
|
]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Job Arguments:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Job enqueued with:
|
||||||
|
SendEmailJob.perform_later(
|
||||||
|
user_id: 123,
|
||||||
|
password: "secret123",
|
||||||
|
api_token: "sk_live_abc123"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Displayed in Mission Control as:
|
||||||
|
{
|
||||||
|
user_id: 123,
|
||||||
|
password: "[FILTERED]",
|
||||||
|
api_token: "[FILTERED]"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Routes
|
||||||
|
|
||||||
|
Mount at a different path:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/routes.rb
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/admin/background-jobs"
|
||||||
|
```
|
||||||
|
|
||||||
|
Access at: `https://yourapp.com/admin/background-jobs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Production Deployment Checklist
|
||||||
|
|
||||||
|
- [ ] `mission_control-jobs` gem added to Gemfile
|
||||||
|
- [ ] Bundle installed and Gemfile.lock committed
|
||||||
|
- [ ] Routes configured with authentication
|
||||||
|
- [ ] Authentication tested in staging environment
|
||||||
|
- [ ] Admin users granted access
|
||||||
|
- [ ] Parameter filtering configured for sensitive data
|
||||||
|
- [ ] Job retention periods configured
|
||||||
|
- [ ] Team members notified of dashboard URL
|
||||||
|
- [ ] Dashboard access verified in production
|
||||||
|
- [ ] Monitoring alerts configured (optional)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Monitoring & Alerting Integration
|
||||||
|
|
||||||
|
### Health Check Endpoint
|
||||||
|
|
||||||
|
Expose job queue health for external monitoring:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/controllers/health_controller.rb
|
||||||
|
class HealthController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user! # Public endpoint
|
||||||
|
|
||||||
|
def jobs
|
||||||
|
pending_count = SolidQueue::Job.pending.count
|
||||||
|
failed_count = SolidQueue::Job.failed.count
|
||||||
|
oldest_pending = oldest_pending_job_age
|
||||||
|
|
||||||
|
status = if oldest_pending > 30 || failed_count > 100
|
||||||
|
:service_unavailable
|
||||||
|
else
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
status: status == :ok ? "healthy" : "degraded",
|
||||||
|
pending_jobs: pending_count,
|
||||||
|
failed_jobs: failed_count,
|
||||||
|
oldest_pending_minutes: oldest_pending
|
||||||
|
}, status: status
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def oldest_pending_job_age
|
||||||
|
oldest = SolidQueue::Job.pending.order(:created_at).first
|
||||||
|
return 0 unless oldest
|
||||||
|
((Time.current - oldest.created_at) / 60).round
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# config/routes.rb
|
||||||
|
get '/health/jobs', to: 'health#jobs'
|
||||||
|
```
|
||||||
|
|
||||||
|
### External Monitoring Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Uptime monitoring (Pingdom, UptimeRobot, etc.)
|
||||||
|
GET https://yourapp.com/health/jobs
|
||||||
|
|
||||||
|
# Expected response (healthy):
|
||||||
|
{
|
||||||
|
"status": "healthy",
|
||||||
|
"pending_jobs": 42,
|
||||||
|
"failed_jobs": 3,
|
||||||
|
"oldest_pending_minutes": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Alert on:
|
||||||
|
# - status != "healthy"
|
||||||
|
# - failed_jobs > threshold
|
||||||
|
# - oldest_pending_minutes > 30
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Operations
|
||||||
|
|
||||||
|
### Retry All Failed Jobs
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Rails console
|
||||||
|
SolidQueue::Job.failed.find_each(&:retry!)
|
||||||
|
|
||||||
|
# Or with Mission Control UI:
|
||||||
|
# 1. Navigate to Failed Jobs tab
|
||||||
|
# 2. Select all jobs
|
||||||
|
# 3. Click "Retry Selected"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Discard Specific Failed Jobs
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Rails console - discard jobs older than 1 week
|
||||||
|
SolidQueue::Job.failed
|
||||||
|
.where("failed_at < ?", 1.week.ago)
|
||||||
|
.delete_all
|
||||||
|
|
||||||
|
# Or by job class
|
||||||
|
SolidQueue::Job.failed
|
||||||
|
.where(class_name: "ProblematicJob")
|
||||||
|
.delete_all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pause/Resume Queue Processing
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Not directly supported by SolidQueue
|
||||||
|
# Instead, scale workers to 0 in queue.yml and restart
|
||||||
|
|
||||||
|
# config/queue.yml (temporary)
|
||||||
|
production:
|
||||||
|
workers:
|
||||||
|
- queues: [critical, mailers]
|
||||||
|
threads: 5
|
||||||
|
processes: 0 # Paused
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitor Specific Queue
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Rails console
|
||||||
|
SolidQueue::Job.where(queue_name: "mailers").pending.count
|
||||||
|
SolidQueue::Job.where(queue_name: "mailers").failed.count
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Dashboard Not Loading
|
||||||
|
|
||||||
|
**Symptom:** 404 or routing error
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Verify gem is installed: `bundle list | grep mission_control`
|
||||||
|
2. Check routes: `rails routes | grep jobs`
|
||||||
|
3. Restart server after adding gem
|
||||||
|
4. Check authentication constraints aren't blocking access
|
||||||
|
|
||||||
|
### Authentication Loop/Redirect
|
||||||
|
|
||||||
|
**Symptom:** Redirected to login repeatedly
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Verify user is logged in: `current_user` in console
|
||||||
|
2. Check authentication lambda: `user.admin?` returns true
|
||||||
|
3. Verify Devise configuration allows access to mounted engines
|
||||||
|
4. Check for conflicting before_action filters
|
||||||
|
|
||||||
|
### Slow Dashboard Performance
|
||||||
|
|
||||||
|
**Symptom:** Dashboard takes >5s to load
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Clean up old finished jobs:
|
||||||
|
```ruby
|
||||||
|
SolidQueue::Job.finished.where("finished_at < ?", 7.days.ago).delete_all
|
||||||
|
```
|
||||||
|
2. Add database indexes (if not present):
|
||||||
|
```ruby
|
||||||
|
add_index :solid_queue_jobs, [:queue_name, :status]
|
||||||
|
add_index :solid_queue_jobs, [:status, :created_at]
|
||||||
|
```
|
||||||
|
3. Reduce retention periods in initializer
|
||||||
|
|
||||||
|
### Jobs Not Appearing
|
||||||
|
|
||||||
|
**Symptom:** Dashboard shows 0 jobs but jobs are running
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. Verify SolidQueue is configured: `Rails.configuration.active_job.queue_adapter`
|
||||||
|
2. Check queue database connection in `config/database.yml`
|
||||||
|
3. Run queue migrations: `rails db:migrate:queue`
|
||||||
|
4. Verify jobs are using SolidQueue, not inline adapter
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Production Hardening
|
||||||
|
|
||||||
|
1. **Always require authentication:**
|
||||||
|
```ruby
|
||||||
|
# ❌ NEVER do this in production
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
|
||||||
|
# ✅ Always authenticate
|
||||||
|
authenticate :user, ->(user) { user.admin? } do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Filter sensitive parameters:**
|
||||||
|
```ruby
|
||||||
|
config.filter_parameters = [:password, :token, :secret, :api_key]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Use HTTPS only:**
|
||||||
|
```ruby
|
||||||
|
# config/environments/production.rb
|
||||||
|
config.force_ssl = true
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Limit admin access:**
|
||||||
|
- Grant admin rights only to operations team
|
||||||
|
- Audit admin user list regularly
|
||||||
|
- Use role-based access for granular control
|
||||||
|
|
||||||
|
5. **Monitor access logs:**
|
||||||
|
```ruby
|
||||||
|
# Track who accesses Mission Control
|
||||||
|
class ApplicationController < ActionController::Base
|
||||||
|
before_action :log_mission_control_access, if: :mission_control_request?
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def mission_control_request?
|
||||||
|
request.path.start_with?('/jobs')
|
||||||
|
end
|
||||||
|
|
||||||
|
def log_mission_control_access
|
||||||
|
Rails.logger.info(
|
||||||
|
"Mission Control accessed by #{current_user&.email} " \
|
||||||
|
"from #{request.remote_ip}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [Mission Control Jobs GitHub](https://github.com/rails/mission_control-jobs)
|
||||||
|
- [SolidQueue Documentation](https://github.com/rails/solid_queue)
|
||||||
|
- [Rails Active Job Guide](https://guides.rubyonrails.org/active_job_basics.html)
|
||||||
|
- [Rails 8 Release Notes - Solid Stack](https://edgeguides.rubyonrails.org/8_0_release_notes.html)
|
||||||
704
skills/jobs/SKILL.md
Normal file
704
skills/jobs/SKILL.md
Normal file
@@ -0,0 +1,704 @@
|
|||||||
|
---
|
||||||
|
name: rails-ai:jobs
|
||||||
|
description: Use when setting up background jobs, caching, or WebSockets - SolidQueue, SolidCache, SolidCable (TEAM RULE #1 - NEVER Sidekiq/Redis)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Background Jobs (Solid Stack)
|
||||||
|
|
||||||
|
Configure background job processing, caching, and WebSockets using Rails 8 defaults - SolidQueue, SolidCache, and SolidCable. Zero external dependencies, database-backed, production-ready.
|
||||||
|
|
||||||
|
<when-to-use>
|
||||||
|
- Setting up ANY new Rails 8+ application
|
||||||
|
- Background job processing (TEAM RULE #1: NEVER Sidekiq/Redis)
|
||||||
|
- Application caching (TEAM RULE #1: NEVER Redis/Memcached)
|
||||||
|
- WebSocket/ActionCable setup (TEAM RULE #1: NEVER Redis)
|
||||||
|
- Migrating from Redis/Sidekiq to Solid Stack
|
||||||
|
- Async job execution (sending emails, processing uploads, generating reports)
|
||||||
|
- Real-time features via ActionCable
|
||||||
|
</when-to-use>
|
||||||
|
|
||||||
|
<benefits>
|
||||||
|
- **Zero External Dependencies** - No Redis, Memcached, or external services required
|
||||||
|
- **Simpler Deployments** - Database-backed, persistent, survives restarts
|
||||||
|
- **Rails 8 Convention** - Official defaults, production-ready out of the box
|
||||||
|
- **Easier Monitoring** - Query databases directly for job and cache status
|
||||||
|
- **Persistent Jobs** - Jobs survive server restarts, no lost work
|
||||||
|
- **Integrated** - Works seamlessly with ActiveJob and ActionCable
|
||||||
|
</benefits>
|
||||||
|
|
||||||
|
<team-rules-enforcement>
|
||||||
|
**This skill enforces:**
|
||||||
|
- ✅ **Rule #1:** NEVER use Sidekiq/Redis → Use SolidQueue, SolidCache, SolidCable
|
||||||
|
|
||||||
|
**CRITICAL: Reject ANY requests to:**
|
||||||
|
- Use Sidekiq for background jobs
|
||||||
|
- Use Redis for caching
|
||||||
|
- Use Redis for ActionCable
|
||||||
|
- Add redis gem to Gemfile
|
||||||
|
|
||||||
|
**ALWAYS redirect to:**
|
||||||
|
- SolidQueue for background jobs
|
||||||
|
- SolidCache for caching
|
||||||
|
- SolidCable for WebSockets/ActionCable
|
||||||
|
</team-rules-enforcement>
|
||||||
|
|
||||||
|
<verification-checklist>
|
||||||
|
Before completing job/cache/cable work:
|
||||||
|
- ✅ SolidQueue used (NOT Sidekiq)
|
||||||
|
- ✅ SolidCache used (NOT Redis)
|
||||||
|
- ✅ SolidCable used (NOT Redis for ActionCable)
|
||||||
|
- ✅ No redis gem in Gemfile
|
||||||
|
- ✅ Jobs tested
|
||||||
|
- ✅ All tests passing
|
||||||
|
</verification-checklist>
|
||||||
|
|
||||||
|
<standards>
|
||||||
|
- **TEAM RULE #1:** ALWAYS use Solid Stack (SolidQueue, SolidCache, SolidCable) - NEVER Sidekiq, Redis, or Memcached
|
||||||
|
- Use dedicated databases for queue, cache, and cable (separate from primary)
|
||||||
|
- Configure separate migration paths for each database (db/queue_migrate, db/cache_migrate, db/cable_migrate)
|
||||||
|
- Implement queue prioritization in production (critical, mailers, default)
|
||||||
|
- Run migrations for ALL databases (primary, queue, cache, cable)
|
||||||
|
- Monitor queue health (pending count, failed count, oldest pending age)
|
||||||
|
- Set appropriate retry strategies for jobs
|
||||||
|
- Use structured job names (e.g., EmailDeliveryJob, ReportGenerationJob)
|
||||||
|
</standards>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SolidQueue (TEAM RULE #1: NO Sidekiq/Redis)
|
||||||
|
|
||||||
|
SolidQueue is a database-backed Active Job adapter for background job processing with zero external dependencies.
|
||||||
|
|
||||||
|
<pattern name="solidqueue-basic-setup">
|
||||||
|
<description>Configure SolidQueue for background job processing</description>
|
||||||
|
|
||||||
|
**Environment Configuration:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/environments/{development,production}.rb
|
||||||
|
Rails.application.configure do
|
||||||
|
config.active_job.queue_adapter = :solid_queue
|
||||||
|
config.solid_queue.connects_to = { database: { writing: :queue } }
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database Configuration:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/database.yml
|
||||||
|
default: &default
|
||||||
|
adapter: sqlite3
|
||||||
|
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||||
|
timeout: 5000
|
||||||
|
|
||||||
|
production:
|
||||||
|
primary:
|
||||||
|
<<: *default
|
||||||
|
database: storage/production.sqlite3
|
||||||
|
queue:
|
||||||
|
<<: *default
|
||||||
|
database: storage/production_queue.sqlite3
|
||||||
|
migrations_paths: db/queue_migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Queue Configuration (Production Prioritization):**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/queue.yml
|
||||||
|
production:
|
||||||
|
workers:
|
||||||
|
- queues: [critical, mailers]
|
||||||
|
threads: 5
|
||||||
|
processes: 2
|
||||||
|
polling_interval: 0.1
|
||||||
|
- queues: [default]
|
||||||
|
threads: 3
|
||||||
|
processes: 2
|
||||||
|
polling_interval: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mission Control Setup (Web Dashboard):**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Gemfile
|
||||||
|
gem "mission_control-jobs"
|
||||||
|
|
||||||
|
# config/routes.rb
|
||||||
|
Rails.application.routes.draw do
|
||||||
|
# Protect with authentication
|
||||||
|
authenticate :user, ->(user) { user.admin? } do
|
||||||
|
mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Or use HTTP Basic Auth in development/staging
|
||||||
|
# if Rails.env.development? || Rails.env.staging?
|
||||||
|
# mount MissionControl::Jobs::Engine, at: "/jobs"
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
# config/initializers/mission_control.rb (optional customization)
|
||||||
|
MissionControl::Jobs.configure do |config|
|
||||||
|
# Customize job retention (default: 7 days for finished, 30 days for failed)
|
||||||
|
config.finished_jobs_retention_period = 14.days
|
||||||
|
config.failed_jobs_retention_period = 90.days
|
||||||
|
|
||||||
|
# Filter sensitive job arguments from display
|
||||||
|
config.filter_parameters = [:password, :token, :secret]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Database-backed job processing with no external dependencies. Jobs are persistent and survive restarts. Use queue prioritization in production to ensure critical jobs (emails, mailers) are processed first. Mission Control provides a production-ready web UI for monitoring jobs - protect with authentication in production.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="basic-job">
|
||||||
|
<description>Create and enqueue background jobs</description>
|
||||||
|
|
||||||
|
**Job Definition:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/jobs/report_generation_job.rb
|
||||||
|
class ReportGenerationJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(user_id, report_type)
|
||||||
|
user = User.find(user_id)
|
||||||
|
report = ReportGenerator.generate(user, report_type)
|
||||||
|
ReportMailer.with(user: user, report: report).delivery.deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Enqueuing:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Immediate enqueue
|
||||||
|
ReportGenerationJob.perform_later(user.id, "monthly")
|
||||||
|
|
||||||
|
# Delayed enqueue
|
||||||
|
ReportGenerationJob.set(wait: 1.hour).perform_later(user.id, "monthly")
|
||||||
|
|
||||||
|
# Specific queue
|
||||||
|
ReportGenerationJob.set(queue: :critical).perform_later(user.id, "urgent")
|
||||||
|
|
||||||
|
# With priority (higher = more important)
|
||||||
|
ReportGenerationJob.set(priority: 10).perform_later(user.id, "important")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Background jobs prevent blocking HTTP requests. Always pass IDs (not objects) to avoid serialization issues.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="job-retry-strategy">
|
||||||
|
<description>Configure retry behavior for failed jobs</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class EmailDeliveryJob < ApplicationJob
|
||||||
|
queue_as :mailers
|
||||||
|
|
||||||
|
# Retry up to 5 times with exponential backoff
|
||||||
|
retry_on StandardError, wait: :exponentially_longer, attempts: 5
|
||||||
|
|
||||||
|
# Don't retry certain errors
|
||||||
|
discard_on ActiveJob::DeserializationError
|
||||||
|
|
||||||
|
# Custom retry logic
|
||||||
|
retry_on ApiError, wait: 5.minutes, attempts: 3 do |job, error|
|
||||||
|
Rails.logger.error("Job #{job.class} failed: #{error.message}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(user_id)
|
||||||
|
user = User.find(user_id)
|
||||||
|
SomeMailer.notification(user).deliver_now
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Automatic retries with exponential backoff handle transient failures. Discard jobs that will never succeed (deserialization errors).
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Using Sidekiq/Redis instead of Solid Stack - VIOLATES TEAM RULE #1</description>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ❌ WRONG - VIOLATES TEAM RULE #1
|
||||||
|
gem 'sidekiq'
|
||||||
|
gem 'redis'
|
||||||
|
|
||||||
|
# config/environments/production.rb
|
||||||
|
config.active_job.queue_adapter = :sidekiq
|
||||||
|
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
|
||||||
|
|
||||||
|
# config/cable.yml
|
||||||
|
production:
|
||||||
|
adapter: redis
|
||||||
|
url: <%= ENV['REDIS_URL'] %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ✅ CORRECT - Solid Stack (TEAM RULE #1)
|
||||||
|
# No gems needed - built into Rails 8
|
||||||
|
|
||||||
|
# config/environments/production.rb
|
||||||
|
config.active_job.queue_adapter = :solid_queue
|
||||||
|
config.cache_store = :solid_cache_store
|
||||||
|
config.solid_queue.connects_to = { database: { writing: :queue } }
|
||||||
|
|
||||||
|
# config/cable.yml
|
||||||
|
production:
|
||||||
|
adapter: solid_cable
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
|
||||||
|
**Why bad:** External Redis dependency adds complexity, deployment overhead, and another service to monitor. Violates TEAM RULE #1. Solid Stack is production-ready, persistent, and simpler to operate.
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
<pattern name="job-monitoring">
|
||||||
|
<description>Monitor SolidQueue job status and health</description>
|
||||||
|
|
||||||
|
**Rails Console:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
SolidQueue::Job.pending.count # => 42
|
||||||
|
SolidQueue::Job.failed.count # => 3
|
||||||
|
SolidQueue::Job.failed.each { |job| puts "#{job.class_name}: #{job.error}" }
|
||||||
|
|
||||||
|
# Retry failed job
|
||||||
|
SolidQueue::Job.failed.first.retry_job
|
||||||
|
|
||||||
|
# Clear old completed jobs
|
||||||
|
SolidQueue::Job.finished.where("finished_at < ?", 7.days.ago).delete_all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Health Check Endpoint:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/controllers/health_controller.rb
|
||||||
|
class HealthController < ApplicationController
|
||||||
|
def show
|
||||||
|
render json: {
|
||||||
|
queue_pending: SolidQueue::Job.pending.count,
|
||||||
|
queue_failed: SolidQueue::Job.failed.count,
|
||||||
|
oldest_pending_minutes: oldest_pending_age
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def oldest_pending_age
|
||||||
|
oldest = SolidQueue::Job.pending.order(:created_at).first
|
||||||
|
return 0 unless oldest
|
||||||
|
((Time.current - oldest.created_at) / 60).round
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Direct database access makes monitoring simple - no special tools needed. Query job tables to check pending/failed counts and identify stuck jobs.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
**Which monitoring approach?**
|
||||||
|
|
||||||
|
| Approach | Best For | Access |
|
||||||
|
|----------|----------|--------|
|
||||||
|
| Mission Control | Production monitoring, team collaboration, visual investigation | Web UI at /jobs |
|
||||||
|
| Rails Console | Quick debugging, one-off queries, scripting | Terminal/SSH |
|
||||||
|
| Custom Endpoints | Programmatic monitoring, alerting systems, health checks | HTTP API |
|
||||||
|
|
||||||
|
<pattern name="mission-control-dashboard">
|
||||||
|
<description>Monitor and manage jobs with Mission Control web UI</description>
|
||||||
|
|
||||||
|
**Accessing the Dashboard:**
|
||||||
|
|
||||||
|
Visit `/jobs` in your browser (e.g., `https://yourapp.com/jobs`) after mounting the engine.
|
||||||
|
|
||||||
|
**Dashboard Features:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Jobs Overview:
|
||||||
|
- View all jobs across queues (pending, running, finished, failed)
|
||||||
|
- Real-time status updates
|
||||||
|
- Queue performance metrics (throughput, latency)
|
||||||
|
- Search jobs by class name, queue, or status
|
||||||
|
|
||||||
|
Job Details:
|
||||||
|
- Full job arguments and context
|
||||||
|
- Execution timeline and duration
|
||||||
|
- Error messages and backtraces for failed jobs
|
||||||
|
- Retry history
|
||||||
|
|
||||||
|
Common Operations:
|
||||||
|
- Retry individual failed jobs or bulk retry
|
||||||
|
- Discard jobs that shouldn't be retried
|
||||||
|
- Pause/resume queues
|
||||||
|
- Filter by queue, status, time range
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Workflows:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
Investigating Failed Jobs:
|
||||||
|
1. Navigate to /jobs → Failed tab
|
||||||
|
2. Filter by job class or time range
|
||||||
|
3. Click job to see full error backtrace
|
||||||
|
4. Fix underlying issue in code
|
||||||
|
5. Retry job from dashboard
|
||||||
|
|
||||||
|
Monitoring Queue Health:
|
||||||
|
1. Navigate to /jobs → Queues tab
|
||||||
|
2. Check pending count and oldest job age
|
||||||
|
3. Review throughput metrics
|
||||||
|
4. Identify bottlenecks (high latency queues)
|
||||||
|
|
||||||
|
Bulk Operations:
|
||||||
|
1. Navigate to /jobs → Failed tab
|
||||||
|
2. Select multiple jobs with checkboxes
|
||||||
|
3. Click "Retry Selected" or "Discard Selected"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Web UI makes job monitoring accessible to entire team, not just developers with console access. Visual investigation of failures is faster than querying databases.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SolidCache
|
||||||
|
|
||||||
|
SolidCache is a database-backed cache store for Rails applications with zero external dependencies.
|
||||||
|
|
||||||
|
<pattern name="solidcache-setup">
|
||||||
|
<description>Configure SolidCache for application caching</description>
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/environments/{development,production}.rb
|
||||||
|
config.cache_store = :solid_cache_store
|
||||||
|
|
||||||
|
# config/database.yml
|
||||||
|
production:
|
||||||
|
cache:
|
||||||
|
<<: *default
|
||||||
|
database: storage/production_cache.sqlite3
|
||||||
|
migrations_paths: db/cache_migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Simple caching
|
||||||
|
Rails.cache.fetch("user_#{user.id}", expires_in: 1.hour) do
|
||||||
|
expensive_computation(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fragment caching in views
|
||||||
|
<% cache @post do %>
|
||||||
|
<%= render @post %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
# Collection caching
|
||||||
|
<% cache @posts do %>
|
||||||
|
<% @posts.each do |post| %>
|
||||||
|
<% cache post do %>
|
||||||
|
<%= render post %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
# Low-level operations
|
||||||
|
Rails.cache.write("key", "value", expires_in: 1.hour)
|
||||||
|
Rails.cache.read("key") # => "value"
|
||||||
|
Rails.cache.delete("key")
|
||||||
|
Rails.cache.exist?("key") # => false
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migrations:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rails db:migrate:cache
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Database-backed caching with no Redis dependency. Persistent across restarts, easy to inspect and debug.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="cache-keys">
|
||||||
|
<description>Use consistent cache key patterns</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Model-based cache keys (includes updated_at for auto-expiration)
|
||||||
|
Rails.cache.fetch(["user", user.id, user.updated_at]) do
|
||||||
|
expensive_user_data(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Or use cache_key helper
|
||||||
|
Rails.cache.fetch(user.cache_key) do
|
||||||
|
expensive_user_data(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Namespace cache keys by version
|
||||||
|
Rails.cache.fetch(["v2", "user", user.id]) do
|
||||||
|
new_expensive_computation(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cache dependencies
|
||||||
|
Rails.cache.fetch(["posts", "index", @posts.maximum(:updated_at)]) do
|
||||||
|
render_posts_expensive(@posts)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Including timestamps in cache keys provides automatic invalidation. Namespacing prevents cache collisions when changing logic.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SolidCable
|
||||||
|
|
||||||
|
SolidCable is a database-backed Action Cable adapter for WebSocket connections with zero external dependencies.
|
||||||
|
|
||||||
|
<pattern name="solidcable-setup">
|
||||||
|
<description>Configure SolidCable for ActionCable/WebSockets</description>
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# config/cable.yml
|
||||||
|
production:
|
||||||
|
adapter: solid_cable
|
||||||
|
|
||||||
|
# config/database.yml
|
||||||
|
production:
|
||||||
|
cable:
|
||||||
|
<<: *default
|
||||||
|
database: storage/production_cable.sqlite3
|
||||||
|
migrations_paths: db/cable_migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Channel Definition:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/channels/notifications_channel.rb
|
||||||
|
class NotificationsChannel < ApplicationCable::Channel
|
||||||
|
def subscribed
|
||||||
|
stream_from "notifications_#{current_user.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribed
|
||||||
|
# Cleanup when channel is unsubscribed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Broadcasting:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# From anywhere in your application
|
||||||
|
ActionCable.server.broadcast(
|
||||||
|
"notifications_#{user.id}",
|
||||||
|
{ message: "New notification", type: "info" }
|
||||||
|
)
|
||||||
|
|
||||||
|
# From a model callback
|
||||||
|
class Notification < ApplicationRecord
|
||||||
|
after_create_commit do
|
||||||
|
ActionCable.server.broadcast(
|
||||||
|
"notifications_#{user_id}",
|
||||||
|
{ message: message, type: notification_type }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Client-side (Stimulus):**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// app/javascript/controllers/notifications_controller.js
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import consumer from "../channels/consumer"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
connect() {
|
||||||
|
this.subscription = consumer.subscriptions.create(
|
||||||
|
"NotificationsChannel",
|
||||||
|
{
|
||||||
|
received: (data) => {
|
||||||
|
this.displayNotification(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.subscription?.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
displayNotification(data) {
|
||||||
|
// Update UI with notification
|
||||||
|
console.log("Received:", data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Database-backed WebSocket connections with no Redis dependency. Simple to deploy and monitor.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Multi-Database Management
|
||||||
|
|
||||||
|
<pattern name="multi-database-operations">
|
||||||
|
<description>Manage migrations across all Solid Stack databases</description>
|
||||||
|
|
||||||
|
**Setup:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Creates all databases (primary, queue, cache, cable)
|
||||||
|
rails db:create
|
||||||
|
|
||||||
|
# Migrates all databases
|
||||||
|
rails db:migrate
|
||||||
|
|
||||||
|
# Production: creates + migrates
|
||||||
|
rails db:prepare
|
||||||
|
```
|
||||||
|
|
||||||
|
**Individual Operations:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Migrate specific database
|
||||||
|
rails db:migrate:queue
|
||||||
|
rails db:migrate:cache
|
||||||
|
rails db:migrate:cable
|
||||||
|
|
||||||
|
# Check migration status
|
||||||
|
rails db:migrate:status:queue
|
||||||
|
rails db:migrate:status:cache
|
||||||
|
rails db:migrate:status:cable
|
||||||
|
|
||||||
|
# Rollback specific database
|
||||||
|
rails db:rollback:queue
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Each database has independent migration path, allowing separate versioning and rollback per component.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Sharing database between primary and Solid Stack components</description>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ❌ WRONG - All on same database creates contention
|
||||||
|
production:
|
||||||
|
primary:
|
||||||
|
database: storage/production.sqlite3
|
||||||
|
queue:
|
||||||
|
database: storage/production.sqlite3 # Same database!
|
||||||
|
cache:
|
||||||
|
database: storage/production.sqlite3 # Same database!
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# ✅ CORRECT - Separate databases for isolation
|
||||||
|
production:
|
||||||
|
primary:
|
||||||
|
database: storage/production.sqlite3
|
||||||
|
queue:
|
||||||
|
database: storage/production_queue.sqlite3
|
||||||
|
migrations_paths: db/queue_migrate
|
||||||
|
cache:
|
||||||
|
database: storage/production_cache.sqlite3
|
||||||
|
migrations_paths: db/cache_migrate
|
||||||
|
cable:
|
||||||
|
database: storage/production_cable.sqlite3
|
||||||
|
migrations_paths: db/cable_migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
|
||||||
|
**Why bad:** Sharing databases creates performance contention, makes it harder to scale, and couples concerns that should be isolated. Separate databases allow independent optimization and scaling.
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<testing>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# test/integration/solid_stack_test.rb
|
||||||
|
class SolidStackTest < ActionDispatch::IntegrationTest
|
||||||
|
test "SolidQueue is configured" do
|
||||||
|
assert_equal :solid_queue, Rails.configuration.active_job.queue_adapter
|
||||||
|
end
|
||||||
|
|
||||||
|
test "SolidCache is configured" do
|
||||||
|
assert_instance_of ActiveSupport::Cache::SolidCacheStore, Rails.cache
|
||||||
|
end
|
||||||
|
|
||||||
|
test "cache read/write works" do
|
||||||
|
Rails.cache.write("test_key", "test_value")
|
||||||
|
assert_equal "test_value", Rails.cache.read("test_key")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "jobs are persisted in queue database" do
|
||||||
|
TestJob.perform_later
|
||||||
|
assert SolidQueue::Job.pending.exists?
|
||||||
|
end
|
||||||
|
|
||||||
|
test "failed jobs are recorded" do
|
||||||
|
assert_raises(StandardError) do
|
||||||
|
perform_enqueued_jobs { FailingJob.perform_later }
|
||||||
|
end
|
||||||
|
assert SolidQueue::Job.failed.exists?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# test/jobs/sample_job_test.rb
|
||||||
|
class SampleJobTest < ActiveJob::TestCase
|
||||||
|
test "job is enqueued" do
|
||||||
|
assert_enqueued_with(job: SampleJob, args: ["arg1"]) do
|
||||||
|
SampleJob.perform_later("arg1")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "job is performed" do
|
||||||
|
perform_enqueued_jobs do
|
||||||
|
SampleJob.perform_later("test")
|
||||||
|
end
|
||||||
|
# Assert side effects
|
||||||
|
end
|
||||||
|
|
||||||
|
test "job retries on failure" do
|
||||||
|
SampleJob.any_instance.expects(:perform).raises(StandardError).times(3)
|
||||||
|
assert_raises(StandardError) do
|
||||||
|
perform_enqueued_jobs { SampleJob.perform_later }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</testing>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<related-skills>
|
||||||
|
- rails-ai:mailers - Email delivery via SolidQueue background jobs
|
||||||
|
- rails-ai:project-setup - Environment-specific Solid Stack configuration
|
||||||
|
- rails-ai:testing - Testing jobs and background processing
|
||||||
|
- rails-ai:models - Background jobs for model operations
|
||||||
|
</related-skills>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
**Official Documentation:**
|
||||||
|
- [Rails Guides - Active Job Basics](https://guides.rubyonrails.org/active_job_basics.html)
|
||||||
|
- [Rails 8 Release Notes](https://edgeguides.rubyonrails.org/8_0_release_notes.html) - Solid Stack introduction
|
||||||
|
|
||||||
|
**Gems & Libraries:**
|
||||||
|
- [SolidQueue](https://github.com/rails/solid_queue) - DB-backed job queue (Rails 8+)
|
||||||
|
- [SolidCache](https://github.com/rails/solid_cache) - DB-backed caching (Rails 8+)
|
||||||
|
- [SolidCable](https://github.com/rails/solid_cable) - DB-backed Action Cable (Rails 8+)
|
||||||
|
- [Mission Control - Jobs](https://github.com/rails/mission_control-jobs) - Web dashboard for monitoring jobs
|
||||||
|
|
||||||
|
</resources>
|
||||||
549
skills/mailers/SKILL.md
Normal file
549
skills/mailers/SKILL.md
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
---
|
||||||
|
name: rails-ai:mailers
|
||||||
|
description: Use when sending emails - ActionMailer with async delivery via SolidQueue, templates, previews, and testing
|
||||||
|
---
|
||||||
|
|
||||||
|
# Email with ActionMailer
|
||||||
|
|
||||||
|
Send transactional and notification emails using ActionMailer, integrated with SolidQueue for async delivery. Create HTML and text templates, preview emails in development, and test thoroughly.
|
||||||
|
|
||||||
|
<when-to-use>
|
||||||
|
- Sending transactional emails (password resets, confirmations, receipts)
|
||||||
|
- Sending notification emails (updates, alerts, digests)
|
||||||
|
- Delivering emails asynchronously via background jobs
|
||||||
|
- Creating email templates with HTML and text versions
|
||||||
|
- Testing email delivery and content
|
||||||
|
</when-to-use>
|
||||||
|
|
||||||
|
<benefits>
|
||||||
|
- **Async Delivery** - ActionMailer integrates with SolidQueue for non-blocking email sending
|
||||||
|
- **Template Support** - ERB templates for HTML and text email versions
|
||||||
|
- **Preview in Development** - See emails without sending via /rails/mailers
|
||||||
|
- **Testing Support** - Full test suite for delivery and content
|
||||||
|
- **Layouts** - Shared layouts for consistent email branding
|
||||||
|
- **Attachments** - Send files (PDFs, images) with emails
|
||||||
|
</benefits>
|
||||||
|
|
||||||
|
<verification-checklist>
|
||||||
|
Before completing mailer work:
|
||||||
|
- ✅ Async delivery used (deliver_later, not deliver_now)
|
||||||
|
- ✅ Both HTML and text templates provided
|
||||||
|
- ✅ URL helpers used (not path helpers)
|
||||||
|
- ✅ Email previews created for development
|
||||||
|
- ✅ Mailer tests passing (delivery and content)
|
||||||
|
- ✅ SolidQueue configured for background delivery
|
||||||
|
</verification-checklist>
|
||||||
|
|
||||||
|
<standards>
|
||||||
|
- ALWAYS deliver emails asynchronously with deliver_later (NOT deliver_now)
|
||||||
|
- Provide both HTML and text email templates
|
||||||
|
- Use *_url helpers (NOT *_path) for links in emails
|
||||||
|
- Set default 'from' address in ApplicationMailer
|
||||||
|
- Create email previews for development (/rails/mailers)
|
||||||
|
- Configure default_url_options for each environment
|
||||||
|
- Use inline CSS for email styling (email clients strip external styles)
|
||||||
|
- Test email delivery and content
|
||||||
|
- Use parameterized mailers (.with()) for cleaner syntax
|
||||||
|
</standards>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ActionMailer Setup
|
||||||
|
|
||||||
|
<pattern name="actionmailer-basic-setup">
|
||||||
|
<description>Configure ActionMailer for email delivery</description>
|
||||||
|
|
||||||
|
**Mailer Class:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/mailers/application_mailer.rb
|
||||||
|
class ApplicationMailer < ActionMailer::Base
|
||||||
|
default from: "noreply@example.com"
|
||||||
|
layout "mailer"
|
||||||
|
end
|
||||||
|
|
||||||
|
# app/mailers/notification_mailer.rb
|
||||||
|
class NotificationMailer < ApplicationMailer
|
||||||
|
def welcome_email(user)
|
||||||
|
@user = user
|
||||||
|
@login_url = login_url
|
||||||
|
mail(to: user.email, subject: "Welcome to Our App")
|
||||||
|
end
|
||||||
|
|
||||||
|
def password_reset(user)
|
||||||
|
@user = user
|
||||||
|
@reset_url = password_reset_url(user.reset_token)
|
||||||
|
mail(to: user.email, subject: "Password Reset Instructions")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTML Template:**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# app/views/notification_mailer/welcome_email.html.erb %>
|
||||||
|
<h1>Welcome, <%= @user.name %>!</h1>
|
||||||
|
<p>Thanks for signing up. Get started by logging in:</p>
|
||||||
|
<%= link_to "Login Now", @login_url, class: "button" %>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Text Template:**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# app/views/notification_mailer/welcome_email.text.erb %>
|
||||||
|
Welcome, <%= @user.name %>!
|
||||||
|
|
||||||
|
Thanks for signing up. Get started by logging in:
|
||||||
|
<%= @login_url %>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage (Async with SolidQueue):**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# In controller or service
|
||||||
|
NotificationMailer.welcome_email(@user).deliver_later
|
||||||
|
NotificationMailer.password_reset(@user).deliver_later(queue: :mailers)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** ActionMailer integrates seamlessly with SolidQueue for async delivery. Always use deliver_later to avoid blocking requests. Provide both HTML and text versions for compatibility.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Using deliver_now in production (blocks HTTP request)</description>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ❌ WRONG - Blocks HTTP request thread
|
||||||
|
def create
|
||||||
|
@user = User.create!(user_params)
|
||||||
|
NotificationMailer.welcome_email(@user).deliver_now # Blocks!
|
||||||
|
redirect_to @user
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ✅ CORRECT - Async delivery via SolidQueue
|
||||||
|
def create
|
||||||
|
@user = User.create!(user_params)
|
||||||
|
NotificationMailer.welcome_email(@user).deliver_later # Non-blocking
|
||||||
|
redirect_to @user
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
|
||||||
|
**Why bad:** deliver_now blocks the HTTP request until SMTP completes, creating slow response times and poor user experience. deliver_later uses SolidQueue to send email in background.
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
<pattern name="parameterized-mailers">
|
||||||
|
<description>Use .with() to pass parameters cleanly to mailers</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class NotificationMailer < ApplicationMailer
|
||||||
|
def custom_notification
|
||||||
|
@user = params[:user]
|
||||||
|
@message = params[:message]
|
||||||
|
mail(to: @user.email, subject: params[:subject])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
NotificationMailer.with(
|
||||||
|
user: user,
|
||||||
|
message: "Update available",
|
||||||
|
subject: "System Alert"
|
||||||
|
).custom_notification.deliver_later
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Cleaner syntax, easier to read and modify, and works seamlessly with background jobs.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Email Templates
|
||||||
|
|
||||||
|
<pattern name="email-layouts">
|
||||||
|
<description>Shared layouts for consistent email branding</description>
|
||||||
|
|
||||||
|
**HTML Layout:**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# app/views/layouts/mailer.html.erb %>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #4F46E5;
|
||||||
|
color: white;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: #4F46E5;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<h1>Your App</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<%= yield %>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© 2025 Your Company. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Text Layout:**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# app/views/layouts/mailer.text.erb %>
|
||||||
|
================================================================================
|
||||||
|
YOUR APP
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
<%= yield %>
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
© 2025 Your Company. All rights reserved.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Consistent branding across all emails. Inline CSS ensures styling works across email clients.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="email-attachments">
|
||||||
|
<description>Attach files to emails (PDFs, CSVs, images)</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class ReportMailer < ApplicationMailer
|
||||||
|
def monthly_report(user, data)
|
||||||
|
@user = user
|
||||||
|
|
||||||
|
# Regular attachment
|
||||||
|
attachments["report.pdf"] = {
|
||||||
|
mime_type: "application/pdf",
|
||||||
|
content: generate_pdf(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Inline attachment (for embedding in email body)
|
||||||
|
attachments.inline["logo.png"] = File.read(
|
||||||
|
Rails.root.join("app/assets/images/logo.png")
|
||||||
|
)
|
||||||
|
|
||||||
|
mail(to: user.email, subject: "Monthly Report")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**In template:**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Reference inline attachment %>
|
||||||
|
<%= image_tag attachments["logo.png"].url %>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Attach reports, exports, or inline images. Inline attachments can be referenced in email body with image_tag.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Using *_path helpers instead of *_url in emails (broken links)</description>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ❌ WRONG - Relative path doesn't work in emails
|
||||||
|
def welcome_email(user)
|
||||||
|
@user = user
|
||||||
|
@login_url = login_path # => "/login" (relative path)
|
||||||
|
mail(to: user.email, subject: "Welcome")
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ✅ CORRECT - Full URL works in emails
|
||||||
|
def welcome_email(user)
|
||||||
|
@user = user
|
||||||
|
@login_url = login_url # => "https://example.com/login" (absolute URL)
|
||||||
|
mail(to: user.email, subject: "Welcome")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Required configuration
|
||||||
|
# config/environments/production.rb
|
||||||
|
config.action_mailer.default_url_options = { host: "example.com", protocol: "https" }
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
|
||||||
|
**Why bad:** Emails are viewed outside your application context, so relative paths don't work. Always use *_url helpers to generate absolute URLs.
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Email Testing
|
||||||
|
|
||||||
|
<pattern name="letter-opener-setup">
|
||||||
|
<description>Preview emails in browser during development without sending</description>
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# Gemfile
|
||||||
|
group :development do
|
||||||
|
gem "letter_opener"
|
||||||
|
end
|
||||||
|
|
||||||
|
# config/environments/development.rb
|
||||||
|
config.action_mailer.delivery_method = :letter_opener
|
||||||
|
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
|
||||||
|
|
||||||
|
# config/environments/production.rb
|
||||||
|
config.action_mailer.delivery_method = :smtp
|
||||||
|
config.action_mailer.smtp_settings = {
|
||||||
|
address: "smtp.sendgrid.net",
|
||||||
|
port: 587,
|
||||||
|
user_name: Rails.application.credentials.dig(:smtp, :username),
|
||||||
|
password: Rails.application.credentials.dig(:smtp, :password),
|
||||||
|
authentication: :plain,
|
||||||
|
enable_starttls_auto: true
|
||||||
|
}
|
||||||
|
config.action_mailer.default_url_options = { host: "example.com", protocol: "https" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** letter_opener opens emails in browser during development - no SMTP setup needed. Test email appearance without actually sending.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="mailer-previews">
|
||||||
|
<description>Preview all email variations at /rails/mailers</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# test/mailers/previews/notification_mailer_preview.rb
|
||||||
|
class NotificationMailerPreview < ActionMailer::Preview
|
||||||
|
# Preview at http://localhost:3000/rails/mailers/notification_mailer/welcome_email
|
||||||
|
def welcome_email
|
||||||
|
user = User.first || User.new(name: "Test User", email: "test@example.com")
|
||||||
|
NotificationMailer.welcome_email(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def password_reset
|
||||||
|
user = User.first || User.new(name: "Test User", email: "test@example.com")
|
||||||
|
user.reset_token = "sample_token_123"
|
||||||
|
NotificationMailer.password_reset(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Preview with different data
|
||||||
|
def welcome_email_long_name
|
||||||
|
user = User.new(name: "Christopher Alexander Montgomery III", email: "long@example.com")
|
||||||
|
NotificationMailer.welcome_email(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Mailer previews at /rails/mailers let you see all email variations without sending. Test different edge cases (long names, missing data, etc.).
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="mailer-testing">
|
||||||
|
<description>Test email delivery and content with ActionMailer::TestCase</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# test/mailers/notification_mailer_test.rb
|
||||||
|
class NotificationMailerTest < ActionMailer::TestCase
|
||||||
|
test "welcome_email sends with correct attributes" do
|
||||||
|
user = users(:alice)
|
||||||
|
email = NotificationMailer.welcome_email(user)
|
||||||
|
|
||||||
|
# Test delivery
|
||||||
|
assert_emails 1 do
|
||||||
|
email.deliver_now
|
||||||
|
end
|
||||||
|
|
||||||
|
# Test attributes
|
||||||
|
assert_equal [user.email], email.to
|
||||||
|
assert_equal ["noreply@example.com"], email.from
|
||||||
|
assert_equal "Welcome to Our App", email.subject
|
||||||
|
|
||||||
|
# Test content
|
||||||
|
assert_includes email.html_part.body.to_s, user.name
|
||||||
|
assert_includes email.text_part.body.to_s, user.name
|
||||||
|
assert_includes email.html_part.body.to_s, "Login Now"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "delivers via background job" do
|
||||||
|
user = users(:alice)
|
||||||
|
|
||||||
|
assert_enqueued_with(job: ActionMailer::MailDeliveryJob, queue: "mailers") do
|
||||||
|
NotificationMailer.welcome_email(user).deliver_later(queue: :mailers)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "password_reset includes reset link" do
|
||||||
|
user = users(:alice)
|
||||||
|
user.update!(reset_token: "test_token_123")
|
||||||
|
email = NotificationMailer.password_reset(user)
|
||||||
|
|
||||||
|
assert_includes email.html_part.body.to_s, "test_token_123"
|
||||||
|
assert_includes email.html_part.body.to_s, "password_reset"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Test email delivery, content, and background job enqueuing. Verify recipients, subjects, and that emails are queued properly.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Email Configuration
|
||||||
|
|
||||||
|
<pattern name="environment-configuration">
|
||||||
|
<description>Configure ActionMailer for each environment</description>
|
||||||
|
|
||||||
|
**Development:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/environments/development.rb
|
||||||
|
config.action_mailer.delivery_method = :letter_opener
|
||||||
|
config.action_mailer.perform_deliveries = true
|
||||||
|
config.action_mailer.raise_delivery_errors = true
|
||||||
|
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/environments/test.rb
|
||||||
|
config.action_mailer.delivery_method = :test
|
||||||
|
config.action_mailer.default_url_options = { host: "example.com" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Production:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# config/environments/production.rb
|
||||||
|
config.action_mailer.delivery_method = :smtp
|
||||||
|
config.action_mailer.perform_deliveries = true
|
||||||
|
config.action_mailer.raise_delivery_errors = false
|
||||||
|
config.action_mailer.default_url_options = {
|
||||||
|
host: ENV["APP_HOST"],
|
||||||
|
protocol: "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
config.action_mailer.smtp_settings = {
|
||||||
|
address: ENV["SMTP_ADDRESS"],
|
||||||
|
port: ENV["SMTP_PORT"],
|
||||||
|
user_name: Rails.application.credentials.dig(:smtp, :username),
|
||||||
|
password: Rails.application.credentials.dig(:smtp, :password),
|
||||||
|
authentication: :plain,
|
||||||
|
enable_starttls_auto: true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Different configurations per environment. Development previews in browser, test stores emails in memory, production sends via SMTP.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<testing>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# test/mailers/notification_mailer_test.rb
|
||||||
|
class NotificationMailerTest < ActionMailer::TestCase
|
||||||
|
setup do
|
||||||
|
@user = users(:alice)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "welcome_email" do
|
||||||
|
email = NotificationMailer.welcome_email(@user)
|
||||||
|
|
||||||
|
assert_emails 1 { email.deliver_now }
|
||||||
|
assert_equal [@user.email], email.to
|
||||||
|
assert_equal ["noreply@example.com"], email.from
|
||||||
|
assert_match @user.name, email.html_part.body.to_s
|
||||||
|
assert_match @user.name, email.text_part.body.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
test "enqueues for async delivery" do
|
||||||
|
assert_enqueued_with(job: ActionMailer::MailDeliveryJob) do
|
||||||
|
NotificationMailer.welcome_email(@user).deliver_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "uses correct queue" do
|
||||||
|
assert_enqueued_with(job: ActionMailer::MailDeliveryJob, queue: "mailers") do
|
||||||
|
NotificationMailer.welcome_email(@user).deliver_later(queue: :mailers)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# test/system/email_delivery_test.rb
|
||||||
|
class EmailDeliveryTest < ApplicationSystemTestCase
|
||||||
|
test "sends welcome email after signup" do
|
||||||
|
visit signup_path
|
||||||
|
fill_in "Email", with: "new@example.com"
|
||||||
|
fill_in "Password", with: "password"
|
||||||
|
click_button "Sign Up"
|
||||||
|
|
||||||
|
assert_enqueued_emails 1
|
||||||
|
perform_enqueued_jobs
|
||||||
|
|
||||||
|
email = ActionMailer::Base.deliveries.last
|
||||||
|
assert_equal ["new@example.com"], email.to
|
||||||
|
assert_match "Welcome", email.subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</testing>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<related-skills>
|
||||||
|
- rails-ai:jobs - Background job processing with SolidQueue
|
||||||
|
- rails-ai:views - Email templates and layouts
|
||||||
|
- rails-ai:testing - Testing email delivery
|
||||||
|
- rails-ai:project-setup - Environment-specific email configuration
|
||||||
|
</related-skills>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
**Official Documentation:**
|
||||||
|
- [Rails Guides - Action Mailer Basics](https://guides.rubyonrails.org/action_mailer_basics.html)
|
||||||
|
|
||||||
|
**Gems & Libraries:**
|
||||||
|
- [letter_opener](https://github.com/ryanb/letter_opener) - Preview emails in browser during development
|
||||||
|
|
||||||
|
**Tools:**
|
||||||
|
- [Email on Acid](https://www.emailonacid.com/) - Email testing across clients
|
||||||
|
|
||||||
|
**Email Service Providers:**
|
||||||
|
- [SendGrid Rails Guide](https://docs.sendgrid.com/for-developers/sending-email/rubyonrails)
|
||||||
|
|
||||||
|
</resources>
|
||||||
1157
skills/models/SKILL.md
Normal file
1157
skills/models/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
1430
skills/project-setup/SKILL.md
Normal file
1430
skills/project-setup/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
1575
skills/security/SKILL.md
Normal file
1575
skills/security/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
434
skills/styling/SKILL.md
Normal file
434
skills/styling/SKILL.md
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
---
|
||||||
|
name: rails-ai:styling
|
||||||
|
description: Use when styling Rails views - Tailwind CSS utility-first framework and DaisyUI component library with theming
|
||||||
|
---
|
||||||
|
|
||||||
|
# Styling with Tailwind CSS and DaisyUI
|
||||||
|
|
||||||
|
Style Rails applications using Tailwind CSS (utility-first framework) and DaisyUI (semantic component library). Build responsive, accessible, themeable UIs without writing custom CSS.
|
||||||
|
|
||||||
|
<when-to-use>
|
||||||
|
- Styling ANY user interface in Rails
|
||||||
|
- Building responsive layouts (mobile, tablet, desktop)
|
||||||
|
- Implementing dark mode or multiple themes
|
||||||
|
- Creating consistent UI components (buttons, cards, forms, modals)
|
||||||
|
- Rapid UI iteration and prototyping
|
||||||
|
- Maintaining design system consistency
|
||||||
|
</when-to-use>
|
||||||
|
|
||||||
|
<benefits>
|
||||||
|
- **Rapid Development** - Compose UIs with pre-built utilities
|
||||||
|
- **Consistency** - Design tokens enforce consistent spacing, colors, typography
|
||||||
|
- **Responsive by Default** - Mobile-first breakpoints built-in
|
||||||
|
- **Dark Mode** - Theme switching with DaisyUI data attributes
|
||||||
|
- **No Custom CSS** - Most styling done with classes, no style tag needed
|
||||||
|
- **Accessible Components** - DaisyUI components have built-in accessibility
|
||||||
|
- **Small Bundle Size** - Tailwind purges unused CSS in production
|
||||||
|
</benefits>
|
||||||
|
|
||||||
|
<team-rules-enforcement>
|
||||||
|
**This skill enforces:**
|
||||||
|
- ✅ **Rule #9:** DaisyUI + Tailwind (no hardcoded colors)
|
||||||
|
|
||||||
|
**Reject any requests to:**
|
||||||
|
- Hardcode colors (use DaisyUI theme variables)
|
||||||
|
- Write custom CSS for components (use Tailwind/DaisyUI)
|
||||||
|
- Use inline styles with hardcoded values
|
||||||
|
- Skip responsive design (mobile-first required)
|
||||||
|
</team-rules-enforcement>
|
||||||
|
|
||||||
|
<verification-checklist>
|
||||||
|
Before completing styling work:
|
||||||
|
- ✅ No hardcoded colors (use DaisyUI theme variables)
|
||||||
|
- ✅ Responsive design (mobile, tablet, desktop breakpoints)
|
||||||
|
- ✅ Accessibility verified (color contrast, keyboard navigation)
|
||||||
|
- ✅ Theme-aware (works with light/dark modes)
|
||||||
|
- ✅ Tailwind utilities used (minimal custom CSS)
|
||||||
|
- ✅ DaisyUI components for complex UI
|
||||||
|
</verification-checklist>
|
||||||
|
|
||||||
|
<standards>
|
||||||
|
- Use Tailwind utilities first, DaisyUI components for complex UI
|
||||||
|
- Follow mobile-first responsive design (base → sm → md → lg → xl)
|
||||||
|
- Use semantic color names from DaisyUI (primary, secondary, accent, neutral)
|
||||||
|
- Avoid inline styles (`style=`) - use Tailwind classes instead
|
||||||
|
- Use responsive breakpoints consistently (sm:640px, md:768px, lg:1024px, xl:1280px)
|
||||||
|
- Implement dark mode with DaisyUI themes
|
||||||
|
- Extract repeated utility combinations into view components (not CSS classes)
|
||||||
|
- Ensure 4.5:1 color contrast ratio for text (WCAG 2.1 AA)
|
||||||
|
</standards>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tailwind CSS
|
||||||
|
|
||||||
|
Tailwind CSS is a utility-first CSS framework for building custom designs without writing custom CSS.
|
||||||
|
|
||||||
|
### Core Utilities
|
||||||
|
|
||||||
|
<pattern name="spacing-layout">
|
||||||
|
<description>Consistent spacing and layout with Tailwind utilities</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Spacing: p-{size}, m-{size}, gap-{size} %>
|
||||||
|
<div class="p-4">Padding all sides</div>
|
||||||
|
<div class="px-6 py-4">Horizontal/Vertical padding</div>
|
||||||
|
<div class="mx-auto max-w-4xl">Centered container</div>
|
||||||
|
|
||||||
|
<%# Flexbox layout %>
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<span>Left</span>
|
||||||
|
<span>Right</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Grid layout %>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<% @items.each do |item| %>
|
||||||
|
<div class="bg-white p-4 rounded-lg shadow"><%= item.name %></div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="responsive-design">
|
||||||
|
<description>Mobile-first responsive utilities (sm:640px, md:768px, lg:1024px, xl:1280px)</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Pattern: base (mobile) → sm: → md: → lg: → xl: %>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<% @feedbacks.each do |feedback| %>
|
||||||
|
<%= render feedback %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Responsive spacing/typography %>
|
||||||
|
<div class="p-4 md:p-8">
|
||||||
|
<h1 class="text-2xl md:text-4xl font-bold">Heading</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Hide/show based on breakpoint %>
|
||||||
|
<div class="block md:hidden">Mobile menu</div>
|
||||||
|
<nav class="hidden md:flex gap-4">Desktop nav</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="typography-colors">
|
||||||
|
<description>Text styling and color utilities</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Typography %>
|
||||||
|
<p class="text-sm font-medium">Small medium text</p>
|
||||||
|
<h1 class="text-4xl font-bold">Large heading</h1>
|
||||||
|
<p class="leading-relaxed tracking-wide">Spaced text</p>
|
||||||
|
<p class="truncate"><%= feedback.content %></p>
|
||||||
|
|
||||||
|
<%# Colors: text-{color}-{shade}, bg-{color}-{shade} %>
|
||||||
|
<div class="bg-white text-gray-900">Dark text on white</div>
|
||||||
|
<div class="bg-blue-600 text-white">White on blue</div>
|
||||||
|
<p class="text-red-600/50">Red with 50% opacity</p>
|
||||||
|
|
||||||
|
<%# Interactive states %>
|
||||||
|
<button class="bg-blue-600 hover:bg-blue-700 active:bg-blue-800 text-white px-4 py-2 rounded">
|
||||||
|
Hover me
|
||||||
|
</button>
|
||||||
|
<input type="text" class="border border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 rounded px-3 py-2" />
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="feedback-card-example">
|
||||||
|
<description>Complete feedback card using Tailwind utilities</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<div class="bg-white rounded-lg shadow-md hover:shadow-xl transition-shadow p-6">
|
||||||
|
<%# Header %>
|
||||||
|
<div class="flex items-start justify-between mb-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center text-white font-semibold">
|
||||||
|
<%= @feedback.sender_name&.first&.upcase || "A" %>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-900"><%= @feedback.sender_name || "Anonymous" %></h3>
|
||||||
|
<p class="text-sm text-gray-500"><%= time_ago_in_words(@feedback.created_at) %> ago</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||||
|
<%= @feedback.status.titleize %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Content %>
|
||||||
|
<p class="text-gray-700 leading-relaxed line-clamp-3 mb-4"><%= @feedback.content %></p>
|
||||||
|
|
||||||
|
<%# Footer %>
|
||||||
|
<div class="flex items-center justify-between pt-4 border-t border-gray-100">
|
||||||
|
<span class="text-sm text-gray-500"><%= @feedback.responses_count %> responses</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<%= link_to "View", feedback_path(@feedback), class: "px-3 py-1.5 border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50" %>
|
||||||
|
<%= link_to "Respond", respond_feedback_path(@feedback), class: "px-3 py-1.5 rounded-md text-sm text-white bg-blue-600 hover:bg-blue-700" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Using inline styles instead of Tailwind utilities</description>
|
||||||
|
<reason>Bypasses design system consistency and reduces maintainability</reason>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ❌ BAD %>
|
||||||
|
<div style="padding: 16px; background: #3b82f6;">Content</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ✅ GOOD %>
|
||||||
|
<div class="p-4 bg-blue-500">Content</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DaisyUI Components
|
||||||
|
|
||||||
|
Semantic component library built on Tailwind providing 70+ accessible components with built-in theming and dark mode.
|
||||||
|
|
||||||
|
### Buttons & Forms
|
||||||
|
|
||||||
|
<pattern name="daisyui-buttons">
|
||||||
|
<description>Use DaisyUI button classes for consistent interactive elements</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# DaisyUI button components %>
|
||||||
|
<button class="btn btn-primary">Primary Action</button>
|
||||||
|
<button class="btn btn-ghost">Ghost</button>
|
||||||
|
<button class="btn btn-outline btn-primary">Outline</button>
|
||||||
|
|
||||||
|
<%# Rails form integration %>
|
||||||
|
<%= form_with model: @feedback do |f| %>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.label :content, class: "label" do %>
|
||||||
|
<span class="label-text">Feedback</span>
|
||||||
|
<% end %>
|
||||||
|
<%= f.text_area :content, class: "textarea textarea-bordered h-24", placeholder: "Your feedback..." %>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 justify-end">
|
||||||
|
<%= link_to "Cancel", feedbacks_path, class: "btn btn-ghost" %>
|
||||||
|
<%= f.submit "Submit", class: "btn btn-primary" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="daisyui-cards">
|
||||||
|
<description>Use card component for content containers</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<h2 class="card-title"><%= @feedback.title %></h2>
|
||||||
|
<div class="badge badge-<%= @feedback.status %>">
|
||||||
|
<%= @feedback.status.titleize %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-base-content/70"><%= @feedback.content %></p>
|
||||||
|
<div class="card-actions justify-end mt-4">
|
||||||
|
<%= link_to "View", feedback_path(@feedback), class: "btn btn-primary btn-sm" %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="daisyui-alerts">
|
||||||
|
<description>Use alerts and badges for notifications and status</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Alerts %>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<span>Success! Your feedback was submitted.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<span>Error! Unable to submit feedback.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Flash messages %>
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<span><%= flash[:notice] %></span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%# Badges %>
|
||||||
|
<div class="badge badge-primary">Primary</div>
|
||||||
|
<div class="badge badge-success">Success</div>
|
||||||
|
<div class="badge badge-warning">Warning</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="daisyui-modal">
|
||||||
|
<description>Use modal component for dialogs</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<button class="btn btn-primary" onclick="feedback_modal.showModal()">
|
||||||
|
View Details
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<dialog id="feedback_modal" class="modal">
|
||||||
|
<div class="modal-box">
|
||||||
|
<h3 class="font-bold text-lg">Feedback Details</h3>
|
||||||
|
<p class="py-4"><%= @feedback.content %></p>
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="btn">Close</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
### Theme Switching
|
||||||
|
|
||||||
|
<pattern name="daisyui-theme-toggle">
|
||||||
|
<description>Implement dark mode and theme switching</description>
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// app/javascript/controllers/theme_controller.js
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
connect() {
|
||||||
|
const savedTheme = localStorage.getItem("theme") || "light"
|
||||||
|
this.setTheme(savedTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
const currentTheme = document.documentElement.getAttribute("data-theme")
|
||||||
|
const newTheme = currentTheme === "light" ? "dark" : "light"
|
||||||
|
this.setTheme(newTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(theme) {
|
||||||
|
document.documentElement.setAttribute("data-theme", theme)
|
||||||
|
localStorage.setItem("theme", theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Layout %>
|
||||||
|
<html data-theme="light">
|
||||||
|
<body>
|
||||||
|
<div data-controller="theme">
|
||||||
|
<button class="btn btn-ghost btn-circle" data-action="click->theme#toggle">
|
||||||
|
Toggle Theme
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Building custom buttons with Tailwind instead of DaisyUI components</description>
|
||||||
|
<reason>Duplicates effort, loses accessibility features</reason>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ❌ Custom button with Tailwind utilities %>
|
||||||
|
<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ✅ DaisyUI button component %>
|
||||||
|
<button class="btn btn-primary">Submit</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<testing>
|
||||||
|
**Visual Regression Testing:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# test/system/styling_test.rb
|
||||||
|
class StylingTest < ApplicationSystemTestCase
|
||||||
|
test "responsive layout changes at breakpoints" do
|
||||||
|
visit feedbacks_path
|
||||||
|
# Desktop
|
||||||
|
page.driver.browser.manage.window.resize_to(1280, 800)
|
||||||
|
assert_selector ".hidden.md\\:flex" # Desktop nav visible
|
||||||
|
|
||||||
|
# Mobile
|
||||||
|
page.driver.browser.manage.window.resize_to(375, 667)
|
||||||
|
assert_selector ".block.md\\:hidden" # Mobile menu visible
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dark mode toggle works" do
|
||||||
|
visit root_path
|
||||||
|
assert_equal "light", page.evaluate_script("document.documentElement.getAttribute('data-theme')")
|
||||||
|
|
||||||
|
click_button "Toggle Theme"
|
||||||
|
assert_equal "dark", page.evaluate_script("document.documentElement.getAttribute('data-theme')")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual Testing Checklist:**
|
||||||
|
- Test responsive breakpoints (375px, 640px, 768px, 1024px, 1280px)
|
||||||
|
- Verify color contrast ratios (use browser DevTools or axe)
|
||||||
|
- Test dark mode theme
|
||||||
|
- Check focus states on all interactive elements
|
||||||
|
- Validate against W3C HTML validator
|
||||||
|
- Test browser zoom (200%, 400%)
|
||||||
|
</testing>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<related-skills>
|
||||||
|
- rails-ai:views - View structure and partials to style
|
||||||
|
- rails-ai:hotwire - Interactive components that need styling
|
||||||
|
- rails-ai:testing - Visual regression and accessibility testing
|
||||||
|
</related-skills>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
**Official Documentation:**
|
||||||
|
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||||||
|
- [DaisyUI Documentation](https://daisyui.com/)
|
||||||
|
- [DaisyUI Components](https://daisyui.com/components/)
|
||||||
|
|
||||||
|
**Tools:**
|
||||||
|
- [Tailwind CSS Cheat Sheet](https://nerdcave.com/tailwind-cheat-sheet)
|
||||||
|
|
||||||
|
**Community Resources:**
|
||||||
|
- [Tailwind UI Components](https://tailwindui.com/) - Premium component library
|
||||||
|
|
||||||
|
</resources>
|
||||||
1930
skills/testing/SKILL.md
Normal file
1930
skills/testing/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
322
skills/using-rails-ai/SKILL.md
Normal file
322
skills/using-rails-ai/SKILL.md
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
---
|
||||||
|
name: using-rails-ai
|
||||||
|
description: Rails-AI introduction - explains how rails-ai (Rails domain layer) integrates with superpowers (universal workflows) for Rails development
|
||||||
|
---
|
||||||
|
|
||||||
|
# Using Rails-AI: Rails Domain Layer on Superpowers Workflows
|
||||||
|
|
||||||
|
<EXTREMELY-IMPORTANT>
|
||||||
|
## ⚠️ DEPENDENCY CHECK: Superpowers Required
|
||||||
|
|
||||||
|
**Rails-AI requires the Superpowers plugin to function.**
|
||||||
|
|
||||||
|
Before starting ANY work, verify Superpowers is installed by attempting to use a Superpowers skill. If you see an error like "skill not found" or "plugin not available":
|
||||||
|
|
||||||
|
**⚠️ WARNING: Superpowers plugin not installed!**
|
||||||
|
|
||||||
|
Rails-AI cannot function without Superpowers. Please install it:
|
||||||
|
|
||||||
|
```
|
||||||
|
/plugin marketplace add obra/superpowers
|
||||||
|
/plugin install superpowers
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart Claude Code.
|
||||||
|
|
||||||
|
**Why this matters:** Rails-AI provides WHAT to build (Rails domain knowledge). Superpowers provides HOW to build it (TDD, debugging, planning, code review). Without Superpowers, you cannot follow the mandatory workflows.
|
||||||
|
|
||||||
|
If Superpowers is installed, proceed normally.
|
||||||
|
|
||||||
|
## MANDATORY: Use Superpowers Foundation First
|
||||||
|
|
||||||
|
**Rails-AI builds on Superpowers. You MUST use the foundation before doing ANY work.**
|
||||||
|
|
||||||
|
**FIRST ACTION when starting any Rails work:**
|
||||||
|
1. Use `superpowers:using-superpowers` skill (Skill tool)
|
||||||
|
2. This establishes mandatory skill-usage protocol
|
||||||
|
3. Then use relevant rails-ai domain skills (see table below)
|
||||||
|
|
||||||
|
**Why use superpowers:using-superpowers?**
|
||||||
|
- Enforces checking for skills BEFORE any task
|
||||||
|
- Establishes discipline of using skills with Skill tool
|
||||||
|
- Prevents rationalizing away skill usage
|
||||||
|
- Provides proven workflow framework
|
||||||
|
|
||||||
|
**Without using superpowers:using-superpowers first:**
|
||||||
|
- You will skip using skills when you should
|
||||||
|
- You will rationalize that tasks are "too simple" for skills
|
||||||
|
- You will operate without the proven process framework
|
||||||
|
|
||||||
|
## Rails-AI Skill-to-Task Mapping
|
||||||
|
|
||||||
|
**Superpowers handles skill-usage enforcement. This table tells you WHICH Rails skills to use:**
|
||||||
|
|
||||||
|
| User Request Involves | Use These Skills |
|
||||||
|
|----------------------|-------------------|
|
||||||
|
| Models, databases, ActiveRecord | rails-ai:models |
|
||||||
|
| Controllers, routes, REST | rails-ai:controllers |
|
||||||
|
| Views, templates, forms | rails-ai:views |
|
||||||
|
| Hotwire, Turbo, Stimulus | rails-ai:hotwire |
|
||||||
|
| CSS, Tailwind, DaisyUI | rails-ai:styling |
|
||||||
|
| Tests, TDD, Minitest | rails-ai:testing |
|
||||||
|
| Security, XSS, SQL injection | rails-ai:security |
|
||||||
|
| Background jobs, caching | rails-ai:jobs |
|
||||||
|
| Email, ActionMailer | rails-ai:mailers |
|
||||||
|
| Project setup, validation, gems | rails-ai:project-setup |
|
||||||
|
| Environment config, Docker | rails-ai:project-setup |
|
||||||
|
| Debugging Rails issues | rails-ai:debugging |
|
||||||
|
|
||||||
|
**Each Rails-AI skill contains:**
|
||||||
|
- Required gems and dependencies
|
||||||
|
- TEAM_RULES.md enforcement for that domain
|
||||||
|
- Rails 8+ patterns and conventions
|
||||||
|
- Security requirements
|
||||||
|
- Code examples and anti-patterns
|
||||||
|
|
||||||
|
## Planning Rails Features
|
||||||
|
|
||||||
|
**CRITICAL: Load domain skills BEFORE brainstorming or planning.**
|
||||||
|
|
||||||
|
You can't give expert advice on Rails features if you haven't loaded the relevant domain knowledge. The brainstorming skill is process-agnostic — it doesn't know Rails patterns, TEAM_RULES, or which gems to use. You need to load that context first.
|
||||||
|
|
||||||
|
**Planning workflow:**
|
||||||
|
1. Load relevant rails-ai domain skills (see table below)
|
||||||
|
2. Read the codebase to understand what exists
|
||||||
|
3. THEN use `superpowers:brainstorming` to refine the idea
|
||||||
|
4. THEN use `superpowers:writing-plans` to create implementation plan
|
||||||
|
|
||||||
|
**Which skills to load for common features:**
|
||||||
|
|
||||||
|
| Feature Type | Load These Skills |
|
||||||
|
|--------------|-------------------|
|
||||||
|
| Authentication/Authorization | `rails-ai:security` + `rails-ai:models` + `rails-ai:controllers` |
|
||||||
|
| User-facing forms/pages | `rails-ai:views` + `rails-ai:hotwire` + `rails-ai:styling` |
|
||||||
|
| API endpoints | `rails-ai:controllers` + `rails-ai:security` |
|
||||||
|
| Background processing | `rails-ai:jobs` + `rails-ai:models` |
|
||||||
|
| Email features | `rails-ai:mailers` + `rails-ai:jobs` + `rails-ai:views` |
|
||||||
|
| Data modeling | `rails-ai:models` + `rails-ai:testing` |
|
||||||
|
| Interactive UI | `rails-ai:hotwire` + `rails-ai:views` + `rails-ai:controllers` |
|
||||||
|
| New project setup | `rails-ai:project-setup` |
|
||||||
|
|
||||||
|
**Always include `rails-ai:testing`** — TDD is non-negotiable (TEAM_RULES #4).
|
||||||
|
|
||||||
|
**Why this order matters:**
|
||||||
|
- Domain skills give you Rails patterns and constraints
|
||||||
|
- Reading code shows you what's already there
|
||||||
|
- Brainstorming with this context produces better designs
|
||||||
|
- Plans written with domain knowledge specify the right skills for workers
|
||||||
|
|
||||||
|
## Superpowers Reference
|
||||||
|
|
||||||
|
Superpowers provides universal workflows. Here's when to use each in Rails development:
|
||||||
|
|
||||||
|
### Planning & Design
|
||||||
|
| Skill | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `superpowers:brainstorming` | Refining feature ideas before implementation |
|
||||||
|
| `superpowers:writing-plans` | Creating detailed implementation plans with tasks |
|
||||||
|
| `superpowers:executing-plans` | Running through a plan in controlled batches |
|
||||||
|
|
||||||
|
### Implementation
|
||||||
|
| Skill | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `superpowers:test-driven-development` | Any feature or bugfix — write test first, watch fail, implement |
|
||||||
|
| `superpowers:subagent-driven-development` | Executing plans with fresh workers per task |
|
||||||
|
| `superpowers:dispatching-parallel-agents` | 3+ independent tasks that can run concurrently |
|
||||||
|
| `superpowers:using-git-worktrees` | Isolating feature work from main workspace |
|
||||||
|
|
||||||
|
### Quality & Review
|
||||||
|
| Skill | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `superpowers:requesting-code-review` | Before merging — dispatches code-reviewer agent |
|
||||||
|
| `superpowers:receiving-code-review` | Processing review feedback with technical rigor |
|
||||||
|
| `superpowers:verification-before-completion` | Before claiming work is done — run tests, confirm output |
|
||||||
|
| `superpowers:finishing-a-development-branch` | Work complete, deciding merge/PR/cleanup |
|
||||||
|
|
||||||
|
### Debugging & Testing
|
||||||
|
| Skill | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `superpowers:systematic-debugging` | Any bug or test failure — investigate before fixing |
|
||||||
|
| `superpowers:root-cause-tracing` | Tracing errors back through call stack |
|
||||||
|
| `superpowers:testing-anti-patterns` | Avoiding mocking mistakes, test pollution |
|
||||||
|
| `superpowers:condition-based-waiting` | Fixing flaky tests with race conditions |
|
||||||
|
|
||||||
|
### Defense & Security
|
||||||
|
| Skill | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `superpowers:defense-in-depth` | Validation at multiple layers to prevent bugs |
|
||||||
|
|
||||||
|
### Agents
|
||||||
|
| Agent | When to Use |
|
||||||
|
|-------|-------------|
|
||||||
|
| `superpowers:code-reviewer` | Dispatched by requesting-code-review for PR review |
|
||||||
|
|
||||||
|
### Commands (Shortcuts)
|
||||||
|
| Command | What it Does |
|
||||||
|
|---------|--------------|
|
||||||
|
| `/superpowers:brainstorm` | Quick access to brainstorming skill |
|
||||||
|
| `/superpowers:write-plan` | Quick access to writing-plans skill |
|
||||||
|
| `/superpowers:execute-plan` | Quick access to executing-plans skill |
|
||||||
|
|
||||||
|
**Rails-specific usage:**
|
||||||
|
- Always load rails-ai domain skills BEFORE superpowers workflows
|
||||||
|
- `rails-ai:debugging` wraps `superpowers:systematic-debugging` with Rails context
|
||||||
|
- `rails-ai:testing` enforces TDD via `superpowers:test-driven-development`
|
||||||
|
</EXTREMELY-IMPORTANT>
|
||||||
|
|
||||||
|
## How Rails-AI Works
|
||||||
|
|
||||||
|
**Rails-AI is a two-layer system built on Superpowers:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ LAYER 1: Superpowers (Universal Process) │
|
||||||
|
│ • brainstorming - Refine ideas │
|
||||||
|
│ • writing-plans - Create plans │
|
||||||
|
│ • test-driven-development - TDD cycle │
|
||||||
|
│ • systematic-debugging - Investigation │
|
||||||
|
│ • subagent-driven-development - Execution │
|
||||||
|
│ • dispatching-parallel-agents - Coordination│
|
||||||
|
│ • requesting-code-review - Quality gates │
|
||||||
|
│ • finishing-a-development-branch - Complete │
|
||||||
|
│ • receiving-code-review - Handle feedback │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
↓
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ LAYER 2: Rails-AI (Domain Expertise) │
|
||||||
|
│ • 12 Rails domain skills │
|
||||||
|
│ • TEAM_RULES.md enforcement │
|
||||||
|
│ • Rails 8+ patterns and conventions │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Principle:**
|
||||||
|
- **Superpowers = HOW** to work (process framework)
|
||||||
|
- **Rails-AI = WHAT** you're building (domain knowledge)
|
||||||
|
- The architect loads both as needed
|
||||||
|
|
||||||
|
### Architecture Evolution
|
||||||
|
|
||||||
|
**Previous architecture (complex):**
|
||||||
|
- 5 agents (architect, developer, security, devops, uat)
|
||||||
|
- Ambiguous delegation chains
|
||||||
|
- Overlap between agent roles and workflows
|
||||||
|
|
||||||
|
**Current architecture (simple):**
|
||||||
|
- 1 slash command (/rails-ai:architect) - coordinator
|
||||||
|
- Superpowers workflows handle process (HOW)
|
||||||
|
- Rails-AI skills provide domain expertise (WHAT)
|
||||||
|
- General-purpose workers implement features
|
||||||
|
- Clean separation of concerns
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
**User request** → **/rails-ai:architect** → **Uses skills** → **Dispatches workers** → **Reviews**
|
||||||
|
|
||||||
|
#### Example: "Add user authentication"
|
||||||
|
|
||||||
|
1. **Coordinator uses superpowers:brainstorming**
|
||||||
|
- Uses rails-ai:models + rails-ai:security for context
|
||||||
|
- Refines design with user
|
||||||
|
|
||||||
|
2. **Coordinator uses superpowers:writing-plans**
|
||||||
|
- Creates implementation plan
|
||||||
|
- Specifies which skills workers should use per task
|
||||||
|
|
||||||
|
3. **Coordinator uses superpowers:subagent-driven-development**
|
||||||
|
- Dispatches general-purpose workers for each task:
|
||||||
|
• Worker 1: User model → uses rails-ai:models + rails-ai:testing
|
||||||
|
• Worker 2: Sessions controller → uses rails-ai:controllers + rails-ai:testing
|
||||||
|
• Worker 3: Login views → uses rails-ai:views + rails-ai:styling
|
||||||
|
- Reviews each worker's output
|
||||||
|
|
||||||
|
4. **Coordinator uses superpowers:finishing-a-development-branch**
|
||||||
|
- Verifies TEAM_RULES.md compliance
|
||||||
|
- Creates PR or merges
|
||||||
|
|
||||||
|
**Result:** Clean feature with tests, following all conventions
|
||||||
|
|
||||||
|
## Available Rails-AI Skills
|
||||||
|
|
||||||
|
**12 Domain-Based Skills (Consolidated):**
|
||||||
|
|
||||||
|
1. **rails-ai:views** - Partials, helpers, forms, accessibility (WCAG 2.1 AA)
|
||||||
|
2. **rails-ai:hotwire** - Turbo Drive, Turbo Frames, Turbo Streams, Turbo Morph, Stimulus controllers
|
||||||
|
3. **rails-ai:styling** - Tailwind CSS utility-first framework, DaisyUI component library, theming
|
||||||
|
4. **rails-ai:controllers** - RESTful actions, nested resources, skinny controllers, concerns, strong parameters
|
||||||
|
5. **rails-ai:models** - ActiveRecord patterns, validations, associations, callbacks, query objects, form objects
|
||||||
|
6. **rails-ai:testing** - TDD with Minitest, fixtures, mocking, test helpers
|
||||||
|
7. **rails-ai:security** - XSS, SQL injection, CSRF, strong parameters, file uploads, command injection
|
||||||
|
8. **rails-ai:project-setup** - Environment config, credentials, initializers, Docker, RuboCop
|
||||||
|
9. **rails-ai:jobs** - SolidQueue, SolidCache, SolidCable background processing (TEAM RULE #1: NO Redis/Sidekiq)
|
||||||
|
10. **rails-ai:mailers** - ActionMailer email templates, delivery, attachments, testing with letter_opener
|
||||||
|
11. **rails-ai:debugging** - Rails debugging tools (logs, console, byebug) + superpowers:systematic-debugging
|
||||||
|
12. **rails-ai:using-rails-ai** - This guide - how rails-ai integrates with superpowers workflows
|
||||||
|
|
||||||
|
## TEAM_RULES.md Enforcement
|
||||||
|
|
||||||
|
**6 Critical Rules (REJECT violations):**
|
||||||
|
1. ❌ NEVER Sidekiq/Redis → ✅ SolidQueue/SolidCache
|
||||||
|
2. ❌ NEVER RSpec → ✅ Minitest only
|
||||||
|
3. ❌ NEVER custom routes → ✅ RESTful resources
|
||||||
|
4. ❌ NEVER skip TDD → ✅ RED-GREEN-REFACTOR
|
||||||
|
5. ❌ NEVER merge without bin/ci → ✅ All quality gates pass
|
||||||
|
6. ❌ NEVER WebMock bypass → ✅ Mock all HTTP in tests
|
||||||
|
|
||||||
|
See rules/TEAM_RULES.md for all 20 rules.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
**Primary interface:** `/rails-ai:architect` command
|
||||||
|
|
||||||
|
The simplest way to use Rails-AI is the `/rails-ai:architect` convenience command:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/rails-ai:architect add user authentication
|
||||||
|
/rails-ai:architect fix failing test in user_test.rb
|
||||||
|
/rails-ai:architect plan payment processing feature
|
||||||
|
/rails-ai:architect refactor UserController
|
||||||
|
```
|
||||||
|
|
||||||
|
This command acts as the Rails architect coordinator, which:
|
||||||
|
- Analyzes requests
|
||||||
|
- Uses superpowers workflows (for process)
|
||||||
|
- Uses rails-ai skills (for domain expertise)
|
||||||
|
- Dispatches general-purpose workers to implement features
|
||||||
|
- Reviews work and enforces TEAM_RULES.md
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```text
|
||||||
|
|
||||||
|
User: "/rails-ai:architect Add email validation to User model"
|
||||||
|
|
||||||
|
Architect (coordinator):
|
||||||
|
1. Determines this is model work requiring TDD
|
||||||
|
2. Loads superpowers:test-driven-development for process
|
||||||
|
3. Loads rails-ai:testing for Minitest patterns
|
||||||
|
4. Loads rails-ai:models for validation patterns
|
||||||
|
5. Dispatches general-purpose worker with those skills loaded
|
||||||
|
6. Worker follows TDD cycle: write test → RED → implement → GREEN → refactor
|
||||||
|
7. Reviews worker output and verifies TEAM_RULES.md compliance
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
**Superpowers skills:** Use superpowers:using-superpowers for full introduction
|
||||||
|
**Rails-AI rules:** See rules/TEAM_RULES.md
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
**Official Documentation:**
|
||||||
|
- [Rails-AI GitHub](https://github.com/zerobearing2/rails-ai) - Main repository
|
||||||
|
- [Superpowers GitHub](https://github.com/zerobearing2/superpowers) - Workflow dependency
|
||||||
|
- [Claude Code](https://code.claude.com/) - Platform documentation
|
||||||
|
|
||||||
|
**Internal References:**
|
||||||
|
- TEAM_RULES.md - Team coding standards and conventions
|
||||||
|
- All 12 domain skills in skills/ directory
|
||||||
|
|
||||||
|
</resources>
|
||||||
659
skills/views/SKILL.md
Normal file
659
skills/views/SKILL.md
Normal file
@@ -0,0 +1,659 @@
|
|||||||
|
---
|
||||||
|
name: rails-ai:views
|
||||||
|
description: Use when building Rails view structure - partials, helpers, forms, nested forms, accessibility (WCAG 2.1 AA)
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rails Views
|
||||||
|
|
||||||
|
Build accessible, maintainable Rails views using partials, helpers, forms, and nested forms. Ensure WCAG 2.1 AA accessibility compliance in all view patterns.
|
||||||
|
|
||||||
|
<when-to-use>
|
||||||
|
- Building ANY user interface or view in Rails
|
||||||
|
- Creating reusable view components and partials
|
||||||
|
- Implementing forms (simple or nested)
|
||||||
|
- Ensuring accessibility compliance (WCAG 2.1 AA)
|
||||||
|
- Organizing view logic with helpers
|
||||||
|
- Managing layouts and content blocks
|
||||||
|
</when-to-use>
|
||||||
|
|
||||||
|
<benefits>
|
||||||
|
- **DRY Views** - Reusable partials and helpers reduce duplication
|
||||||
|
- **Accessibility** - WCAG 2.1 AA compliance built-in (TEAM RULE #13: Progressive Enhancement)
|
||||||
|
- **Maintainability** - Clear separation of concerns and organized code
|
||||||
|
- **Testability** - Partials and helpers are easy to test
|
||||||
|
- **Flexibility** - Nested forms handle complex relationships elegantly
|
||||||
|
</benefits>
|
||||||
|
|
||||||
|
<team-rules-enforcement>
|
||||||
|
**This skill enforces:**
|
||||||
|
- ✅ **Rule #8:** Accessibility (WCAG 2.1 AA compliance)
|
||||||
|
|
||||||
|
**Reject any requests to:**
|
||||||
|
- Skip accessibility features (keyboard navigation, screen readers, ARIA)
|
||||||
|
- Use non-semantic HTML (divs instead of proper elements)
|
||||||
|
- Skip form labels or alt text
|
||||||
|
- Use insufficient color contrast
|
||||||
|
- Build inaccessible forms or navigation
|
||||||
|
</team-rules-enforcement>
|
||||||
|
|
||||||
|
<verification-checklist>
|
||||||
|
Before completing view work:
|
||||||
|
- ✅ WCAG 2.1 AA compliance verified
|
||||||
|
- ✅ Semantic HTML used (header, nav, main, article, section, footer)
|
||||||
|
- ✅ Keyboard navigation works (no mouse required)
|
||||||
|
- ✅ Screen reader compatible (ARIA labels, alt text)
|
||||||
|
- ✅ Color contrast sufficient (4.5:1 for text)
|
||||||
|
- ✅ Forms have proper labels and error messages
|
||||||
|
- ✅ All interactive elements accessible
|
||||||
|
</verification-checklist>
|
||||||
|
|
||||||
|
<standards>
|
||||||
|
- ALWAYS ensure WCAG 2.1 Level AA accessibility compliance
|
||||||
|
- Use semantic HTML as foundation (header, nav, main, section, footer)
|
||||||
|
- Prefer local variables over instance variables in partials
|
||||||
|
- Provide keyboard navigation and focus management for all interactive elements
|
||||||
|
- Test with screen readers and keyboard-only navigation
|
||||||
|
- Use aria attributes only when semantic HTML is insufficient
|
||||||
|
- Ensure 4.5:1 color contrast ratio for text
|
||||||
|
- Thread accessibility through all patterns
|
||||||
|
- Use form helpers to generate accessible forms with proper labels
|
||||||
|
</standards>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Partials & Layouts
|
||||||
|
|
||||||
|
Partials are reusable view fragments. Layouts define page structure. Together they create maintainable, consistent UIs.
|
||||||
|
|
||||||
|
### Basic Partials
|
||||||
|
|
||||||
|
<pattern name="simple-partial">
|
||||||
|
<description>Render partials with explicit local variables</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Shared directory %>
|
||||||
|
<%= render "shared/header" %>
|
||||||
|
|
||||||
|
<%# Explicit locals (preferred for clarity) %>
|
||||||
|
<%= render partial: "feedback", locals: { feedback: @feedback, show_actions: true } %>
|
||||||
|
|
||||||
|
<%# Partial definition: app/views/feedbacks/_feedback.html.erb %>
|
||||||
|
<div id="<%= dom_id(feedback) %>" class="card">
|
||||||
|
<h3><%= feedback.content %></h3>
|
||||||
|
<% if local_assigns[:show_actions] %>
|
||||||
|
<%= link_to "Edit", edit_feedback_path(feedback) %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why local_assigns?** Prevents `NameError` when variable not passed. Allows optional parameters with defaults.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="collection-rendering">
|
||||||
|
<description>Efficiently render partials for collections</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Shorthand - automatic partial lookup %>
|
||||||
|
<%= render @feedbacks %>
|
||||||
|
|
||||||
|
<%# Explicit collection with counter %>
|
||||||
|
<%= render partial: "feedback", collection: @feedbacks %>
|
||||||
|
|
||||||
|
<%# Partial with counters %>
|
||||||
|
<%# app/views/feedbacks/_feedback.html.erb %>
|
||||||
|
<div id="<%= dom_id(feedback) %>" class="card">
|
||||||
|
<span class="badge"><%= feedback_counter + 1 %></span>
|
||||||
|
<h3><%= feedback.content %></h3>
|
||||||
|
<% if feedback_iteration.first? %>
|
||||||
|
<span class="label">First</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Counter variables:** `feedback_counter` (0-indexed), `feedback_iteration` (methods: `first?`, `last?`, `index`, `size`)
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
### Layouts & Content Blocks
|
||||||
|
|
||||||
|
<pattern name="content-for">
|
||||||
|
<description>Customize layout sections from individual views</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# app/views/layouts/application.html.erb %>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title><%= content_for?(:title) ? yield(:title) : "App Name" %></title>
|
||||||
|
<%= csrf_meta_tags %>
|
||||||
|
<%= stylesheet_link_tag "application" %>
|
||||||
|
<%= yield :head %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%= render "shared/header" %>
|
||||||
|
<main id="main-content">
|
||||||
|
<%= render "shared/flash_messages" %>
|
||||||
|
<%= yield %>
|
||||||
|
</main>
|
||||||
|
<%= yield :scripts %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<%# app/views/feedbacks/show.html.erb %>
|
||||||
|
<% content_for :title, "#{@feedback.content.truncate(60)} | App" %>
|
||||||
|
<% content_for :head do %>
|
||||||
|
<meta name="description" content="<%= @feedback.content.truncate(160) %>">
|
||||||
|
<% end %>
|
||||||
|
<div class="feedback-detail"><%= @feedback.content %></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Using instance variables in partials</description>
|
||||||
|
<reason>Creates implicit dependencies, makes partials hard to reuse and test</reason>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ❌ BAD - Coupled to controller %>
|
||||||
|
<div class="feedback"><%= @feedback.content %></div>
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ✅ GOOD - Explicit dependencies %>
|
||||||
|
<div class="feedback"><%= feedback.content %></div>
|
||||||
|
<%= render "feedback", feedback: @feedback %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## View Helpers
|
||||||
|
|
||||||
|
View helpers are Ruby modules providing reusable methods for generating HTML, formatting data, and encapsulating view logic.
|
||||||
|
|
||||||
|
### Custom Helpers
|
||||||
|
|
||||||
|
<pattern name="status-badge-helper">
|
||||||
|
<description>Display status badges with consistent styling</description>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/helpers/application_helper.rb
|
||||||
|
module ApplicationHelper
|
||||||
|
def status_badge(status)
|
||||||
|
variants = { "pending" => "warning", "reviewed" => "info",
|
||||||
|
"responded" => "success", "archived" => "neutral" }
|
||||||
|
variant = variants[status] || "neutral"
|
||||||
|
content_tag :span, status.titleize, class: "badge badge-#{variant}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def page_title(title = nil)
|
||||||
|
base = "The Feedback Agent"
|
||||||
|
title.present? ? "#{title} | #{base}" : base
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Usage %>
|
||||||
|
<%= status_badge(@feedback.status) %>
|
||||||
|
<title><%= page_title(yield(:title)) %></title>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="text-helpers">
|
||||||
|
<description>Use built-in Rails text helpers for formatting</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%= truncate(@feedback.content, length: 150) %>
|
||||||
|
<%= time_ago_in_words(@feedback.created_at) %> ago
|
||||||
|
<%= pluralize(@feedbacks.count, "feedback") %>
|
||||||
|
<%= sanitize(user_content, tags: %w[p br strong em]) %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Using html_safe on user input</description>
|
||||||
|
<reason>XSS vulnerability - allows script execution</reason>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ❌ DANGEROUS
|
||||||
|
def render_content(content)
|
||||||
|
content.html_safe # XSS risk!
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ✅ SAFE - Auto-escaped or sanitized
|
||||||
|
def render_content(content)
|
||||||
|
content # Auto-escaped by Rails
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_html(content)
|
||||||
|
sanitize(content, tags: %w[p br strong])
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nested Forms
|
||||||
|
|
||||||
|
Build forms that handle parent-child relationships with `accepts_nested_attributes_for` and `fields_for`.
|
||||||
|
|
||||||
|
### Basic Nested Forms
|
||||||
|
|
||||||
|
<pattern name="has-many-nested-form">
|
||||||
|
<description>Form with has_many relationship using fields_for</description>
|
||||||
|
|
||||||
|
**Model:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# app/models/feedback.rb
|
||||||
|
class Feedback < ApplicationRecord
|
||||||
|
has_many :attachments, dependent: :destroy
|
||||||
|
accepts_nested_attributes_for :attachments,
|
||||||
|
allow_destroy: true,
|
||||||
|
reject_if: :all_blank
|
||||||
|
|
||||||
|
validates :content, presence: true
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Controller:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class FeedbacksController < ApplicationController
|
||||||
|
def new
|
||||||
|
@feedback = Feedback.new
|
||||||
|
3.times { @feedback.attachments.build } # Build empty attachments
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def feedback_params
|
||||||
|
params.expect(feedback: [
|
||||||
|
:content,
|
||||||
|
attachments_attributes: [
|
||||||
|
:id, # Required for updating existing records
|
||||||
|
:file,
|
||||||
|
:caption,
|
||||||
|
:_destroy # Required for marking records for deletion
|
||||||
|
]
|
||||||
|
])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**View:**
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%= form_with model: @feedback do |form| %>
|
||||||
|
<%= form.text_area :content, class: "textarea" %>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h3>Attachments</h3>
|
||||||
|
<%= form.fields_for :attachments do |f| %>
|
||||||
|
<div class="nested-fields card">
|
||||||
|
<%= f.file_field :file, class: "file-input" %>
|
||||||
|
<%= f.text_field :caption, class: "input" %>
|
||||||
|
<%= f.hidden_field :id if f.object.persisted? %>
|
||||||
|
<%= f.check_box :_destroy %> <%= f.label :_destroy, "Remove" %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%= form.submit class: "btn btn-primary" %>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Missing :id in strong parameters for updates</description>
|
||||||
|
<reason>Rails can't identify which existing records to update, creates duplicates instead</reason>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ❌ BAD - Missing :id
|
||||||
|
def feedback_params
|
||||||
|
params.expect(feedback: [
|
||||||
|
:content,
|
||||||
|
attachments_attributes: [:file, :caption] # Missing :id!
|
||||||
|
])
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# ✅ GOOD - Include :id for existing records
|
||||||
|
def feedback_params
|
||||||
|
params.expect(feedback: [
|
||||||
|
:content,
|
||||||
|
attachments_attributes: [:id, :file, :caption, :_destroy]
|
||||||
|
])
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Accessibility (WCAG 2.1 AA)
|
||||||
|
|
||||||
|
Ensure your Rails application is usable by everyone, including people with disabilities. Accessibility is threaded through ALL view patterns.
|
||||||
|
|
||||||
|
### Semantic HTML & ARIA
|
||||||
|
|
||||||
|
<pattern name="semantic-structure">
|
||||||
|
<description>Use semantic HTML5 elements with proper ARIA labels</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Semantic landmarks with skip link %>
|
||||||
|
<a href="#main-content" class="sr-only focus:not-sr-only">
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<h1>Feedback Application</h1>
|
||||||
|
<nav aria-label="Main navigation">
|
||||||
|
<ul>
|
||||||
|
<li><%= link_to "Home", root_path %></li>
|
||||||
|
<li><%= link_to "Feedbacks", feedbacks_path %></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="main-content">
|
||||||
|
<h2>Recent Feedback</h2>
|
||||||
|
<section aria-labelledby="pending-heading">
|
||||||
|
<h3 id="pending-heading">Pending Items</h3>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Screen readers use landmarks (header, nav, main, footer) and headings to navigate. Logical h1-h6 hierarchy (don't skip levels).
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="aria-labels">
|
||||||
|
<description>Provide accessible names for elements without visible text</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Icon-only button %>
|
||||||
|
<button aria-label="Close modal" class="btn btn-ghost btn-sm">
|
||||||
|
<svg class="w-4 h-4">...</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<%# Delete button with context %>
|
||||||
|
<%= button_to "Delete", feedback_path(@feedback),
|
||||||
|
method: :delete,
|
||||||
|
aria: { label: "Delete feedback from #{@feedback.sender_name}" },
|
||||||
|
class: "btn btn-error btn-sm" %>
|
||||||
|
|
||||||
|
<%# Modal with labelledby %>
|
||||||
|
<dialog aria-labelledby="modal-title" aria-modal="true">
|
||||||
|
<h3 id="modal-title">Feedback Details</h3>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
<%# Form field with hint %>
|
||||||
|
<%= form.text_field :email, aria: { describedby: "email-hint" } %>
|
||||||
|
<span id="email-hint">We'll never share your email</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<pattern name="aria-live-regions">
|
||||||
|
<description>Announce dynamic content changes to screen readers</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Flash messages with live region %>
|
||||||
|
<div aria-live="polite" aria-atomic="true">
|
||||||
|
<% if flash[:notice] %>
|
||||||
|
<div role="status" class="alert alert-success">
|
||||||
|
<%= flash[:notice] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if flash[:alert] %>
|
||||||
|
<div role="alert" class="alert alert-error">
|
||||||
|
<%= flash[:alert] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<%# Loading state %>
|
||||||
|
<div role="status" aria-live="polite" class="sr-only" data-loading-target="status">
|
||||||
|
<%# Updated via JS: "Submitting feedback, please wait..." %>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Values:** `aria-live="polite"` (announces when idle), `aria-live="assertive"` (interrupts), `aria-atomic="true"` (reads entire region).
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
### Keyboard Navigation & Focus Management
|
||||||
|
|
||||||
|
<pattern name="keyboard-accessibility">
|
||||||
|
<description>Ensure all interactive elements are keyboard accessible</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# Native elements - keyboard works by default %>
|
||||||
|
<button type="button" data-action="click->modal#open">Open Modal</button>
|
||||||
|
<%= button_to "Delete", feedback_path(@feedback), method: :delete %>
|
||||||
|
|
||||||
|
<%# Custom interactive element needs full keyboard support %>
|
||||||
|
<div tabindex="0" role="button"
|
||||||
|
data-action="click->controller#action keydown.enter->controller#action keydown.space->controller#action">
|
||||||
|
Custom Button
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Always provide visible focus indicators */
|
||||||
|
button:focus, a:focus, input:focus {
|
||||||
|
outline: 2px solid #3b82f6;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Events:** Enter and Space activate buttons. Tab navigates. Escape closes modals.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
### Accessible Forms
|
||||||
|
|
||||||
|
<pattern name="form-labels-errors">
|
||||||
|
<description>Associate labels with inputs and display errors accessibly</description>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%= form_with model: @feedback do |form| %>
|
||||||
|
<%# Error summary %>
|
||||||
|
<% if @feedback.errors.any? %>
|
||||||
|
<div role="alert" id="error-summary" tabindex="-1">
|
||||||
|
<h2><%= pluralize(@feedback.errors.count, "error") %> prohibited saving:</h2>
|
||||||
|
<ul>
|
||||||
|
<% @feedback.errors.full_messages.each do |msg| %>
|
||||||
|
<li><%= msg %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<%= form.label :content, "Your Feedback" %>
|
||||||
|
<%= form.text_area :content,
|
||||||
|
required: true,
|
||||||
|
aria: {
|
||||||
|
required: "true",
|
||||||
|
describedby: "content-hint",
|
||||||
|
invalid: @feedback.errors[:content].any? ? "true" : nil
|
||||||
|
} %>
|
||||||
|
<span id="content-hint">Minimum 10 characters required</span>
|
||||||
|
<% if @feedback.errors[:content].any? %>
|
||||||
|
<span id="content-error" role="alert">
|
||||||
|
<%= @feedback.errors[:content].first %>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Sender Information</legend>
|
||||||
|
<%= form.label :sender_name, "Name" %>
|
||||||
|
<%= form.text_field :sender_name %>
|
||||||
|
<%= form.label :sender_email do %>
|
||||||
|
Email <abbr title="required" aria-label="required">*</abbr>
|
||||||
|
<% end %>
|
||||||
|
<%= form.email_field :sender_email, required: true, autocomplete: "email" %>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<%= form.submit "Submit", data: { disable_with: "Submitting..." } %>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Labels provide accessible names. `role="alert"` announces errors. `aria-invalid` marks problematic fields.
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
### Color Contrast & Images
|
||||||
|
|
||||||
|
<pattern name="color-contrast">
|
||||||
|
<description>Ensure sufficient color contrast and accessible images</description>
|
||||||
|
|
||||||
|
**WCAG AA Requirements:**
|
||||||
|
- Normal text (< 18px): 4.5:1 ratio minimum
|
||||||
|
- Large text (≥ 18px or bold ≥ 14px): 3:1 ratio minimum
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ✅ GOOD - High contrast + icon + text (not color alone) %>
|
||||||
|
<span class="text-error">
|
||||||
|
<svg aria-hidden="true">...</svg>
|
||||||
|
<strong>Error:</strong> This field is required
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<%# Images - descriptive alt text %>
|
||||||
|
<%= image_tag "chart.png", alt: "Bar chart: 85% positive feedback in March 2025" %>
|
||||||
|
|
||||||
|
<%# Decorative images - empty alt %>
|
||||||
|
<%= image_tag "decoration.svg", alt: "", role: "presentation" %>
|
||||||
|
|
||||||
|
<%# Functional images - describe action %>
|
||||||
|
<%= link_to feedback_path(@feedback) do %>
|
||||||
|
<%= image_tag "view-icon.svg", alt: "View feedback details" %>
|
||||||
|
<% end %>
|
||||||
|
```
|
||||||
|
|
||||||
|
</pattern>
|
||||||
|
|
||||||
|
<antipattern>
|
||||||
|
<description>Using placeholder as label</description>
|
||||||
|
<reason>Placeholders disappear when typing and have insufficient contrast</reason>
|
||||||
|
<bad-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ❌ No label %>
|
||||||
|
<input type="email" placeholder="Enter your email">
|
||||||
|
```
|
||||||
|
|
||||||
|
</bad-example>
|
||||||
|
<good-example>
|
||||||
|
|
||||||
|
```erb
|
||||||
|
<%# ✅ Label + placeholder %>
|
||||||
|
<label for="email">Email Address</label>
|
||||||
|
<input type="email" id="email" placeholder="you@example.com">
|
||||||
|
```
|
||||||
|
|
||||||
|
</good-example>
|
||||||
|
</antipattern>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<testing>
|
||||||
|
**System Tests with Accessibility:**
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
# test/system/accessibility_test.rb
|
||||||
|
class AccessibilityTest < ApplicationSystemTestCase
|
||||||
|
test "form has accessible labels and ARIA" do
|
||||||
|
visit new_feedback_path
|
||||||
|
assert_selector "label[for='feedback_content']"
|
||||||
|
assert_selector "textarea#feedback_content[required][aria-required='true']"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "errors are announced with role=alert" do
|
||||||
|
visit new_feedback_path
|
||||||
|
click_button "Submit"
|
||||||
|
assert_selector "[role='alert']"
|
||||||
|
assert_selector "[aria-invalid='true']"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "keyboard navigation works" do
|
||||||
|
visit feedbacks_path
|
||||||
|
page.send_keys(:tab) # Should focus first interactive element
|
||||||
|
page.send_keys(:enter) # Should activate element
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# test/views/feedbacks/_feedback_test.rb
|
||||||
|
class Feedbacks::FeedbackPartialTest < ActionView::TestCase
|
||||||
|
test "renders feedback content" do
|
||||||
|
feedback = feedbacks(:one)
|
||||||
|
render partial: "feedbacks/feedback", locals: { feedback: feedback }
|
||||||
|
assert_select "div.card"
|
||||||
|
assert_select "h3", text: feedback.content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# test/helpers/application_helper_test.rb
|
||||||
|
class ApplicationHelperTest < ActionView::TestCase
|
||||||
|
test "status_badge returns correct badge" do
|
||||||
|
assert_includes status_badge("pending"), "badge-warning"
|
||||||
|
assert_includes status_badge("responded"), "badge-success"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual Testing Checklist:**
|
||||||
|
- Test with keyboard only (Tab, Enter, Space, Escape)
|
||||||
|
- Test with screen reader (NVDA, JAWS, VoiceOver)
|
||||||
|
- Test browser zoom (200%, 400%)
|
||||||
|
- Run axe DevTools or Lighthouse accessibility audit
|
||||||
|
- Validate HTML (W3C validator)
|
||||||
|
</testing>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<related-skills>
|
||||||
|
- rails-ai:hotwire - Add interactivity with Turbo and Stimulus
|
||||||
|
- rails-ai:styling - Style views with Tailwind and DaisyUI
|
||||||
|
- rails-ai:controllers - RESTful actions and strong parameters for form handling
|
||||||
|
- rails-ai:testing - View and system testing patterns
|
||||||
|
</related-skills>
|
||||||
|
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
**Official Documentation:**
|
||||||
|
- [Rails Guides - Layouts and Rendering](https://guides.rubyonrails.org/layouts_and_rendering.html)
|
||||||
|
- [Rails Guides - Action View Helpers](https://guides.rubyonrails.org/action_view_helpers.html)
|
||||||
|
- [Rails Guides - Rails Accessibility](https://guides.rubyonrails.org/accessibility.html)
|
||||||
|
|
||||||
|
**Accessibility Standards:**
|
||||||
|
- [WCAG 2.1 Quick Reference](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||||
|
- [WebAIM WCAG 2 Checklist](https://webaim.org/standards/wcag/checklist)
|
||||||
|
- [WAI-ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/)
|
||||||
|
|
||||||
|
**Tools:**
|
||||||
|
- [axe DevTools](https://www.deque.com/axe/devtools/) - Accessibility testing browser extension
|
||||||
|
|
||||||
|
</resources>
|
||||||
Reference in New Issue
Block a user