diff --git a/CODEOWNERS b/CODEOWNERS
index a3a25283..4b49d0ed 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1 +1 @@
-* @CMEONE @25DanielG
+* @CMEONE @benjaminxiaa
diff --git a/config/development.ts b/config/development.ts
index 3bbf91f2..072d89c1 100644
--- a/config/development.ts
+++ b/config/development.ts
@@ -36,7 +36,7 @@ const config: Config = {
adminTokens: secret.development.auth.adminTokens
},
features: ["scouting", "resources", "tps"],
- year: 2025
+ year: 2026
};
export default config;
diff --git a/config/examplesecret.json b/config/examplesecret.json
index 2f62185e..0accfdf5 100644
--- a/config/examplesecret.json
+++ b/config/examplesecret.json
@@ -130,5 +130,35 @@
"admin": "admin"
}
}
+ },
+ "production2025": {
+ "db": {
+ "database": "tpw-prod",
+ "username": "",
+ "password": "",
+ "host": "localhost",
+ "port": 27017
+ },
+ "auth": {
+ "cookieKeys": ["1", "2", "3", "4", "I declare a thumb war"],
+ "access": {
+ "restricted": false,
+ "username": "admin",
+ "password": "admin"
+ },
+ "ci": {
+ "deploy": ""
+ },
+ "scoutingKeys": ["1", "2", "3", "4", "I declare a thumb war"],
+ "tba": "",
+ "scoutingAdmins": ["admin"],
+ "scoutingInternal": {
+ "teamNumber": "admin",
+ "accessToken": "admin"
+ },
+ "adminTokens": {
+ "admin": "admin"
+ }
+ }
}
}
diff --git a/config/index.ts b/config/index.ts
index 134e6e78..8b06e379 100644
--- a/config/index.ts
+++ b/config/index.ts
@@ -3,6 +3,7 @@ import staging from "./staging";
import production from "./production";
import production2023 from "./production2023";
import production2024 from "./production2024";
+import production2025 from "./production2025";
const env = process.env.NODE_ENV || "development";
@@ -12,7 +13,8 @@ if (
"staging",
"production",
"production2023",
- "production2024"
+ "production2024",
+ "production2025"
].includes(env)
)
throw new Error(`Config file for environment ${env} could not be found.`);
@@ -20,6 +22,7 @@ const config: Config = {
production,
production2023,
production2024,
+ production2025,
staging,
development
}[env];
diff --git a/config/production.ts b/config/production.ts
index 9c35a345..510e3656 100644
--- a/config/production.ts
+++ b/config/production.ts
@@ -11,7 +11,7 @@ const config: Config = {
db: secret.production.db,
auth: secret.production.auth,
features: ["scouting", "tps"],
- year: 2025
+ year: 2026
};
export default config;
diff --git a/config/production2025.ts b/config/production2025.ts
new file mode 100644
index 00000000..c9cc51aa
--- /dev/null
+++ b/config/production2025.ts
@@ -0,0 +1,17 @@
+import { Config } from ".";
+// @ts-ignore
+import secret from "./secret";
+
+const config: Config = {
+ branch: "2025",
+ server: {
+ port: 18925,
+ domain: "2025.thepurplewarehouse.com"
+ },
+ db: secret.production2025.db,
+ auth: secret.production2025.auth,
+ features: ["scouting", "tps"],
+ year: 2025
+};
+
+export default config;
diff --git a/config/scouting/2025/index.ts b/config/scouting/2025/index.ts
index e27c3732..8f21e496 100644
--- a/config/scouting/2025/index.ts
+++ b/config/scouting/2025/index.ts
@@ -1131,6 +1131,127 @@ export function formatParsedData(data, categories, teams) {
.join("\n")}`;
}
+export interface picklist {
+ team: string;
+ "avg-auto-pieces": number;
+ "avg-tele-pieces": number;
+ "avg-l1": number;
+ "avg-l2": number;
+ "avg-l3": number;
+ "avg-l4": number;
+ "avg-proc": number;
+ "avg-net": number;
+ "deep-climbs": number;
+}
+
+export async function formPicklist(
+ data: { [team: string]: any[] },
+ categories,
+ teams: any[]
+) {
+ let analysis: picklist[] = [];
+ console.log(teams);
+ for (const t1 of teams) {
+ const t = t1.team_number;
+ let dat = data[t];
+ if (!dat) continue;
+ let autoPieces = 0;
+ let telePieces = 0;
+ let l1 = 0;
+ let l2 = 0;
+ let l3 = 0;
+ let l4 = 0;
+ let proc = 0;
+ let net = 0;
+ let deepClimbs = 0;
+ let total = 0;
+ for (const d of dat) {
+ if (!d || !d.accuracy || !d.accuracy.calculated) {
+ continue;
+ }
+ let acc = d.accuracy.percentage;
+
+ autoPieces += acc * find(d, "counters", categories, "25-20", 0);
+ telePieces += acc * find(d, "counters", categories, "25-24", 0);
+ let autocoral = find(d, "data", categories, "25-18", []);
+ let telecoral = find(d, "data", categories, "25-22", []);
+ let autol4 = autocoral.filter((el) => el == 0).length;
+ let autol3 = autocoral.filter((el) => el == 1).length;
+ let autol2 = autocoral.filter((el) => el == 2).length;
+ let autol1 = autocoral.filter((el) => el == 3).length;
+ let telel4 = telecoral.filter((el) => el == 0).length;
+ let telel3 = telecoral.filter((el) => el == 1).length;
+ let telel2 = telecoral.filter((el) => el == 2).length;
+ let telel1 = telecoral.filter((el) => el == 3).length;
+ l1 += acc * (autol1 + telel1);
+ l2 += acc * (autol2 + telel2);
+ l3 += acc * (autol3 + telel3);
+ l4 += acc * (autol4 + telel4);
+ let autoalgae = find(d, "data", categories, "25-17", []);
+ let telealgae = find(d, "data", categories, "25-21", []);
+ let autoNe = autoalgae.filter((el) => el == 4).length;
+ let autoPr = autoalgae.filter((el) => el == 5).length;
+ let teleNe = telealgae.filter((el) => el == 4).length;
+ let telePr = telealgae.filter((el) => el == 5).length;
+ net += acc * (autoNe + teleNe);
+ proc += acc * (autoPr + telePr);
+ let climb = find(d, "abilities", categories, "25-8", 0);
+ if (acc > 0.5) deepClimbs += climb == 3 ? 1 : 0;
+
+ total += acc;
+ }
+ if (dat.length == 0 || total == 0) {
+ analysis.push({
+ team: t,
+ "avg-auto-pieces": NaN,
+ "avg-tele-pieces": NaN,
+ "avg-l1": NaN,
+ "avg-l2": NaN,
+ "avg-l3": NaN,
+ "avg-l4": NaN,
+ "avg-proc": NaN,
+ "avg-net": NaN,
+ "deep-climbs": NaN
+ });
+ } else {
+ analysis.push({
+ team: t,
+ "avg-auto-pieces": autoPieces / total,
+ "avg-tele-pieces": telePieces / total,
+ "avg-l1": l1 / total,
+ "avg-l2": l2 / total,
+ "avg-l3": l3 / total,
+ "avg-l4": l4 / total,
+ "avg-proc": proc / total,
+ "avg-net": net / total,
+ "deep-climbs": deepClimbs
+ });
+ }
+ }
+
+ return formatPicklist(analysis);
+}
+
+function formatPicklist(analysis) {
+ return `entry,team,"avg auto pieces","avg tele pieces","avg l1","avg l2","avg l3","avg l4","avg processor","avg net","# of deep climbs"\n${analysis
+ .map((entry, i) => {
+ return [
+ i,
+ entry.team || 0,
+ entry["avg-auto-pieces"],
+ entry["avg-tele-pieces"],
+ entry["avg-l1"],
+ entry["avg-l2"],
+ entry["avg-l3"],
+ entry["avg-l4"],
+ entry["avg-proc"],
+ entry["avg-net"],
+ entry["deep-climbs"]
+ ].join(",");
+ })
+ .join("\n")}`;
+}
+
export function notes() {
return ``;
}
@@ -1547,6 +1668,7 @@ const scouting2025 = {
preload,
formatData,
formatParsedData,
+ formPicklist,
notes,
analysis,
compare,
diff --git a/config/scouting/2026/exampleaccuracy.ts b/config/scouting/2026/exampleaccuracy.ts
new file mode 100644
index 00000000..7f3e7ad7
--- /dev/null
+++ b/config/scouting/2026/exampleaccuracy.ts
@@ -0,0 +1,5 @@
+let accuracy = async function (event, matches, data, categories, teams) {
+ return {};
+};
+
+export default accuracy;
diff --git a/config/scouting/2026/index.ts b/config/scouting/2026/index.ts
new file mode 100644
index 00000000..8da1905d
--- /dev/null
+++ b/config/scouting/2026/index.ts
@@ -0,0 +1,1463 @@
+import { execSync, exec } from "child_process";
+import * as fs from "fs";
+import { getMatchesFull } from "../../../helpers/tba";
+import { getAllDataByEvent } from "../../../helpers/scouting";
+import accuracy2025 from "./accuracy";
+
+export interface parsedRow {
+ match: number;
+ team: string;
+ alliance: string;
+ leave: boolean;
+ "fuel ground intake": boolean;
+ "fuel station intake": boolean;
+ "can ferry": boolean;
+ "traverse under trench": boolean;
+ "traverse over bump": boolean;
+ "auto l1 climb": boolean;
+ "auto fuel scoring": string;
+ "teleop fuel scoring": string;
+ "climb level": number;
+ "climb time": number;
+ "brick time": number;
+ "defense time": number;
+ "driver skill": number;
+ "defense skill": number;
+ speed: number;
+ stability: number;
+ "intake consistency": number;
+ scouter: string;
+ comments: string;
+ accuracy: number | "";
+ timestamp: number;
+}
+
+export function categories() {
+ return [
+ {
+ name: "Leave Starting Zone",
+ identifier: "26-0",
+ dataType: "boolean"
+ },
+ {
+ name: "Fuel Ground Intake",
+ identifier: "26-1",
+ dataType: "boolean"
+ },
+ {
+ name: "Fuel Station Intake",
+ identifier: "26-2",
+ dataType: "boolean"
+ },
+ {
+ name: "Can Ferry",
+ identifier: "26-3",
+ dataType: "boolean"
+ },
+ {
+ name: "Traverse Under Trench",
+ identifier: "26-4",
+ dataType: "boolean"
+ },
+ {
+ name: "Traverse Over Bump",
+ identifier: "26-5",
+ dataType: "boolean"
+ },
+ {
+ name: "Auto L1 Climb",
+ identifier: "26-6",
+ dataType: "boolean"
+ },
+ { name: "Auto Fuel Scoring", identifier: "26-7", dataType: "array" }, // 'asf' 'amf' | auto score, auto miss
+ { name: "Teleop Fuel Scoring", identifier: "26-8", dataType: "array" }, // 'saf', 'maf', 'hif' | score active, miss active, shot inactive
+ { name: "Climb Level", identifier: "26-9" },
+ { name: "Climb Time", identifier: "26-10" },
+ { name: "Brick Time", identifier: "26-11" },
+ { name: "Defense Time", identifier: "26-12" },
+ { name: "Driver Skill Rating", identifier: "26-13" },
+ { name: "Defense Skill Rating", identifier: "26-14" },
+ { name: "Robot Speed Rating", identifier: "26-15" },
+ { name: "Robot Stability Rating", identifier: "26-16" },
+ { name: "Intake Consistency Rating", identifier: "26-17" },
+ {
+ name: "Auto Fuel Scoring Locations",
+ identifier: "26-18",
+ dataType: "array"
+ },
+ /*
+ * LOCATIONS
+ * (0 = Hub)
+ */
+ { name: "Auto Fuel Count", identifier: "26-19" },
+ {
+ name: "Teleop Fuel Scoring Locations",
+ identifier: "26-20",
+ dataType: "array"
+ },
+ { name: "Teleop Fuel Count", identifier: "26-21" }
+ ];
+}
+
+export function layout() {
+ return [
+ {
+ type: "layout",
+ direction: "preset-manager",
+ components: [
+ {
+ type: "layout",
+ direction: "preset",
+ name: "auto",
+ team: {
+ type: "function",
+ definition: ((state) =>
+ `AUTO (${state.teamNumber})`).toString()
+ },
+ components: [
+ {
+ type: "layout",
+ direction: "rows",
+ components: [
+ {
+ type: "checkbox",
+ label: "Leave Starting Zone",
+ default: false,
+ data: "26-0"
+ },
+ {
+ type: "checkbox",
+ label: "Fuel Ground Intake",
+ default: false,
+ data: "26-1"
+ },
+ {
+ type: "checkbox",
+ label: "Fuel Station Intake",
+ default: false,
+ data: "26-2"
+ },
+ {
+ type: "checkbox",
+ label: "Can Ferry?",
+ default: false,
+ data: "26-3"
+ },
+ {
+ type: "checkbox",
+ label: "Traverse Under Trench",
+ default: false,
+ data: "26-4"
+ },
+ {
+ type: "checkbox",
+ label: "Traverse Over Bump",
+ default: false,
+ data: "26-5"
+ },
+ {
+ type: "checkbox",
+ label: "Did L1 Climb?",
+ default: false,
+ data: "26-6"
+ },
+ {
+ type: "timer",
+ label: "Brick Time",
+ default: 0,
+ data: "26-11",
+ name: "brick_time",
+ restricts: ["cage_time", "defense_time"]
+ },
+ {
+ type: "locations",
+ increment: 8,
+ src: {
+ type: "function",
+ definition: ((state) =>
+ `/img/2026hub.png`).toString()
+ },
+ default: {
+ locations: [],
+ values: [],
+ counter: 0
+ },
+ data: {
+ values: "26-7",
+ locations: "26-18",
+ counter: "26-19"
+ },
+ rows: 1,
+ columns: 1,
+ orientation: 0,
+ flip: false,
+ disabled: [],
+ marker: {
+ type: "function",
+ definition: ((state) => {
+ return `${state.locations
+ .filter((location) =>
+ ["fsa", "fsi"].includes(
+ location.value
+ )
+ )
+ .map((location, i, arr) => {
+ if (i > 5) {
+ return "";
+ } else {
+ let colors = [
+ "#ebebeb",
+ "#fd3f0d",
+ "#b700ff",
+ "#5300ff",
+ "#000000"
+ ];
+ if (
+ arr.length > 18 ||
+ i + 12 < arr.length
+ ) {
+ return `
`;
+ } else if (
+ arr.length > 12 ||
+ i + 6 < arr.length
+ ) {
+ return ``;
+ } else if (
+ arr.length > 6 ||
+ i < arr.length
+ ) {
+ return ``;
+ }
+ }
+ return "";
+ })
+ .filter(
+ (marker) => marker != ""
+ )
+ .slice(0, 6)
+ .join("")}`;
+ }).toString()
+ },
+ // TODO: Score Active, Missed Active, Shot Inactive
+ options: [
+ {
+ label: "Scored",
+ value: "fsa",
+ tracks: ["fsi"],
+ type: "counter",
+ show: {
+ type: "function",
+ definition: ((state) => {
+ return state.index == 0;
+ }).toString()
+ }
+ },
+ {
+ label: "Missed",
+ value: "fma",
+ tracks: ["fmi"],
+ type: "counter",
+ show: {
+ type: "function",
+ definition: ((state) => {
+ return state.index == 0;
+ }).toString()
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ type: "layout",
+ direction: "preset",
+ name: "teleop",
+ team: {
+ type: "function",
+ definition: ((state) =>
+ `TELEOP (${state.teamNumber})`).toString()
+ },
+ components: [
+ {
+ type: "layout",
+ direction: "rows",
+ components: [
+ {
+ type: "checkbox",
+ label: "Fuel Ground Intake",
+ default: false,
+ data: "26-1"
+ },
+ {
+ type: "checkbox",
+ label: "Fuel Station Intake",
+ default: false,
+ data: "26-2"
+ },
+ {
+ type: "checkbox",
+ label: "Can Ferry?",
+ default: false,
+ data: "26-3"
+ },
+ {
+ type: "checkbox",
+ label: "Traverse Under Trench",
+ default: false,
+ data: "26-4"
+ },
+ {
+ type: "checkbox",
+ label: "Traverse Over Bump",
+ default: false,
+ data: "26-5"
+ },
+ {
+ type: "timer",
+ label: "Brick Time",
+ default: 0,
+ data: "26-11",
+ name: "brick_time",
+ restricts: ["cage_time", "defense_time"]
+ },
+ {
+ type: "timer",
+ label: "Defense Time",
+ default: 0,
+ data: "26-12",
+ name: "defense_time",
+ restricts: ["cage_time", "brick_time"]
+ },
+ {
+ type: "locations",
+ increment: 5,
+ src: {
+ type: "function",
+ definition: ((state) =>
+ `/img/2026hub.png`).toString()
+ },
+ default: {
+ locations: [],
+ values: [],
+ counter: 0
+ },
+ data: {
+ values: "26-8",
+ locations: "26-20",
+ counter: "26-21"
+ },
+ rows: 1,
+ columns: 1,
+ orientation: 0,
+ flip: false,
+ disabled: [],
+ marker: {
+ type: "function",
+ definition: ((state) => {
+ return `${state.locations
+ .filter((location) =>
+ [
+ "fsa",
+ "fma",
+ "fhi"
+ ].includes(location.value)
+ )
+ .map((location, i, arr) => {
+ if (i > 5) {
+ return "";
+ } else {
+ let colors = [
+ "#ebebeb",
+ "#fd3f0d",
+ "#b700ff",
+ "#5300ff",
+ "#000000"
+ ];
+ if (
+ arr.length > 18 ||
+ i + 12 < arr.length
+ ) {
+ return ``;
+ } else if (
+ arr.length > 12 ||
+ i + 6 < arr.length
+ ) {
+ return ``;
+ } else if (
+ arr.length > 6 ||
+ i < arr.length
+ ) {
+ return ``;
+ }
+ }
+ return "";
+ })
+ .filter(
+ (marker) => marker != ""
+ )
+ .slice(0, 6)
+ .join("")}`;
+ }).toString()
+ },
+ // TODO: Score Active, Missed Active, Shot Inactive
+ options: [
+ {
+ label: "Scored (Active)",
+ value: "fsa",
+ tracks: ["fma", "fhi"],
+ type: "counter",
+ show: {
+ type: "function",
+ definition: ((state) => {
+ return state.index == 0;
+ }).toString()
+ }
+ },
+ {
+ label: "Missed (Active)",
+ value: "fma",
+ tracks: ["fsa", "fhi"],
+ type: "counter",
+ show: {
+ type: "function",
+ definition: ((state) => {
+ return state.index == 0;
+ }).toString()
+ }
+ },
+ {
+ label: "Shot (Inactive)",
+ value: "fhi",
+ tracks: ["fsa", "fma"],
+ type: "counter",
+ show: {
+ type: "function",
+ definition: ((state) => {
+ return state.index == 0;
+ }).toString()
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ type: "layout",
+ direction: "preset",
+ name: "endgame",
+ team: {
+ type: "function",
+ definition: ((state) =>
+ `ENDGAME (${state.teamNumber})`).toString()
+ },
+ components: [
+ {
+ type: "layout",
+ direction: "rows",
+ components: [
+ {
+ type: "select",
+ label: "Climb Level",
+ data: "26-9",
+ default: 0,
+ options: [
+ {
+ label: "None"
+ },
+ {
+ label: "Level 1"
+ },
+ {
+ label: "Level 2"
+ },
+ {
+ label: "Level 3"
+ }
+ ]
+ },
+ {
+ type: "timer",
+ label: "Climb Time",
+ default: 0,
+ data: "26-10",
+ name: "cage_time",
+ restricts: ["defense_time", "brick_time"]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ type: "layout",
+ direction: "preset",
+ name: "comments",
+ team: {
+ type: "function",
+ definition: ((state) =>
+ `COMMENTS (${state.teamNumber})`).toString()
+ },
+ components: [
+ {
+ type: "layout",
+ direction: "rows",
+ components: [
+ {
+ type: "rating",
+ label: "Driver Skill Rating",
+ default: 0,
+ data: "26-13",
+ src: [
+ "/img/star-outline.png",
+ "/img/star-filled.png"
+ ]
+ },
+ {
+ type: "rating",
+ label: "Defense Skill Rating",
+ default: 0,
+ data: "26-14",
+ src: [
+ "/img/star-outline.png",
+ "/img/star-filled.png"
+ ]
+ },
+ {
+ type: "rating",
+ label: "Robot Speed Rating",
+ default: 0,
+ data: "26-15",
+ src: [
+ "/img/star-outline.png",
+ "/img/star-filled.png"
+ ]
+ },
+ {
+ type: "rating",
+ label: "Robot Stability Rating",
+ default: 0,
+ data: "26-16",
+ src: [
+ "/img/star-outline.png",
+ "/img/star-filled.png"
+ ]
+ },
+ {
+ type: "rating",
+ label: "Intake Consistency Rating",
+ default: 0,
+ data: "26-17",
+ src: [
+ "/img/star-outline.png",
+ "/img/star-filled.png"
+ ]
+ },
+ {
+ type: "textbox",
+ placeholder:
+ "Enter notes here (and include team number if scouting practice matches)...",
+ default: "",
+ data: "comments"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ type: "pagebar",
+ direction: "columns",
+ options: [
+ {
+ name: "Auto",
+ html: '',
+ refers: "auto",
+ active: true
+ },
+ {
+ name: "Teleop",
+ html: '',
+ refers: "teleop",
+ active: false
+ },
+ {
+ name: "Endgame",
+ html: '',
+ refers: "endgame",
+ active: false
+ },
+ {
+ name: "Comments",
+ html: '',
+ refers: "comments",
+ active: false
+ }
+ ]
+ }
+ ]
+ },
+ {
+ type: "layout",
+ direction: "rows",
+ components: [
+ {
+ type: "pagebutton",
+ label: "Upload (Online)",
+ page: 2
+ },
+ {
+ type: "pagebutton",
+ label: "QR Code (Offline)",
+ page: 3
+ },
+ {
+ type: "pagebutton",
+ label: "Copy Data (Offline)",
+ page: 4
+ }
+ ]
+ },
+ {
+ type: "layout",
+ direction: "rows",
+ components: [
+ {
+ type: "title",
+ label: "UPLOAD"
+ },
+ {
+ type: "upload"
+ },
+ {
+ type: "pagebutton",
+ label: "Home",
+ page: -2
+ }
+ ]
+ },
+ {
+ type: "layout",
+ direction: "rows",
+ components: [
+ {
+ type: "title",
+ label: "QR CODE"
+ },
+ {
+ type: "qrcode",
+ chunkLength: 30,
+ interval: 500
+ },
+ {
+ type: "pagebutton",
+ label: "Home",
+ page: -2
+ }
+ ]
+ },
+ {
+ type: "layout",
+ direction: "rows",
+ components: [
+ {
+ type: "title",
+ label: "COPY DATA"
+ },
+ {
+ type: "data"
+ },
+ {
+ type: "pagebutton",
+ label: "Home",
+ page: -2
+ }
+ ]
+ }
+ ];
+}
+
+export function preload() {
+ return ["/img/2026hub.png"];
+}
+
+let categoriesInSingular = {
+ abilities: "ability",
+ data: "data",
+ counters: "counter",
+ timers: "timer",
+ ratings: "rating"
+};
+function find(entry, type, categories, category, fallback: any = "") {
+ let value = entry[type].find((d) => d.category == categories[category]);
+ if (value == null) {
+ return fallback;
+ } else {
+ return value[categoriesInSingular[type]];
+ }
+}
+
+export function formatData(data, categories, teams) {
+ return `entry,match,team,alliance,leave,"fuel ground intake","fuel station intake","can ferry","traverse under trench","traverse over bump","auto l1 climb","auto fuel scoring","teleop fuel scoring","climb level","climb time","brick time","defense time","driver skill","defense skill",speed,stability,"intake consistency",scouter,comments,accuracy,timestamp\n${data
+ .map((entry, i) => {
+ return [
+ i,
+ entry.match || 0,
+ entry.team || 0,
+ entry.color || "unknown",
+ find(entry, "abilities", categories, "26-0", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-1", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-2", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-3", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-4", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-5", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-6", false)
+ ? "true"
+ : "false",
+ `"[${find(entry, "data", categories, "26-7", []).join(", ")}]"`,
+ `"[${find(entry, "data", categories, "26-8", []).join(", ")}]"`,
+ parseInt(find(entry, "abilities", categories, "26-9", 0)),
+ parseInt(find(entry, "timers", categories, "26-10", 0)),
+ parseInt(find(entry, "timers", categories, "26-11", 0)),
+ parseInt(find(entry, "timers", categories, "26-12", 0)),
+ parseInt(find(entry, "ratings", categories, "26-13", "")),
+ parseInt(find(entry, "ratings", categories, "26-14", "")),
+ parseInt(find(entry, "ratings", categories, "26-15", "")),
+ parseInt(find(entry, "ratings", categories, "26-16", "")),
+ parseInt(find(entry, "ratings", categories, "26-17", "")),
+ JSON.stringify(
+ `${entry.contributor.username || "username"} (${
+ teams[entry.contributor.team] || 0
+ })`
+ ),
+ JSON.stringify(entry.comments || ""),
+ entry.accuracy && entry.accuracy.calculated
+ ? parseFloat(entry.accuracy.percentage.toFixed(4))
+ : "",
+ entry.serverTimestamp
+ ].join(",");
+ })
+ .join("\n")}`;
+}
+
+export function parseFormatted(format: string): parsedRow[] {
+ const parseArr = (value: string): string[] => {
+ return value.replace(/\[|\]/g, "").split(/,\s*/).filter(Boolean);
+ };
+ const simplify = (row: string): string[] => {
+ const vals: string[] = [];
+ let current = "",
+ iq = false;
+ for (let i = 0; i < row.length; ++i) {
+ const char = row[i],
+ n = row[i + 1];
+ if (char === '"' && iq && n === '"') (current += '"'), i++;
+ else if (char === '"') iq = !iq;
+ else if (char === "," && !iq)
+ vals.push(current.trim()), (current = "");
+ else current += char;
+ }
+ return current ? [...vals, current.trim()] : vals;
+ };
+
+ const rows = format.split("\n").slice(1);
+ return rows.map((row, i) => {
+ const columns = simplify(row);
+ return {
+ entry: i,
+ match: parseInt(columns[1], 10),
+ team: columns[2],
+ alliance: columns[3],
+ leave: columns[4] === "true",
+ "fuel ground intake": columns[5] === "true",
+ "fuel station intake": columns[6] === "true",
+ "can ferry": columns[7] === "true",
+ "traverse under trench": columns[8] === "true",
+ "traverse over bump": columns[9] === "true",
+ "auto l1 climb": columns[10] === "true",
+ "auto fuel scoring": parseArr(columns[11]).join(", "),
+ "teleop fuel scoring": parseArr(columns[12]).join(", "),
+ "climb level": parseInt(columns[13], 10),
+ "climb time": parseInt(columns[14], 10),
+ "brick time": parseInt(columns[15], 10),
+ "defense time": parseInt(columns[16], 10),
+ "driver skill": parseInt(columns[17], 10),
+ "defense skill": parseInt(columns[18], 10),
+ speed: parseInt(columns[19], 10),
+ stability: parseInt(columns[20], 10),
+ "intake consistency": parseInt(columns[21], 10),
+ scouter: columns[22].replace(/^"|"$/g, ""),
+ comments: columns[23].replace(/^"|"$/g, ""),
+ accuracy: columns[24] ? parseFloat(columns[24]) : "",
+ timestamp: parseInt(columns[25], 10)
+ };
+ });
+}
+
+let parsedScoring = {
+ fsa: "Scored when Active",
+ fma: "Missed when Active",
+ fhi: "Shot when Inactive"
+};
+
+export function formatParsedData(data, categories, teams) {
+ return `entry,match,team,alliance,leave,"fuel ground intake","fuel station intake","can ferry","traverse under trench","traverse over bump","auto l1 climb","auto fuel scoring","teleop fuel scoring","climb level","climb time","brick time","defense time","driver skill","defense skill",speed,stability,"intake consistency",scouter,comments,accuracy,timestamp\n${data
+ .map((entry, i) => {
+ return [
+ i,
+ entry.match || 0,
+ entry.team || 0,
+ entry.color || "unknown",
+ find(entry, "abilities", categories, "26-0", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-1", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-2", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-3", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-4", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-5", false)
+ ? "true"
+ : "false",
+ find(entry, "abilities", categories, "26-6", false)
+ ? "true"
+ : "false",
+ `"${find(entry, "data", categories, "26-7", [])
+ .map((entry) => parsedScoring[entry])
+ .join("\\n")}"`,
+ `"${find(entry, "data", categories, "26-8", [])
+ .map((entry) => parsedScoring[entry])
+ .join("\\n")}"`,
+ ["none", "level 1", "level 2", "level 3"][
+ parseInt(find(entry, "abilities", categories, "26-9", 0))
+ ],
+ `${(
+ parseInt(find(entry, "timers", categories, "26-10", 0)) /
+ 1000
+ ).toFixed(3)}s`,
+ `${(
+ parseInt(find(entry, "timers", categories, "26-11", 0)) /
+ 1000
+ ).toFixed(3)}s`,
+ `${(
+ parseInt(find(entry, "timers", categories, "26-12", 0)) /
+ 1000
+ ).toFixed(3)}s`,
+ "⭐".repeat(
+ parseInt(
+ find(entry, "ratings", categories, "26-13", "") + 1
+ )
+ ),
+ "⭐".repeat(
+ parseInt(
+ find(entry, "ratings", categories, "26-14", "") + 1
+ )
+ ),
+ "⭐".repeat(
+ parseInt(
+ find(entry, "ratings", categories, "26-15", "") + 1
+ )
+ ),
+ "⭐".repeat(
+ parseInt(
+ find(entry, "ratings", categories, "26-16", "") + 1
+ )
+ ),
+ "⭐".repeat(
+ parseInt(
+ find(entry, "ratings", categories, "26-17", "") + 1
+ )
+ ),
+ JSON.stringify(
+ `${entry.contributor.username || "username"} (${
+ teams[entry.contributor.team] || 0
+ })`
+ ),
+ JSON.stringify(entry.comments || ""),
+ entry.accuracy && entry.accuracy.calculated
+ ? `${parseFloat(
+ (entry.accuracy.percentage * 100).toFixed(2)
+ )}%`
+ : "",
+ entry.serverTimestamp
+ ].join(",");
+ })
+ .join("\n")}`;
+}
+
+/*
+
+export interface picklist {
+ team: string;
+ "avg-auto-pieces": number;
+ "avg-tele-pieces": number;
+ "avg-l1": number;
+ "avg-l2": number;
+ "avg-l3": number;
+ "avg-l4": number;
+ "avg-proc": number;
+ "avg-net": number;
+ "deep-climbs": number;
+}
+
+export async function formPicklist(
+ data: { [team: string]: any[] },
+ categories,
+ teams: any[]
+) {
+ let analysis: picklist[] = [];
+ console.log(teams);
+ for (const t1 of teams) {
+ const t = t1.team_number;
+ let dat = data[t];
+ if (!dat) continue;
+ let autoPieces = 0;
+ let telePieces = 0;
+ let l1 = 0;
+ let l2 = 0;
+ let l3 = 0;
+ let l4 = 0;
+ let proc = 0;
+ let net = 0;
+ let deepClimbs = 0;
+ let total = 0;
+ for (const d of dat) {
+ if (!d || !d.accuracy || !d.accuracy.calculated) {
+ continue;
+ }
+ let acc = d.accuracy.percentage;
+
+ autoPieces += acc * find(d, "counters", categories, "26-20", 0);
+ telePieces += acc * find(d, "counters", categories, "26-24", 0);
+ let autocoral = find(d, "data", categories, "26-18", []);
+ let telecoral = find(d, "data", categories, "26-22", []);
+ let autol4 = autocoral.filter((el) => el == 0).length;
+ let autol3 = autocoral.filter((el) => el == 1).length;
+ let autol2 = autocoral.filter((el) => el == 2).length;
+ let autol1 = autocoral.filter((el) => el == 3).length;
+ let telel4 = telecoral.filter((el) => el == 0).length;
+ let telel3 = telecoral.filter((el) => el == 1).length;
+ let telel2 = telecoral.filter((el) => el == 2).length;
+ let telel1 = telecoral.filter((el) => el == 3).length;
+ l1 += acc * (autol1 + telel1);
+ l2 += acc * (autol2 + telel2);
+ l3 += acc * (autol3 + telel3);
+ l4 += acc * (autol4 + telel4);
+ let autoalgae = find(d, "data", categories, "26-17", []);
+ let telealgae = find(d, "data", categories, "26-21", []);
+ let autoNe = autoalgae.filter((el) => el == 4).length;
+ let autoPr = autoalgae.filter((el) => el == 5).length;
+ let teleNe = telealgae.filter((el) => el == 4).length;
+ let telePr = telealgae.filter((el) => el == 5).length;
+ net += acc * (autoNe + teleNe);
+ proc += acc * (autoPr + telePr);
+ let climb = find(d, "abilities", categories, "26-8", 0);
+ if (acc > 0.5) deepClimbs += climb == 3 ? 1 : 0;
+
+ total += acc;
+ }
+ if (dat.length == 0 || total == 0) {
+ analysis.push({
+ team: t,
+ "avg-auto-pieces": NaN,
+ "avg-tele-pieces": NaN,
+ "avg-l1": NaN,
+ "avg-l2": NaN,
+ "avg-l3": NaN,
+ "avg-l4": NaN,
+ "avg-proc": NaN,
+ "avg-net": NaN,
+ "deep-climbs": NaN
+ });
+ } else {
+ analysis.push({
+ team: t,
+ "avg-auto-pieces": autoPieces / total,
+ "avg-tele-pieces": telePieces / total,
+ "avg-l1": l1 / total,
+ "avg-l2": l2 / total,
+ "avg-l3": l3 / total,
+ "avg-l4": l4 / total,
+ "avg-proc": proc / total,
+ "avg-net": net / total,
+ "deep-climbs": deepClimbs
+ });
+ }
+ }
+
+ return formatPicklist(analysis);
+}
+
+function formatPicklist(analysis) {
+ return `entry,team,"avg auto pieces","avg tele pieces","avg l1","avg l2","avg l3","avg l4","avg processor","avg net","# of deep climbs"\n${analysis
+ .map((entry, i) => {
+ return [
+ i,
+ entry.team || 0,
+ entry["avg-auto-pieces"],
+ entry["avg-tele-pieces"],
+ entry["avg-l1"],
+ entry["avg-l2"],
+ entry["avg-l3"],
+ entry["avg-l4"],
+ entry["avg-proc"],
+ entry["avg-net"],
+ entry["deep-climbs"]
+ ].join(",");
+ })
+ .join("\n")}`;
+} */
+
+export function notes() {
+ return ``;
+}
+/*
+function run(command) {
+ return new Promise(async (resolve, reject) => {
+ exec(command, (error, stdout, stderr) => {
+ resolve({ error, stdout, stderr });
+ });
+ });
+}
+
+export async function analysis(event, teamNumber) {
+ let analyzed = [];
+ let data: any = {
+ offenseRankings: [],
+ predictions: []
+ };
+ try {
+ let matchesFull = (await getMatchesFull(event)) as any;
+ let allScoutingData = await getAllDataByEvent(event);
+ let allParsedData = parseFormatted(allScoutingData);
+ let allScoutedTeams = [
+ ...new Set(
+ allScoutingData
+ .split("\n")
+ .slice(1)
+ .map((entry) => entry.split(",")[2])
+ )
+ ];
+ fs.writeFileSync(`../${event}-tba.json`, JSON.stringify(matchesFull));
+ fs.writeFileSync(`../${event}.csv`, allScoutingData);
+ let allTeams = [
+ ...new Set(
+ matchesFull
+ .map(
+ (match) =>
+ `${match.alliances.red.team_keys
+ .map((team) => team.replace("frc", ""))
+ .join(",")},${match.alliances.blue.team_keys
+ .map((team) => team.replace("frc", ""))
+ .join(",")}`
+ )
+ .join(",")
+ .split(",")
+ )
+ ];
+ let hasAllTeams = true;
+ for (let i = 0; i < allTeams.length && hasAllTeams; i++) {
+ hasAllTeams = allScoutedTeams.includes(allTeams[i]);
+ }
+ const rankings = computeRankings(allParsedData);
+
+ const processRankings = async (rs) => {
+ let rankingsTeams = Object.keys(rs);
+ let rankingsArr = [];
+ for (let i = 0; i < rankingsTeams.length; i++) {
+ rankingsArr.push({
+ teamNumber: rankingsTeams[i],
+ offenseScore: rs[rankingsTeams[i]]["off-score"],
+ defenseScore: rs[rankingsTeams[i]]["def-score"]
+ });
+ }
+ let offense = rankingsArr
+ .sort((a, b) => b.offenseScore - a.offenseScore)
+ .map((ranking) => ({
+ team: ranking.teamNumber,
+ offense: ranking.offenseScore.toFixed(2)
+ }));
+ let defense = rankingsArr
+ .sort((a, b) => b.defenseScore - a.defenseScore)
+ .map((ranking) => ranking.teamNumber);
+ data.offenseRankings = offense;
+ let tableRankings = [["Team", "TPW Offense Score"]];
+ function ending(num) {
+ if (num % 100 >= 4 && num % 100 <= 20) {
+ return "th";
+ } else if (num % 10 == 1) {
+ return "st";
+ } else if (num % 10 == 2) {
+ return "nd";
+ } else if (num % 10 == 3) {
+ return "rd";
+ } else {
+ return "th";
+ }
+ }
+ for (let i = 0; i < offense.length; i++) {
+ tableRankings.push([
+ `${offense[i].team}`,
+ offense[i].offense
+ ]);
+ }
+ return tableRankings;
+ };
+
+ // undefined teamNumber means we are just requesting the rankings
+ // all processing underneath of graphs+predictions requires teamNumber
+ if (teamNumber == undefined) {
+ analyzed.push({
+ type: "table",
+ category: "rank",
+ label: "Rankings",
+ values: await processRankings(rankings)
+ });
+ return { display: analyzed, data: data }; // return only the rankings
+ }
+
+ const graph0 = getGraph(0, allParsedData, teamNumber);
+ const graph3 = getGraph(3, allParsedData, teamNumber);
+ const graph4 = getGraph(4, allParsedData, teamNumber);
+ const graph5 = getGraph(5, allParsedData, teamNumber);
+ const graph1 = getGraph(1, allParsedData, teamNumber);
+ const graph2 = getGraph(2, allParsedData, teamNumber);
+
+ let matches = matchesFull
+ .filter((match: any) => match.comp_level == "qm")
+ .filter(
+ (match: any) =>
+ match.alliances.blue.team_keys.includes(
+ `frc${teamNumber}`
+ ) ||
+ match.alliances.red.team_keys.includes(`frc${teamNumber}`)
+ )
+ .sort((a: any, b: any) => a.match_number - b.match_number);
+ let predictions = [];
+ for (let i = 0; i < matches.length; i++) {
+ let match = matches[i] as any;
+ let r1 = match.alliances.red.team_keys[0].replace("frc", "");
+ let r2 = match.alliances.red.team_keys[1].replace("frc", "");
+ let r3 = match.alliances.red.team_keys[2].replace("frc", "");
+ let b1 = match.alliances.blue.team_keys[0].replace("frc", "");
+ let b2 = match.alliances.blue.team_keys[1].replace("frc", "");
+ let b3 = match.alliances.blue.team_keys[2].replace("frc", "");
+ if (
+ !allScoutedTeams.includes(r1) ||
+ !allScoutedTeams.includes(r2) ||
+ !allScoutedTeams.includes(r3) ||
+ !allScoutedTeams.includes(b1) ||
+ !allScoutedTeams.includes(b2) ||
+ !allScoutedTeams.includes(b3)
+ )
+ continue;
+ let prediction = computePrediction(
+ b1,
+ b2,
+ b3,
+ r1,
+ r2,
+ r3,
+ allParsedData,
+ "../",
+ event as string
+ );
+ prediction.match = match.match_number;
+ prediction.win = match.alliances[
+ prediction.winner
+ ].team_keys.includes(`frc${teamNumber}`);
+ let predictionRed =
+ prediction.red / (prediction.red + prediction.blue);
+ let predictionBlue =
+ prediction.blue / (prediction.blue + prediction.red);
+ if (predictionRed > 0.85) {
+ predictionRed = 0.75 + ((predictionRed - 0.85) / 0.15) * 0.1;
+ predictionBlue = 1 - predictionRed;
+ } else if (predictionBlue > 0.85) {
+ predictionBlue = 0.75 + ((predictionBlue - 0.85) / 0.15) * 0.1;
+ predictionRed = 1 - predictionBlue;
+ }
+ prediction.red = predictionRed;
+ prediction.blue = predictionBlue;
+ predictions.push(prediction);
+ }
+
+ data.predictions = predictions;
+
+ analyzed.push({
+ type: "config",
+ category: "score",
+ label: "Algae Scoring",
+ value: graph0
+ });
+ analyzed.push({
+ type: "config",
+ category: "score",
+ label: "Coral Scoring",
+ value: graph3
+ });
+ analyzed.push({
+ type: "config",
+ category: "score",
+ label: "Score Proportion",
+ value: graph4
+ });
+ analyzed.push({
+ type: "config",
+ category: "score",
+ label: "Shot Accuracy",
+ value: graph5
+ });
+ analyzed.push({
+ type: "config",
+ category: "overall",
+ label: "Radar Chart
(Single Team)",
+ value: graph1
+ });
+ analyzed.push({
+ type: "config",
+ category: "overall",
+ label: "Radar Chart
(Compared to Best Scores)",
+ value: graph2
+ });
+ analyzed.push({
+ type: "predictions",
+ category: "predict",
+ label: "Predictions",
+ values: predictions
+ });
+ analyzed.push({
+ type: "table",
+ category: "rank",
+ label: "Rankings",
+ values: processRankings(rankings)
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ return { display: analyzed, data: data };
+}
+
+export async function compare(event, teamNumbers) {
+ teamNumbers = [...new Set(teamNumbers)].sort((a: string, b: string) =>
+ a.length != b.length ? a.length - b.length : a.localeCompare(b)
+ );
+ let comparison = [];
+ try {
+ let matchesFull = (await getMatchesFull(event)) as any;
+ let allScoutingData = await getAllDataByEvent(event);
+ let allParsedData = parseFormatted(allScoutingData);
+ fs.writeFileSync(`../${event}-tba.json`, JSON.stringify(matchesFull));
+ fs.writeFileSync(`../${event}.csv`, await getAllDataByEvent(event));
+ const graph1 = getGraph(1, allParsedData, teamNumbers as string[]);
+ const graph2 = getGraph(2, allParsedData, teamNumbers as string[]);
+ comparison.push({
+ type: "config",
+ category: "overall",
+ label: `Radar Chart
(${
+ teamNumbers.length == 1
+ ? "Single Team"
+ : `${teamNumbers.length} Teams`
+ })`,
+ value: graph1
+ });
+ comparison.push({
+ type: "config",
+ category: "overall",
+ label: "Radar Chart
(Compared to Best Scores)",
+ value: graph2
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ return { display: comparison, data: {} };
+}
+
+export async function predict(event, redTeamNumbers, blueTeamNumbers) {
+ redTeamNumbers = [...new Set(redTeamNumbers)].sort((a: string, b: string) =>
+ a.length != b.length ? a.length - b.length : a.localeCompare(b)
+ );
+ blueTeamNumbers = [...new Set(blueTeamNumbers)].sort(
+ (a: string, b: string) =>
+ a.length != b.length ? a.length - b.length : a.localeCompare(b)
+ );
+ let analyzed = [];
+ let data: any = {
+ predictions: []
+ };
+ try {
+ let matchesFull = (await getMatchesFull(event)) as any;
+ let allScoutingData = await getAllDataByEvent(event);
+ fs.writeFileSync(`../${event}-tba.json`, JSON.stringify(matchesFull));
+ fs.writeFileSync(`../${event}.csv`, allScoutingData);
+ let allParsedData = parseFormatted(allScoutingData);
+
+ let predictions = [];
+ let r1 = redTeamNumbers[0];
+ let r2 = redTeamNumbers[1];
+ let r3 = redTeamNumbers[2];
+ let b1 = blueTeamNumbers[0];
+ let b2 = blueTeamNumbers[1];
+ let b3 = blueTeamNumbers[2];
+ let prediction = computePrediction(
+ b1,
+ b2,
+ b3,
+ r1,
+ r2,
+ r3,
+ allParsedData,
+ "../",
+ event as string
+ );
+ let predictionRed = prediction.red / (prediction.red + prediction.blue);
+ let predictionBlue =
+ prediction.blue / (prediction.blue + prediction.red);
+ if (predictionRed > 0.85) {
+ predictionRed = 0.75 + ((predictionRed - 0.85) / 0.15) * 0.1;
+ predictionBlue = 1 - predictionRed;
+ } else if (predictionBlue > 0.85) {
+ predictionBlue = 0.75 + ((predictionBlue - 0.85) / 0.15) * 0.1;
+ predictionRed = 1 - predictionBlue;
+ }
+ prediction.red = predictionRed;
+ prediction.blue = predictionBlue;
+ predictions.push(prediction);
+
+ data.predictions = predictions;
+
+ analyzed.push({
+ type: "predictions",
+ label: "Prediction",
+ values: predictions
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ return { display: analyzed, data: data };
+}
+
+export async function accuracy(event, matches, data, categories, teams) {
+ return await accuracy2025(event, matches, data, categories, teams);
+}
+
+/*
+export async function tps(data, categories, teams) {
+ return data.map((entry) => {
+ if(!entry.event.startsWith("2024")) {
+ return {
+ silentlyFail: true,
+ hash: event.hash
+ };
+ }
+ return {
+ silentlyFail: false,
+ hash: event.hash,
+ entry: {
+ metadata: {
+ event: entry.event || "2024all-prac",
+ match: {
+ level: "qm",
+ number: entry.match || 0,
+ set: 1
+ },
+ bot: entry.team || 0,
+ timestamp: entry.clientTimestamp,
+ scouter: {
+ name: entry.contributor.username || "username",
+ team: teams[entry.contributor.team] || 0,
+ app: "thepurplewarehouse.com"
+ }
+ },
+ abilities: {
+ "auto-leave-starting-zone": find(entry, "abilities", categories, "24-0", false),
+ "ground-pick-up": find(entry, "abilities", categories, "24-1", false),
+ "auto-center-line-pick-up": find(entry, "abilities", categories, "24-18", false),
+ "teleop-stage-level-2024": parseInt(find(entry, "abilities", categories, "24-4", false)),
+ "teleop-spotlight-2024": find(entry, "abilities", categories, "24-5", false)
+ },
+ counters: {},
+ data: {
+ "auto-scoring-2024": find(entry, "data", categories, "24-2", []),
+ "teleop-scoring-2024": find(entry, "data", categories, "24-3", []),
+ notes: entry.comments || ""
+ },
+ ratings: {
+ "driver-skill": parseInt(find(entry, "ratings", categories, "24-9", 0)),
+ "defense-skill": parseInt(find(entry, "ratings", categories, "24-10", 0)),
+ speed: parseInt(find(entry, "ratings", categories, "24-11", 0)),
+ stability: parseInt(find(entry, "ratings", categories, "24-12", 0)),
+ "intake-consistency": parseInt(find(entry, "ratings", categories, "24-13", 0))
+ },
+ timers: {
+ "brick-time": parseInt(find(entry, "timers", categories, "24-7", 0))
+ "defense-time": parseInt(find(entry, "timers", categories, "24-8", 0)),
+ "stage-time-2024": parseInt(find(entry, "timers", categories, "24-6", 0))
+ }
+ },
+ privacy: [
+ {
+ path: "data.notes",
+ private: true,
+ type: "redacted",
+ detail: "[redacted for privacy]",
+ teams: [entry.team]
+ },
+ {
+ path: "metadata.scouter.name",
+ private: true,
+ type: "scrambled",
+ detail: 16,
+ teams: [entry.team]
+ }
+ ],
+ threshold: 10,
+ serverTimestamp: entry.serverTimestamp
+ };
+ });
+}
+*/
+
+const scouting2025 = {
+ categories,
+ layout,
+ preload,
+ formatData,
+ formatParsedData,
+ notes
+ /* formPicklist,
+ analysis,
+ compare,
+ predict,
+ accuracy,
+ tps*/
+};
+export default scouting2025;
diff --git a/config/scouting/index.ts b/config/scouting/index.ts
index 90151c5c..bdfd22bb 100644
--- a/config/scouting/index.ts
+++ b/config/scouting/index.ts
@@ -1,12 +1,14 @@
import scouting2023 from "./2023";
import scouting2024 from "./2024";
import scouting2025 from "./2025";
+import scouting2026 from "./2026";
import config from "../";
const scoutingConfig: any = {
"2023": scouting2023,
"2024": scouting2024,
- "2025": scouting2025
+ "2025": scouting2025,
+ "2026": scouting2026
};
// let year = new Date().toLocaleDateString().split("/")[2];
@@ -20,6 +22,13 @@ if (scoutingConfig[year].formatParsedData != null) {
} else {
scoutingConfig.formatParsedData = scoutingConfig[year].formatData;
}
+if (scoutingConfig[year].formPicklist != null) {
+ scoutingConfig.formPicklist = scoutingConfig[year].formPicklist;
+} else {
+ scoutingConfig.formPicklist = async (data, teams) => {
+ return "";
+ };
+}
scoutingConfig.notes = scoutingConfig[year].notes;
scoutingConfig.analysis = scoutingConfig[year].analysis;
scoutingConfig.accuracy = scoutingConfig[year].accuracy;
diff --git a/config/secret.ts b/config/secret.ts
index a6201940..8513fa7a 100644
--- a/config/secret.ts
+++ b/config/secret.ts
@@ -25,6 +25,9 @@ try {
if (secretFile.production2024 != null) {
secret.production2024 = secretFile.production2024;
}
+ if (secretFile.production2025 != null) {
+ secret.production2025 = secretFile.production2025;
+ }
}
} catch (err) {}
diff --git a/config/staging.ts b/config/staging.ts
index 22aa65e7..e2e17b56 100644
--- a/config/staging.ts
+++ b/config/staging.ts
@@ -11,7 +11,7 @@ const config: Config = {
db: secret.staging.db,
auth: secret.staging.auth,
features: ["scouting", "tps"],
- year: 2025
+ year: 2026
};
export default config;
diff --git a/helpers/scouting.ts b/helpers/scouting.ts
index ba7eacdc..557ec9f0 100644
--- a/helpers/scouting.ts
+++ b/helpers/scouting.ts
@@ -2,7 +2,7 @@ import ScoutingEntry from "../models/scoutingEntry";
import ScoutingCategory from "../models/scoutingCategory";
import Team from "../models/team";
import { getTeamByNumber } from "./teams";
-import { getEventTeams } from "./tba";
+import { getEventTeams, getTeamEvents } from "./tba";
import * as crypto from "crypto";
import scoutingConfig from "../config/scouting";
import config from "../config";
@@ -218,10 +218,110 @@ export function getLevelAndProgress(xp) {
level: 9,
progress: (xp - 8000) / 2000
};
+ } else if (xp < 12000) {
+ return {
+ level: 10,
+ progress: (xp - 10000) / 2000
+ };
+ } else if (xp < 14000) {
+ return {
+ level: 11,
+ progress: (xp - 12000) / 2000
+ };
+ } else if (xp < 16000) {
+ return {
+ level: 12,
+ progress: (xp - 14000) / 2000
+ };
+ } else if (xp < 18000) {
+ return {
+ level: 13,
+ progress: (xp - 16000) / 2000
+ };
+ } else if (xp < 20000) {
+ return {
+ level: 14,
+ progress: (xp - 18000) / 2000
+ };
+ } else if (xp < 22000) {
+ return {
+ level: 15,
+ progress: (xp - 20000) / 2000
+ };
+ } else if (xp < 24000) {
+ return {
+ level: 16,
+ progress: (xp - 22000) / 2000
+ };
+ } else if (xp < 26000) {
+ return {
+ level: 17,
+ progress: (xp - 24000) / 2000
+ };
+ } else if (xp < 28000) {
+ return {
+ level: 18,
+ progress: (xp - 26000) / 2000
+ };
+ } else if (xp < 30000) {
+ return {
+ level: 19,
+ progress: (xp - 28000) / 2000
+ };
+ } else if (xp < 32000) {
+ return {
+ level: 20,
+ progress: (xp - 30000) / 2000
+ };
+ } else if (xp < 35000) {
+ return {
+ level: 21,
+ progress: (xp - 32000) / 3000
+ };
+ } else if (xp < 39000) {
+ return {
+ level: 22,
+ progress: (xp - 35000) / 4000
+ };
+ } else if (xp < 44000) {
+ return {
+ level: 23,
+ progress: (xp - 39000) / 5000
+ };
+ } else if (xp < 50000) {
+ return {
+ level: 24,
+ progress: (xp - 44000) / 6000
+ };
+ } else if (xp < 57000) {
+ return {
+ level: 25,
+ progress: (xp - 50000) / 7000
+ };
+ } else if (xp < 65000) {
+ return {
+ level: 26,
+ progress: (xp - 57000) / 8000
+ };
+ } else if (xp < 74000) {
+ return {
+ level: 27,
+ progress: (xp - 65000) / 9000
+ };
+ } else if (xp < 84000) {
+ return {
+ level: 28,
+ progress: (xp - 74000) / 10000
+ };
+ } else if (xp < 95000) {
+ return {
+ level: 29,
+ progress: (xp - 84000) / 11000
+ };
} else {
return {
- level: 10 + Math.floor((xp - 10000) / 2500),
- progress: (xp % 2500) / 2500
+ level: 30 + Math.floor((xp - 95000) / 12000),
+ progress: (xp % 12000) / 12000
};
}
}
@@ -449,7 +549,7 @@ export async function addEntry(
}
});
await entry.save();
- if(!event.endsWith("-prac")) {
+ if (!event.endsWith("-prac")) {
pendingAccuracy.add(event);
}
}
@@ -666,6 +766,35 @@ export async function getSharedData(
}
}
+export async function getPicklistData(event: string, teamNumber: string) {
+ let teams = await getEventTeams(event, config.year);
+ let team = (await getTeamByNumber(teamNumber)) || { _id: "" };
+ let events = new Set();
+ for (const t of teams) {
+ let tevents = await getTeamEvents(config.year, t.team_number);
+ for (let i = 0; i < tevents.length; ++i) {
+ events.add(tevents[i].key);
+ }
+ }
+ events.delete(`${config.year}all-prac`);
+ // get all the data from all of the events
+ let categories;
+ let aData: { [team: string]: any[] } = {};
+ for (const event of events) {
+ let { data, categories: nCategories } = await getAllRawDataByEvent(
+ event
+ );
+ if (categories == null) categories = nCategories;
+ for (const d of data) {
+ if (isNaN(d.team)) continue;
+ if (aData[d.team] == null) aData[d.team] = [];
+ aData[d.team].push(d);
+ }
+ }
+
+ return await scoutingConfig.formPicklist(aData, categories, teams);
+}
+
export async function getTeamsAtEvent(
event: string,
teamNumber: string,
@@ -769,7 +898,7 @@ export async function getTeamData(
export async function updatePendingAccuracy() {
let events = [...pendingAccuracy] as any;
- for(let i = 0; i < events.length; i++) {
+ for (let i = 0; i < events.length; i++) {
pendingAccuracy.delete(events[i]);
await updateAccuracy(events[i]);
}
diff --git a/index.ts b/index.ts
index fcdaca91..bdc4a2a0 100644
--- a/index.ts
+++ b/index.ts
@@ -15,7 +15,11 @@ import { addAPIHeaders } from "./helpers/utils";
import config from "./config";
import { registerComponentsWithinDirectory } from "./helpers/componentRegistration";
-import { getStats, initializeCategories, updatePendingAccuracy } from "./helpers/scouting";
+import {
+ getStats,
+ initializeCategories,
+ updatePendingAccuracy
+} from "./helpers/scouting";
// import loginRouter from "./routers/login"; // contains base route "/"
import defaultRouter from "./routers/default"; // contains base route "/"
@@ -209,7 +213,7 @@ app.use(serve("./static", {}));
let calculatingAccuracy = false;
setInterval(async () => {
- if(!calculatingAccuracy) {
+ if (!calculatingAccuracy) {
calculatingAccuracy = true;
console.log("calculating accuracy");
await updatePendingAccuracy();
diff --git a/package-lock.json b/package-lock.json
index eb882dc8..7749ad87 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -175,6 +175,97 @@
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "string-width-cjs": {
+ "version": "npm:string-width@4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "dependencies": {
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "strip-ansi-cjs": {
+ "version": "npm:strip-ansi@6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "wrap-ansi-cjs": {
+ "version": "npm:wrap-ansi@7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ }
}
},
"@jridgewell/resolve-uri": {
@@ -4702,36 +4793,6 @@
"strip-ansi": "^7.0.1"
}
},
- "string-width-cjs": {
- "version": "npm:string-width@4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "requires": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
- },
- "emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
- },
- "strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "requires": {
- "ansi-regex": "^5.0.1"
- }
- }
- }
- },
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@@ -4755,21 +4816,6 @@
"ansi-regex": "^6.0.1"
}
},
- "strip-ansi-cjs": {
- "version": "npm:strip-ansi@6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "requires": {
- "ansi-regex": "^5.0.1"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
- }
- }
- },
"strip-final-newline": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
@@ -5157,67 +5203,6 @@
}
}
},
- "wrap-ansi-cjs": {
- "version": "npm:wrap-ansi@7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "requires": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
- },
- "ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "requires": {
- "color-convert": "^2.0.1"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
- },
- "emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
- },
- "string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "requires": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- }
- },
- "strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "requires": {
- "ansi-regex": "^5.0.1"
- }
- }
- }
- },
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
diff --git a/package.json b/package.json
index 12da5392..1a3ed22d 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"start": "NODE_ENV=production node build/index.js",
"start2023": "NODE_ENV=production2023 node build/index.js",
"start2024": "NODE_ENV=production2024 node build/index.js",
+ "start2025": "NODE_ENV=production2025 node build/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npm i && rm -rf build && tsc",
"build-noinstall": "rm -rf build && tsc",
@@ -20,28 +21,33 @@
"deploy-prod": "npm run build && npm run start",
"deploy-prod2023": "npm run build && npm run start2023",
"deploy-prod2024": "npm run build && npm run start2024",
+ "deploy-prod2025": "npm run build && npm run start2025",
"deploy-dev-noinstall": "npm run build-noinstall && npm run dev",
"deploy-staging-noinstall": "npm run build-noinstall && npm run staging",
"deploy-prod-noinstall": "npm run build-noinstall && npm run start",
"deploy-prod2023-noinstall": "npm run build-noinstall && npm run start2023",
"deploy-prod2024-noinstall": "npm run build-noinstall && npm run start2024",
+ "deploy-prod2025-noinstall": "npm run build-noinstall && npm run start2025",
"drop-changes": "git stash & git stash drop",
"update-dev": "git fetch && npm run drop-changes & git checkout main && git pull && npm run deploy-dev",
"update-staging": "git fetch && npm run drop-changes & git checkout staging && git pull && npm run deploy-staging",
"update-prod": "git fetch && npm run drop-changes & git checkout prod && git pull && npm run deploy-prod",
"update-prod2023": "git fetch && npm run drop-changes & git checkout 2023 && git pull && npm run deploy-prod2023",
"update-prod2024": "git fetch && npm run drop-changes & git checkout 2024 && git pull && npm run deploy-prod2024",
+ "update-prod2025": "git fetch && npm run drop-changes & git checkout 2025 && git pull && npm run deploy-prod2025",
"pm2-dev": "pm2 start \"npm run update-dev\"",
"pm2-staging": "pm2 start \"npm run update-staging\"",
"pm2-prod": "pm2 start \"npm run update-prod\"",
"pm2-prod2023": "pm2 start \"npm run update-prod2023\"",
"pm2-prod2024": "pm2 start \"npm run update-prod2024\"",
+ "pm2-prod2025": "pm2 start \"npm run update-prod2025\"",
"logs": "pm2 logs",
"stop-dev": "pm2 stop \"npm run update-dev\"",
"stop-staging": "pm2 stop \"npm run update-staging\"",
"stop-prod": "pm2 stop \"npm run update-prod\"",
"stop-prod2023": "pm2 stop \"npm run update-prod2023\"",
- "stop-prod2024": "pm2 stop \"npm run update-prod2024\""
+ "stop-prod2024": "pm2 stop \"npm run update-prod2024\"",
+ "stop-prod2025": "pm2 stop \"npm run update-prod2025\""
},
"repository": {
"type": "git",
diff --git a/routers/api/scouting.ts b/routers/api/scouting.ts
index e5f6443e..38cde773 100644
--- a/routers/api/scouting.ts
+++ b/routers/api/scouting.ts
@@ -21,7 +21,8 @@ import {
getTeamData,
getTotalIncentives,
getLevelAndProgress,
- aggregateLeaderboard
+ aggregateLeaderboard,
+ getPicklistData
} from "../../helpers/scouting";
import {
getTeamByNumber,
@@ -212,6 +213,24 @@ router.get(
}
);
+router.get(
+ "/entry/data/event/:event/picklist",
+ requireScoutingAuth,
+ async (ctx, next) => {
+ addAPIHeaders(ctx);
+ ctx.body = {
+ success: true,
+ body: {
+ csv: await getPicklistData(
+ ctx.params.event,
+ ctx.session.scoutingTeamNumber
+ ),
+ notes: scoutingConfig.notes()
+ }
+ };
+ }
+);
+
router.get(
"/entry/data/event/:event/tba",
requireScoutingAuth,
diff --git a/static/img/2026hub.png b/static/img/2026hub.png
new file mode 100644
index 00000000..fba3a1da
Binary files /dev/null and b/static/img/2026hub.png differ
diff --git a/static/js/scoutingsdk.js b/static/js/scoutingsdk.js
index 2c8d7ed3..b01202dc 100644
--- a/static/js/scoutingsdk.js
+++ b/static/js/scoutingsdk.js
@@ -2454,6 +2454,14 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
+
+
@@ -2462,6 +2470,44 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
element.querySelector(".data-popup .close-btn").onclick = () => {
closeOptions();
};
+ element.querySelector(
+ ".data-popup .export-content .picklist-confirm"
+ ).onclick = async () => {
+ let eventCode = element.querySelector(
+ ".data-window > select.event-code"
+ ).value;
+
+ let furl = `/api/v1/scouting/entry/data/event/${encodeURIComponent(
+ eventCode
+ )}/picklist`;
+
+ try {
+ let data = await (await fetch(furl)).json();
+ if (data.success) {
+ element.querySelector(".red").innerHTML = " ";
+ let csv = data.body.csv;
+ let download =
+ "data:text/csv;charset=utf-8," +
+ encodeURIComponent(csv);
+ let link = document.createElement("a");
+ link.style.display = "none";
+ link.setAttribute("href", download);
+ link.setAttribute(
+ "download",
+ `tpw-picklist-${eventCode}.csv`
+ );
+ element.appendChild(link);
+ link.click();
+ link.remove();
+ } else {
+ element.querySelector(".red").innerHTML =
+ data.error || "Unknown error.";
+ }
+ } catch (err) {
+ console.log("export csv error", err);
+ }
+ closeOptions();
+ };
element.querySelector(
".data-popup .export-content .export-confirm"
).onclick = async () => {
@@ -2472,74 +2518,40 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
let toggle = element.querySelector(
".data-popup input#export-toggle"
).checked; // true = my team's data
- if (toggle) {
- if (!teamNumber) {
- console.error("export error: no team number found.");
- return;
- }
- try {
- let data = await (
- await fetch(
- `/api/v1/scouting/entry/data/event/${encodeURIComponent(
- eventCode
- )}/csv/${teamNumber}`
- )
- ).json();
- if (data.success) {
- element.querySelector(".red").innerHTML = " ";
- let csv = data.body.csv;
- let download =
- "data:text/csv;charset=utf-8," +
- encodeURIComponent(csv);
- let link = document.createElement("a");
- link.style.display = "none";
- link.setAttribute("href", download);
- link.setAttribute(
- "download",
- `tpw-scouting-${eventCode}-${teamNumber}.csv`
- );
- element.appendChild(link);
- link.click();
- link.remove();
- } else {
- element.querySelector(".red").innerHTML =
- data.error || "Unknown error.";
- }
- } catch (err) {
- console.log("export csv error", err);
- }
- } else {
- try {
- let data = await (
- await fetch(
- `/api/v1/scouting/entry/data/event/${encodeURIComponent(
- eventCode
- )}/csv`
- )
- ).json();
- if (data.success) {
- element.querySelector(".red").innerHTML = " ";
- let csv = data.body.csv;
- let download =
- "data:text/csv;charset=utf-8," +
- encodeURIComponent(csv);
- let link = document.createElement("a");
- link.style.display = "none";
- link.setAttribute("href", download);
- link.setAttribute(
- "download",
- `tpw-scouting-${eventCode}.csv`
- );
- element.appendChild(link);
- link.click();
- link.remove();
- } else {
- element.querySelector(".red").innerHTML =
- data.error || "Unknown error.";
- }
- } catch (err) {
- console.log("export csv error", err);
+ if (toggle && !teamNumber) {
+ console.error("export error: no team number found.");
+ return;
+ }
+
+ let furl = `/api/v1/scouting/entry/data/event/${encodeURIComponent(
+ eventCode
+ )}/csv`;
+ furl += toggle ? `/${teamNumber.toString()}` : "";
+
+ try {
+ let data = await (await fetch(furl)).json();
+ if (data.success) {
+ element.querySelector(".red").innerHTML = " ";
+ let csv = data.body.csv;
+ let download =
+ "data:text/csv;charset=utf-8," +
+ encodeURIComponent(csv);
+ let link = document.createElement("a");
+ link.style.display = "none";
+ link.setAttribute("href", download);
+ link.setAttribute(
+ "download",
+ `tpw-scouting-${eventCode}-${teamNumber}.csv`
+ );
+ element.appendChild(link);
+ link.click();
+ link.remove();
+ } else {
+ element.querySelector(".red").innerHTML =
+ data.error || "Unknown error.";
}
+ } catch (err) {
+ console.log("export csv error", err);
}
closeOptions();
};
@@ -3386,10 +3398,18 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
)}`;
};
- _this.showLocationPopup = (index, options, locations, values, state) => {
+ _this.showLocationPopup = (
+ index,
+ options,
+ locations,
+ values,
+ state,
+ increment
+ ) => {
return new Promise(async (resolve, reject) => {
locations = [...locations];
values = [...values];
+ increment = increment || 5; // default to 5 if not specified
let locationData = locations.map((loc, i) => {
return {
value: values[i],
@@ -3451,7 +3471,15 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
? "Deselect"
: "Select"
}`
- : ``
+ )}">+
+ `
}
@@ -3868,6 +3905,10 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
if (component.options instanceof Array) {
options = component.options;
}
+ let increment = 5;
+ if (typeof component.increment == "number") {
+ increment = component.increment;
+ }
let defaultValue = {
locations: [],
values: [],
@@ -3950,7 +3991,8 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
data.data[component.data.values],
defaultValue.values
),
- getState()
+ getState(),
+ increment
);
if (result != null) {
await _this.setData(