Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ model User {
role UserRole @default(ANALYST)
teamSourceRule Json @default("{\"mode\": \"EXCLUDE\", \"items\": []}")
tournamentSourceRule Json @default("{\"mode\": \"EXCLUDE\", \"items\": []}")
includePracticeMatches Boolean @default(false)
ApiKeys ApiKey[]
mutablePicklists MutablePicklist[]
sharedPicklists SharedPicklist[]
Expand Down Expand Up @@ -331,6 +332,7 @@ enum UserRole {
}

enum MatchType {
PRACTICE
QUALIFICATION
ELIMINATION
}
5 changes: 5 additions & 0 deletions src/handler/analysis/autoPaths/autoPathsTeam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ const config = {
teamMatchData: {
teamNumber: teamNumber,
tournamentKey: sourceTnmtFilter,
matchType: ctx.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
},
scouter: {
sourceTeamNumber: sourceTeamFilter,
Expand Down
47 changes: 7 additions & 40 deletions src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,15 +387,6 @@ const config: AnalysisFunctionConfig<typeof argsSchema, typeof returnSchema> = {
.filter((t) => t <= autoEnd)
.sort((a, b) => a - b);
const first = filteredAutoTimes[0];
try {
console.debug("autoClimbStartTime", {
reportIndex: idx,
autoClimb: ac,
rawClimbTimes,
filteredAutoTimes,
first,
});
} catch {}
if (ac === "SUCCEEDED" && first !== undefined) {
const remaining = autoEnd - first;
const clamped = remaining >= 0 ? remaining : 0;
Expand Down Expand Up @@ -437,19 +428,6 @@ const config: AnalysisFunctionConfig<typeof argsSchema, typeof returnSchema> = {
.filter((t) => t > autoEnd && t <= 158)
.sort((a, b) => a - b);
const firstTeleop = filteredTeleopTimes[0];
console.debug("lXStartTime", {
reportIndex: idx,
endgameClimb: eg,
required,
scoutReportUuid: (r as any).uuid,
events: (r.events ?? []).map((e) => ({
action: e.action,
time: e.time,
})),
rawClimbTimes,
filteredTeleopTimes,
firstTeleop,
});
if (eg === required && firstTeleop !== undefined) {
const remaining = 158 - firstTeleop;
const clamped = remaining >= 0 ? remaining : 0;
Expand Down Expand Up @@ -484,7 +462,13 @@ const config: AnalysisFunctionConfig<typeof argsSchema, typeof returnSchema> = {
}

// Finish setting up filters to decrease server load
const tmdFilter: Prisma.TeamMatchDataWhereInput = {};
const tmdFilter: Prisma.TeamMatchDataWhereInput = {
matchType: ctx.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
};
// Team filter
tmdFilter.teamNumber = { in: teams };

Expand Down Expand Up @@ -585,23 +569,6 @@ const config: AnalysisFunctionConfig<typeof argsSchema, typeof returnSchema> = {
dataPoint: matchValue,
tournamentName: row.tournament.name,
});
// Debug logging for climb start metrics
if (
metric === Metric.autoClimbStartTime ||
metric === Metric.l1StartTime ||
metric === Metric.l2StartTime ||
metric === Metric.l3StartTime
) {
try {
console.debug("timeline", {
team,
match: row.key,
tnmt,
reports: row.scoutReports.length,
matchValue,
});
} catch {}
}
// Accumulate per tournament
perTeamTournamentValues[team][tnmt].push(matchValue);
}
Expand Down
5 changes: 5 additions & 0 deletions src/handler/analysis/coreAnalysis/averageManyFast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ const config: AnalysisFunctionConfig<typeof argsSchema, z.ZodType> = {
const tmdWhere: Prisma.TeamMatchDataWhereInput = {
teamNumber: { in: args.teams },
...(tnmtFilter && { tournamentKey: tnmtFilter }),
matchType: ctx.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
};

const srWhere: Prisma.ScoutReportWhereInput = teamFilter
Expand Down
5 changes: 5 additions & 0 deletions src/handler/analysis/coreAnalysis/nonEventMetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const config = {
teamRule.mode === "INCLUDE"
? `sc."sourceTeamNumber" = ANY($2)`
: `sc."sourceTeamNumber" != ALL($2)`;
const practiceCondition = ctx.user.includePracticeMatches
? "TRUE"
: `tmd."matchType" != 'PRACTICE'`;

const ARRAY_METRICS = new Set<MetricsBreakdown>([
MetricsBreakdown.robotRole,
Expand All @@ -71,6 +74,7 @@ const config = {
WHERE tmd."teamNumber" = $3
AND ${tournamentCondition}
AND ${teamCondition}
AND ${practiceCondition}
GROUP BY value
`
: `
Expand All @@ -82,6 +86,7 @@ const config = {
WHERE tmd."teamNumber" = $3
AND ${tournamentCondition}
AND ${teamCondition}
AND ${practiceCondition}
GROUP BY s."${args.metric}"
`;

Expand Down
5 changes: 5 additions & 0 deletions src/handler/analysis/csv/getReportCSV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ export const getReportCSV = async (
const where: any = {
teamMatchData: {
tournamentKey: params.data.tournamentKey,
matchType: req.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
},
};
const parsedRule = dataSourceRuleSchema(z.number()).safeParse(
Expand Down
5 changes: 5 additions & 0 deletions src/handler/analysis/csv/getTeamCSV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,11 @@ export const getTeamCSV = async (
...(tournamentFilter
? { tournamentKey: tournamentFilter }
: {}),
matchType: req.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
},
...(teamFilter
? { scouter: { sourceTeamNumber: teamFilter } }
Expand Down
6 changes: 6 additions & 0 deletions src/handler/analysis/picklist/endgamePicklistTeamFast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const endgamePicklistTeamFast = async (
team: number,
sourceTeamFilter: { in?: number[]; notIn?: number[] } | undefined,
sourceTnmtFilter: { in?: string[]; notIn?: string[] } | undefined,
sourcePracticeMatchFilter: boolean | undefined,
): Promise<number> => {
try {
// Get data
Expand All @@ -35,6 +36,11 @@ export const endgamePicklistTeamFast = async (
teamMatchData: {
teamNumber: team,
...(sourceTnmtFilter && { tournamentKey: sourceTnmtFilter }),
matchType: sourcePracticeMatchFilter
? undefined
: {
not: "PRACTICE",
},
},
...(sourceTeamFilter && {
scouter: { sourceTeamNumber: sourceTeamFilter },
Expand Down
5 changes: 5 additions & 0 deletions src/handler/analysis/teamLookUp/breakdownMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export const breakdownMetrics = createAnalysisHandler({
where: {
teamMatchData: {
teamNumber: params.team,
matchType: ctx.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
},
},
});
Expand Down
5 changes: 5 additions & 0 deletions src/handler/analysis/teamLookUp/categoryMetrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export const categoryMetrics = createAnalysisHandler({
where: {
teamMatchData: {
teamNumber: params.team,
matchType: ctx.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
},
},
});
Expand Down
10 changes: 10 additions & 0 deletions src/handler/analysis/teamLookUp/getNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export const getNotes = createAnalysisHandler({
where: {
teamMatchData: {
teamNumber: params.team,
matchType: ctx.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
},
},
});
Expand Down Expand Up @@ -63,6 +68,11 @@ export const getNotes = createAnalysisHandler({
teamMatchData: {
teamNumber: params.team,
tournamentKey: sourceTnmtFilter,
matchType: ctx.user.includePracticeMatches
? undefined
: {
not: "PRACTICE",
},
},
scouter: {
sourceTeamNumber: sourceTeamFilter,
Expand Down
78 changes: 78 additions & 0 deletions src/handler/manager/addTournamentMatches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,84 @@ export const addTournamentMatches = async (
headers: { "X-TBA-Auth-Key": process.env.TBA_KEY },
});

const nexusResponse = await fetch(
`https://frc.nexus/api/v1/event/${tournamentKey}`,
{
method: "GET",
headers: {
"Nexus-Api-Key": process.env.NEXUS_KEY ?? "",
},
},
);

console.log(nexusResponse);

if (!nexusResponse.ok) {
const errorMessage = await nexusResponse.text();
console.error("Error getting live event status:", errorMessage);
return;
}

const data = await nexusResponse.json();

console.log(data);

Comment thread
jackattack-4 marked this conversation as resolved.
Outdated
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",
},
});
}
Comment thread
jackattack-4 marked this conversation as resolved.
Outdated
}
Comment thread
jackattack-4 marked this conversation as resolved.
Outdated
Comment on lines +32 to +102

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
}

const json = await eventResponse.json();

const { remap_teams } = z
Expand Down
1 change: 0 additions & 1 deletion src/handler/manager/getTeams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ export const getTeams = async (
);
rows.unshift(rows[indexOfTeamNumber]);
rows.splice(indexOfTeamNumber + 1, 1);
console.log(rows);
}
}
res.status(200).send({ teams: rows, count: count });
Expand Down
1 change: 1 addition & 0 deletions src/handler/manager/managerConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
const ReverseMatchTypeMap: Record<MatchType, number> = {
[MatchType.QUALIFICATION]: 0,
[MatchType.ELIMINATION]: 1,
[MatchType.PRACTICE]: 2,
Comment on lines 77 to +81

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
};
const ScouterScheduleMap = {
0: "team1",
Expand All @@ -99,7 +100,7 @@
0: "qm",
1: "em",
};
const MatchEnumToAbrivation: Record<MatchType, string> = {

Check failure on line 103 in src/handler/manager/managerConstants.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Property 'PRACTICE' is missing in type '{ QUALIFICATION: string; ELIMINATION: string; }' but required in type 'Record<MatchType, string>'.
[MatchType.QUALIFICATION]: "qm",
[MatchType.ELIMINATION]: "em",
};
Expand Down
14 changes: 8 additions & 6 deletions src/handler/manager/scoutershifts/generateSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import prismaClient from "../../../prismaClient.js";
// import { writeFileSync } from "fs";
// import { join } from "path";
// import { homedir } from "os";
import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth.js";
import { Response } from "express";
import { Request, Response } from "express";

const shiftScouterEx = [
["Christian", "Oren", "Ben"],
Expand Down Expand Up @@ -80,6 +79,10 @@ const generateSchedule = async (
},
});

if (!matchesResponse || !teamsResponse) {
throw "NO_SCHEDULE";
}

matchesResponse.data = matchesResponse.data.filter(
(match: any) => match.comp_level === "qm",
);
Expand Down Expand Up @@ -171,10 +174,7 @@ const generateSchedule = async (
return csvRows.join("\n");
};

export const superScoutingSchedule = async (
req: AuthenticatedRequest,
res: Response,
) => {
export const superScoutingSchedule = async (req: Request, res: Response) => {
try {
const params = z.object({ tournamentKey: z.string() }).parse(req.query);

Expand All @@ -184,6 +184,8 @@ export const superScoutingSchedule = async (
} catch (error) {
if (error instanceof ZodError) {
res.status(400).send("Bad input");
} else if (error === "NO_SCHEDULE") {
res.status(404).send("No schedule available");
} else {
res.status(500).send("Internal server error");
}
Expand Down
Loading
Loading