Commit graph

8 commits

Author SHA1 Message Date
Chris Parsons
c9fdb12a09 Enforce unique team names within a season
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m31s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m19s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Add findTeamByNameInSeason (exact case-insensitive match via lower()) to the team model
- Validate uniqueness in the user-facing team settings rename action
- Validate uniqueness in admin assign-owner and remove-owner paths that auto-generate names
- Reject whitespace-only team names that would trim to an empty string
- Add DB-level unique index on (season_id, lower(name)) to close the TOCTOU race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 16:55:08 -07:00
Chris Parsons
fbcecb490e
Add Discord draft pick announcements (#460)
* Add Discord draft pick announcements

Posts a message to the league Discord webhook each time a pick is made, announcing the picked participant and pinging the next team on the clock if their owner has opted into Discord notifications. Enabled via a separate toggle in league settings (independent from standings update notifications).

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

* Address code review feedback on Discord pick announcements

- Fix "on the clock" timing: read currentPickNumber fresh from DB post-autodraft-chain (matches sendOnTheClockEmail guarantee)
- Remove outer try/catch from notifyPickMadeOnDiscord so callers' .catch() is not dead code
- Add missing draft-discord.server.test.ts with 12 tests covering all early-exit and happy paths
- Fix silent empty-string fallback for missing pickedSlot: warn and skip instead
- Eliminate sequential season→league DB queries by accepting leagueId as a direct param
- Show "save webhook URL to configure options" hint when URL is typed but not yet saved
- Remove block-scope braces at both call sites (plain const declarations)
- Remove redundant "Round N, Pick M" description line (title already carries this info)
- Inline pickInRoundFor helper to avoid circular import with draft-utils

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

* Fix lint errors from review fixes

- Remove unused logger import (no longer needed after removing try/catch)
- Remove unused OWNER_ID constant in test fixture
- Use toSorted() instead of sort() in sendDraftOrderNotification

https://claude.ai/code/session_01Tvwsv3LfL9JUqxoLct8dTn

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20 19:55:48 -07:00
Chris Parsons
0ba2b391f8
Fix draft order initialization for teams added mid-season (#449)
* Fix draft order showing only new team when league size increased before order was set

When a commissioner increased the team count on a league that had teams
but no draft order set yet, the server created draft slots only for the
newly-added teams. This left the DB with N+K teams but only K slots,
causing the drag-and-drop list to display only the K new teams.

Two fixes:
1. Server: only append new draft slots when an order was already set
   (existingSlots.length > 0). If no order exists yet, skip slot creation
   so the page correctly treats the order as unset for all teams.
2. Frontend: buildDraftOrderTeams() appends any unslotted teams after the
   slotted ones, so a partially-corrupt DB state still shows all teams.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Address code review feedback on draft order bug fix

- Move buildDraftOrderTeams to app/lib/draft-order.ts so it is importable
  and testable; remove the local copy from the settings component
- Add unit tests for buildDraftOrderTeams covering the empty, full, and
  partial-slot cases
- Tighten the draft slot guard from existingSlots.length > 0 to
  existingSlots.length === currentTeamCount so partial legacy states are
  treated the same as "order not set"
- Condense the 3-line server comment to a single line per project style
- Rename getNumTeamsInSeason → getNumDraftSlotsBySeasonId to reflect what
  the function actually counts, and update its one call site
- Add tests for the server-side slot-creation guard logic

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

* Fix lint: move shouldAppendDraftSlots to outer scope

oxlint (consistent-function-scoping) requires functions that don't close
over any variables to be defined at the outer scope rather than inside a
describe block.

https://claude.ai/code/session_01M3H55gnMxRztJK9KMXqqZo

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-19 18:22:47 -07:00
Chris Parsons
3c757c1f73
Replace settings redirect with toast notification (#445)
* Show toast instead of redirecting after league settings save

Commishes now stay on the settings page when saving, with a Sonner
toast confirming the save rather than being navigated to the league
homepage.

https://claude.ai/code/session_01PewdKxVfAbqasJbmk2UGw4

* Fix toast firing for unrelated intents after settings save

Tag update responses with intent: "update" so the success useEffect
only fires for the main settings form, not for owner/commissioner/
reset-draft actions which also return { success: true }. Also replaces
the remaining ?updated=true redirect (no-season edge case) with the
same response shape for consistency.

https://claude.ai/code/session_01PewdKxVfAbqasJbmk2UGw4

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 16:08:10 -07:00
Chris Parsons
c71eb69648
Add Discord notifications for draft order changes (#441)
* Add Discord notification when draft order is set or randomized

Posts the full draft order to the league's Discord webhook after each
set-draft-order or randomize-draft-order action, with distinct titles
(📋 vs 🎲) so members can tell how it was determined.

https://claude.ai/code/session_01XrBxu358RK6q8iDgRQbMFx

* Refine Discord draft order notification: league name/link, usernames, polish

- Replace seasonName with leagueName + leagueUrl; title links to the league
- Distinguish manual vs randomized in title ("Manually Set" vs "Randomized")
- Show owner username next to each team name in the draft order list
- Remove footer in favour of the linked embed title
- Add 4096-char description clamp consistent with standings notification
- Use a teamMap in both intent branches to avoid O(n²) find-in-loop
- Add test for all-unowned-teams rendering no trailing parentheses

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Remove emoji from draft order notification titles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-17 23:20:51 -07:00
Chris Parsons
d468385d90
Add auto-start draft at scheduled time (#437) (#438)
Commissioners can now opt in to having a draft start automatically at
its scheduled draftDateTime rather than requiring a manual "Start Draft"
click. The 1-second timer loop checks for eligible seasons each tick and
calls the new shared startDraft() service, which is also used by the
existing commissioner HTTP route.

- Add autoStartDraft boolean to seasons table (migration 0108)
- Extract core start logic into app/services/draft-autostart.ts so both
  the API route and the timer can call it without AsyncLocalStorage
- Add checkAndAutoStartDrafts() to the timer tick; disables autoStartDraft
  and stops retrying if no draft order is set at fire time
- Guard against null draftDateTime in both the timer query (isNotNull)
  and server actions (autoStartDraft forced false when datetime is null)
- Add "Auto-start at scheduled time" checkbox to league creation wizard
  (step 3) and league settings, gated on date+time being set
- Show countdown + "Start Now" button in draft room when auto-start is
  scheduled; reuses the existing nowForPauseCheck 1-second ticker
- Show orange warning banner on league homepage to commissioners when
  auto-start is within 1 hour and no draft order has been configured

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:37:29 -07:00
Chris Parsons
469cc82e15
Add opt-in Discord pings to standings notifications (#429)
* 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>
2026-05-15 10:06:54 -07:00
Chris Parsons
05fe1493a3
Refactor league settings into per-section components (#379)
* Refactor league settings into per-section components (#347)

Extract all settings sections from the monolithic 1009-line route file into
individual components under app/components/league/settings/. Route file drops
to ~300 lines. Separates draft-order dirty state from general settings dirty
state, deduplicates section-change handling, and fixes several bugs found
during review (typo in pointsFor5th, wrong mock in tests, lint violations).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix Stop hook loop: suppress output on typecheck success

The Stop hook was producing stdout on every run, causing Claude Code to
feed it back as context and rewake the model each turn. Now emits a
systemMessage JSON only on failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Typescript fix

* Remove type asserting

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:19:50 -07:00