Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for importing and optionally including PRACTICE matches in analysis by introducing a new user setting (includePracticeMatches) plus endpoints to read/update it, and applying the filter across analysis queries.
Changes:
- Add
PRACTICEtoMatchType, addUser.includePracticeMatcheswith defaultfalse. - Add manager settings endpoints + settings update support for
includePracticeMatches. - Update analysis and CSV/picklist queries to optionally include/exclude practice matches; update match importer to pull practice matches from Nexus.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/manager/settings.routes.ts | Adds OpenAPI + routing for /practicesource endpoints (but PUT /settings spec now out of sync with handler). |
| src/routes/manager/scoutershifts.routes.ts | Moves /generate route registration (currently makes it unauthenticated). |
| src/handler/manager/settings/updateSettings.ts | Requires and persists includePracticeMatches during settings updates. |
| src/handler/manager/settings/getPracticeSource.ts | New handler to return includePracticeMatches. |
| src/handler/manager/settings/addPracticeSource.ts | New handler to update includePracticeMatches. |
| src/handler/manager/scoutershifts/generateSchedule.ts | Adds “no schedule” 404 handling; widens request type to non-authenticated Request. |
| src/handler/manager/getTeams.ts | Removes a stray console.log. |
| src/handler/manager/addTournamentMatches.ts | Imports practice matches from Nexus and writes them to teamMatchData (currently includes debug logs, early-return behavior, and an import-time invocation). |
| src/handler/analysis/teamLookUp/getNotes.ts | Applies practice-match filtering based on user setting. |
| src/handler/analysis/teamLookUp/categoryMetrics.ts | Applies practice-match filtering based on user setting. |
| src/handler/analysis/teamLookUp/breakdownMetrics.ts | Applies practice-match filtering based on user setting. |
| src/handler/analysis/picklist/endgamePicklistTeamFast.ts | Adds practice-match filtering parameter. |
| src/handler/analysis/csv/getTeamCSV.ts | Applies practice-match filtering based on user setting. |
| src/handler/analysis/csv/getReportCSV.ts | Applies practice-match filtering based on user setting. |
| src/handler/analysis/coreAnalysis/nonEventMetric.ts | Adds SQL condition to exclude PRACTICE matches unless enabled. |
| src/handler/analysis/coreAnalysis/averageManyFast.ts | Applies practice-match filtering based on user setting. |
| src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts | Removes debug logging; applies practice-match filtering based on user setting. |
| src/handler/analysis/autoPaths/autoPathsTeam.ts | Applies practice-match filtering based on user setting. |
| prisma/schema.prisma | Adds User.includePracticeMatches and MatchType.PRACTICE. |
Comments suppressed due to low confidence (1)
src/handler/manager/addTournamentMatches.ts:318
- This module calls
addTournamentMatches("2026cancmp")at import time. That will trigger external API calls and DB writes whenever the module is loaded (including in prod startup/tests), which is a serious side effect. Remove the invocation or move it behind an explicit CLI/script entrypoint.
addTournamentMatches("2026cancmp");
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }; | ||
| const ReverseMatchTypeMap: Record<MatchType, number> = { | ||
| [MatchType.QUALIFICATION]: 0, | ||
| [MatchType.ELIMINATION]: 1, | ||
| [MatchType.PRACTICE]: 2, |
There was a problem hiding this comment.
ReverseMatchTypeMap now supports MatchType.PRACTICE, but the forward MatchTypeMap still only maps 0 and 1. Any code that converts a numeric matchType (e.g. request params) to the Prisma enum will not be able to resolve practice matches (value 2) correctly. Add 2: MatchType.PRACTICE to MatchTypeMap to keep the mappings consistent.
| .object({ | ||
| teamSource: z.array(z.number()), | ||
| tournamentSource: z.array(z.string()), | ||
| includePracticeMatches: z.boolean().optional().default(false), | ||
| }) |
There was a problem hiding this comment.
includePracticeMatches is optional in the API schema, but the Zod parser sets a default of false. If a client updates settings without sending this field, the user’s existing includePracticeMatches value will be overwritten to false unintentionally. Remove the .default(false) and only include includePracticeMatches in the Prisma update when it is provided (or explicitly keep the existing DB value).
| const nexusResponse = await fetch( | ||
| `https://frc.nexus/api/v1/event/${tournamentKey}`, | ||
| { | ||
| method: "GET", | ||
| headers: { | ||
| "Nexus-Api-Key": process.env.NEXUS_KEY ?? "", | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| if (!nexusResponse.ok) { | ||
| const errorMessage = await nexusResponse.text(); | ||
| console.error("Error getting live event status:", errorMessage); | ||
| } else { | ||
| const data = await nexusResponse.json(); | ||
|
|
||
| for (const match of data.matches) { | ||
| if (match.label.startsWith("Practice")) { | ||
| const practiceMatchNumber = parseInt(match.label.split(" ")[1]); | ||
| if (isNaN(practiceMatchNumber)) { | ||
| continue; | ||
| } | ||
|
|
||
| const matchKey = `${tournamentKey}_pr${practiceMatchNumber}`; | ||
| for (let i = 0; i < match.redTeams.length; i++) { | ||
| const teamNumber = Number(match.redTeams[i]); | ||
| const currMatchKey = `${matchKey}_${i}`; | ||
| await prismaClient.teamMatchData.upsert({ | ||
| where: { | ||
| key: currMatchKey, | ||
| }, | ||
| update: { | ||
| tournamentKey: tournamentKey, | ||
| matchNumber: practiceMatchNumber, | ||
| teamNumber: teamNumber, | ||
| matchType: "PRACTICE", | ||
| }, | ||
| create: { | ||
| key: currMatchKey, | ||
| tournamentKey: tournamentKey, | ||
| matchNumber: practiceMatchNumber, | ||
| teamNumber: teamNumber, | ||
| matchType: "PRACTICE", | ||
| }, | ||
| }); | ||
| } | ||
| for (let i = 0; i < match.blueTeams.length; i++) { | ||
| const teamNumber = Number(match.blueTeams[i]); | ||
| const currMatchKey = `${matchKey}_${i + 3}`; | ||
|
|
||
| await prismaClient.teamMatchData.upsert({ | ||
| where: { | ||
| key: currMatchKey, | ||
| }, | ||
| update: { | ||
| tournamentKey: tournamentKey, | ||
| matchNumber: practiceMatchNumber, | ||
| teamNumber: teamNumber, | ||
| matchType: "PRACTICE", | ||
| }, | ||
| create: { | ||
| key: currMatchKey, | ||
| tournamentKey: tournamentKey, | ||
| matchNumber: practiceMatchNumber, | ||
| teamNumber: teamNumber, | ||
| matchType: "PRACTICE", | ||
| }, | ||
| }); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The Nexus practice-match import is inside the same outer try as the existing TBA import. If nexusResponse.json() throws or data.matches is missing/unexpected, the function will jump to the catch and skip importing qualification/elimination matches entirely. Wrap the Nexus logic in its own try/catch (and/or validate data before iterating) so failures in Nexus don’t prevent the main TBA match import.
| const nexusResponse = await fetch( | |
| `https://frc.nexus/api/v1/event/${tournamentKey}`, | |
| { | |
| method: "GET", | |
| headers: { | |
| "Nexus-Api-Key": process.env.NEXUS_KEY ?? "", | |
| }, | |
| }, | |
| ); | |
| if (!nexusResponse.ok) { | |
| const errorMessage = await nexusResponse.text(); | |
| console.error("Error getting live event status:", errorMessage); | |
| } else { | |
| const data = await nexusResponse.json(); | |
| for (const match of data.matches) { | |
| if (match.label.startsWith("Practice")) { | |
| const practiceMatchNumber = parseInt(match.label.split(" ")[1]); | |
| if (isNaN(practiceMatchNumber)) { | |
| continue; | |
| } | |
| const matchKey = `${tournamentKey}_pr${practiceMatchNumber}`; | |
| for (let i = 0; i < match.redTeams.length; i++) { | |
| const teamNumber = Number(match.redTeams[i]); | |
| const currMatchKey = `${matchKey}_${i}`; | |
| await prismaClient.teamMatchData.upsert({ | |
| where: { | |
| key: currMatchKey, | |
| }, | |
| update: { | |
| tournamentKey: tournamentKey, | |
| matchNumber: practiceMatchNumber, | |
| teamNumber: teamNumber, | |
| matchType: "PRACTICE", | |
| }, | |
| create: { | |
| key: currMatchKey, | |
| tournamentKey: tournamentKey, | |
| matchNumber: practiceMatchNumber, | |
| teamNumber: teamNumber, | |
| matchType: "PRACTICE", | |
| }, | |
| }); | |
| } | |
| for (let i = 0; i < match.blueTeams.length; i++) { | |
| const teamNumber = Number(match.blueTeams[i]); | |
| const currMatchKey = `${matchKey}_${i + 3}`; | |
| await prismaClient.teamMatchData.upsert({ | |
| where: { | |
| key: currMatchKey, | |
| }, | |
| update: { | |
| tournamentKey: tournamentKey, | |
| matchNumber: practiceMatchNumber, | |
| teamNumber: teamNumber, | |
| matchType: "PRACTICE", | |
| }, | |
| create: { | |
| key: currMatchKey, | |
| tournamentKey: tournamentKey, | |
| matchNumber: practiceMatchNumber, | |
| teamNumber: teamNumber, | |
| matchType: "PRACTICE", | |
| }, | |
| }); | |
| } | |
| } | |
| } | |
| try { | |
| const nexusResponse = await fetch( | |
| `https://frc.nexus/api/v1/event/${tournamentKey}`, | |
| { | |
| method: "GET", | |
| headers: { | |
| "Nexus-Api-Key": process.env.NEXUS_KEY ?? "", | |
| }, | |
| }, | |
| ); | |
| if (!nexusResponse.ok) { | |
| const errorMessage = await nexusResponse.text(); | |
| console.error("Error getting live event status:", errorMessage); | |
| } else { | |
| const data = await nexusResponse.json(); | |
| if ( | |
| typeof data !== "object" || | |
| data === null || | |
| !("matches" in data) || | |
| !Array.isArray(data.matches) | |
| ) { | |
| console.error("Invalid Nexus event payload:", data); | |
| } else { | |
| for (const match of data.matches) { | |
| if ( | |
| typeof match !== "object" || | |
| match === null || | |
| typeof match.label !== "string" || | |
| !Array.isArray(match.redTeams) || | |
| !Array.isArray(match.blueTeams) | |
| ) { | |
| continue; | |
| } | |
| if (match.label.startsWith("Practice")) { | |
| const practiceMatchNumber = parseInt(match.label.split(" ")[1]); | |
| if (isNaN(practiceMatchNumber)) { | |
| continue; | |
| } | |
| const matchKey = `${tournamentKey}_pr${practiceMatchNumber}`; | |
| for (let i = 0; i < match.redTeams.length; i++) { | |
| const teamNumber = Number(match.redTeams[i]); | |
| const currMatchKey = `${matchKey}_${i}`; | |
| await prismaClient.teamMatchData.upsert({ | |
| where: { | |
| key: currMatchKey, | |
| }, | |
| update: { | |
| tournamentKey: tournamentKey, | |
| matchNumber: practiceMatchNumber, | |
| teamNumber: teamNumber, | |
| matchType: "PRACTICE", | |
| }, | |
| create: { | |
| key: currMatchKey, | |
| tournamentKey: tournamentKey, | |
| matchNumber: practiceMatchNumber, | |
| teamNumber: teamNumber, | |
| matchType: "PRACTICE", | |
| }, | |
| }); | |
| } | |
| for (let i = 0; i < match.blueTeams.length; i++) { | |
| const teamNumber = Number(match.blueTeams[i]); | |
| const currMatchKey = `${matchKey}_${i + 3}`; | |
| await prismaClient.teamMatchData.upsert({ | |
| where: { | |
| key: currMatchKey, | |
| }, | |
| update: { | |
| tournamentKey: tournamentKey, | |
| matchNumber: practiceMatchNumber, | |
| teamNumber: teamNumber, | |
| matchType: "PRACTICE", | |
| }, | |
| create: { | |
| key: currMatchKey, | |
| tournamentKey: tournamentKey, | |
| matchNumber: practiceMatchNumber, | |
| teamNumber: teamNumber, | |
| matchType: "PRACTICE", | |
| }, | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Error importing Nexus practice matches:", error); |
PRACTICEmatch typeincludePracticeMatchesand related endpointsincludePracticeMatchesas a filter in analysis