Commit graph

17 commits

Author SHA1 Message Date
Chris Parsons
3f6af2665e Don't ping owners on rank-only standings changes in Discord
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 3m24s
🚀 Deploy / ʦ🔍 Typecheck & Lint (pull_request) Successful in 1m23s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 3m17s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m28s
🚀 Deploy / 🐳 Build (push) Successful in 1m11s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
The first score of a season displaces every other team's rank (e.g.
everyone becomes T2), which fired a mass @-ping of the whole league even
though only one owner actually scored. The Standings Changes section
pinged every team whose rank *or* points changed.

Now the section pings an owner only when that team's points genuinely
changed this event; rank-only shufflers are still displayed with their
rank delta, but by name and without a mention. Mirrors the existing
"display but don't ping" behaviour in the Scored Matches section and
preserves pings for pure season-standings sports (F1, IndyCar), where a
real scorer's points always change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 22:02:06 -07:00
58e656f15e claude/wimbledon-qp-scoring-bug-xevhlf (#127)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 3m14s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m20s
🚀 Deploy / 🐳 Build (push) Successful in 1m8s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #127
2026-07-03 21:40:11 +00:00
Claude
8900bf79dc
Announce drafted tennis players eliminated in non-scoring rounds
A player drafted in a tennis major (e.g. Jakob Mensik, out in the Round of
64) got no Discord announcement when the bracket was scored by sync. The
first three Grand Slam rounds are non-scoring, so an early-round loser earns
0 QP, gets no event_results row, and is dropped from the
"Qualifying Points Update" notification — the only announcement the tennis
sync emits mid-tournament.

Detect players knocked out on each sync and surface them:

- populateBracketFromDraw now returns newlyDecidedLoserIds: losers of
  matches that transition to complete on this run. Idempotent across
  re-syncs since playoff_matches persist, so a knockout is announced once.
- syncTennisDraw threads that set into notifyQualifyingPointsUpdate and
  fires the notification even when no QP changed.
- notifyQualifyingPointsUpdate builds an eliminated list scoped to players
  drafted in the league, deduped against QP earners (so a Round-of-16 loser
  who scores isn't listed twice), tagging the drafting manager.
- sendQualifyingPointsUpdateNotification renders a "Knocked Out" section and
  pings those managers; the QP Standings block is skipped when a sync only
  reports knockouts.

Tests cover the new detection, dedup, manager tagging, knockout-only
notifications, and rendering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkPWxSCunhPFknNUTXm4aZ
2026-07-03 14:42:06 +00:00
b0c25beb90 claude/discord-qualifying-points-t55s3x (#121)
Some checks failed
🚀 Deploy / 🐳 Build (push) Failing after 43s
🚀 Deploy / 🚀 Deploy (push) Has been skipped
🚀 Deploy / 🧪 Test (push) Successful in 2m53s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m18s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #121
2026-07-01 17:29:45 +00:00
e3e8485e26 elimination announcements (#114)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 2m53s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m17s
🚀 Deploy / 🐳 Build (push) Successful in 1m9s
🚀 Deploy / 🚀 Deploy (push) Successful in 12s
2026-06-28 19:36:33 +00:00
ef29c1fcd9 claude/discord-pick-notification-refactor-FeusJ (#58)
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 1m36s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m23s
🚀 Deploy / 🔍 Lint (push) Successful in 50s
🚀 Deploy / 🐳 Build (push) Successful in 14m19s
🚀 Deploy / 🚀 Deploy (push) Successful in 13s
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #58
2026-05-29 16:24:22 +00:00
4a2f542fb1 Fix Discord pick notifications silently dropped during autodraft chains (#57)
All checks were successful
🚀 Deploy / 🔍 Lint (push) Successful in 50s
🚀 Deploy / 🐳 Build (push) Successful in 14m19s
🚀 Deploy / 🚀 Deploy (push) Successful in 11s
🚀 Deploy / 🧪 Test (push) Successful in 1m31s
🚀 Deploy / ʦ TypeScript (push) Successful in 1m21s
## Summary

- **Per-league queue**: replaced the single global notification queue with a \`Map<leagueId, queue>\` so concurrent drafts in different leagues drain independently and don't block each other
- **Deadlock fix**: wrapped the drainer loop in \`try/finally\` so \`drainingLeagues\` is always cleaned up even if a task throws unexpectedly — previously a crash would leave the flag set permanently, silently dropping all future notifications for that league
- **Wait cap fix**: moved \`Math.min\` to wrap \`(retryAfter * (attempt+1))\` so the 10s ceiling applies to the final product, not just the base value — settings actions that directly await webhook calls were previously blockable for up to 30s on a rate-limited third attempt

## Root cause of the original problem

During an autodraft chain, \`notifyPickMadeOnDiscord\` was called fire-and-forget for each pick in the chain, causing all webhook requests to fly concurrently. Discord's per-webhook rate limit (~5 req/2s) rejected most of them; the single retry logic fired for all simultaneously, hit the limit again, and the \`.catch()\` swallowed the errors silently.

## Test plan

- [ ] Enable autodraft for 5+ consecutive teams; trigger the chain; confirm all picks announce in Discord in order
- [ ] Confirm a slow/rate-limited webhook in League A does not delay League B's announcements
- [ ] \`npm run test:run\` — 158 test files, 2336 tests, all pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Chris Parsons <chrisparsons1127@gmail.com>
Reviewed-on: #57
2026-05-27 16:41:08 +00:00
Chris Parsons
77f520823c Fix Discord pick announcements arriving out of order and add 429 retry
All checks were successful
🚀 Deploy / 🧪 Test (pull_request) Successful in 1m30s
🚀 Deploy / ʦ TypeScript (pull_request) Successful in 1m16s
🚀 Deploy / 🔍 Lint (pull_request) Successful in 48s
🚀 Deploy / 🐳 Build (pull_request) Has been skipped
🚀 Deploy / 🚀 Deploy (pull_request) Has been skipped
- Move notifyPickMadeOnDiscord() before checkAndTriggerNextAutodraft() in
  both executeAutoPick() and the manual pick action. Previously the
  triggering pick's announcement fired after all chained autodraft picks
  had already announced, causing Discord to show e.g. pick #106 before
  #105 before #104. Now each pick announces itself before the next pick
  in the chain is triggered.

- Add a single retry with Retry-After delay in sendDiscordWebhook() on
  HTTP 429. A burst of consecutive autodraft picks could hit Discord's
  per-webhook rate limit and silently drop announcements. Use || 1
  (not ?? 1) to handle non-numeric/NaN headers, and cap at 10 s so
  that awaited callers such as the draft-order settings actions are
  never hung for a full global rate-limit window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 21:29:02 -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
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
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
611e1ccf0a
Show tied ranks with T prefix across standings and Discord (#239)
* Show tied ranks with T prefix across standings and Discord notifications, fixes #197

- Add buildTiedRankChecker() utility to standings-display.ts; replaces
  four copies of inline rank-count logic spread across components,
  the league home route, and the Discord service
- Prefix shared ranks with "T" (e.g. "T3") in StandingsTable (league
  panel), StandingsTable (full standings), the league home preview, and
  Discord webhook notifications
- Add useMemo wrapping in StandingsTable components so tie detection
  does not recompute on every render
- Fix compareTeamsForRanking to round total points to hundredths before
  comparing, preventing floating-point noise from producing spurious
  non-ties in the standings

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

* Fix discord test to expect escaped period in rank display

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 20:45:15 -07:00
Chris Parsons
d271cc6792
Refactor standings notifications to show only changed teams (#182)
* Refine Discord webhook scoring notifications (fixes #180, #181)

- Filter scored matches to only show matchups where a manager scores
  Brackt points (winner drafted) or has a team eliminated (loser drafted);
  matches with no manager involvement are suppressed entirely.
- Escape Discord markdown characters (_*~`|\) in team names and usernames
  to prevent formatting issues (e.g. double-underscore names causing
  unintended underlines).
- Replace full standings with a "Standings Changes" section that shows
  only teams whose points changed, plus any teams whose rank shifted as a
  result, with ↑N / ↓N indicators for rank movement.
- Pass previousRanks map from scoring-calculator to the notification so
  rank-displaced teams (who didn't score points themselves) are included.
- Update test webhook in league settings to demonstrate rank changes and
  a sample scored match.
- Update all discord service tests to cover the new filtering, escaping,
  and standings-change behaviour.

https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18

* fix: only set winnerUsername when winning team's score actually changed

Previously, winnerUsername was set for any drafted winner regardless of
whether points were actually scored. This caused R64 wins (where the
scoring system may not award points until later rounds) to appear in
Discord's Scored Matches section even when no Brackt points were earned.

Now winnerUsername is only set when the winner's team's totalPoints
changed after recalculation. loserUsername (eliminations) is always set
when the loser is drafted, since being knocked out is always notable.

https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18

* Refactor Discord notification logic in scoring calculator

- Compute changedTeamIds once up front; derive hasChanges from it
  instead of duplicating the same iteration
- Merge usernameForParticipant and winnerUsernameForParticipant into a
  single function with a requireScoreChange flag
- Replace hasDraftedParticipantMatches with hasScoredMatchesToShow,
  which checks that at least one scoredMatch has a displayable username
  — prevents sending a contentless Discord embed when a drafted winner
  doesn't score any points and the loser isn't drafted
- Update inline comment to be sport-agnostic

https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-19 15:52:57 -07:00
Chris Parsons
7236c8aefb
Add Discord usernames, fix calculateTeamScore partial-score bug, and add admin re-score action (#160)
**Discord webhook improvements**
- Show team owner username in parentheses in standings (e.g. "1. Alpha FC (christhrowsrocks) — 150 pts")
- Show username in parentheses in match results for drafted participants
  (e.g. "Sporting (christhrowsrocks) def. Bodø/Glimt (apatel)"), omitting
  the parenthesis for the undrafted side
- Username lookup only runs after the webhook URL guard to avoid wasted DB queries
- 4 new tests covering all username display combinations

**Fix calculateTeamScore partial-score counting bug**
- isPartialScore participants were incorrectly counted in participantsCompleted
  and placementCounts, causing standings to show wrong remaining count and
  phantom placement badges (e.g. "5th×1") for still-alive bracket participants
- Floor points still flow into totalPoints (guaranteed value for ranking)
- 3 new unit tests covering partial vs finalized behavior in calculateTeamScore

**Admin Force Re-score action**
- New "Fantasy Standings" card on every sports season admin page with a
  Force Re-score button that triggers recalculateStandings on all linked
  fantasy seasons — useful after data corrections without waiting for a
  new scoring event
- Auth guard added to the action (was missing — layout loader only protects GET)
- Returns a meaningful message when no linked seasons exist
- Intent discriminant on action returns so success banners use actionData.intent
  instead of fragile message.includes() checks

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 14:34:09 -07:00
Chris Parsons
beab6400b2
Fix partial bracket scoring, rank labels, and Discord notification bugs (#158)
- NCAAM R64/R32 winners no longer receive T5 floor points; only Sweet
  Sixteen winners entering the first scoring round get a floor
- AFL T5-T6 and T7-T8 score as separate tiers (avg([5,6]) and avg([7,8]))
  instead of one shared avg([5-8]) pool
- Eliminated teams now always show their correct final rank (e.g. T33 for
  NCAAM R64 losers) even during partial round scoring, by deriving the
  rank label from the total match count per round rather than the dynamic
  still-alive count
- Discord notifications now fire when a drafted participant is eliminated
  with 0 points (e.g. R64 losers whose team score doesn't change)
- Discord notifications no longer include all prior matches — scoped to
  the current batch via matchIds
- Discord section headers have spacing; rank medals replaced with numbers
- Rounds auto-complete when all matches are marked done; manual Complete
  Round dropdown removed
- Fixed double Discord notification when the last match in a round
  triggers auto-complete: processPlayoffEvent now accepts skipRecalculate
  so the caller controls when the notification fires
- Fixed falsy check on finalPosition=0 in calculateTeamScore
- Undrafted 0-point participants filtered from Eliminated Teams display

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 12:34:10 -07:00
Chris Parsons
79f7a41837
Add Discord webhook notifications for standings updates (#157)
* Add Discord webhook notifications for standings updates

Adds a Discord webhook integration that posts standings to a configured
Discord channel whenever scores change after a scoring event. Commissioners
set the webhook URL in league settings and can send a test notification.

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

* Fix Discord notification edge cases from code review

- Fix point delta sign: negative diffs now correctly show -5 pts not +-5 pts
- Replace hardcoded soccer emoji with a neutral bullet (works for all sports)
- Truncate embed description at Discord's 4096-char limit
- Thread eventId through processMatchResult and bracket batch handler so
  scored matches appear in single-match notifications too
- Pass eventName to finalizeQualifyingPoints ("Final Standings") and
  processSeasonStandings ("Season Complete") so those notifications have context
- Test notification now fetches current season to show "League 2025" format

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

* Fix process-match-result tests: add sportsSeasons to db mock

recalculateAffectedLeagues now queries sportsSeasons to get the sport
name for Discord notifications; the test mock db needed the stub added.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 11:16:36 -07:00
Chris Parsons
dbffddc9ff
Partial bracket scoring, code review fixes, and double-chance logic (#156)
## Partial bracket scoring
- `processMatchResult`: new exported function that scores a single match
  immediately (loser → final placement, winner → provisional floor).
  Called from `set-winner` and `set-round-winners` so points are awarded
  as soon as a winner is set, before the full round is complete.
- `set-winner`: passes `eventName` to `processMatchResult`.
- `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues`
  and `updateProbabilitiesAfterResult` once after the loop instead of per-match
  (`skipSideEffects: true` per match).

## Code review fixes
- **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table +
  `getRoundConfig()` helper, eliminating three parallel `if/else` chains in
  `processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`.
  AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory
  comment about why the `bracketTemplateId` guard is required.
- **skipSideEffects** (C2): new param on `processMatchResult`; bracket server
  uses it to batch standings/probability recalc in `set-round-winners`.
- **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`.
- **Round validation** (W2): `set-round-winners` now guards `match.round === round`
  before processing each assignment.
- **Comments** (C3/S3/W3): added notes on non-scoring loser assumption,
  `isScoring ?? true` default, and AFL Semi-Finals template requirement.

## PlayoffBracket eliminated-teams fix + tests
- Fixed `computeEliminatedByRound` to track participant *appearances* (not just
  wins), so AFL QF losers who advance to Semi-Finals via double-chance are
  correctly excluded from the QF eliminated list.
- Extracted the logic as an exported pure function for testability.
- Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser →
  SF loss, and normal advancement not protecting a later loser.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 10:50:30 -07:00