* fix(design-system): replace undefined utility classes and broken /N modifiers
Audit of class-name resolution in views surfaced two related silent
failures across ~17 files:
1. Class names that don't exist anywhere in the design system. Tailwind
silently drops them and the element renders with no CSS for that
property.
- bg-primary (and bg-primary/5, /10, /90): never defined as a
custom utility, no --color-primary in @theme. Used as a CTA bg
in 8 places, all rendered transparent.
- text-inverted: typo of text-inverse.
- text-primary-foreground: shadcn/Radix vocabulary, not in our
token system.
- bg-accent / border-accent / text-accent: same shadcn vocabulary;
not defined.
2. Slash modifier (/N) used on custom @utility blocks. Modifiers only
resolve on Tailwind theme colors (anything in tokens.json color.*).
Custom @utility blocks compile to static @apply statements and
silently drop the /N variant. Affected uses:
- border-surface-inset/50 across provider account selectors.
- border-secondary/30, /40 in admin SSO form and simplefin setup.
- bg-surface-inset/30, /40 in settings preferences and simplefin.
Fixes:
| From | To |
|---------------------------------------------------|------------------------------------------------------|
| bg-primary text-white (and similar primary CTAs) | button-bg-primary text-inverse |
| bg-primary text-primary-foreground (badges) | button-bg-primary text-inverse |
| bg-primary text-inverted (typo) | button-bg-primary text-inverse |
| bg-primary text-primary (broken active pill) | bg-inverse text-inverse |
| bg-primary (status dot) | bg-inverse |
| bg-primary/5, bg-primary/10 (subtle accent bg) | bg-gray-tint-5, bg-gray-tint-10 |
| hover:bg-primary/90 | hover:button-bg-primary-hover |
| border-accent bg-accent/10 text-accent (badges) | border-secondary bg-surface-inset text-secondary |
| border-surface-inset/50 | border-secondary |
| border-secondary/30, /40 | border-tertiary |
| bg-surface-inset/30 | bg-surface-inset (full strength) |
| bg-surface-inset/40 | bg-container-inset |
Also documents the alpha-modifier limitation in design/tokens/README.md
under a new "Alpha modifiers in views (/N syntax)" section, with the
opacity-N convention for custom utilities and a note that the
gray-tint-5 / gray-tint-10 family (and similar pre-resolved tints) are
theme colors and accept /N modifiers natively.
The accent-badge mapping uses neutral semantics for now. A dedicated
brand-accent token (text-link-tint-10 etc.) is worth considering as a
follow-up if the "highlighted metadata badge" pattern recurs.
* fix(design-system): replace undefined divide-primary / divide-secondary with alpha tokens
Same class of bug as the rest of this PR: divide-{name} requires the
name to be a theme color (i.e. expose --color-{name}), and our custom
@utility utilities (primary, secondary, etc.) do not. Tailwind silently
drops the unrecognized class and rows render with no separator.
Spotted six instances during the visual audit:
- admin/users/index.html.erb (×2): users table + pending invitations
- admin/sso_providers/index.html.erb (×2): configured + legacy lists
- transactions/categorizes/_transaction_list.html.erb: categorize sidebar
- settings/preferences/show.html.erb: divide-secondary/60 (also broken)
Swapped to the alpha-black/white pattern already used elsewhere in the
codebase (imports/cleans/show, transactions/_summary, etc.):
divide-y divide-primary
-> divide-y divide-alpha-black-200 theme-dark:divide-alpha-white-200
divide-y divide-secondary/60
-> divide-y divide-alpha-black-100 theme-dark:divide-alpha-white-100
The lighter (-100) variant on the preferences list matches the original
intent of /60 (more subtle).
* Add invited users with delete button to admin users page
Shows pending invitations per family below active users in /admin/users/.
Each invitation row has a red Delete button aligned with the role column.
Alt/option-clicking any Delete button changes all invitation button labels
to "Delete All" and destroys all pending invitations for that family.
- Add admin routes: DELETE /admin/invitations/:id and DELETE /admin/families/:id/invitations
- Add Admin::InvitationsController with destroy and destroy_all actions
- Load pending invitations grouped by family in users controller index
- Render invitation rows in a dashed-border tbody below active user rows
- Add admin-invitation-delete Stimulus controller for alt-click behavior
- Add i18n strings for invitation UI and flash messages
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Fix destroy_all using params[:id] from member route
The member route /admin/families/:id/invitations sets params[:id],
not params[:family_id], so Family.find was always receiving nil.
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Fix translation key in destroy_all to match locale
t(".success_all") looked up a nonexistent key; the locale defines
admin.invitations.destroy_all.success, so t(".success") is correct.
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Scope bulk delete to pending invitations and allow re-inviting emails
- destroy_all now uses family.invitations.pending.destroy_all so accepted
and expired invitation history is preserved
- Replace blanket email uniqueness validation with a custom check scoped
to pending invitations only, so the same email can be invited again
after an invitation is deleted or expires
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Drop unconditional unique DB index on invitations(email, family_id)
The model-level uniqueness check was already scoped to pending
invitations, but the blanket unique index on (email, family_id)
still caused ActiveRecord::RecordNotUnique when re-inviting an
email that had any historical invitation record in the same family
(e.g. after an accepted invite or after an account deletion).
Replace it with no DB-level unique constraint — the
no_duplicate_pending_invitation_in_family model validation is the
sole enforcer and correctly scopes uniqueness to pending rows only.
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Replace blanket unique index with partial unique index on pending invitations
Instead of dropping the DB-level uniqueness constraint entirely, replace
the unconditional unique index on (email, family_id) with a partial unique
index scoped to WHERE accepted_at IS NULL. This enforces the invariant at
the DB layer (no two non-accepted invitations for the same email in a
family) while allowing re-invites once a prior invitation has been accepted.
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
* Fix migration version and make remove_index reversible
- Change Migration[8.0] to Migration[7.2] to match the rest of the codebase
- Pass column names to remove_index so Rails can reconstruct the old index on rollback
https://claude.ai/code/session_01F8WaH5TmtdUWwhHnVoQ6Gm
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
* Display user admins grouped
* Start family/groups collapsed
* Sort by number of transactions
* Display subscription status
* Fix tests
* Use Stimulus
* First cut of a simplified "intro" UI layout
* Linter
* Add guest role and intro-only access
* Fix guest role UI defaults (#940)
Use enum predicate to avoid missing role helper.
* Remove legacy user role mapping (#941)
Drop the unused user role references in role normalization
and SSO role mapping forms to avoid implying a role that
never existed.
Refs: #0
* Remove role normalization (#942)
Remove role normalization
Roles are now stored directly without legacy mappings.
* Revert role mapping logic
* Remove `normalize_role_settings`
* Remove unnecessary migration
* Make `member` the default
* Broken `.erb`
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
- Simplified `name_id_format` selection logic in SSO provider form.
- Switched raw database query to sanitized SQL in client secret tests.
- Added condition to log JIT account creation only when identity persists.
- Sanitized failure reasons in SSO login failure handling.
- Added SSO provider connection test policy tests for super admin and regular users.
Multi-provider SSO support:
- Database-backed SSO provider management with admin UI
- Support for OpenID Connect, Google OAuth2, GitHub, and SAML 2.0
- Flipper feature flag (db_sso_providers) for dynamic provider loading
- ProviderLoader service for YAML or database configuration
Admin functionality:
- Admin::SsoProvidersController for CRUD operations
- Admin::UsersController for super_admin role management
- Pundit policies for authorization
- Test connection endpoint for validating provider config
User provisioning improvements:
- JIT (just-in-time) account creation with configurable default role
- Changed default JIT role from admin to member (security)
- User attribute sync on each SSO login
- Group/role mapping from IdP claims
SSO identity management:
- Settings::SsoIdentitiesController for users to manage connected accounts
- Issuer validation for OIDC identities
- Unlink protection when no password set
Audit logging:
- SsoAuditLog model tracking login, logout, link, unlink, JIT creation
- Captures IP address, user agent, and metadata
Advanced OIDC features:
- Custom scopes per provider
- Configurable prompt parameter (login, consent, select_account, none)
- RP-initiated logout (federated logout to IdP)
- id_token storage for logout
SAML 2.0 support:
- omniauth-saml gem integration
- IdP metadata URL or manual configuration
- Certificate and fingerprint validation
- NameID format configuration