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/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 67d04d50..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];
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/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/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 20a9d41a..b01202dc 100644
--- a/static/js/scoutingsdk.js
+++ b/static/js/scoutingsdk.js
@@ -3398,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],
@@ -3463,7 +3471,15 @@ ${_this.escape(teamNumber)} (Blue ${i + 1})
? "Deselect"
: "Select"
}`
- : ``
+ )}">+
+ `
}
@@ -3880,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: [],
@@ -3962,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(