Skip to content

Fix various UI performance or UI minor correctness issues - #415

Open
chondl wants to merge 6 commits into
avgupta456:masterfrom
chondl:match-page-fixes
Open

Fix various UI performance or UI minor correctness issues#415
chondl wants to merge 6 commits into
avgupta456:masterfrom
chondl:match-page-fixes

Conversation

@chondl

@chondl chondl commented Jul 11, 2026

Copy link
Copy Markdown

This PR fixes six distinct performance or minor correctness UI issues. Verified with a real browser against a local rig (CockroachDB + fake-gcs + full 2026 season, 3724 teams / 215 events / 18372 matches).

1. Match page: infinite media-request loop

Symptom. On every /match/{key} page the browser fired an endless stream of requests to TBA's /team/frc{N}/media/{year} endpoint — measured ~11,747 requests in 5 seconds on a single open page. Every production visitor has been silently doing this to TBA.

Root cause. frontend/src/pagesContent/match/[match_id]/imageRow.tsx:13-14 recomputed the teams array with .concat() on every render, producing a new array reference each time. The useEffect(..., [teams, year]) at line 19 then called getMediaUrls and setMedia, whose state update re-rendered the component, produced a fresh teams reference, and re-triggered the effect — an unbounded loop.

Fix. Wrap teams in useMemo(..., [data]) so its reference is stable across renders and the effect runs only when the underlying match data changes.

2. Match page: Previous/Next buttons don't update the page

Symptom. Clicking the prev/next arrows changed the URL and document title but left the displayed match unchanged.

Root cause. frontend/src/pages/match/[match_id].tsx:26 guarded the fetch with if (!match_id || data) return;. Because Next.js reuses the same component instance across /match/* route changes, data was already truthy from the previous match, so the guard short-circuited and never fetched the new match.

Fix. Compare the loaded key against the route — if (!match_id || data?.match?.key == match_id) return; — matching the existing pattern in pages/event/[event_id].tsx:25. The effect already depends on [match_id, data], so it now refetches when the route changes and skips redundant fetches once the correct match is loaded.

3. Strength of Schedule renders blank (or NaN) when pre-event EPAs are identical

Symptom. On the Strength of Schedule tab the RP/Rank/EPA/Composite score columns fail to populate. Two manifestations: score columns entirely blank, or NaN in EPA Score and Composite Score.

Root cause. strengthOfSchedule() (in frontend/src/pagesContent/event/[event_id]/worker.ts) runs a "Before Event" pass that computes epaSd, the standard deviation of every team's pre-event start EPA, and builds a Gaussian to score EPA-based schedule strength:

const distrib = Gaussian(0, (epaSd * epaSd * 5) / N);

Early in a season — before ratings diverge — every team at an event shares the same cold-start EPA, so epaSd is 0, the variance is 0, and gaussian throws Error('Variance must be > 0'). Because the worker's message handler calls strengthOfSchedule(...) without await/catch, the throw becomes a silent unhandled promise rejection: no message is posted and the table stays blank. A floating-point variant produces the NaN symptom — with all EPAs equal, Math.sqrt(sum(x^2)/n - avg^2) occasionally takes the root of a tiny negative rounding residue, so epaSd is NaN, gaussian does not throw (NaN <= 0 is false), and every EPA percentile is NaN. Both are the same degeneracy: EPA carries no schedule signal when all ratings are identical.

Fix. Floor the variance with || 1e-9, which treats both 0 and NaN as falsy and substitutes a negligible positive variance; with deltaEPA == 0 the CDF at the mean is 0.5, so every team gets a neutral 0.5 EPA percentile — the correct answer when EPA is non-informative. Any real positive variance passes through untouched. Matches the || 0 fallback idiom already used throughout this worker.

4. Simulation tab shows EPA 0 for every team

Symptom. On the Simulation tab the EPA column reads 0 for all teams (the predicted ranks, RP means, and percentiles are correct).

Root cause. frontend/src/pagesContent/event/[event_id]/simulation.tsx reads the team EPA as teamEvent.epa.total_points.mean. That matches the APITeamEvent TypeScript type, but not the runtime shape: the backend serves epa.total_points as a plain number, so .mean is undefined and ?? 0 renders 0.

Fix. Read epa.breakdown.total_points — the same field the SOS tab and the simulation worker already use — which is present at runtime.

5. Noteworthy matches rank a null/placeholder match incorrectly

Symptom. /v3/site/noteworthy_matches/2026 returned a 0/null-score, fully-DQed placeholder match (2026txmca_sf6m1) at the top of "Highest Clean Scores" (and the other lists), ahead of real high scores.

Root cause. backend/src/db/functions/noteworthy_matches.py — the lists order_by(desc(...)) without specifying null placement. CockroachDB orders NULLs first under DESC. For 2016+ the sort uses the no_foul columns; a match with no clean result on either alliance yields greatest(...) = NULL (and sum = NULL for combined), so it sorts to the top. (The value only surfaces on a NULLS-FIRST-defaulting DB, which is why it appears on staging.)

Fix. Add .nullslast() to every noteworthy order_by. Null-result matches fall past the top-30 cutoff; real high-scoring matches rank first; legitimate single-alliance-DQ matches still rank by their scoring alliance.

6. Every blob/API resource fetched twice on team pages

Symptom. On /team/*, each event/team blob was downloaded exactly twice per page load (observed on the production build, so not a StrictMode artifact).

Root cause. frontend/src/api/storage.tsx query() had no in-flight dedup. Several components request the same resource concurrently; each awaits getWithExpiry(), all miss IndexedDB (nothing is written until a fetch resolves), and all issue their own fetch.

Fix. Add a module-level in-flight promise map keyed by storageKey. Concurrent callers for the same key share one fetch; the entry clears once it settles. The fetch body is extracted into fetchAndStore unchanged.

Verification

  • Match page: after the fix the steady-state media-request count is 0 (was ~11.7k/5s), and prev/next navigation updates the displayed match (Qual 43 ↔ Qual 44) with the arrows re-pointing correctly.
  • Strength of Schedule: epaSd == 0 event — before: Gaussian throws, SOS blank; after: all columns populate (EPA Score 0.5). epaSd == NaN (float rounding) event — before: EPA/Composite NaN; after: populate (EPA Score 0.5). Event with a genuine non-zero EPA spread: EPA percentile 0.387 before and after — unchanged (the fix is inert on the non-degenerate path).
  • Simulation: EPA column 0 before, real per-team EPA after; predicted-rank distributions unchanged and still vary run-to-run.
  • Noteworthy: simulating staging's NULLS-FIRST ordering, 2026txmca_sf6m1/_sf9m1 ranked first or second above the real 964-point match; with the patched query, 2026dal_f1m1 (964) is first across all six lists and the placeholder is gone.
  • Duplicate fetches: each event/team blob downloaded once per page load after the dedup (was twice).

chondl added 6 commits July 9, 2026 23:08
Floor the EPA-percentile Gaussian variance so it is always > 0.

When every team at an event shares the same pre-event start EPA (true for
all events early in a season, before ratings diverge), epaSd is 0, so the
variance (epaSd^2 * 5 / N) is 0 -- or NaN when floating-point error makes
the variance argument to Math.sqrt slightly negative. gaussian() throws on
variance <= 0, which rejected the un-awaited strengthOfSchedule() promise in
the worker and left the SOS table blank; the NaN path instead rendered NaN in
the EPA/Composite columns. Both are the same degeneracy. '|| 1e-9' treats 0
and NaN alike, yielding a neutral 0.5 EPA percentile, and leaves every real
positive variance untouched.
Read the team EPA from epa.breakdown.total_points, the field the SOS tab and
the simulation worker already use. simulation.tsx read epa.total_points.mean,
which matches the stale APITeamEvent type but not the runtime shape: the backend
serves epa.total_points as a plain number, so .mean is undefined and every row
rendered EPA 0.
CockroachDB orders NULLs first under ORDER BY ... DESC, so a match whose clean
score is null on both alliances (a fully-DQed / placeholder match, e.g.
2026txmca_sf6m1) sorted to the top of every noteworthy list -- ranking a
0/null result as the highest clean score. greatest()/sum() over the no_foul
columns yields null when both alliances lack a clean result. Add .nullslast()
to every noteworthy order_by so these matches fall to the bottom (out of the
top 30) and real high-scoring matches rank first, while legitimate single-DQ
matches keep ranking by their scoring alliance.
On team pages multiple components concurrently request the same event/blob
resources. Each caller awaits getWithExpiry(), all miss IndexedDB (nothing is
written until a fetch completes), and all issue their own network fetch, so
every blob/API resource was downloaded twice per page load. Add a module-level
in-flight promise map keyed by storageKey: concurrent callers for the same key
now share one fetch, and the entry is cleared once it settles.
@vercel

vercel Bot commented Jul 11, 2026

Copy link
Copy Markdown

@chondl is attempting to deploy a commit to the avgupta456's projects Team on Vercel.

A member of the Team first needs to authorize it.

@chondl chondl changed the title Fix various UI performance or minor correctness issues Fix various UI performance or UI minor correctness issues Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant