* Add opt-in Discord ping to standings notifications
Users who have linked their Discord account can enable a ping preference
in Settings > Notifications. When enabled, their bracket username is replaced
with a Discord @mention (<@userId>) in league standings update messages,
sending them a push notification and showing their Discord display name.
Adds discordPingEnabled column to users, a findDiscordIdsByUserIds model
helper, allowed_mentions support to the Discord webhook payload, and a
NotificationsSection settings component.
https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs
* Remove Display Name field from user profile settings
Username is the only user-facing name field. The display_name column
remains in the database since BetterAuth writes the OAuth provider name
there, and it serves as a programmatic fallback via getUserDisplayName.
https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs
* Address code review feedback on Discord ping feature
- Fix broken Account settings link in NotificationsSection: replace the
non-functional ?section=account href with an onNavigateToAccount callback
that drives the parent's useState-based section switcher
- Replace hand-rolled toggle button with the project's Switch component
(Radix UI) for consistent sizing, accessibility, and dark-mode support
- Guard update-discord-ping action: return an error if the user attempts
to enable pings without a Discord account linked
- Surface action error in NotificationsSection UI
- Cap allowed_mentions.users at 100 to avoid Discord rejecting the payload
in large leagues
- Remove the showWinner alias in scoring-calculator; inline winnerScoreChanged
- Add comment to league settings test notification explaining why discordUserId
is omitted
- Clarify database schema comment: displayName is write-only from BetterAuth
https://claude.ai/code/session_01NUv93WRrufHhpZSMyY4bjs
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add account deletion and multi-section settings page
- Rename /user-profile → /settings with 301 redirect from old URL
- Add multi-section settings nav (Profile, Account, API placeholder, Data & Privacy)
reusing existing SettingsDesktopNav/SettingsMobileGridNav components
- Implement account deletion via anonymization: wipes all PII from users row,
releases team ownerships, removes commissioner records, deletes sessions/accounts
- Add data export request form that emails privacy@brackt.com via Resend
- Add deletedAt timestamp column to users table (migration 0100)
- Add anonymizeUserAccount() to user model
- Add removeAllCommissionersByUserId() to commissioner model
- Tests for both new model functions
- Update UserMenu "Profile" link → "Settings" at /settings
https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X
* Address code review feedback on settings/account deletion
1. Wrap anonymizeUserAccount in a DB transaction so partial failures
(e.g. session delete succeeds but user update fails) can't leave
accounts in an inconsistent state
2. Escape user email and notes with escapeHtml() before interpolating
into the data request email body
3. Reset AlertDialog confirmed state when dialog is dismissed via
backdrop click or Escape key (onOpenChange handler)
4. Extract accounts DB query to app/models/account.ts (findLinkedAccountsByUserId)
to comply with the "always query through app/models/" convention
5. Replace dynamic imports() in action handlers with static top-level imports
6. Remove the unused hard-delete deleteUser() function
7. Add lastDataRequestAt timestamp to users (migration 0101) and enforce
a 30-day server-side cooldown on data export requests
8. Replace fragile actionData type casts with a proper ActionData
discriminated union; narrowing now works without `as` assertions
9. Strengthen tests: verify which schema tables are passed to delete()
and that operations run inside the transaction
https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X
* Fix no-non-null-assertion lint error in PrivacySection
Replace non-null assertion with optional chaining on dataRequestCooldownUntil
to satisfy oxlint no-non-null-assertion rule.
https://claude.ai/code/session_017Hvmof82Xr3UwKFc3pnC4X
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix BetterAuth field mapping and add owner-prefixed team names
- Fix auth.server.ts: use camelCase Drizzle field names for BetterAuth
adapter (was snake_case, causing user inserts to fail); add
generateId: "uuid" so PostgreSQL UUID columns accept generated IDs
- Add prependOwnerToTeamName / stripOwnerFromTeamName utilities
- Invite join, league creation, and settings assign/remove all now
manage the owner-prefix on team names atomically
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Complete BetterAuth migration: auth flows, rename, and cleanup
- Add /forgot-password and /reset-password pages (full password reset flow)
- Add 'Forgot password?' link on login page
- Fix register.tsx: add explicit window.location fallback after signUp
- Rename commissionerAuditLog.actorClerkId → actorUserId (migration 0084)
and update all references in model, routes, components, and tests
- Rewrite docs/agents/auth.md to document BetterAuth (removes all Clerk refs)
- Update test fixtures and schema comments to remove Clerk ID references;
use UUID-format IDs throughout test data
- Update docs/agents/domain-models.md ownerId description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix BetterAuth ID generation, clean up review issues
- Set generateId: false so BetterAuth omits id from inserts; add
$defaultFn(crypto.randomUUID) to accounts/sessions/verifications
so Drizzle fills it in (fixes null id constraint violation on signup)
- Move renameTeam to static import; rename before removeTeamOwner so
a failed rename leaves owner intact rather than leaving a stale prefix
- Clarify stripOwnerFromTeamName toTitleCase assumption in comment
- Annotate reset-password token fallback to explain loader guarantee
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add overnight pause feature for draft timers
Protects players from having their pick timer expire while asleep.
Admins configure a nightly window (league-wide or per-user timezone);
the timer freezes during that window while autodraft can still fire.
Fixes#66
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix TS error: guard getUserDisplayName against undefined user
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Migrate authentication from Clerk to BetterAuth (#322)
Replaces @clerk/react-router with self-hosted better-auth to eliminate
the external Clerk dependency and keep all user/session data in our own
PostgreSQL database.
**What changed**
- New: auth.server.ts (BetterAuth config w/ Drizzle adapter, bcrypt, Resend), auth-client.ts, api.auth.$.ts handler
- New: /login and /register pages with email+password and Google/Discord OAuth; open-redirect guard on redirectTo param
- New: UserMenu component replacing Clerk's UserButton
- Schema: sessions, accounts, verifications tables; emailVerified column; clerkId made nullable
- Migrations 0081 (BetterAuth tables) and 0082 (accounts extra columns for v1.6.9)
- All ~30 route files: getAuth → auth.api.getSession, isUserAdminByClerkId → isUserAdmin
- root.tsx: isAdmin read directly from session.user.isAdmin (no extra DB query)
- useDraftAuthRecovery: removed Clerk JWT refresh logic; replaced with cookie-session check
- models/user.ts: removed findUserByClerkId, findOrCreateUser, updateUserByClerkId (webhook pattern)
- Deleted: app/routes/api/webhooks/clerk.ts; uninstalled @clerk/react-router, @clerk/themes, svix
- scripts/migrate.mjs: extended with idempotent Clerk → BetterAuth data migration (FK conversion, email_verified, OAuth accounts)
- scripts/migrate-clerk-passwords.mjs: one-time script to import bcrypt hashes from Clerk CSV export
- BETTERAUTH_MIGRATION.md: dev and production runbooks
- All test mocks updated: vi.mock('~/lib/auth.server') instead of @clerk/react-router/server
- Test fixtures: added emailVerified field
**Follow-up (post-stable)**
- Rename actor_clerk_id column → actor_user_id in commissioner_audit_log
- Drop clerk_id column from users once migration confirmed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add .npmrc with legacy-peer-deps for better-auth/drizzle peer dep conflict
better-auth@1.6.9 declares peerOptional deps on drizzle-orm ^0.45.2 and
drizzle-kit >=0.31.4, but we run drizzle-orm ~0.36.3 / drizzle-kit ~0.28.1.
The adapter works correctly at runtime with our versions — the peer dep is
only for stricter type checking. This unblocks npm ci in CI without a risky
drizzle major-version upgrade.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>