The live "what's left" list for moving the CASA UI off Bootstrap and onto the Tailwind
design system on the casadesign branch.
- How to build anything here is defined in
design.md— the permanent source of truth. This file is only the ordered what's-left. - Roughly ordered by value ÷ effort. Check items off, then commit + push at every
checkpoint (see the workflow in
design.md). [x]done ·[~]in progress ·[ ]not started.
All migration phases (0–6) are shipped, including the opt-in case_contacts_new_design table
(now a bespoke server-rendered casa_app table; its dead DataTable stack was removed). But a
straggler audit found several reachable leaf pages never in the phase plan that are still on the
Bootstrap application layout — see Straggler pages below. They block deleting the Bootstrap
layout, so resume there — fund_requests#new, casa_admins, other_duties, and notes#edit
and the error#index test page are done; case_assignments#index was orphaned dead code (deleted).
All straggler HTML pages are now migrated. Next: verify no controller still renders an HTML page
on the Bootstrap application layout (the remaining default-layout controllers are data-only —
CSV/XLSX reports, preference_sets, api/*, and AJAX sub-resources), then the endgame — the
deferred sentence-case sweep, the contrast audit, vendoring Bootstrap Icons, dead-code removal,
and finally deleting the application layout + application.scss. Remaining product question:
hearing type / judge on case lists (stakeholder).
Beyond the migration: a Metrics & Analytics capability shipped on top of the design system —
the platform activity charts now live in an authenticated all-CASA Metrics console (nav'd in the
all_casa_admin sidebar) plus a per-chapter Analytics page (org-scoped via MetricsReport,
admin+supervisor, beside Reports in the casa_app sidebar, with chapter KPI cards), and the public
/health was slimmed to an ops status endpoint. See design.md > Migration status.
Built as ONE self-contained casa_app view (volunteers#edit on layout: "casa_app"); the shared
Bootstrap user-edit partials stay for the supervisor/admin edit pages. All 745 system-spec
examples green (incl. the 2 previously-red impersonation examples, fixed by the new casa_app
impersonation banner), the view + request specs green, and no horizontal overflow at 375–1280.
The select chevron was darkest-pixel verified (minima 114 = slate-500). See the Person edit
page pattern in design.md.
- Header: back-to-case-or-volunteers + "Edit volunteer" + Impersonate. (Reminder is a visible inline card, not a Dialog — the non-JS specs check/uncheck it directly.)
- Profile form: email, display name, phone, DOB, mailing address, reimbursement checkbox
(editable vs read-only per
update_user_setting?; confirmable email-change flow). - Account card: org, invited / accepted / last-login dates, learning hours.
- Status / activation card: active pill, deactivate (UJS
data-confirm, since a:jsspec drivesaccept_confirm), activate, resend invitation, reactivation SMS. - Manage cases card (card list) + assign-a-case form.
- Manage supervisor card + assign form (supervisor name is dark text, not a flow-trap link).
- Follow-up: this page's spec-locked Title Case labels (Submit, Assign Case, Resend Invitation, Current Supervisor:, Assign/Select a Supervisor, Save Note, ...) are queued for the sweep below.
- Fix stale supervisor dashboard request spec (it still asserted a redirect after the dashboard began rendering).
- Add a
SupervisorDashboardservice spec (stats, per-volunteer status,needs_attention). - Run the full suite (
bundle exec rspec, background) to confirm green. Last full run (post Metrics/Analytics): 3470 examples, 5 failures, 42 pending — 4 are headless-chrome<unknown>/WebDriverErrorcrash flakes (each passes in isolation), and 1 was a real stale-spec red:truncated_text_component_specstill asserted the pre-renamespan.text-boldwhile the component rendersspan.font-bold(from the earlier d-none->hidden / text-bold->font-bold migration) — now fixed. No failure was caused by the Metrics/Analytics work. - Sweep shipped pages for Font Awesome
fas fa-*icons (they don't render on Tailwind layouts). Verified: 0fas/far/fal fa-*usages remain in casa_app views — the migrated pages usebi bi-*. - Fix a latent, order-dependent test flake (pre-existing, not design work):
CaseContactvalidatesoccurred_atwithless_than: Time.zone.tomorrow + 1.dayinapp/models/case_contact.rb— a value captured at class-load time, so if the class first loads while a spec is time-traveled to the past, the "future" cutoff freezes and later specs fail with "Date can't be in the future". Make it a lambda (less_than: -> { Time.zone.tomorrow + 1.day }).
- Notifications (
notifications#index). - Edit profile (
users#edit) — profile, change password, change email, languages. Rebuilt the Bootstrap collapse accordions with adisclosureStimulus controller. - Impersonation banner inside
casa_app(layouts/_impersonation_banner, above the top bar;.headerhook + amber-400/amber-950 ~8:1). Fixed the 2 impersonation examples that had been red since the volunteer dashboard moved to casa_app. - Help link + sign-out in the account menu reach a consistent destination.
- Flash / notifier parity — the
casa_appflash strip carries a basealertclass plus the flash key (.notice/.alert) alongsiderole="status"/"alert", mirroring the Bootstrapflash_classmapping so both legacy hooks match:.noticefor the SweetAlert-notifier specs and.alertfor the shared not-authorized redirect (it ships asflash[:notice], locked by ~dozens of request specs, so it only reads as an alert because every flash box is an.alert). A richer SweetAlert-style toast is still optional, not required.
- Volunteer dashboard / landing — triage (active cases, cases needing a contact, hours), keeping the single-active-case fast-path to "log a contact". Added an org announcement banner to the shell so it isn't lost on dashboard pages.
- Admin dashboard — org-wide triage (active volunteers/cases, unassigned cases, cases needing a contact) using batched/aggregate queries, not per-row.
- Cases index (
casa_cases#index) — bespoke table + server-side filter selects + Pagy pagination. Replaced the two always-empty Hearing Type / Judge columns (their case-level columns were removed in 2023 — that data lives on court dates now) with a single "Next court date" column (preloaded, no N+1); sentence-cased the column headers. Follow-ups: column-visibility picker intentionally not planned (only 6 columns; a responsive layout is the better mobile investment); free-text search shipped (case number + active volunteer name, submit-on-Enter) with live as-you-type search as a follow-up; sortable headers shipped (server-side ?sort=, aria-sort, double-caret indicator); action-button labels kept Title Case (spec-coupled). - Case show (
casa_cases#show) oncasa_app: Tailwind fact card, court dates, assigned volunteers + reminder, and the case-contact list (newcasa_cases/_case_contact_card). Report generation is a native<dialog>(modal + court-report Stimulus controllers, no Bootstrap modal / jQuery), the thank-you dialog auto-opens on the success redirect, and Add-to-Calendar uses theadd-to-calendarcontroller. Infra added: modal / add-to-calendar / court-report / local-storage-reset controllers andCaseContactDecorator#medium_icon. Follow-ups: consolidate the show-specific contact card + reminder form with the shared Bootstrap partials when the contacts index / volunteers-edit migrate. Verified responsive (375-1280) and WCAG AA. - Case new / edit (
casa_cases#new/#edit). Both already render onlayout: "casa_app"(Tailwind cards, chevron selects,shared/form_errors, the court-orders + volunteer-assignment twins,month_year_select, the shared contact-type TomSelect, and aconfirm_buttondeactivate). The orphaned Bootstrapcasa_cases/_form.html.erbis dead (new/edit render inline) — see dead-code. Verified green this session (52 examples; the lone failure, a supervisor-unassign:jsexample, is a load-only Selenium flake that passes 3/3 in isolation). - Case contacts index + drafts shipped on casa_app. The main index keeps filterrific
(the
.filter-formauto-submits on.filter-inputchange, targeting the:case_contactsturbo-frame) — its Bootstrap collapse was swapped for thedisclosurecontroller (panel always in the DOM so rack_test finds "Other filters", buthiddenunlessexpand_filters?so JS users see it collapsed). The_case_contactcard +_followupwere rewritten in place (both pages are casa_app now): Tailwind card,TruncatedTextComponentreused, and the reminder JS hooks kept (.followup-button/#followup-button-<id>/#resolve,data-turbo:false). Inert class hooks the specs use (.full-card,.container-fluid.mb-1,.card-title) preserved; thestrong.text-primarycolor assertion + the view-spec title sentence-cased. Chevrons darkest-pixel verified (114); no overflow 375-1280. The opt-incase_contacts_new_designtable is now migrated too (below), so the whole case area is on casa_app. - Case-contact multi-step form (
case_contacts/form/details+ the_contact_topic_answerandshared/_additional_expense_formnested rows) — SHIPPED on casa_app. Wicked single-step wizard rendered onlayout "casa_app"(the JSON autosave path skips the layout). Rebuilt in Tailwind card sections (Details / Notes / Reimbursement) preserving all three Stimulus contracts verbatim: autosave (data-autosave-target="form"+input->autosave#saveon the text fields + the<small role="alert" data-autosave-target="alert">No changes have been saved.</small>lines a non-JS spec asserts), case-contact-form (reimbursement show/hide — its JS now toggles Tailwindhiddeninstead of Bootstrapd-none, safe since only this form uses the controller), and casa-nested-form (.nested-form-wrapper+ template/target/wrapper targets + hidden id/_destroy fields). Contact types stay grouped.contact-form-type-checkboxcheckboxes; relevant cases stay the sharedForm::MultipleSelectComponent(TomSelect). Swappedshared/error_messagesfor the Tailwindshared/form_errors. Duration is an inline Tailwind twin (like learning hours), soForm::HourMinuteDurationComponentis now dead (see dead-code).form_title+ the field/action Title Case labels are spec-locked (queued for the sweep). One non-JS spec ("selects the only case") was updated: it matched the legacy sidebar's case list, so it now asserts the widget selection (have_select) with:jslike its multi-case sibling. Chevron darkest-pixel verified (114 = slate-500); no overflow 375-1280; 55 form-spec examples green bar the pre-existing cross-orgedit_spec:76. (Two expense:jsspecs flake only under full-dir concurrent load; they pass 5/5 in isolation — pre-existing timing, JS logic unchanged.) See the Case-contact form pattern indesign.md. -
case_contacts_new_design(feature flag:new_case_contact_table) — SHIPPED on casa_app as a bespoke server-rendered table, retiring the jQuery serverSide DataTable. Reuses the existingCaseContactfilter scopes server-side (contact-type as a subquery so multi-type rows don't duplicate) +?sort=+ Pagy; adisclosurefilter panel that auto-submits, with the case picker a reusedmultiple-selectTomSelect (chevron darkest-pixel verified to match the single-selects, 153 vs 151 at DPR-1). Rows expand (multi-<tbody>disclosure) to topic answers + notes; row actions are inline (icon ghost buttons + Dialog delete-confirm / set-reminder note), never a dropdown (clipped byoverflow-x-auto). Dropped the DataTables column-visibility picker (design already rejected it for the cases index).followups#create/#resolvenowredirect_back_or_toandcase_contacts#destroyalreadyredirect_to request.referer, so reminders/delete stay on the list. 38 request + 22 system examples green. See Expandable rows + inline row actions indesign.md. Also removed the now-dead datatable stack: the#datatableaction + its route,CaseContactDatatable(+ spec), and thedefineCaseContactsTableblock indashboard.js(+ its jest test + the POST /datatable request examples).
- Volunteers index + edit. Both on casa_app. The index is a bespoke Tailwind table
(server-side filters + search + sortable headers + Pagy) that reuses
VolunteerDatatable's SQL via#index_relation/#index_count(GET filters mapped into the DataTables param shape); the column-visibility picker is dropped (design.md); bulk supervisor assignment is preserved (select-all + native-dialog modal + disable-form, desktop-only checkboxes). Retired the jQuery DataTable by moving#volunteersoff the<table>onto its wrapper sodashboard.js's$('table#volunteers').DataTable()no longer matches. Follow-ups: the now-unusedvolunteers#datatableJSON action +dashboard.jsvolunteer block are dead (see the dead-code item);hours_spent_in_days(30)is still per-row (N+1 parity with the retired datatable). The new / invite forms (volunteers#new+supervisors#new) are also on casa_app now — self-contained Tailwind forms usingshared/_form_errors; the shared not-authorized denial renders a.alertvia the flash-parity base class (fixed the 3.alertnew/invite specs). - Supervisors index + edit. Both on casa_app. The index is bespoke Tailwind: a supervisor
roster + a "volunteers without supervisors" list + a "CASA cases without court dates" list, with
a server-side status filter (auto-submit). Retired the serverSide DataTable by moving
#supervisorsoff the<table>onto its wrapper (like volunteers). The per-supervisor contact stats are now labeled pills (color + icon + word: N attempting / N not attempting / N transition-aged, or "No assigned volunteers") instead of the old color-only bar chart; the transition-aged column is a Yes/No pill, not the 🦋/🐛 emoji; the always-blank Hearing type / Judge columns are dropped (court-date-less cases never have them). The edit page is a self-contained casa_app column (Profile / Account / Status / Volunteers):supervisors/_manage_activeis now Tailwind and a newsupervisors/_manage_volunteerstwin replaces the shared Bootstrapmanage_volunteers— the shared_edit_form/_invite_loginstay for the casa_admin edit page, rebuilt inline here. "Edit supervisor" heading; Deactivate keeps the UJSdata-confirm. Both view + system specs rewritten to semantic hooks ([data-stat],.supervisor-filters,[data-test=cases-without-court-dates]); chevrons darkest-pixel verified (114 = slate-500); no overflow 375–1280. Follow-ups: the now-deadsupervisors#datatableJSON action +dashboard.jssupervisor block + the now-unusedshared/_manage_volunteers(see the dead-code item); the per-supervisor stat methods stay per-row (N+1 parity with the retired datatable). - Case assignments — no standalone page to migrate: the assign / unassign / reimbursement
actions are already casa_app via the
volunteers/_manage_cases,casa_cases/_volunteer_assignment, andsupervisors/_manage_volunteerstwins (thecase_assignments#indexview is orphaned — no route points at it). - Learning hours (volunteer + supervisor/admin views + show + new/edit). All on casa_app.
Volunteer index = a bespoke entries table (desktop table + mobile card twin, reused by the
supervisor/admin
volunteers#show). Supervisor/admin index = a per-volunteer roster with a YTD total, server-rendered + Pagy paginated, retiring the client-side DataTable (dropped thetable#learning-hoursidlearning_hours.jstargets). The new/edit form rebuilds the Hour(s)/Minute(s) inputs as a Tailwind twin ofForm::HourMinuteDurationComponent(left untouched — still used by the Bootstrap case-contacts form); form-field + submit labels are spec-locked (queued for the sweep). Show is a fact card + Edit/Delete. Column headers sentence-cased ("Learning type", "Time spent", "Time completed this year"); the system + request specs updated to match. Chevrons darkest-pixel verified (114 = slate-500); no overflow 375-1280. Follow-up: the now-deadlearning_hours.jsDataTable init + the unrenderedlearning_hours/_confirm_notepartial (see the dead-code item). - Reimbursements (
reimbursements#index). On casa_app: a bespoke Tailwind table (desktop table + mobile card twin) reusing the controller's policy-scoped query + Pagy, retiring the jQuery serverSide DataTable (the#reimbursements-datatabletable is gone, so the globally- requiredsrc/reimbursements.jsno-ops). Server-side filters via theauto-submitcontroller: a single-select Volunteer dropdown (was a select2 multi; options come from the status-scoped set so selecting one does not collapse the list) + an occurred-at date range (the controller's date parse switched toDate.parseso native date inputs' ISO values work; volunteer filter guarded with.present?so "All" doesn'twhere(creator_id: "")). "Needs review" / "Reimbursement complete" tabs (sentence-cased) + column headers sentence-cased; "Download mileage report" CSV stays. Mark-complete is a per-row PATCH toggle (_reimbursement_toggle, checkbox auto-submits — CSP-safe, no inline onchange). Added a Reimbursements item to the casa_app sidebar (guarded bypolicy(:reimbursement).index?) so the page is reachable. Deleted 5 orphaned partials (_datatable,_table,_reimbursement_complete,_filter_trigger,_occurred_at_filter_input). Chevron darkest-pixel 114; no overflow 375-1280; system + request- view + datatable + policy + notifier specs green (49). Follow-up: the now-dead
src/reimbursements.js+reimbursements#datatableaction +ReimbursementDatatable+with_datatableroute (see dead-code).
- view + datatable + policy + notifier specs green (49). Follow-up: the now-dead
- Reports hub (
reports#index). On casa_app: a "Quick exports" grid (6 one-click CSV buttons) + the filterable "Case Contacts Report" builder. Kept the.report-form-submit+data-disable-withcontract (src/reports.jsnative-submits so the CSV download bypasses Turbo; the leading icons are pointer-events-none soevent.targetis the button). The 4 filter selects stay NATIVE<select multiple>with their spec-locked ids (multiple-select-field1..4) — select2 dropped (it hid the native select, breakingselect .. from:/have_select); person-name options are now honorific-free (formatted_name). The column filter moved from a Bootstrap modal to a native<details>; deleted the orphanedreports/_filter. Individual report endpoints are CSV downloads (no HTML views). Spec-locked Title Case headings / labels / button names kept verbatim (sweep). No overflow 375-1280; specs green bar 2 pre-existing volunteer not-authorized redirects (they land on "/" notcasa_cases_path— the app-wide redirect-target issue, unrelated to this view). - Organization settings (
casa_org#edit). On casa_app: the org-profile form (name, logo, court-report template + download, feature toggles) + the Twilio credentials, revealed by a newtwilioStimulus controller that flips the fields' visibility + required/disabled from the "Enable Twilio" checkbox — replacing (and deleting) the jQuery + Bootstrap-collapsesrc/casa_org.js, which had to go because it also disabled the twilio fields by id globally and clobbered the controller. The 11 admin entity lists (contact types/groups/topics, judges, hearing types, languages, learning types/topics, placement types, custom links, sent emails) are restyled Tailwind tables keeping their spec-locked ids + cells + row ids + Edit/New links; contact-topic + custom-link deletes moved from the Bootstrap dropdown/Modal::to the Dialogshared/confirm_button. Section anchor ids (#contact-types, #case-contact-topics, ...) kept for deep links. Title-Case headings/labels kept verbatim (sweep). 33 view + system + request examples green; no page overflow 375-1280. The 11 admin tables now card-stack on mobile (hidden md:blocktable +md:hidden<dl>cards per design.md; measured fitting 375-1280). The entity CRUD pages the New/Edit links point at (contact_types#new, judges, ...) are still Bootstrap (Phase 5). - Court report generator (
case_court_reports#index). On casa_app: reuses thecourt-reportStimulus controller + Dialog from the case-show modal (posts the case + date range to the JSON generate endpoint, spinner, opens the docx). The case picker is a new searchable single-select TomSelect (searchable-selectcontroller) — its text search covers the volunteer names embedded in the option labels, so the select2 volunteer-name search is preserved (and both volunteers and admins now get it). Dropped select2; native#case-selectionstays (TomSelect hides it, so option assertions usevisible: :all) with its data-lookup/transition option attributes. Dates are native date inputs (ISO) like the show-page modal. Added a Court reports sidebar item (guarded bysee_court_reports_page?). Rewrote the ~250-line select2 + Bootstrap-modal system spec +CaseCourtReportHelpersfor the Dialog + TomSelect; the two old client-validation examples (hide-error-on-select, clear-on-reopen) were dropped — the empty selection now surfaces the server's "not found" error via the shared controller. 49 system + view + request examples green; no overflow 375-1280; TomSelect chevron is the documented global.ts-wrapper::aftercaret.
- Simple settings CRUD forms (batch 1) — judges, languages, placement types, and
learning-hour types + topics. Each
_formnow renders one sharedshared/_settings_form(casa_app card: name + optionalActive?checkbox + optional description + Submit; delegatesform_with model:so it infers the url/param key), and each controller gotlayout "casa_app"@active_nav = "settings". Reached from the settings page (New/Edit links); redirects back to settings on save. The old breadcrumb was dropped (nocurrent_organizationdependency, so the no-layout view specs pass). 41 system + view + request examples green; no overflow. Reuseshared/_settings_formfor the remaining name(+active) resources below.
- Contact types, contact type groups, contact topics (batch 2). Contact type groups reuses
shared/_settings_form(name + Active? + description); contact types + contact topics get custom casa_app forms — contact types adds a group chevron-select (idcontact_type_contact_type_group_idkept for the system spec), contact topics has question/details + Active? + Exclude from Court Report?. Controllers onlayout "casa_app"+@active_nav = "settings". 45 system + request examples green; chevron darkest-pixel 114; no overflow. - Hearing types + checklist items + custom org links (batch 3). The hearing-type form is
name + Active? in a card, plus (once persisted) a Tailwind checklist-items table with New/Edit
links and a
button_toDelete (turbo_confirm only — a non-JS destroy spec drives "Delete", so no Dialog). checklist_items new/edit is a custom casa_app form (Category / Description / Mandatory) nested under the hearing type; custom org links is a casa_app form (Display text / URL / Active?). 57 system + view + request examples green; the checklist table now card-stacks on mobile (md:hidden<dl>cards per design.md), like the settings admin tables. - Mileage rates, placements, banners (batch 4). Mileage rates: a casa_app settings index
(date / amount / Active? / Edit, no delete) + a date+currency+Active? form. Placements (nested
under a case): a casa_app index whose per-row Delete is a Dialog with a visible "Close" +
button_to "Confirm"so the non-:jsdestroy spec passes (rack_test can't match an aria-label X); a date + chevron-select form; a fact-card show. Banners: a casa_app#bannerstable (kepttd.min-widthfor a system spec) with ashared/_confirm_buttonDelete, and a form with the rich-text content editor + a liverevealwarning (new Stimulus controller) when another banner is already active. Trix needed its stylesheet folded into the casa_app tailwind bundle (@import "trix/dist/trix.css") — without it the toolbar was unstyled and overflowed the page to ~900px. 58 system + view + request examples green; chevron darkest-pixel 114; pages fit 375–1280 (tables + the Trix toolbar scroll within their own containers). - Court dates + bulk court dates (batch 5). Both nested court-date forms render on casa_app,
sharing court_dates/_fields (date + due-date + judge/hearing chevron-selects) and the casa_app
court-orders twin (casa_cases/_court_orders, siblings off, resource "court_date"), so the
court-order-form nested sub-form + its dialogs are shared with the case-edit page. Submit stays
inside .top-page-actions (spec hook); the show page keeps its
- xpath structure (Title-Case colons ride the sweep) and deletes a future date via UJS method: :delete + data-confirm (native window.confirm, like the volunteer deactivate). 80 examples green; chevron 114; pages fit 375-1280. (bulk new_spec:15 is a pre-existing travel_to flake — fails identically on the legacy views when run after the edit spec.)
- Imports (batch 5b). CSV import page on casa_app: the Bootstrap JS tabs became server-side link tabs keyed on ?import_type= (ids + "Import X" labels kept for the system spec), only the active panel renders, and the CSV error is an inline alert (was a Bootstrap modal). The SMS opt-in step is an inline amber panel that keeps id="smsOptIn" + the file/button ids + the enable-on-check script, so the global src/import.js (button enable + localStorage file persistence across the opt-in reload) keeps working. 21 system + request examples green; page fits 375-1280 (tabs scroll within their nav on the narrowest screen).
- Emancipation (batch 5c). The emancipation checklist show page is on casa_app with every AJAX/collapse hook preserved verbatim (the .emancipation-category h6 + data-is-open, its child .category-collapse-icon, the adjacent .category-options sibling the Toggler reads via .next(), the .emacipation-category-input-label-pair div, .check-item options, and a #notifications element so the Notifier — hence the whole src/case_emancipation.js — constructs and binds). The checklist index is a plain Tailwind table (dropped the all-case-emancipations id so the src/emancipations.js DataTable no-ops). 42 system + view + request examples green; pages fit 375-1280.
Phase 5 (admin long-tail CRUD) is complete.
- All-CASA-admin shell + org management (6a). New Tailwind
all_casa_adminlayout (own sidebar: CASA organizations / Patch Notes / Edit Profile / Feature Flags / Pg Dashboard + Log Out; simplified topbar; footer with the Ruby For Good / site-issue / SMS-terms links; inline flash keeping a .header-flash hook). Set on the base AllCasaAdminsController (patch_notes stays onapplicationuntil 6b). Migrated the dashboard (org table), casa_orgs new/show (+ metrics), and casa_admins new/edit/_form to Tailwind cards/tables/forms; the profile edit uses the disclosure controller for the change-password panel (kept id=collapseOne; specs moved off Bootstrap .collapse/.show to .hidden). Kept shared/error_messages (specs lock #error_explanation- the "N errors prohibited" format). 89 examples green (the one red, all_casa_admin_spec:14 casa_admin "/" -> /supervisors, is a pre-existing regular-app routing failure — red on legacy too).
- All-CASA-admin patch_notes index (6b) — the JS clone-CRUD page. Kept the exact DOM the
jQuery reads (
#patch-note-listchild order; each.card-body's direct textarea / .label-and-select x2 / .patch-note-button-controls; the .button-edit/.button-delete hooks) and restyled to Tailwind cards/native-selects; the shell now also loads the separateall_casa_adminJS bundle (itstablesDataTable no-ops on the plain Tailwind tables) and renders thelayouts/components/_notifier(the Notifier needs its.messagescontainer). Ported the JS's injected Edit/Delete/Save/Cancel buttons from Bootstrap+FontAwesome to button_classes strings + bi-* icons. Also fixed the emancipation show page, which had the same bare#notifications(its AJAX save feedback was silently dropped). The all-CASA-admin authenticated area is now fully on the shell. 23 patch_notes + 91 all-casa examples green (only pre-existing all_casa_admin_spec:14 red); page fits 375-1280. - All-CASA-admin auth (6c): sessions/new + passwords/new rebuilt on the casa_auth split-screen shell to match devise/sessions/new (kept the "All CASA Log In" heading, Email/Password labels, the .actions wrapper + "Log in", and flash for the sign-out landing). 13 examples green; pages fit 375-1280.
- static#index landing page — the public marketing page rebuilt on the design system (compiled tailwind.css + Figtree + brand palette + a brand-gradient hero, replacing the Tailwind CDN + Alpine). Kept the spec-locked #organizations section, .org_logo images, and the "CASA Organizations Powered by Our App" heading. 4 examples green; page fits 375-1280 (the header's decorative blur is clipped by overflow-hidden, like casa_auth).
Phase 6 is complete — the all-CASA-admin area (shell + org management + auth + patch notes) and the public landing page are all on the design system. (The regular-user Devise pages — sign-in / password / invitation — were done in the foundation phase.)
- Health / metrics page (
health#index): rebuilt as bespoke server-rendered SVG (line charts with a distinct line-style + marker per series, heatmap-as-table, table twins, stat tiles with correct totals, and zero / no-data / empty states) on a minimalmetricslayout; retired the Chart.js + jQuerydisplay_app_metric.js. Follow-ups (date-range filter, Stimulus hover, and the unused chart.js deps removal now shipped): a validated dark-mode palette.- Charts relocated (shipped): the chart math was extracted into a scope-parameterized
MetricsReportand the charts moved off the publichealth#indexinto an authenticated all-CASA Metrics console + a per-chapter Analytics page (org-scoped, admin+supervisor, with chapter KPI cards)./healthis now a minimal self-contained ops status page + deploy-time JSON (no public cross-org data); themetricslayout/JS bundle + the unused legacy/healthJSON graph endpoints were retired. Cross-org isolation is unit-tested inspec/models/metrics_report_spec.rb.
- Charts relocated (shipped): the chart math was extracted into a scope-parameterized
Found by auditing controllers not on casa_app / casa_auth / all_casa / metrics (Bootstrap markers
present, zero casa_app markers). These reachable leaf pages were never in the phase plan and must
migrate before application.html.erb + application.scss can be deleted.
-
fund_requests#new— SHIPPED on casa_app (layout "casa_app",@active_nav = "cases", back-link to the case). Reused the casa_cases/new form pattern (max-w-2xl card, 2-col grid of short fields + full-width textareas,shared/form_errors, bottom primary submit); kept the field label substrings the spec fills by + the* / **footnotes. Sentence-cased the feature copy (h1/page-title, "Submit fund request", the flash, and the case-show link), updating the 2 spec assertions. 2 system + 11 request examples green; no overflow at 500/768/1280. -
casa_admins#index/new/edit— SHIPPED on casa_app (layout "casa_app"). Rebuilt as self-contained casa_app views mirroring the supervisors sibling: a bespoke index table (#adminshook on the wrapper, "Deactivated" tag), a minimal new form, and an edit page (Profile / Account / Status) whose Deactivate keeps the UJSdata-confirmthe:jsspec drives. Kept the legacy "N errors prohibited this Casa admin from being saved:" format +#error_explanationvia a casa_app-styledcasa_admins/_errorspartial. Retired the sharedcasa_admins/_form+ itsshared/_edit_form+shared/_invite_login(now dead — see dead-code). Spec-locked Title Case ("Create New Casa Admin", "Receive Monthly Learning Hours Report", "Resend Invitation", "Change to Supervisor") kept verbatim, queued for the sentence-case sweep. 53 examples green; no overflow 500/768/1280. -
other_duties#index/new/edit— SHIPPED on casa_app (layout "casa_app"). Index is a bespoke Tailwind table grouped by volunteer (desktop table + mobile card twin); new/edit stay thinrender "form"wrappers over a rebuilt casa_app_form(the Hour(s)/Minute(s) duration twin like learning hours; the controller'sconvert_duration_minutesstill combines them). Notes keepsrequired+ the default#other_duty_notesid (a:jsspec asserts the HTML5 validation message). Swappedshared/error_messagesforshared/form_errors(no spec locks the legacy format here). Spec-locked Title Case ("Other Duties", "New Duty", "Editing Duty", "Occurred On", "Duty Duration", "Enter Notes") kept verbatim, queued for the sweep. 26 examples green (view + system + request); no overflow 500/768/1280. -
notes#edit— SHIPPED on casa_app (layout "casa_app",@active_nav = "volunteers"). A single volunteer-note edit form (max-w-2xl card, back-link to the volunteer edit page); Note has no validations so update always redirects back. Keptnote[content]+ the spec-locked "Update Note" button (queued for the sweep). 23 examples green (system + request). -
case_assignments#index— not a page:resources :case_assignments, only: %i[create destroy]has no index route or action, and the view set no@volunteer, socase_assignments/index.html.erbwas orphaned dead code that could never render (it even used the DataTableid='casa_cases'). Deleted it rather than migrate. 45 request examples green. -
error#index— SHIPPED. This is the public dev/QA "Intentional error test" page (GET /error; a button POSTs to raise a test exception), not the 404/422/500 handler (those stay staticpublic/*.html). Since it's viewable logged-out, it can't use the casa_app shell (needscurrent_user), so added a minimal logged-outlayouts/error.html.erb(compiled tailwind- Figtree, no app JS) and rebuilt the page as a centered Tailwind card. 2 request examples green.
Data-only actions on the default layout render no HTML and need no migration: the *_reports
CSV/XLSX exports, preference_sets, android_app_associations, and the api/* endpoints.
- Edit-profile shared labels — DONE (sweep batch 3). "Update Profile", "Change Password", "New Password"(+ Confirmation), "Current Password", "Update Password", "Change Email", and "Enable Twilio For Text Messaging" (Twilio kept) sentence-cased across users/edit + all_casa_admins/edit + all_casa_admins/casa_admins/edit and every coupled spec (incl. the shared phone-error example + the standalone "Password Confirmation" substring locators).
- Contrast audit — DONE. Replaced the readable
text-slate-400text/placeholders withslate-500for WCAG AA: the case_contacts_new_design "No date" label + its—/medium-type placeholders, and the case-contact form's "last used" meta timestamp. Lefttext-slate-400on decorativearia-hiddenempty-state icons (WCAG-exempt) and the supervisor-dashboard aria-hidden—(paired with ansr-only"No recent activity"). Pills/badges usetext-{color}-700on a-50tint (andslate-600onslate-100) — all AA. Deferred to the axe pass: the fainttext-slate-400chevron on the supervisor-dashboard "view" icon-links (non-text contrast, WCAG 1.4.11). - Vendor Bootstrap Icons + Figtree — DONE. Self-hosted: font binaries under
public/vendor/{bootstrap-icons,figtree}/, the@font-face/glyph CSS vendored underapp/assets/stylesheets/vendor/and@imported bytailwind.css; dropped the Google Fonts + jsdelivr CDN<link>s from all six shells (casa_app, casa_auth, all_casa_admin, error, metrics, static landing). Absoluteurl()s so the Tailwind CLI (Lightning CSS) leaves them untouched; served frompublic/by the same static middleware as the compiledtailwind.css. - Nav-toggle -> Stimulus — DONE (aligns with the jQuery -> Stimulus migration, #5016).
The inline mobile-nav
<script>incasa_app.html.erband the identical one inall_casa_admin.html.erbare replaced by a sharednav-drawercontroller (app/javascript/controllers/nav_drawer_controller.js):sidebar/backdrop/buttontargets;click->nav-drawer#toggleon the button,click->nav-drawer#closeon the backdrop, andkeydown.esc@window->nav-drawer#close. Jest-covered (open/close/backdrop/Escape). - Table strategy (revised) — build tables bespoke in Tailwind (server-side filters +
Pagy pagination + Turbo Drive), matching the dashboard; retire jQuery DataTables
page-by-page. Theming DataTables was rejected (couldn't meet WCAG/design). Cases index
is the reference pattern (
shared/_pagination,auto-submitcontroller). - Per-page accessibility pass (axe) — DONE (initial sweep). Added
axe-core-rspec+spec/system/accessibility/axe_spec.rb(headless-chrome:js) assertingbe_axe_cleanon 13 migrated pages (dashboard, cases index/show/edit, case contacts, volunteers, supervisors, reports, learning hours, reimbursements, admins, org settings, sign-in). Fixed every violation it found (allselect-name, critical): associated the reports multiselect<label>s with their spec-lockedmultiple-select-fieldNids, gave the month/yearselect_tags anaria-label(_month_year_select— fixes birth + date-in-care), and gaveForm::MultipleSelectComponent's TomSelect<select>anaria-label. Still open: the non-text-contrast chevron noted above, and extending the sweep to the remaining pages/forms. - Responsive pass on each migrated page: verify mobile / tablet / desktop; give data
tables a stacked / card layout on small screens rather than horizontal scroll. Complete:
cases index, health metrics, supervisor + volunteer dashboards, notifications, edit profile,
and the casa_auth pages (sign-in, forgot / reset password, accept invitation). All verified by
measurement at true 375 / 414 / 768 / 1024 / 1280 widths via a CDP device-metrics override
(
bin/measure-responsive.mjs), which bypasses headless Chrome's 500px minimum-window clamp so media queries evaluate at real phone width. The health heatmap is a density matrix, so it keeps horizontal scroll + a sticky day-of-week axis rather than card-stacking (which would destroy the visualization); its chart-twin data tables are months-as-rows (narrow) inside a collapsed<details>. A contrast pass over the swept pages fixed three AA misses: notification / patch-note timestamps and all input placeholders slate-400 -> slate-500, and the auth aside footer white/60 -> white/80. - Dead-code removal — DONE (7 verified batches, each committed with specs/build green):
- orphaned partials +
Form::HourMinuteDurationComponent(+spec) +CaseContactDecorator#form_page_notes(notifications/_notification+ itsnotification_row_class/notification_iconhelpers,shared/_manage_volunteers,shared/_edit_form,shared/_invite_login,casa_cases/_form,learning_hours/_confirm_note). - Bootstrap sidebar/header layout partials (
layouts/_sidebar,_header,_all_casa_admin_sidebar+ specs) +Sidebar::Link/GroupComponent(keptSidebarHelper, live viacases_index_title). - reimbursements serverSide DataTable stack (
src/reimbursements.js+ itsapplication.jsrequire,reimbursements#datatable+ its sole-caller helper,ReimbursementDatatable+ spec,with_datatableoff the reimbursements route). dashboard.js(466 lines) +volunteers#datatable/supervisors#datatableactions + thewith_datatableconcern (all three refs, incl. case_contacts whose action never existed) +SupervisorDatatable(+spec) + itsbrakeman.ignoreentry. KeptVolunteerDatatable(live: volunteers index reuses its SQL) +Notifier(shared).- the now-dead
datatable?Pundit predicates in volunteer/supervisor/casa_admin/case_contact policies +ReimbursementPolicy#datatable?(+ their spec coverage). learning_hours.jsDataTable init (keptdatatable.js, still used byall_casa_admin/tables.js).- the 33 orphaned Bootstrap-era SCSS partials under
shared/(except the manifest-linkednoscript.css),pages/, andbase/— their compiled rootapplication.scsswas already gone and the CSS build is Tailwind-only.casa_cases/_generate_report_modalwas already gone. Not dead — needs a deliberate migration, not a sweep:CasaCase#hearing_type/#judge+ thehearing_type_name/judge_namedelegates are still consumed bycase_court_report_context.rbandPreferenceSet's case-column-visibility keys, so removing them would entangle the court report + column prefs. Follow-up (done): dropped the now-unusedsass+bootstrap-scssdeps frompackage.json(nobuild:css/importer remained after the SCSS + CDN removal).
- orphaned partials +
- Sentence-case sweep — DONE. Proper nouns/acronyms (CASA, IEP, Twilio) kept; free-form org
data untouched. Done in verified batches, each with its coupled system/view/request specs updated
in lockstep (incl. specs that matched old labels by case-sensitive substring, e.g.
fill_in "Court Date"/"Password Confirmation"): 1 case-detail labels · 2 court_dates · 3 edit-profile · 4a casa_admins/other_duties/notes · 4b volunteer/supervisor edit + manage partials · 4c learning hours + ~12 settings CRUD forms · 4d case-contact form + CTAs + dialog titles · 4e/4f remaining headings/buttons/field labels. Left as data (not headings): CSV/XLSX export column headers, the emancipation DOCX filename/template, model validation messages, mailer copy, and the notifier "Emancipation Checklist Reminder". Dead Bootstrap shared partials skipped (see dead-code). Round 2 (comprehensive re-sweep) — DONE: round 1 was incomplete. It missed block-form button labels (</i> Labelbetweendo %>/<% end %>), labels inside Ruby arrays (the all-CASA sidebar nav), severalcontent_for :page_titles, andspec/supporthelper couplings; it also left two "kept verbatim / Title Case for specs" cop-out comments. Re-swept all ofapp/views+app/componentswith four grep patterns until zero remained -- text nodes>X<, quoted"X", text-after-tag-at-EOL[>}%] X$, and indented pure-text^\s*X$-- covering settings, reports/court reports, casa_cases, the New/Create/Manage/Edit buttons, reminders/followups, the court-report modal, imports, banners, case groups, all-CASA, and the landing page; ~37 coupled specs (incl. shared support helpers) updated in lockstep. When auditing, use all four patterns, not just>X<. - Contact-type default rename (guarded after-party, do once pages are migrated). For
existing orgs, rename the contact types + the "Social Services" group whose names exactly
match the old Title Case defaults to the new sentence-case defaults ("Foster Parent" ->
"Foster parent", etc., per
ContactTypeGroup::DEFAULT_CONTACT_TYPE_GROUPS). Exact-match only so org customizations are left alone; associations are by id so contacts are unaffected; guard the per-org name-uniqueness edge case (skip a rename if the sentence-case name already exists in that org). New orgs already seed the new names. - Decommission the Bootstrap
applicationlayout +application.scss— DONE.- Deleted
layouts/application.html.erb(+ its view spec),layouts/devise.html.erb,app/assets/stylesheets/application.scss, and the legacypublic/assets/css/{lineicons,materialdesignicons.min,main}.css. - Moved Devise's last un-customized default controllers onto
casa_authso nothing needslayouts/devise: thinlayout "casa_auth"subclassesUsers::ConfirmationsController,AllCasaAdmins::PasswordsController,AllCasaAdmins::InvitationsController(routed viadevise_for). Styleddevise/confirmations/newand rebuiltdevise/invitations/newon casa_auth — this also fixes the all-CASA password/invite pages, which had been rendering Tailwind views on the CSS-less Bootstrap devise shell (unstyled). - Rewired the build off Bootstrap SCSS:
build:css/build:css:dev(package.json) now run the Tailwind CLI instead ofsass, and thecss:Procfile.devprocess becametw:. (These were briefly renamed tobuild:tailwind, which broke the Heroku deploy —cssbundling-rails'css:buildhook shells out tonpm run build:cssby name. Renamed back, and moved@tailwindcss/cli+tailwindcssintodependenciesso a production install keeps the CLI, same asesbuild.) - Verified: all 6 devise HTML pages render 200 on casa_auth; auth specs green; full request+view sweep adds no new failures.
application.jsstays —casa_app+casa_authstill load it. Now-orphaned by these deletions (queue in dead-code):layouts/_sidebar,_header,_all_casa_admin_sidebar; every Bootstrap SCSS partial underapp/assets/stylesheets/(shared/*,base/*, bootstrap overrides); and the legacy icon-font assets the deleted CSS referenced.
- Deleted
- Refresh stale view/request specs — DONE. The pre-existing reds asserted old Bootstrap
markup for pages migrated in earlier phases. Fixed:
casa_cases/index(stub the_filter+shared/paginationpartials the bespoke view now renders + assign@pagy),casa_cases/new+casa_cases/show(sentence-cased labels — "Youth's birth month and year", "Assign a volunteer", "Current placement";have_contentfor the HTML-escaped apostrophe),casa_cases/edit(added the#volunteer-assignmentid back to the_volunteer_assignmenttwin), and the devisepasswords/new+invitations/editview specs +users/invitationsrequest spec ("Reset your password", "Set your password", "Confirm password", "Activate account"). Retiredspec/views/volunteers/index.html.erb_spec.rbper ADR 0006 — the bespoke index is coupled toVolunteerDatatable's computedsupervisor_name, so a view spec feeding plain records can't render it, andspec/system/volunteers/index_spec.rbalready covers the roster + supervisor column (incl. the inactive-supervisor case) + New-volunteer nav.
- Hearing type & judge on case lists. The old cases index (and
supervisors/index) showed per-case "Hearing Type" and "Judge" columns, but their backing columns were removed fromcasa_casesin 2023 (migration20230729213608) — that data moved to court dates, so those columns have rendered blank for every case since. Both migrated indexes have now dropped them (the cases index shows "Next court date" instead;supervisors/indexlists cases without court dates, so type/judge/next-date are all N/A there). Ask stakeholders: is next court date the right thing to surface on the main roster, or do they want the upcoming hearing's type/judge (sourced from court dates)? The answer decides whether to delete the deadCasaCaseassociations.
Verified still open against main at the time of writing.
-
Re-enable 3 flaky case-contact system specs.
edit_spec"successfully edits case contact",contact_topic_answers"removes an answer when its topic is unchecked", andcase_contacts_new_design"hides drafts when Hide drafts is checked" arexit'd with a FLAKY comment. They are non-deterministic Selenium timing, not order-dependent state: the same seed passes and fails across runs, and each passes in isolation. Per the repo policy they need a GitHub tracking issue whose number goes into the three comments, thenxit->itonce the race is fixed. -
Two view specs raise
SystemStackError.spec/views/mileage_rates/index.html.erb_spec.rb:13andspec/views/casa_admins/admins_table.html.erb_spec.rb:4. Both templates rendercasa_org/_settings_frame->_settings_rail, which callsedit_casa_org_path(current_organization);current_organizationis not stubbed in a view spec and recurses. Pre-existing, unrelated to the design work, and still red onmain. -
Sort is not a filter, but still lives on the filterrific form.
Clear filterstherefore has to carrysorted_bythrough by hand (clear_filters_path) so clearing does not silently reorder the list. Moving the sort control off the filterrific form would remove that coupling — it is the last standards gap in the filter bar. -
Emails do not show the chapter name consistently.
layouts/mailerprints it only when a mailer sets@casa_organization. Devise mailers set nothing andlearning_hours_mailersets@casa_org, so those fall back to the generic "Court Appointed Special Advocate (CASA)". Either set@casa_organizationeverywhere or have the layout also read@casa_org/ the resource's org. Whether Devise emails should name the org is a product call. -
patch_notes/index_specstill reads a raw Selenium handle.spec/system/all_casa_admins/patch_notes/index_spec.rb:31doesfirst(:css, "textarea").native.send_keys(...). That call sits outside Capybara'ssynchronize, so aStaleElementReferenceErrorraised while the page settles is never retried — the exact failure fixed inother_duties/new_spec(3/20 -> 0/20) and the same antipattern removed fromcourt_dates/new_specandbulk_court_dates/new_spec. It is the last one left in the system specs. Left alone for now because it is materially safer than those were: the textarea is static (not cloned from a<template>) and already scoped bywithin "#new-patch-note", and it has not been observed failing across repeated full-suite runs. Swap it forfind("#new-patch-note textarea").set(...)next time that file is touched, or sooner if it ever flakes in CI. -
Contact types on the case-contact form are still an exposed grouped checkbox fieldset, while the filter and
casa_casesnew/edit use the searchable multiselect. Deliberate: it is that page's primary required input and each row carries a per-type recency hint that does not survive collapsing into chips. Revisit only if the recency hint can move into option subtext (ascasa_casesdoes).