Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:08:30 +08:00
commit 773b898589
19 changed files with 11663 additions and 0 deletions

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

File diff suppressed because it is too large Load Diff

260
skills/debugging/SKILL.md Normal file
View 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
View 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>

View 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
View 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
View 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>&copy; 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1575
skills/security/SKILL.md Normal file

File diff suppressed because it is too large Load Diff

434
skills/styling/SKILL.md Normal file
View 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

File diff suppressed because it is too large Load Diff

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