diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f2e1a8d..7a641d8c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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[] @@ -331,6 +332,7 @@ enum UserRole { } enum MatchType { + PRACTICE QUALIFICATION ELIMINATION } diff --git a/src/handler/analysis/autoPaths/autoPathsTeam.ts b/src/handler/analysis/autoPaths/autoPathsTeam.ts index fedec324..5f84ba17 100644 --- a/src/handler/analysis/autoPaths/autoPathsTeam.ts +++ b/src/handler/analysis/autoPaths/autoPathsTeam.ts @@ -86,6 +86,11 @@ const config = { teamMatchData: { teamNumber: teamNumber, tournamentKey: sourceTnmtFilter, + matchType: ctx.user.includePracticeMatches + ? undefined + : { + not: "PRACTICE", + }, }, scouter: { sourceTeamNumber: sourceTeamFilter, diff --git a/src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts b/src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts index f69ad63f..b1939d5a 100644 --- a/src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts +++ b/src/handler/analysis/coreAnalysis/arrayAndAverageTeams.ts @@ -387,15 +387,6 @@ const config: AnalysisFunctionConfig = { .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; @@ -437,19 +428,6 @@ const config: AnalysisFunctionConfig = { .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; @@ -484,7 +462,13 @@ const config: AnalysisFunctionConfig = { } // 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 }; @@ -585,23 +569,6 @@ const config: AnalysisFunctionConfig = { 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); } diff --git a/src/handler/analysis/coreAnalysis/averageManyFast.ts b/src/handler/analysis/coreAnalysis/averageManyFast.ts index 73e6c769..087df315 100644 --- a/src/handler/analysis/coreAnalysis/averageManyFast.ts +++ b/src/handler/analysis/coreAnalysis/averageManyFast.ts @@ -77,6 +77,11 @@ const config: AnalysisFunctionConfig = { const tmdWhere: Prisma.TeamMatchDataWhereInput = { teamNumber: { in: args.teams }, ...(tnmtFilter && { tournamentKey: tnmtFilter }), + matchType: ctx.user.includePracticeMatches + ? undefined + : { + not: "PRACTICE", + }, }; const srWhere: Prisma.ScoutReportWhereInput = teamFilter diff --git a/src/handler/analysis/coreAnalysis/nonEventMetric.ts b/src/handler/analysis/coreAnalysis/nonEventMetric.ts index 8bf8ffd6..92f482a1 100644 --- a/src/handler/analysis/coreAnalysis/nonEventMetric.ts +++ b/src/handler/analysis/coreAnalysis/nonEventMetric.ts @@ -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.robotRole, @@ -71,6 +74,7 @@ const config = { WHERE tmd."teamNumber" = $3 AND ${tournamentCondition} AND ${teamCondition} + AND ${practiceCondition} GROUP BY value ` : ` @@ -82,6 +86,7 @@ const config = { WHERE tmd."teamNumber" = $3 AND ${tournamentCondition} AND ${teamCondition} + AND ${practiceCondition} GROUP BY s."${args.metric}" `; diff --git a/src/handler/analysis/csv/getReportCSV.ts b/src/handler/analysis/csv/getReportCSV.ts index fbdc6933..44bffbdf 100644 --- a/src/handler/analysis/csv/getReportCSV.ts +++ b/src/handler/analysis/csv/getReportCSV.ts @@ -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( diff --git a/src/handler/analysis/csv/getTeamCSV.ts b/src/handler/analysis/csv/getTeamCSV.ts index 7092934c..502b94cc 100644 --- a/src/handler/analysis/csv/getTeamCSV.ts +++ b/src/handler/analysis/csv/getTeamCSV.ts @@ -222,6 +222,11 @@ export const getTeamCSV = async ( ...(tournamentFilter ? { tournamentKey: tournamentFilter } : {}), + matchType: req.user.includePracticeMatches + ? undefined + : { + not: "PRACTICE", + }, }, ...(teamFilter ? { scouter: { sourceTeamNumber: teamFilter } } diff --git a/src/handler/analysis/picklist/endgamePicklistTeamFast.ts b/src/handler/analysis/picklist/endgamePicklistTeamFast.ts index ad041218..c92c1476 100644 --- a/src/handler/analysis/picklist/endgamePicklistTeamFast.ts +++ b/src/handler/analysis/picklist/endgamePicklistTeamFast.ts @@ -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 => { try { // Get data @@ -35,6 +36,11 @@ export const endgamePicklistTeamFast = async ( teamMatchData: { teamNumber: team, ...(sourceTnmtFilter && { tournamentKey: sourceTnmtFilter }), + matchType: sourcePracticeMatchFilter + ? undefined + : { + not: "PRACTICE", + }, }, ...(sourceTeamFilter && { scouter: { sourceTeamNumber: sourceTeamFilter }, diff --git a/src/handler/analysis/teamLookUp/breakdownMetrics.ts b/src/handler/analysis/teamLookUp/breakdownMetrics.ts index 145ab272..511a00be 100644 --- a/src/handler/analysis/teamLookUp/breakdownMetrics.ts +++ b/src/handler/analysis/teamLookUp/breakdownMetrics.ts @@ -33,6 +33,11 @@ export const breakdownMetrics = createAnalysisHandler({ where: { teamMatchData: { teamNumber: params.team, + matchType: ctx.user.includePracticeMatches + ? undefined + : { + not: "PRACTICE", + }, }, }, }); diff --git a/src/handler/analysis/teamLookUp/categoryMetrics.ts b/src/handler/analysis/teamLookUp/categoryMetrics.ts index 97e165ef..41c430e5 100644 --- a/src/handler/analysis/teamLookUp/categoryMetrics.ts +++ b/src/handler/analysis/teamLookUp/categoryMetrics.ts @@ -34,6 +34,11 @@ export const categoryMetrics = createAnalysisHandler({ where: { teamMatchData: { teamNumber: params.team, + matchType: ctx.user.includePracticeMatches + ? undefined + : { + not: "PRACTICE", + }, }, }, }); diff --git a/src/handler/analysis/teamLookUp/getNotes.ts b/src/handler/analysis/teamLookUp/getNotes.ts index d7ca9b56..bd69f779 100644 --- a/src/handler/analysis/teamLookUp/getNotes.ts +++ b/src/handler/analysis/teamLookUp/getNotes.ts @@ -35,6 +35,11 @@ export const getNotes = createAnalysisHandler({ where: { teamMatchData: { teamNumber: params.team, + matchType: ctx.user.includePracticeMatches + ? undefined + : { + not: "PRACTICE", + }, }, }, }); @@ -63,6 +68,11 @@ export const getNotes = createAnalysisHandler({ teamMatchData: { teamNumber: params.team, tournamentKey: sourceTnmtFilter, + matchType: ctx.user.includePracticeMatches + ? undefined + : { + not: "PRACTICE", + }, }, scouter: { sourceTeamNumber: sourceTeamFilter, diff --git a/src/handler/manager/addTournamentMatches.ts b/src/handler/manager/addTournamentMatches.ts index 96fd6404..4485f26d 100644 --- a/src/handler/manager/addTournamentMatches.ts +++ b/src/handler/manager/addTournamentMatches.ts @@ -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", + }, + }); + } + } + } + } + const json = await eventResponse.json(); const { remap_teams } = z diff --git a/src/handler/manager/getTeams.ts b/src/handler/manager/getTeams.ts index a1c589ba..dd1afc6b 100644 --- a/src/handler/manager/getTeams.ts +++ b/src/handler/manager/getTeams.ts @@ -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 }); diff --git a/src/handler/manager/managerConstants.ts b/src/handler/manager/managerConstants.ts index c4b06dbd..820bca5f 100644 --- a/src/handler/manager/managerConstants.ts +++ b/src/handler/manager/managerConstants.ts @@ -78,6 +78,7 @@ const MatchTypeMap: Record = { const ReverseMatchTypeMap: Record = { [MatchType.QUALIFICATION]: 0, [MatchType.ELIMINATION]: 1, + [MatchType.PRACTICE]: 2, }; const ScouterScheduleMap = { 0: "team1", @@ -102,6 +103,7 @@ const MatchTypeToAbrivation = { const MatchEnumToAbrivation: Record = { [MatchType.QUALIFICATION]: "qm", [MatchType.ELIMINATION]: "em", + [MatchType.PRACTICE]: "pr", }; export { diff --git a/src/handler/manager/scoutershifts/generateSchedule.ts b/src/handler/manager/scoutershifts/generateSchedule.ts index 785b6a71..6fb2b1c9 100644 --- a/src/handler/manager/scoutershifts/generateSchedule.ts +++ b/src/handler/manager/scoutershifts/generateSchedule.ts @@ -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"], @@ -80,6 +79,10 @@ const generateSchedule = async ( }, }); + if (!matchesResponse || !teamsResponse) { + throw "NO_SCHEDULE"; + } + matchesResponse.data = matchesResponse.data.filter( (match: any) => match.comp_level === "qm", ); @@ -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); @@ -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"); } diff --git a/src/handler/manager/settings/addPracticeSource.ts b/src/handler/manager/settings/addPracticeSource.ts new file mode 100644 index 00000000..853660ed --- /dev/null +++ b/src/handler/manager/settings/addPracticeSource.ts @@ -0,0 +1,31 @@ +import { Response } from "express"; +import prismaClient from "../../../prismaClient.js"; +import z from "zod"; +import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth.js"; + +export const addPracticeSource = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + const user = req.user; + + const params = z + .object({ + includePracticeMatches: z.boolean(), + }) + .parse(req.body); + await prismaClient.user.update({ + where: { + id: user.id, + }, + data: { + includePracticeMatches: params.includePracticeMatches, + }, + }); + res.status(200).send("Settings successfully updated"); + } catch (error) { + console.error(error); + res.status(500).send({ error: "Internal server error" }); + } +}; diff --git a/src/handler/manager/settings/getPracticeSource.ts b/src/handler/manager/settings/getPracticeSource.ts new file mode 100644 index 00000000..c71ec8bc --- /dev/null +++ b/src/handler/manager/settings/getPracticeSource.ts @@ -0,0 +1,14 @@ +import { Response } from "express"; +import { AuthenticatedRequest } from "../../../lib/middleware/requireAuth.js"; + +export const getPracticeSource = async ( + req: AuthenticatedRequest, + res: Response, +): Promise => { + try { + res.status(200).json(req.user.includePracticeMatches); + } catch (error) { + console.error(error); + res.status(500).send("Error getting practice source"); + } +}; diff --git a/src/handler/manager/settings/updateSettings.ts b/src/handler/manager/settings/updateSettings.ts index a9a7fa85..eb7b57bc 100644 --- a/src/handler/manager/settings/updateSettings.ts +++ b/src/handler/manager/settings/updateSettings.ts @@ -25,6 +25,7 @@ export const updateSettings = async ( .object({ teamSource: z.array(z.number()), tournamentSource: z.array(z.string()), + includePracticeMatches: z.boolean().optional().default(false), }) .parse(req.body); @@ -38,6 +39,7 @@ export const updateSettings = async ( params.tournamentSource, await allTournaments, ), + includePracticeMatches: params.includePracticeMatches, }, }); res.status(200).send("Settings successfully updated"); diff --git a/src/routes/manager/scoutershifts.routes.ts b/src/routes/manager/scoutershifts.routes.ts index d5147207..cca6dc0b 100644 --- a/src/routes/manager/scoutershifts.routes.ts +++ b/src/routes/manager/scoutershifts.routes.ts @@ -59,11 +59,10 @@ registry.registerPath({ }, security: [{ bearerAuth: [] }], }); +router.get("/generate", superScoutingSchedule); router.use(requireAuth, requireVerifiedTeam); -router.get("/generate", superScoutingSchedule); - router.post("/:uuid", updateScouterShift); router.delete("/:uuid", deleteScouterShift); diff --git a/src/routes/manager/settings.routes.ts b/src/routes/manager/settings.routes.ts index 3fe56968..510654a1 100644 --- a/src/routes/manager/settings.routes.ts +++ b/src/routes/manager/settings.routes.ts @@ -11,6 +11,8 @@ import { registry } from "../../lib/openapi.js"; import { z } from "zod"; import { getTeamEmail } from "../../handler/manager/settings/getTeamEmail.js"; import { requireVerifiedTeam } from "../../lib/middleware/requireVerifiedTeam.js"; +import { addPracticeSource } from "../../handler/manager/settings/addPracticeSource.js"; +import { getPracticeSource } from "../../handler/manager/settings/getPracticeSource.js"; const updateTeamEmails = rateLimit({ windowMs: 2 * 60 * 1000, @@ -34,6 +36,7 @@ registry.registerPath({ schema: z.object({ teamSource: z.array(z.number().int()), tournamentSource: z.array(z.string()), + includePracticeMatches: z.boolean().optional(), }), }, }, @@ -107,6 +110,48 @@ registry.registerPath({ security: [{ bearerAuth: [] }], }); +registry.registerPath({ + method: "get", + path: "/v1/manager/settings/practicesource", + tags: ["Manager - Settings"], + summary: "Get practice source", + responses: { + 200: { + description: "Practice source", + content: { "application/json": { schema: z.boolean() } }, + }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + +registry.registerPath({ + method: "post", + path: "/v1/manager/settings/practicesource", + tags: ["Manager - Settings"], + summary: "Add practice source", + request: { + body: { + content: { + "application/json": { + schema: z.object({ includePracticeMatches: z.boolean() }), + }, + }, + }, + }, + responses: { + 200: { + description: "Added", + content: { "text/plain": { schema: z.string() } }, + }, + 400: { description: "Invalid request" }, + 401: { description: "Unauthorized" }, + 500: { description: "Server error" }, + }, + security: [{ bearerAuth: [] }], +}); + registry.registerPath({ method: "get", path: "/v1/manager/settings/tournamentsource", @@ -196,6 +241,9 @@ router.put("/", updateSettings); router.get("/teamsource", getTeamSource); router.post("/teamsource", addTeamSource); +router.get("/practicesource", getPracticeSource); +router.post("/practicesource", addPracticeSource); + router.get("/tournamentsource", getTournamentSource); router.post("/tournamentsource", addTournamentSource);