Skip to content

Latest commit

 

History

History
84 lines (63 loc) · 11.3 KB

File metadata and controls

84 lines (63 loc) · 11.3 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this app is

CASA is a Rails app used by CASA (Court Appointed Special Advocate) chapters to track volunteer work with foster youth. The domain shape: a CasaCase (a youth) is assigned to a Volunteer, who logs CaseContacts; Supervisors oversee volunteers; CasaAdmins run the chapter. The app is multi-tenant — every record belongs to a CasaOrg, and cross-org data leaks are the most important class of bug to avoid.

Common commands

Task Command
One-time setup bin/setup
Run app (web + JS/CSS watchers) bin/dev then visit http://localhost:3000
Run full RSpec suite bin/rails spec
Run a single spec file bundle exec rspec spec/path/to/file_spec.rb
Run a single example bundle exec rspec spec/path/to/file_spec.rb:LINE
Run JS tests npm run test
Run all linters w/ autofix bin/lint (standardrb + erb_lint + standardjs + factory_bot:lint)
Update env after pulling main bin/update (migrate, bundle, npm, after_party)
Run post-deploy tasks bundle exec rake after_party:run
Local mailer previews http://localhost:3000/rails/mailers

Seed login credentials (password 12345678 for all):

  • volunteer1@example.com, supervisor1@example.com, casa_admin1@example.com at /users/sign_in
  • allcasaadmin@example.com at /all_casa_admins/sign_in

Tech stack

Rails 7.2 on Ruby (.ruby-version), PostgreSQL, Hotwire (Turbo + Stimulus), Bootstrap 5, ESBuild, Devise + Devise-Invitable, Pundit, Draper, ViewComponent, Delayed Job, Flipper, Paranoia (soft deletes), Strong Migrations. Linting: Standard.rb + standard-rails, StandardJS, erb-lint. Testing: RSpec + Capybara (system tests via Selenium/Chrome), Jest. There is no UI sign-up — users are admin-invited only (ADR 0002).

Architecture: things that require reading multiple files to understand

Two user tables (ADR 0003)

There are two separate Devise models: User (with subclasses Volunteer, Supervisor, CasaAdmin — these are real STI subclasses, each with its own model file) and AllCasaAdmin (a separate top-level model and table for super-admins who span orgs). Devise is configured for both in config/routes.rb, and there is a parallel app/controllers/all_casa_admins/ namespace + app/models/all_casa_admins/ for the all-casa flows. When touching auth, check both sides.

Multi-tenancy is enforced in three places

  1. Models include ByOrganizationScope (app/models/concerns/by_organization_scope.rb) which gives .by_organization(casa_org).
  2. The Organizational controller concern (app/controllers/concerns/organizational.rb) provides current_organization from the signed-in user.
  3. Pundit policy scopes filter by org: scope.by_organization(user.casa_org).

Every controller index action should call policy_scope; every show/update/destroy should call authorize. Controllers typically use after_action :verify_authorized (with explicit except:). When adding a new model with org-scoped data, all three places must be wired up or queries will leak across orgs.

Authorization is policy-based (Pundit)

Policies live in app/policies/. They define both predicate methods (update?, destroy?) and permitted_attributes (role-based field allowlists for strong params). When adding a model attribute that admins/supervisors/volunteers should be able to set, update permitted_attributes in the policy — not just the controller.

Layering: where logic goes

  • Controllers stay thin. Standard pattern: set_record before_action, authorize, save/render with :unprocessable_content on failure.
  • Service objects (app/services/) — ServiceName.new(args).perform. Used for CSV exports, SMS reminders, complex multi-record operations.
  • Decorators (app/decorators/, Draper) — presentation logic. Access view helpers via h.helper_method. Called as model.decorate. Date formatting, conditional display strings, etc. belong here, NOT in models or ERB.
  • ViewComponents (app/components/) — reusable UI primitives (badges, modals, sidebar, dropdown menus, form bits).
  • Parameter objects (app/values/) — builder-pattern wrappers for non-trivial strong-params logic, e.g. VolunteerParameters.new(params).with_password(pw).without_active.
  • Concerns (app/{models,controllers}/concerns/) — shared behavior via extend ActiveSupport::Concern. Notable model concerns: ByOrganizationScope, Roles, Api, and a CasaCase/ directory of concern modules used by the CasaCase model.

Frontend

Hotwire-first: Turbo for navigation/forms, Stimulus controllers in app/javascript/controllers/. There is an in-progress migration from inline JS / jQuery to Stimulus — new code should be Stimulus, but legacy jQuery is not flagged. JS is bundled via ESBuild (bin/asset_bundling_scripts/build_js.js); SCSS via the sass CLI. Both have watchers in Procfile.dev. Email views require inline CSS for client compatibility (ADR 0007).

Notable conventions

  • Soft deletes via Paranoia: destroy marks deleted, doesn't hard-delete. Be deliberate when reasoning about uniqueness or "where is this record."
  • Strong Migrations is enabled and will block unsafe DDL (column removal without safety_assured, non-concurrent index adds on large tables, type changes). Reversibility is required.
  • Soft-deletion-aware uniqueness: validate_uniqueness_of(...).scoped_to(:casa_org_id) is the typical pattern — uniqueness is per-org.
  • Enums use prefix syntax: enum :status, {active: 0, inactive: 1}, prefix: :status.
  • Post-deployment tasks run via After Party (bundle exec rake after_party:run), invoked automatically by bin/update and on Heroku release.

Testing conventions

  • System tests are preferred over controller tests (ADR 0006 — app/controllers/concerns/users/ and spec/controllers/ exist but are minimal). New UI behavior should land in spec/system/.
  • Factories use traits for variants: create(:casa_case, :active). bin/lint runs factory_bot:lint to catch invalid factories.
  • Use build for unit tests; create only when persistence is required. let for lazy, let! when the record must exist before the example.
  • shoulda-matchers handles association/validation tests.
  • No sleep in tests — rely on Capybara waiting.
  • Flaky tests are disabled with xit + a tracking issue, never deleted.

Style

This project uses Standard.rb (not vanilla RuboCop). Don't fight it on spacing/quotes/trailing commas. There is also a .standard_todo.yml of grandfathered violations — leave older files alone unless touching them substantively. Older migration files (2020–2024) are intentionally excluded from linting.

Design system (casadesign branch)

This branch introduces a Tailwind CSS v4 design system, documented in design.md (repo root) — the source of truth for the new aesthetic. Read it before UI work and keep it updated. New pages use Tailwind-only layouts and coexist with the legacy Bootstrap UI (migrate page-by-page). On every page, show user names without honorific prefixes (first + last only) — display_person for new UI, formatted_name at existing .display_name sites — never mutate the stored display_name. Workflow: on casadesign, commit and push to the casadesign branch at every checkpoint (do not wait for approval). Buttons come from the button_classes(:primary|:secondary|:danger|:danger_outline|:success) helper (one shared 40px height token, never hand-written class strings; before finishing UI work, grep the touched views for clickable elements with a hand-rolled button shape (inline-flex + rounded-lg + px-/py- + bg-/border-) and convert them to button_classes (the tertiary ghost is the only documented exception): this drift, a bespoke string at a different height next to the 40px token, is a recurring bug). Section headings and subtitles are sentence case with no trailing colon (colons only on inline key:value fact labels; audit grep '<h[123][^>]*>[^<]*:</h'); sentence case also covers app-shipped content (seed defaults and constants like ContactTypeGroup::DEFAULT_CONTACT_TYPE_GROUPS, shown as the multiselect chips), not just view copy, so before finishing scan the touched views AND app-shipped names/defaults for Title Case or ALL-CAPS and fix them (proper nouns/acronyms CASA, IEP excepted; never force-case free-form org data); person names are dark identifying text (font-medium text-slate-800), not brand nav links, and avoid sending the user out of the current flow via a name without a clear path back. Multiselects (TomSelect) load blank with a placeholder, never pre-selected with every option (the dropdown's Select/Unselect all covers that), and their chevron is a .ts-wrapper::after base64-SVG caret with z-index: 2 (TomSelect's opaque .ts-control paints over a plain ::after, so without the z-index the caret is hidden behind it; a content glyph escape and a raw data URI both broke in the build). Verify chevrons at the pixel level (screenshot + darkest-pixel), not by computed style. Native-dialog modals use the Dialog:: ViewComponent suite (Dialog::GroupComponent + Dialog::HeaderComponent/BodyComponent/FooterComponent), not the legacy Bootstrap Modal::*. Page headers use one primary CTA plus a More overflow menu (native <details> + the dropdown controller): keep core actions visible and overflow only occasional ones, never bury a core action (also breaks non-JS specs, which can't open <details>). On mobile, collapse the remaining visible secondaries into More too (render twice with hidden sm:contents / sm:hidden) so only the primary CTA and More share the top line. Leading icons on menu/list items top-align to the first line (items-start), never centered against wrapped text. When you build and verify a component or pattern, document it in design.md immediately (do not wait for approval) and reuse it from there: design.md is the source of truth, so match it and never re-derive or let it drift. Before finishing a new or changed component, check it against design.md and its sibling components and pixel-verify the match (e.g. the multiselect chevron must match the single-select chevron, measured via screenshot + darkest-pixel, never eyeballed or trusted from computed style): rendering is not the same as matching. When a partial is still rendered by legacy Bootstrap pages (e.g. shared/_manage_volunteers, shared/_court_order_list), never restyle it in place: add a casa_app-specific Tailwind twin (casa_cases/_volunteer_assignment, casa_cases/_court_orders) that preserves every JS hook (ids, classes, data-actions, field names, DOM adjacencies) and leave the shared partial for the Bootstrap pages.

Where to look for more

  • .github/instructions/ruby.instructions.md and .github/instructions/copilot-review.instructions.md — the project's own review checklist; the source of truth for "what is a bug-shaped change here."
  • doc/architecture-decisions/ — ADRs (especially 0002 no-UI-signup, 0003 two-user-tables, 0006 few-controller-tests, 0007 inline-email-CSS).
  • doc/productsense.md — product philosophy.
  • db/schema.rb — paste into dbdiagram.io for an ERD.