diff --git a/sites/scouting/src/matrix.js b/sites/scouting/src/matrix.js index 3ca1fa0..0236cec 100644 --- a/sites/scouting/src/matrix.js +++ b/sites/scouting/src/matrix.js @@ -13,7 +13,10 @@ Drive's API returns 404, not 403, for a file outside the token's grant, so it looks identical to a bad file ID) and spreadsheets. */ -import { detectFormat, parseGwAppV11, parseWarOrganV11, parseV11List, generateDiscordText, buildAbbreviationIndex } from "./vendor/40k-compactor/index.js"; +import { generateDiscordText, buildAbbreviationIndex } from "./vendor/40k-compactor/index.js"; +// parseNRGW/parseNRTournament aren't re-exported from the package's own +// index.js (only from this internal module) — pulled from here directly. +import { detectFormat, parseGwAppV11, parseWarOrganV11, parseV11List, parseNRGW, parseNRTournament } from "./vendor/40k-compactor/modules/parsers.js"; import skippableWargear from "./vendor/40k-compactor/skippable_wargear.json"; const GOOGLE_CLIENT_ID = "698705216189-rqbg3f7vlosi3077c3n410a7lpmsn16r.apps.googleusercontent.com"; @@ -25,14 +28,12 @@ const SHEETS_API = "https://sheets.googleapis.com/v4/spreadsheets"; /* ---- List cleaning (vendored 40k-compactor — see vendor/40k-compactor/NOTICE.md) ---- */ -// detectFormat() only distinguishes the 11th-edition dialects this parser -// set understands; NR_TOURNAMENT isn't separately parseable from the -// functions this tool exports (no dedicated parser in the vendored code), -// so it falls back to the generic V11 parser same as an unrecognized format. const PARSERS = { GW_APP_V11: parseGwAppV11, WAR_ORGAN_V11: parseWarOrganV11, V11_GENERIC: parseV11List, + NR_GW: parseNRGW, + NR_TOURNAMENT: parseNRTournament, }; // Returns cleaned, sheet-friendly (plain, not Discord-markdown) list text, @@ -125,10 +126,17 @@ const batchUpdate = (token, fileId, requests) => body: JSON.stringify({ requests }), }); +// RAW, not USER_ENTERED: everything written here (names, factions, win +// rates, list text) is a literal label, never meant to be interpreted. +// USER_ENTERED's smart-parsing was actively harmful — e.g. a blank name +// next to a win rate produced " (62.5%)", which Sheets read as accounting +// notation for a *negative* number and silently replaced the whole cell +// with e.g. "-62.50%"; list text starting with certain characters came +// back as "#ERROR!" for the same reason. const batchUpdateValues = (token, fileId, data) => googleFetch(`${SHEETS_API}/${fileId}/values:batchUpdate`, token, { method: "POST", - body: JSON.stringify({ valueInputOption: "USER_ENTERED", data }), + body: JSON.stringify({ valueInputOption: "RAW", data }), }); /* ---- Template layout ---- @@ -243,10 +251,14 @@ const a1 = (sheetTitle, col, row) => `'${sheetTitle.replace(/'/g, "''")}'!${colL /* ---- Row/column value builders ---- */ +// fullName() takes the nested BCP user object ({firstName, lastName}), not +// the roster/teamplayer entry itself — same convention main.js's own +// playerRowHTML() uses, matched here. +const nameOf = member => fullNameOf(member.user || {}) || member.user?.nickname || "Unknown"; + function playerLabel(member) { - const name = fullNameOf(member); const faction = member.faction?.name || "Unknown"; - return `${name} - ${faction}`; + return `${nameOf(member)} - ${faction}`; } // fullName/fmtWpct live in main.js today; passed in rather than duplicated. @@ -258,11 +270,10 @@ export function configureMatrixHelpers({ fullName, fmtWpct }) { } function opponentNameAndWinRate(member, careerRecords) { - const name = fullNameOf(member); const uid = member.userId || member.user?.id; const rec = careerRecords.get(uid); const wpct = rec ? fmtWpctOf(rec.w, rec.l, rec.t) : "—"; - return `${name} (${wpct})`; + return `${nameOf(member)} (${wpct})`; } /* ---- Orchestration ---- */