diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index d9751921d..138c038e9 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,48 +1,19 @@
-# This is a Ruby on Rails application.
+# This project is a Ruby on Rails application.
-For project overview, tech stack, architecture reference (models, controllers, services, testing), and more, read `AGENTS.md`.
-
-## Setup
-
-Full setup (bundle, npm, database create/migrate/seed):
-```
-bin/setup
-```
-
-If you just need frontend dependencies:
-```
-npm ci
-```
-
-## AI Instruction Files
-
-When the user says "AI files", "AI instructions", "tell AI to", or "remember to always", these are the files.
-If you notice the user repeatedly correcting the same pattern, suggest adding it to the AI files with a concrete proposal.
-
-| File | Purpose |
-|---|---|
-| `CLAUDE.md` | Coding rules and conventions (this file) |
-| `AGENTS.md` | Architecture reference + project details |
-| `.github/copilot-instructions.md` | Coding rules for Copilot (duplicated from CLAUDE.md — keep in sync) |
-| `ai/` | Shell script shortcuts for common dev tasks |
-
-## Related Files
-
-When changing a model or controller, check whether these related files need updates:
-
-| If you change... | Also check... |
-|---|---|
-| Model | Decorator, policy, factory, model spec |
-| Controller | Policy, request spec, routing spec, views |
-| View | System spec, Stimulus controller (if interactive) |
-| Service | Service spec |
-| Decorator | Decorator spec |
-| Mailer (add/remove) | Mailer spec, mailer preview (follow existing patterns) |
-| Add/remove model, concern, service, or gem | AGENTS.md |
+# Frontend requirements:
+- Strongly prefer Stimulus for JavaScript behavior — do not write raw/inline JS or jQuery
+- Always use Tailwind CSS utility classes for styling — do not write custom CSS unless absolutely necessary
+- Prefer Turbo for navigation and form submissions before reaching for Stimulus
+- ES6+ syntax, ESM imports/exports
+- Stimulus controller naming: `[name]_controller.js`
-## Code Style
+# PRs
+- After completing work, create a pull request using `gh pr create`
+- Once the PR is created, prepend the PR number to the branch name (e.g., rename `maebeale/fix-login` to `maebeale/1234-fix-login`) using `git branch -m` and `git push origin -u` with the new name, then delete the old remote branch
+- On every push, update the PR title and description to reflect the current diff
+# Code style requirements:
- Use modern Ruby syntax
- Prefer early returns and guard clauses
- Avoid unnecessary and/or complex conditionals
@@ -51,59 +22,56 @@ When changing a model or controller, check whether these related files need upda
- Use `presence` over blank checks
- Use `Arel.sql` for raw SQL in order clauses
- Avoid `update_all` unless explicitly intended
-- Prefer service objects under app/services/
+- Prefer service objects under app/services
- Prefer POROs over concerns when possible
- Use `after_commit` instead of `after_save` for side effects
-## RuboCop (rubocop-rails-omakase)
-
+# RuboCop (rubocop-rails-omakase)
This project uses rubocop-rails-omakase. All code MUST follow these rules:
-### Strings
-- **Always use double quotes** for strings: `"foo"` not `'foo'`
-
-### Spacing
-- **Spaces inside array brackets:** `[ a, b, c ]` not `[a, b, c]` (empty arrays: `[]`)
-- **Spaces inside hash braces:** `{ a: 1, b: 2 }` not `{a: 1}` (empty hashes: `{}`)
-- **Spaces inside block braces:** `foo { bar }` not `foo {bar}` (empty blocks: `foo { }`)
-- **No spaces inside parens:** `foo(bar)` not `foo( bar )`
-- **No spaces inside reference brackets:** `hash[:key]` not `hash[ :key ]`
-- **Space before block braces:** `foo { }` not `foo{ }`
-
-### Commas
-- **No trailing commas** in arrays, hashes, or method arguments
-
-### Indentation
-- **2-space indentation**, no tabs
-- **Consistent indentation** at normal level — do NOT indent methods under `private`/`protected`
-- **Align `end` with the variable** in assignments:
- ```ruby
- result = if condition
- value
- end
- ```
-- **Align `when` with `end`**, not with `case`
-
-### Whitespace
-- **No trailing whitespace** on any line
-- **No trailing blank lines** at end of file
-- **No empty lines** inside class, module, method, or block bodies
-
-### Syntax
-- **Use `%w[]` and `%i[]`** with square bracket delimiters (not parens)
-- **Use modern hash syntax:** `{ key: value }` not `{ :key => value }`
-- **No redundant returns** — omit `return` on last expression
-- **Use `flat_map`** instead of `.map { }.flatten`
-- **No redundant `.to_s`** inside string interpolation
-- **Use `Foo.method`** not `Foo::method` for method calls
-- **No parentheses around conditions:** `if foo` not `if (foo)`
-- **No semicolons** to separate statements
-
-## HTML/ERB Formatting
-
-### Tag Attributes
-- **Closing `>` on same line as last attribute** — do not put `>` on its own line
-- When attributes span multiple lines, keep the closing `>` with the last attribute
+## Strings
+- Always use double quotes: `"foo"` not `'foo'`
+
+## Spacing
+- Spaces inside array brackets: `[ a, b, c ]` not `[a, b, c]` (empty arrays: `[]`)
+- Spaces inside hash braces: `{ a: 1, b: 2 }` not `{a: 1}` (empty hashes: `{}`)
+- Spaces inside block braces: `foo { bar }` not `foo {bar}` (empty blocks: `foo { }`)
+- No spaces inside parens: `foo(bar)` not `foo( bar )`
+- No spaces inside reference brackets: `hash[:key]` not `hash[ :key ]`
+- Space before block braces: `foo { }` not `foo{ }`
+
+## Commas
+- No trailing commas in arrays, hashes, or method arguments
+
+## Indentation
+- 2-space indentation, no tabs
+- Consistent indentation at normal level — do NOT indent methods under `private`/`protected`
+- Align `end` with the variable in assignments
+- Align `when` with `end`, not with `case`
+
+## Whitespace
+- No trailing whitespace on any line
+- No trailing blank lines at end of file
+- No empty lines inside class, module, method, or block bodies
+
+## Syntax
+- Use `%w[]` and `%i[]` with square bracket delimiters (not parens)
+- Use modern hash syntax: `{ key: value }` not `{ :key => value }`
+- No redundant returns — omit `return` on last expression
+- Use `flat_map` instead of `.map { }.flatten`
+- No redundant `.to_s` inside string interpolation
+- Use `Foo.method` not `Foo::method` for method calls
+- No parentheses around conditions: `if foo` not `if (foo)`
+- No semicolons to separate statements
+
+# Git
+- When rebasing onto main, review incoming changes for their intent and flag any oversights — missing tests, incomplete migrations, broken assumptions, or conflicts between the two branches. Check both directions: schema/model changes on either branch that affect views, partials, or layouts on the other (e.g., main redesigned a table's CSS but your branch adds new columns to it, or vice versa)
+
+# HTML/ERB Formatting
+
+## Tag attributes
+- When a tag has long attributes, place the closing `>` on the same line as the last attribute
+- Do NOT put the closing `>` on its own line
- Example (GOOD):
```erb
```
-
-## JavaScript
-
-- ES6+ syntax, ESM imports/exports, `const`/`let` (no `var`)
-- Use `const` for fixed values — not `SCREAMING_SNAKE_CASE` constants (e.g., `const styleId = "foo"` not `const STYLE_ID = "foo"`)
-- **Strongly prefer Stimulus** for JavaScript behavior — do not write raw/inline JS or jQuery
-- **Always use Tailwind CSS** utility classes for styling — do not write custom CSS unless absolutely necessary
-- **Prefer Font Awesome (free)** icons over inline SVGs — use `icon("fa-solid fa-foo")` helper. Inline SVGs are acceptable when a specific icon design is preferred.
-- Prefer Turbo for navigation and form submissions before reaching for Stimulus
-- Controller naming: `[name]_controller.js`
-- Keep controllers focused and small
-
-### Stimulus Conventions
-
-Follow the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introduction) and reference docs. Key rules:
-
-**Targets over querySelector** — declare `static targets = [...]` and use `data-[controller]-target` attributes in views. Never use `this.element.querySelector` or `document.getElementById` to find elements that could be targets. Exception: elements outside the controller's scope (e.g., in a parent view).
-
-**Values API for state** — use `static values = { name: Type }` for any state that persists or drives UI. Do not store state in instance variables when a value would work. Use `[name]ValueChanged()` callbacks for reactive updates instead of manual syncing.
-
-**Actions over manual listeners** — use `data-action` attributes instead of `addEventListener` in `connect()`. Omit the event when it's the default for the element (`click` for buttons/links, `input` for inputs/textareas, `submit` for forms, `change` for selects). Use `@window` or `@document` suffixes for global events when possible (e.g., `resize@window->controller#layout`). Use action options like `:prevent` and `:stop` instead of calling `event.preventDefault()` in methods.
-
-**Classes API for CSS** — use `static classes = [...]` when CSS classes need to be configurable from HTML. For standard Tailwind utilities used internally (e.g., `"hidden"`), hardcoding is acceptable.
-
-**Outlets for cross-controller communication** — use `static outlets = [...]` to reference other controllers instead of `document.getElementById` or custom events when the relationship is stable.
-
-**Lifecycle discipline** — every listener, timer, or observer created in `connect()` must be cleaned up in `disconnect()`. Store bound handler references so they can be removed. Use `initialize()` for one-time setup (e.g., binding functions).
-
-**Target lifecycle callbacks** — use `[name]TargetConnected(element)` and `[name]TargetDisconnected(element)` to respond to dynamically added/removed targets (e.g., cocoon nested fields, Turbo streams).
-
-**Visibility** — toggle the `hidden` class via `classList.toggle("hidden", condition)` instead of setting `style.display`. Use `class="hidden"` in HTML for initial hidden state, not `style="display:none"`.
-
-## Migrations
-
-- Name migration files using **UTC timestamps** (e.g., `20260228143000`), not sequential numbers (e.g., `20260228000007`)
-- Multiple branches adding migrations on the same date will collide if they use sequential numbering
-
-## Git
-
-- Default branch is `main`
-- Commit messages should explain why, not what
-- CI runs via GitHub Actions (`.github/workflows/`)
-- **When rebasing onto main**, review incoming changes for their intent and flag any oversights — missing tests, incomplete migrations, broken assumptions, or conflicts between the two branches. Check both directions: schema/model changes on either branch that affect views, partials, or layouts on the other (e.g., main redesigned a table's CSS but your branch adds new columns to it, or vice versa)
-
-## PRs
-
-- **Push to a draft PR early** — push commits and create a draft PR (`gh pr create --draft`) as soon as work begins, rather than keeping changes in a local branch. Push on every commit.
-- After completing work, **mark the PR ready** using `gh pr ready`
-- Once the PR is created, **prepend the PR number to the branch name** (e.g., rename `maebeale/fix-login` to `maebeale/1234-fix-login`) using `git branch -m` and `git push origin -u` with the new name, then delete the old remote branch
-- Use `docs/pull_request_template.md` for PR description structure
-- Use bullet points, not paragraphs, when filling out each section
-- Description must explain why the change was made, not just what
-- Include screenshots for UI changes
-- **On every push**, update the PR title and content to reflect the current diff
-- **On every push**, update AI instruction files if the diff adds, removes, or renames anything tracked in AGENTS.md — specifically: Stimulus controllers, services, model/controller concerns, mailers, rake tasks, and directory file counts
-
-## Quick Commands
-
-See `ai/` directory for executable scripts:
-
-| Command | What it does |
-|---|---|
-| `ai/test [args]` | Run RSpec |
-| `ai/lint` | Rubocop on all files |
-| `ai/lint --fix` | Auto-fix lint issues |
-| `ai/server` | Start dev services (web + vite) |
-| `ai/console` | Rails console |
-| `ai/routes -g pattern` | Search Rails routes |
-| `ai/db-migrate` | Run database migrations |
diff --git a/AGENTS.md b/AGENTS.md
index c9f365a00..4965990ed 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,38 +1,15 @@
-# AGENTS.md
+# AGENTS.md — AWBW Portal
-Architecture reference for AI agents. For coding rules and conventions, see `CLAUDE.md` (single source of truth).
+Architecture reference for AI agents working on the AWBW Portal codebase. For coding rules and quick commands, see `CLAUDE.md`.
-## Project Overview
+## Project Summary
-AWBW Portal is a Rails 8.1 application (Ruby 4.0.1) for A Window Between Worlds — a platform where workshop leaders manage workshops, resources, community news, stories, and events.
-
-## Tech Stack
-
-- **Backend:** Rails 8.1, Ruby 4.0.1, MySQL (via Trilogy adapter)
-- **Frontend:** Vite, Tailwind CSS v4, Stimulus, Turbo Rails
-- **Auth:** Devise with JWT token support
-- **Authorization:** ActionPolicy (app/policies/)
-- **Rich text:** ActionText with Rhino editor (TipTap-based)
-- **File uploads:** ActiveStorage with DigitalOcean Spaces
-- **Background jobs:** SolidQueue
-- **Caching:** SolidCache
-
-## Setup
-
-Full setup (bundle, npm, database create/migrate/seed):
-```
-bin/setup
-```
-
-If you just need frontend dependencies:
-```
-npm ci
-```
+AWBW Portal is a Rails 8.1 application (Ruby 4.0.1) for A Window Between Worlds — a platform where workshop leaders manage workshops, resources, community news, stories, and events. It uses MySQL, Vite, Tailwind CSS v4, and the Hotwire stack (Stimulus + Turbo).
## Architecture Overview
```
-This codebase (Rails 8.1)
+AWBW Portal (Rails 8.1)
├── Main app (app/) — Workshops, resources, stories, events, people, organizations
├── Frontend — Stimulus + Turbo + Tailwind CSS v4 (Vite bundler)
├── Background jobs — SolidQueue
@@ -48,19 +25,19 @@ This codebase (Rails 8.1)
| Directory | Purpose | Count |
|---|---|---|
-| `app/models/` | ActiveRecord models | ~66 files |
-| `app/services/` | Service objects for complex logic | ~21 files |
-| `app/jobs/` | SolidQueue background jobs | 3 files |
-| `app/models/concerns/` | Shared model modules | 12 concerns |
+| `app/models/` | ActiveRecord models | ~78 files |
+| `app/services/` | Service objects for complex logic | ~15 files |
+| `app/jobs/` | SolidQueue background jobs | 2 files |
+| `app/models/concerns/` | Shared model modules | ~11 concerns |
### Presentation
| Directory | Purpose | Count |
|---|---|---|
-| `app/controllers/` | Rails controllers (admin/, events/) | ~63 files |
-| `app/views/` | ERB templates | ~465 files |
+| `app/controllers/` | Rails controllers (admin/, events/) | ~61 files |
+| `app/views/` | ERB templates | ~434 files |
| `app/decorators/` | Draper decorators for view logic | ~36 files |
-| `app/policies/` | ActionPolicy authorization rules | ~44 files |
+| `app/policies/` | ActionPolicy authorization rules | ~43 files |
| `app/presenters/` | Presentation objects | 1 file |
| `app/helpers/` | View helpers | ~19 files |
| `app/mailers/` | ActionMailer classes | 6 files |
@@ -71,7 +48,7 @@ This codebase (Rails 8.1)
| Directory | Purpose |
|---|---|
| `app/frontend/entrypoints/` | Vite entry points (application.js, application.css) |
-| `app/frontend/javascript/controllers/` | Stimulus controllers (34) |
+| `app/frontend/javascript/controllers/` | Stimulus controllers (~32) |
| `app/frontend/javascript/rhino/` | Rich text editor customizations (mentions, grid) |
| `app/frontend/stylesheets/` | Tailwind CSS and component styles |
@@ -81,7 +58,7 @@ This codebase (Rails 8.1)
|---|---|
| `config/routes.rb` | All routes (single file) |
| `config/database.yml` | MySQL via Trilogy adapter |
-| `config/initializers/` | ~28 initializer files |
+| `config/initializers/` | ~32 initializer files |
| `.github/workflows/` | GitHub Actions CI |
| `Procfile.dev` | Dev services: `vite` + `web` |
| `ai/` | Shell script shortcuts for common dev tasks (see `ai/README.md`) |
@@ -118,24 +95,23 @@ This codebase (Rails 8.1)
| Concern | Purpose |
|---|---|
-| `AhoyTrackable` | Event tracking integration |
-| `AuthorCreditable` | Author attribution |
| `Featureable` | `featured`, `publicly_featured` scopes |
-| `Mentioner` | ActionText @mention extraction and grouping |
-| `NameFilterable` | Name-based filtering |
| `Publishable` | `published`, `publicly_visible` scopes |
-| `PunctuationStrippable` | Strips punctuation from strings |
-| `RemoteSearchable` | AJAX remote search by column |
-| `RichTextSearchable` | Full-text search on ActionText rich_text fields |
| `TagFilterable` | Scope-based filtering by tag names |
| `Trendable` | Trending metrics tracking |
| `WindowsTypeFilterable` | Filter by WindowsType association |
+| `RemoteSearchable` | AJAX remote search by column |
+| `RichTextSearchable` | Full-text search on ActionText rich_text fields |
+| `Mentioner` | ActionText @mention extraction and grouping |
+| `NameFilterable` | Name-based filtering |
+| `PunctuationStrippable` | Strips punctuation from strings |
+| `AhoyTrackable` | Event tracking integration |
## Controllers
### Namespaces
-- **Root level** (~51 controllers): Workshops, stories, resources, events, people, organizations, etc.
+- **Root level** (~48 controllers): Workshops, stories, resources, events, people, organizations, etc.
- **`admin/`**: HomeController, AnalyticsController, AhoyActivitiesController
- **`events/`**: Registrations sub-resource (create/destroy + slug-based show at `/registration/:slug`)
- **Devise overrides**: Registrations, Confirmations, Passwords
@@ -159,7 +135,6 @@ end
- `AhoyTracking` — Event tracking integration
- `Dedupable` — Data deduplication helpers
- `ExternallyRedirectable` — External URL redirection
-- `TagAssignable` — Tag assignment helpers
## Services
@@ -178,27 +153,9 @@ end
- `PersonFromUserService` — Create Person from User account
- `BulkInviteService` — Bulk send welcome instructions and reset created_at for users
- `ModelDeduper` — Deduplication logic
-- `RichTextMigrator` — Rich text migration utility
-- `DisplayImagePresenter` — Image display logic
-
-### Event Registrations
-
-- `EventRegistrationServices::ProcessConfirmation` — Registration confirmation flow
-- `EventRegistrationServices::PublicRegistration` — Public registration handling
-- `ExtendedEventRegistrationFormBuilder` — Extended registration form builder
-- `ShortEventRegistrationFormBuilder` — Short registration form builder
-- `ScholarshipApplicationFormBuilder` — Scholarship form builder
-
-### Notifications
-
- `NotificationServices::CreateNotification` — Notification creation
- `NotificationServices::PersistDeliveredEmail` — Email delivery tracking
-### User Management
-
-- `UserServices::ProcessEmailChange` — Email change processing
-- `UserServices::ProcessEmailManualConfirm` — Manual email confirmation
-
## Decorators (Draper)
All inherit from `ApplicationDecorator` which provides:
@@ -239,7 +196,7 @@ end
## Mailers
-- `ApplicationMailer` — Base, from: `ENV["REPLY_TO_EMAIL"]`
+- `ApplicationMailer` — Base, from: `ENV["REPLY_TO_EMAIL"]` (programs@awbw.org)
- `DeviseMailer` — Custom Devise emails
- `EventMailer` — Event registration confirmations
- `NotificationMailer` — Notification delivery
@@ -249,42 +206,28 @@ end
## Frontend
-### Stimulus Controllers
+### Preferences
+
+- **Strongly prefer Stimulus** for JavaScript behavior — do not write raw/inline JS or jQuery
+- **Always use Tailwind CSS** utility classes for styling — do not write custom CSS unless absolutely necessary
+- Prefer Turbo for navigation and form submissions before reaching for Stimulus
+
+### Stimulus Controllers (32)
-- `affiliation_dates` — Recalculate affiliation date ranges
-- `anchor_highlight` — Highlight anchored elements
+Key controllers:
- `asset_picker` — Asset selection UI
- `autosave` — Auto-save form state
- `carousel` — Swiper-based carousels
- `cocoon` — Nested form handling (cocoon gem)
-- `collection` — Filter form auto-submit with debounce
-- `column_toggle` — Toggle table column visibility
-- `comment_edit_toggle` — Inline comment editing mode
-- `confirm_email` — Email confirmation UI
- `dirty_form` — Unsaved changes detection
-- `dismiss` — Dismissable elements
- `dropdown` — Dropdown menus with keyboard/click-outside handling
- `file_preview` — File upload preview
-- `inactive_toggle` — Gray out expired affiliations
- `optimistic_bookmark` — Instant bookmark UI feedback
-- `org_toggle` — Organization toggle UI
-- `paginated_fields` — Client-side pagination of nested fields
-- `password_toggle` — Show/hide password fields
-- `prefetch_lazy` — Prefetch lazy-loaded content
-- `print_options` — Print options toggle for analytics
- `remote_select` — AJAX-powered select dropdown
-- `rhino_source` — Rich text editor integration
-- `searchable_checkbox` — TomSelect checkbox-style multi-select
- `searchable_select` — Tom Select autocomplete
-- `share_url` — URL sharing/copying
- `sortable` — Drag-drop sorting (SortableJS)
- `tabs` — Tab panel navigation
-- `tag_link_loading` — Loading state for tag links
-- `tags_combination_highlight` — Highlight tags matching selected filters
-- `tags_sync_list_heights` — Sync tag list column heights
-- `timeframe` — Date range filtering
-- `toggle_lock` — Lock/unlock toggle UI
-- `toggle_user_icon` — User icon visibility toggle
+- `rhino_source` — Rich text editor integration
### JS Dependencies
@@ -310,17 +253,17 @@ Custom colors defined in `app/frontend/stylesheets/application.tailwind.css`:
| Directory | Count | Purpose |
|---|---|---|
-| `spec/models/` | ~58 | Model unit tests |
-| `spec/views/` | ~73 | View template tests |
-| `spec/requests/` | ~47 | HTTP request/integration tests |
-| `spec/system/` | ~25 | End-to-end browser tests (Capybara) |
+| `spec/models/` | ~54 | Model unit tests |
+| `spec/views/` | ~72 | View template tests |
+| `spec/requests/` | ~41 | HTTP request/integration tests |
+| `spec/system/` | ~30 | End-to-end browser tests (Capybara) |
| `spec/routing/` | ~13 | Route definition tests |
| `spec/policies/` | ~9 | Authorization policy tests |
-| `spec/decorators/` | ~10 | Decorator tests |
-| `spec/services/` | ~12 | Service object tests |
-| `spec/mailers/` | ~5 | Mailer tests |
-| `spec/helpers/` | ~1 | Helper tests |
-| `spec/factories/` | ~53 | FactoryBot factory definitions |
+| `spec/decorators/` | ~8 | Decorator tests |
+| `spec/services/` | ~4 | Service object tests |
+| `spec/mailers/` | ~3 | Mailer tests |
+| `spec/helpers/` | ~2 | Helper tests |
+| `spec/factories/` | ~52 | FactoryBot factory definitions |
### Configuration
@@ -344,15 +287,6 @@ Common factory traits across models:
- `:admin` (User with super_user=true)
- `:with_primary_asset`, `:with_gallery_assets`
-## Linting & Security
-
-```
-bundle exec rubocop # lint
-bundle exec rubocop -a # auto-fix
-bundle exec brakeman # security scan
-bundle exec bundle-audit check --update
-```
-
## CI Pipeline (GitHub Actions)
### ci.yml
@@ -364,6 +298,11 @@ bundle exec bundle-audit check --update
RuboCop linting on PRs and pushes to main.
+## PR Workflow
+
+- After completing work, create a pull request using `gh pr create`
+- Once the PR is created, prepend the PR number to the branch name (e.g., rename `maebeale/fix-login` to `maebeale/1234-fix-login`) using `git branch -m` and `git push origin -u` with the new name, then delete the old remote branch
+
## Key Library Usage
| Need | Library |
@@ -382,10 +321,18 @@ RuboCop linting on PRs and pushes to main.
| Email styling | Premailer-rails (inline CSS) |
| Positioning | Positioning gem for ordered records |
+## PRs
+
+- On every push, update the PR title and description to reflect the current diff
+
+## Git
+
+- When rebasing onto main, review incoming changes for their intent and flag any oversights — missing tests, incomplete migrations, broken assumptions, or conflicts between the two branches. Check both directions: schema/model changes on either branch that affect views, partials, or layouts on the other (e.g., main redesigned a table's CSS but your branch adds new columns to it, or vice versa)
+
## Rake Tasks
-Located in `lib/tasks/` (4 files):
+Located in `lib/tasks/` (~17 files). Notable:
- `dev.rake` — Development database seeding from XML/CSV
+- `paperclip_to_active_storage.rake` — File upload migration
- `rhino_migrator.rake` — Rich text editor migration
-- `attachment_report.rake` — Attachment reporting
-- `migrate_internal_id_to_filemaker_code.rake` — FileMaker code migration
+- `tag_deduping.rake` — Tag deduplication
diff --git a/CLAUDE.md b/CLAUDE.md
index 8108c0526..937320ff2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,7 +1,23 @@
-# This is a Ruby on Rails application.
-
+# CLAUDE.md
-For project overview, tech stack, architecture reference (models, controllers, services, testing), and more, read `AGENTS.md`.
+## Architecture Reference
+
+For detailed architecture, models, controllers, services, and testing structure, read `AGENTS.md`.
+
+## Project Overview
+
+This is a Ruby on Rails 8.1 application (Ruby 4.0.1) — the Portal for A Window Between Worlds (AWBW). It manages workshops, resources, community news, stories, and events for workshop leaders.
+
+## Tech Stack
+
+- **Backend:** Rails 8.1, Ruby 4.0.1, MySQL (via Trilogy adapter)
+- **Frontend:** Vite, Tailwind CSS v4, Stimulus, Turbo Rails
+- **Auth:** Devise with JWT token support
+- **Authorization:** ActionPolicy (app/policies/)
+- **Rich text:** ActionText with Rhino editor (TipTap-based)
+- **File uploads:** ActiveStorage with DigitalOcean Spaces
+- **Background jobs:** SolidQueue
+- **Caching:** SolidCache
## Setup
@@ -15,32 +31,6 @@ If you just need frontend dependencies:
npm ci
```
-## AI Instruction Files
-
-When the user says "AI files", "AI instructions", "tell AI to", or "remember to always", these are the files.
-If you notice the user repeatedly correcting the same pattern, suggest adding it to the AI files with a concrete proposal.
-
-| File | Purpose |
-|---|---|
-| `CLAUDE.md` | Coding rules and conventions (this file) |
-| `AGENTS.md` | Architecture reference + project details |
-| `.github/copilot-instructions.md` | Coding rules for Copilot (duplicated from CLAUDE.md — keep in sync) |
-| `ai/` | Shell script shortcuts for common dev tasks |
-
-## Related Files
-
-When changing a model or controller, check whether these related files need updates:
-
-| If you change... | Also check... |
-|---|---|
-| Model | Decorator, policy, factory, model spec |
-| Controller | Policy, request spec, routing spec, views |
-| View | System spec, Stimulus controller (if interactive) |
-| Service | Service spec |
-| Decorator | Decorator spec |
-| Mailer (add/remove) | Mailer spec, mailer preview (follow existing patterns) |
-| Add/remove model, concern, service, or gem | AGENTS.md |
-
## Code Style
- Use modern Ruby syntax
@@ -104,48 +94,80 @@ This project uses rubocop-rails-omakase. All code MUST follow these rules:
### Tag Attributes
- **Closing `>` on same line as last attribute** — do not put `>` on its own line
- When attributes span multiple lines, keep the closing `>` with the last attribute
-- Example (GOOD):
- ```erb
-
- ```
-- Example (BAD):
- ```erb
-
- ```
+- Good: `
` or `
`
+- Bad: `
`
-## JavaScript
+## Related Files
-- ES6+ syntax, ESM imports/exports, `const`/`let` (no `var`)
-- Use `const` for fixed values — not `SCREAMING_SNAKE_CASE` constants (e.g., `const styleId = "foo"` not `const STYLE_ID = "foo"`)
-- **Strongly prefer Stimulus** for JavaScript behavior — do not write raw/inline JS or jQuery
-- **Always use Tailwind CSS** utility classes for styling — do not write custom CSS unless absolutely necessary
-- **Prefer Font Awesome (free)** icons over inline SVGs — use `icon("fa-solid fa-foo")` helper. Inline SVGs are acceptable when a specific icon design is preferred.
-- Prefer Turbo for navigation and form submissions before reaching for Stimulus
-- Controller naming: `[name]_controller.js`
-- Keep controllers focused and small
+When changing a model or controller, check whether these related files need updates:
-### Stimulus Conventions
+| If you change... | Also check... |
+|---|---|
+| Model | Decorator, policy, factory, model spec |
+| Controller | Policy, request spec, routing spec, views |
+| View | System spec, Stimulus controller (if interactive) |
+| Service | Service spec |
+| Decorator | Decorator spec |
+| Add/remove model, concern, service, or gem | AGENTS.md, `.github/copilot-instructions.md` |
+| Code style rules | `.github/copilot-instructions.md` (keep in sync) |
-Follow the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introduction) and reference docs. Key rules:
+## Key Directories
-**Targets over querySelector** — declare `static targets = [...]` and use `data-[controller]-target` attributes in views. Never use `this.element.querySelector` or `document.getElementById` to find elements that could be targets. Exception: elements outside the controller's scope (e.g., in a parent view).
+- `app/services/` — Business logic service objects
+- `app/decorators/` — Draper decorators for view presentation
+- `app/policies/` — ActionPolicy authorization rules
+- `app/presenters/` — Presentation objects
+- `app/frontend/` — Vite/JS components (Stimulus controllers, etc.)
-**Values API for state** — use `static values = { name: Type }` for any state that persists or drives UI. Do not store state in instance variables when a value would work. Use `[name]ValueChanged()` callbacks for reactive updates instead of manual syncing.
+## Testing
-**Actions over manual listeners** — use `data-action` attributes instead of `addEventListener` in `connect()`. Omit the event when it's the default for the element (`click` for buttons/links, `input` for inputs/textareas, `submit` for forms, `change` for selects). Use `@window` or `@document` suffixes for global events when possible (e.g., `resize@window->controller#layout`). Use action options like `:prevent` and `:stop` instead of calling `event.preventDefault()` in methods.
+- **Framework:** RSpec (`bundle exec rspec`)
+- **Factories:** FactoryBot (spec/factories/)
+- **Matchers:** Shoulda Matchers
+- **System tests:** Capybara with Selenium
+- **Coverage:** SimpleCov
-**Classes API for CSS** — use `static classes = [...]` when CSS classes need to be configurable from HTML. For standard Tailwind utilities used internally (e.g., `"hidden"`), hardcoding is acceptable.
+Run all tests:
+```
+bundle exec rspec
+```
-**Outlets for cross-controller communication** — use `static outlets = [...]` to reference other controllers instead of `document.getElementById` or custom events when the relationship is stable.
+Run a single test file:
+```
+bundle exec rspec spec/models/some_model_spec.rb
+```
-**Lifecycle discipline** — every listener, timer, or observer created in `connect()` must be cleaned up in `disconnect()`. Store bound handler references so they can be removed. Use `initialize()` for one-time setup (e.g., binding functions).
+## Linting
-**Target lifecycle callbacks** — use `[name]TargetConnected(element)` and `[name]TargetDisconnected(element)` to respond to dynamically added/removed targets (e.g., cocoon nested fields, Turbo streams).
+```
+bundle exec rubocop
+```
-**Visibility** — toggle the `hidden` class via `classList.toggle("hidden", condition)` instead of setting `style.display`. Use `class="hidden"` in HTML for initial hidden state, not `style="display:none"`.
+Auto-fix:
+```
+bundle exec rubocop -a
+```
+
+## Security Scanning
+
+```
+bundle exec brakeman
+bundle exec bundle-audit check --update
+```
+
+## JavaScript
+
+- ES6+ syntax, ESM imports/exports
+- **Strongly prefer Stimulus** for JavaScript behavior — do not write raw/inline JS or jQuery
+- **Always use Tailwind CSS** utility classes for styling — do not write custom CSS unless absolutely necessary
+- Prefer Turbo for navigation and form submissions before reaching for Stimulus
+- Controller naming: `[name]_controller.js`
+- Keep controllers focused and small
+- **Use Stimulus targets and data attributes** to reference DOM elements — avoid `this.element.querySelector` and direct DOM queries. Declare `static targets = [...]` and use `data-[controller]-target` attributes in views.
+- **Use Stimulus shorthand action descriptors and shorthand pairs** — omit the event when it's the default for that element (e.g., `input` for ``/`