Participants eliminated in a non-scoring bracket round (e.g. Round of 16
in a 16-team bracket) receive finalPosition=0 and isPartialScore=false.
The previous calculateTeamScore loop only entered participantsCompleted++
when finalPosition > 0, so these participants were silently treated as
still "remaining" in the standings table — causing e.g. a team with one
eliminated participant to show "25 remaining" instead of "24 remaining".
Fix: add an else-if branch that increments participantsCompleted for any
finalized result (isPartialScore=false) regardless of finalPosition value.
The condition is intentionally broader than `=== 0` to also cover the
theoretical case of finalPosition=null with isPartialScore=false.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
**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>
* Fix bracket point averaging and partial-score display in standings/breakdown
Bracket sports (UCL, NBA, NFL, etc.) use averaged points for tied
positions (e.g. QF losers all share avg of 5th-8th = 20 pts), but
both calculateTeamProjectedScore and getTeamScoreBreakdown were using
raw pointsFor5th (25) instead of the bracket-averaged value. This caused
the team breakdown page to show incorrect actual points (25 vs 20) and
the standings page to show incorrect actualPoints/projectedPoints totals.
Additionally, participants with isPartialScore=true (still alive in a
bracket with a provisional floor position) were being displayed with a
final placement badge (e.g. "5th") instead of "Pending".
Changes:
- calculateTeamProjectedScore: use calculateBracketPoints for bracket
sports; handle isPartialScore participants separately (floor in
actualPoints, incremental EV in projectedPoints, Math.max guard for
EV < floor edge case)
- getTeamScoreBreakdown: same bracket averaging fix; pass isPartialScore
through to picks; use explicit finalPosition != null && > 0 check
- TeamScoreBreakdown component: show Pending badge for isPartialScore
participants; display actual/floor + EV row for all incomplete picks;
add "actual / projected" column header subtitle
- 9 new unit tests covering all scoring branches including the EV-below-
floor clamp edge case
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix typecheck: cast DUMMY_EV_ROW to any in test fixture
ParticipantEV has additional required fields (id, participantId, etc.)
that aren't needed for the mock — cast to any to satisfy the type checker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>