From 30276c7ad5c92b3fe48d8903b9740e0b3ba0c936 Mon Sep 17 00:00:00 2001 From: mandalore Date: Fri, 4 Sep 2026 18:26:57 -0500 Subject: [PATCH] Fix three matrix generation bugs found in a real generated sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnosed directly against a real generated spreadsheet the user shared, rather than guessing: - Player/opponent names came out blank ("- Necrons" instead of "John Smith - Necrons"): fullName() takes the nested BCP user object ({firstName, lastName}), not the roster/teamplayer entry itself — same convention main.js's own playerRowHTML() already follows, now matched here via a shared nameOf() helper (with the same nickname fallback playerRowHTML uses too). - That cascaded into a worse, harder-to-spot bug: with the name blank, a win-rate cell became literally " (62.5%)". Writes used valueInputOption: USER_ENTERED, which smart-parses values — Sheets read "(62.5%)" as accounting notation for a negative number and silently replaced the cell with e.g. "-62.50%" instead of the intended text. Same setting explains the "#ERROR!" cells: some raw list text got parsed as a formula. Switched to RAW, which stores exactly what's sent — correct for the plain descriptive labels this writes, never intended to be interpreted. - Lists with a New Recruit "+++...+++" header came through raw, uncleaned: parseNRGW/parseNRTournament were vendored (needed for NR_GW/NR_TOURMENT detectFormat() results) but never actually wired into the format dispatch table, and aren't re-exported from the package's own index.js (only from the internal modules/parsers.js, which is now imported directly). Verified against the tool's own NR-GW/NR-Tournament sample fixtures with real Node before committing — both now correctly detected and cleaned. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SBzcNkW7JcYipnAX6HfgmK --- sites/scouting/src/matrix.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) 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 ---- */