Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 @@ -96,7 +96,7 @@
);
if (defined.length === 0) return 0;
const total = defined.reduce(
(acc, cur) => acc + accuracyToPercentage[cur.accuracy as any],

Check warning on line 99 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
0,
);
return total / defined.length;
Expand Down Expand Up @@ -128,7 +128,7 @@
matchAggregationFunction = (reports) => {
let total = 0;
reports.forEach((sr) => {
const accuracyEnum = (sr as any).accuracy as

Check warning on line 131 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
| number
| null
| undefined;
Expand All @@ -144,7 +144,7 @@
total += (e.points ?? 0) * accuracyMultiplier;
});
// Include auto climb points (15) when succeeded
const autoClimb = (sr as any).autoClimb as

Check warning on line 147 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
| "SUCCEEDED"
| "FAILED"
| "N_A"
Expand All @@ -171,7 +171,7 @@
matchAggregationFunction = (reports) => {
let total = 0;
reports.forEach((sr) => {
const accuracyEnum = (sr as any).accuracy as

Check warning on line 174 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
| number
| null
| undefined;
Expand Down Expand Up @@ -202,7 +202,7 @@
matchAggregationFunction = (reports) => {
let total = 0;
reports.forEach((sr) => {
const accuracyEnum = (sr as any).accuracy as

Check warning on line 205 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
| number
| null
| undefined;
Expand Down Expand Up @@ -233,7 +233,7 @@
const totalFuel = (r.events ?? [])
.filter((e) => e.action === "STOP_SCORING")
.reduce((acc, cur) => acc + (cur.quantity ?? 0), 0);
const durations = calculateTimeMetric([r] as any, "SCORING");

Check warning on line 236 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const duration = durations.reduce((a, b) => a + b, 0);
return duration > 0 ? totalFuel / duration : 0;
});
Expand All @@ -247,7 +247,7 @@
},
};
matchAggregationFunction = (reports) => {
const feedTime = calculateTimeMetric(reports as any, "FEEDING");

Check warning on line 250 in src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const feeds = reports.flatMap((r) =>
(r.events || []).filter((e) => e.action === "STOP_FEEDING"),
);
Expand Down Expand Up @@ -387,15 +387,6 @@
.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 @@
.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 @@
}

// 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 @@
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
73 changes: 73 additions & 0 deletions src/handler/manager/addTournamentMatches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,79 @@ 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 ?? "",
},
},
);

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",
},
});
}
}
}
Comment on lines +32 to +102
Copy link

Copilot AI Apr 11, 2026

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
2 changes: 2 additions & 0 deletions src/handler/manager/managerConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const MatchTypeMap: Record<number, MatchType> = {
const ReverseMatchTypeMap: Record<MatchType, number> = {
[MatchType.QUALIFICATION]: 0,
[MatchType.ELIMINATION]: 1,
[MatchType.PRACTICE]: 2,
};
const ScouterScheduleMap = {
0: "team1",
Expand All @@ -102,6 +103,7 @@ const MatchTypeToAbrivation = {
const MatchEnumToAbrivation: Record<MatchType, string> = {
[MatchType.QUALIFICATION]: "qm",
[MatchType.ELIMINATION]: "em",
[MatchType.PRACTICE]: "pr",
};

export {
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