From 43f807d3ab9b7fe36efb4002e7d8dcaf82cef03d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:39:17 +0000 Subject: [PATCH 1/7] Initial plan From f78834f163a579d1995486dedfb2511c84aafd71 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:42:09 +0000 Subject: [PATCH 2/7] Keep multiplayer chat messages pinned at top Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com> --- client/play/mp/MultiplayerTossupBonusClient.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/client/play/mp/MultiplayerTossupBonusClient.js b/client/play/mp/MultiplayerTossupBonusClient.js index d7d49e0ec..ac5d84c43 100644 --- a/client/play/mp/MultiplayerTossupBonusClient.js +++ b/client/play/mp/MultiplayerTossupBonusClient.js @@ -117,6 +117,11 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass this.sortPlayerListGroup(); } + endCurrentTossup ({ starred, tossup }) { + super.endCurrentTossup({ starred, tossup }); + moveChatMessagesToTop(); + } + confirmBan ({ targetId, targetUsername }) { if (targetId === this.USER_ID) { showAlert('You were banned from this room by the room owner.', () => window.location.replace('../')); @@ -282,6 +287,7 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass endCurrentBonus ({ bonus, lastPartRevealed, pointsPerPart, starred, teamId }) { super.endCurrentBonus({ bonus, starred }); + moveChatMessagesToTop(); if (lastPartRevealed) { const points = pointsPerPart.reduce((a, b) => a + b, 0); this.room.teams[teamId].bonusStats[points]++; @@ -433,6 +439,7 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass li.appendChild(i); document.getElementById('room-history').prepend(li); + moveChatMessagesToTop(); } logGiveAnswer ({ directive = null, givenAnswer, questionType, username }) { @@ -490,6 +497,7 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass } if (directive) { li.id = ''; } + moveChatMessagesToTop(); } lostBuzzerRace ({ username, userId }) { @@ -765,6 +773,14 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass } }; +function moveChatMessagesToTop () { + const roomHistory = document.getElementById('room-history'); + const chatMessages = Array.from(roomHistory.getElementsByClassName('chat-message')); + chatMessages.reverse().forEach(chatMessage => { + roomHistory.prepend(chatMessage); + }); +} + function attachEventListeners (room, socket, client) { document.getElementById('toggle-distraction-free-mode').addEventListener('change', (event) => { client.distractionFreeMode = event.target.checked; From 1bd1d8558fac62c7e7a3d1642c2486ea93fadcb2 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Thu, 10 Sep 2026 23:40:36 -0400 Subject: [PATCH 3/7] Refactor mixin abstract methods and add generic typing (#578) * Refactor mixin abstract methods and add generic typing * remove comments --- client/play/BonusClient.js | 4 ++ client/play/TossupClient.js | 4 ++ client/play/bonuses/SoloBonusRoom.js | 12 ++-- .../play/mp/MultiplayerTossupBonusClient.js | 4 ++ client/play/tossups/SoloTossupRoom.js | 12 ++-- .../multiplayer/ServerMultiplayerRoomMixin.js | 21 ++++--- shared/BonusRoom.js | 4 ++ shared/QuestionRoom.js | 62 ++++++++++++++++--- shared/TossupRoom.js | 4 ++ 9 files changed, 99 insertions(+), 28 deletions(-) diff --git a/client/play/BonusClient.js b/client/play/BonusClient.js index 3e65fc65e..548460a1c 100644 --- a/client/play/BonusClient.js +++ b/client/play/BonusClient.js @@ -2,6 +2,10 @@ import addBonusGameCard from './bonuses/add-bonus-game-card.js'; import QuestionClient from './QuestionClient.js'; import { MODE_ENUM } from '../../shared/constants.js'; +/** + * @template {typeof QuestionClient} TBase + * @param {TBase} ClientClass + */ export const BonusClientMixin = (ClientClass) => class extends ClientClass { constructor (room, userId, socket) { super(room, userId, socket); diff --git a/client/play/TossupClient.js b/client/play/TossupClient.js index 91a2e74a2..9a7181321 100644 --- a/client/play/TossupClient.js +++ b/client/play/TossupClient.js @@ -3,6 +3,10 @@ import QuestionClient from './QuestionClient.js'; import audio from './audio.js'; import { MODE_ENUM } from '../../shared/constants.js'; +/** + * @template {typeof QuestionClient} TBase + * @param {TBase} ClientClass + */ export const TossupClientMixin = (ClientClass) => class extends ClientClass { constructor (room, userId, socket) { super(room, userId, socket); diff --git a/client/play/bonuses/SoloBonusRoom.js b/client/play/bonuses/SoloBonusRoom.js index bd2491ed5..0fd4bdea3 100644 --- a/client/play/bonuses/SoloBonusRoom.js +++ b/client/play/bonuses/SoloBonusRoom.js @@ -27,15 +27,15 @@ async function getRandomStarredBonus () { } export default class SoloBonusRoom extends BonusRoom { + checkAnswer = api.checkAnswer; + getPacket = getPacket; + getPacketCount = api.getNumPackets; + getRandomBonuses = api.getRandomBonus; + getStarredBonus = getRandomStarredBonus; + constructor (name, categoryManager) { super(name, categoryManager, ['bonuses']); - this.checkAnswer = api.checkAnswer; - this.getRandomBonuses = api.getRandomBonus; - this.getPacket = getPacket; - this.getStarredBonus = getRandomStarredBonus; - this.getPacketCount = api.getNumPackets; - this.settings = { ...this.settings, skip: true, diff --git a/client/play/mp/MultiplayerTossupBonusClient.js b/client/play/mp/MultiplayerTossupBonusClient.js index d7d49e0ec..b47ccedc2 100644 --- a/client/play/mp/MultiplayerTossupBonusClient.js +++ b/client/play/mp/MultiplayerTossupBonusClient.js @@ -7,6 +7,10 @@ import upsertPlayerItem from '../upsert-player-item.js'; import { setYear } from '../year-slider.js'; import { showAlert } from './alert.js'; +/** + * @template {typeof TossupBonusClient} TBase + * @param {TBase} ClientClass + */ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass { constructor (room, userId, socket) { super(room, userId, socket); diff --git a/client/play/tossups/SoloTossupRoom.js b/client/play/tossups/SoloTossupRoom.js index 90f3a6c61..b0973035d 100644 --- a/client/play/tossups/SoloTossupRoom.js +++ b/client/play/tossups/SoloTossupRoom.js @@ -28,6 +28,12 @@ async function getPacket ({ setName, packetNumber }) { } export default class SoloTossupRoom extends TossupRoom { + checkAnswer = api.checkAnswer; + getPacket = getPacket; + getPacketCount = api.getNumPackets; + getRandomTossups = async (args) => await api.getRandomTossup({ ...args }); + getStarredTossup = getStarredTossup; + constructor (name, categoryManager) { super(name, categoryManager, ['tossups']); @@ -38,12 +44,6 @@ export default class SoloTossupRoom extends TossupRoom { showHistory: true, typeToAnswer: true }; - - this.checkAnswer = api.checkAnswer; - this.getRandomTossups = async (args) => await api.getRandomTossup({ ...args }); - this.getPacket = getPacket; - this.getStarredTossup = getStarredTossup; - this.getPacketCount = api.getNumPackets; } async message ({ userId, username }, message) { diff --git a/server/multiplayer/ServerMultiplayerRoomMixin.js b/server/multiplayer/ServerMultiplayerRoomMixin.js index 497909849..72bbb5a0f 100644 --- a/server/multiplayer/ServerMultiplayerRoomMixin.js +++ b/server/multiplayer/ServerMultiplayerRoomMixin.js @@ -4,9 +4,11 @@ import { HEADER, ENDC, OKCYAN, OKBLUE } from '../bcolors.js'; import isAppropriateString from '../moderation/is-appropriate-string.js'; import { MODE_ENUM, QUESTION_TYPE_ENUM, TOSSUP_PROGRESS_ENUM } from '../../shared/constants.js'; import insertTokensIntoHTML from '../../shared/insert-tokens-into-html.js'; -// import TossupRoom from '../../shared/TossupRoom.js'; import RateLimit from '../RateLimit.js'; +// eslint-disable-next-line no-unused-vars +import QuestionRoom from '../../shared/QuestionRoom.js'; + import getRandomTossups from '../../database/qbreader/get-random-tossups.js'; import getRandomBonuses from '../../database/qbreader/get-random-bonuses.js'; import getPacket from '../../database/qbreader/get-packet.js'; @@ -18,19 +20,22 @@ import Team from '../../shared/Team.js'; const BAN_DURATION = 1000 * 60 * 30; // 30 minutes +/** + * @template {typeof QuestionRoom} TBase + * @param {TBase} RoomClass + */ const ServerMultiplayerRoomMixin = (RoomClass) => class extends RoomClass { + checkAnswer = checkAnswer; + getPacket = getPacket; + getPacketCount = getNumPackets; + getRandomBonuses = getRandomBonuses; + getRandomTossups = getRandomTossups; + constructor (name, ownerId, isPermanent, categoryManager, supportedQuestionTypes, isVerified = false) { super(name, categoryManager, supportedQuestionTypes); this.ownerId = ownerId; this.isPermanent = isPermanent; this.isVerified = isVerified; - this.checkAnswer = checkAnswer; - this.getPacketCount = getNumPackets; - - this.getRandomTossups = getRandomTossups; - this.getRandomBonuses = getRandomBonuses; - - this.getPacket = getPacket; this.bannedUserList = new Map(); this.kickedUserList = new Map(); this.votekickList = []; diff --git a/shared/BonusRoom.js b/shared/BonusRoom.js index a339e5e7b..c0bde51bc 100644 --- a/shared/BonusRoom.js +++ b/shared/BonusRoom.js @@ -1,6 +1,10 @@ import { ANSWER_TIME_LIMIT, BONUS_PROGRESS_ENUM, MODE_ENUM } from './constants.js'; import QuestionRoom from './QuestionRoom.js'; +/** + * @template {typeof QuestionRoom} TBase + * @param {TBase} QuestionRoomClass + */ export const BonusRoomMixin = (QuestionRoomClass) => class extends QuestionRoomClass { constructor (name, categoryManager, supportedQuestionTypes = ['bonuses']) { super(name, categoryManager, supportedQuestionTypes); diff --git a/shared/QuestionRoom.js b/shared/QuestionRoom.js index 365667bb4..8fc949f4b 100644 --- a/shared/QuestionRoom.js +++ b/shared/QuestionRoom.js @@ -3,6 +3,9 @@ import { DEFAULT_MIN_YEAR, DEFAULT_MAX_YEAR, MODE_ENUM } from './constants.js'; import CategoryManager from './category-manager.js'; // eslint-disable-line no-unused-vars import Room from './Room.js'; +// eslint-disable-next-line no-unused-vars +import * as types from '../types.js'; + export default class QuestionRoom extends Room { /** * @param {*} name @@ -12,14 +15,6 @@ export default class QuestionRoom extends Room { constructor (name, categoryManager, supportedQuestionTypes) { super(name); - this.checkAnswer = function checkAnswer (answerline, givenAnswer, strictness = 7) { throw new Error('Not implemented'); }; - this.getRandomBonuses = async function getRandomBonuses (args) { throw new Error('Not implemented'); }; - this.getRandomTossups = async function getRandomTossups (args) { throw new Error('Not implemented'); }; - this.getPacket = async function getPacket (args) { throw new Error('Not implemented'); }; - this.getPacketCount = async function getPacketCount (setName) { throw new Error('Not implemented'); }; - this.getStarredTossup = async function getStarredTossup () { throw new Error('Not implemented'); }; - this.getStarredBonus = async function getStarredBonus () { throw new Error('Not implemented'); }; - if (!Array.isArray(supportedQuestionTypes) || supportedQuestionTypes.length === 0) { throw new Error('supportedQuestionTypes must be a non-empty array'); } @@ -126,6 +121,15 @@ export default class QuestionRoom extends Room { } } + /** + * @abstract + * @param {string} answerline + * @param {string} givenAnswer + * @param {number} [strictness] + * @returns {{directive: 'accept' | 'reject' | 'prompt', directedPrompt?: string}} + */ + checkAnswer (answerline, givenAnswer, strictness = 7) { throw new Error('Not implemented'); } + async getNextQuestion (questionType) { if (!this.supportedQuestionTypes.includes(questionType)) { return; } this.queryingQuestion = true; @@ -184,10 +188,52 @@ export default class QuestionRoom extends Room { return this.localPacket[questionType].shift(); } + /** + * @abstract + * @param {object} args + * @param {string} args.setName + * @param {number} args.packetNumber - one-indexed packet number + * @returns {Promise<{tossups?: types.Tossup[], bonuses?: types.Bonus[]}>} + */ + async getPacket (args) { throw new Error('Not implemented'); } + + /** + * @abstract + * @param {string} setName + * @returns {Promise} + */ + async getPacketCount (setName) { throw new Error('Not implemented'); } + + /** + * @abstract + * @param {object} args + * @returns {Promise} + */ + async getRandomBonuses (args) { throw new Error('Not implemented'); } + getRandomQuestions (questionType, query) { return questionType === 'tossups' ? this.getRandomTossups(query) : this.getRandomBonuses(query); } + /** + * @abstract + * @param {object} args + * @returns {Promise} + */ + async getRandomTossups (args) { throw new Error('Not implemented'); } + + /** + * @abstract + * @returns {Promise} + */ + async getStarredBonus () { throw new Error('Not implemented'); } + + /** + * @abstract + * @returns {Promise} + */ + async getStarredTossup () { throw new Error('Not implemented'); } + setCategories ({ username }, { categories, subcategories, alternateSubcategories, percentView, categoryPercents }) { if (!Array.isArray(categories)) { return; } if (!Array.isArray(subcategories)) { return; } diff --git a/shared/TossupRoom.js b/shared/TossupRoom.js index cd477c0f7..b235b130c 100644 --- a/shared/TossupRoom.js +++ b/shared/TossupRoom.js @@ -2,6 +2,10 @@ import { ANSWER_TIME_LIMIT, DEAD_TIME_LIMIT, MODE_ENUM, TOSSUP_PROGRESS_ENUM } f import insertTokensIntoHTML from './insert-tokens-into-html.js'; import QuestionRoom from './QuestionRoom.js'; +/** + * @template {typeof QuestionRoom} TBase + * @param {TBase} QuestionRoomClass + */ export const TossupRoomMixin = (QuestionRoomClass) => class extends QuestionRoomClass { constructor (name, categoryManager, supportedQuestionTypes = ['tossup']) { super(name, categoryManager, supportedQuestionTypes); From 8e5432e81661772dc1500afbed776b9da36f3968 Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Thu, 10 Sep 2026 23:42:14 -0400 Subject: [PATCH 4/7] run `npm audit fix` --- package-lock.json | 228 ++++++++++++++++++++++++++-------------------- 1 file changed, 129 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0520cf66b..9557af9e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1525,9 +1525,9 @@ "dev": true }, "node_modules/baseline-browser-mapping": { - "version": "2.10.37", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", - "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "version": "2.11.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz", + "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1572,9 +1572,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -1585,7 +1585,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.15.1", + "qs": "~6.16.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -1615,6 +1615,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/body-parser/node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -1683,9 +1699,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ { @@ -1703,11 +1719,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" @@ -1826,9 +1842,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -2336,9 +2352,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "version": "1.5.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", + "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", "dev": true, "license": "ISC" }, @@ -4252,9 +4268,9 @@ "dev": true }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -4748,9 +4764,9 @@ } }, "node_modules/morgan": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", - "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.12.0.tgz", + "integrity": "sha512-OHpTRQwn2ezasILW8iKe+Yww1XsfWsZIpUOLF7RDb2g5GwO3trPaRwi7+8BDiJ7HFx2Kg2mfUdCBcVhwYlOz2g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", @@ -4794,16 +4810,19 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/nodemailer": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", - "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.1.1.tgz", + "integrity": "sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -5349,12 +5368,13 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -5942,14 +5962,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -5961,13 +5981,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -6600,9 +6620,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -8154,9 +8174,9 @@ "dev": true }, "baseline-browser-mapping": { - "version": "2.10.37", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", - "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "version": "2.11.22", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.22.tgz", + "integrity": "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==", "dev": true }, "basic-auth": { @@ -8189,9 +8209,9 @@ "dev": true }, "body-parser": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", - "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "version": "1.20.8", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz", + "integrity": "sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==", "requires": { "bytes": "~3.1.2", "content-type": "~1.0.5", @@ -8201,7 +8221,7 @@ "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", - "qs": "~6.15.1", + "qs": "~6.16.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" @@ -8219,6 +8239,15 @@ "toidentifier": "~1.0.1" } }, + "qs": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "requires": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + } + }, "statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -8257,16 +8286,16 @@ } }, "browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "requires": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" } }, "bson": { @@ -8345,9 +8374,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001799", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", - "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true }, "chalk": { @@ -8705,9 +8734,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "version": "1.5.427", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.427.tgz", + "integrity": "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw==", "dev": true }, "emoji-regex": { @@ -10036,9 +10065,9 @@ "dev": true }, "js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -10364,9 +10393,9 @@ } }, "morgan": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", - "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.12.0.tgz", + "integrity": "sha512-OHpTRQwn2ezasILW8iKe+Yww1XsfWsZIpUOLF7RDb2g5GwO3trPaRwi7+8BDiJ7HFx2Kg2mfUdCBcVhwYlOz2g==", "requires": { "basic-auth": "~2.0.1", "debug": "2.6.9", @@ -10398,15 +10427,15 @@ "dev": true }, "node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.55", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.55.tgz", + "integrity": "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==", "dev": true }, "nodemailer": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", - "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==" + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.1.1.tgz", + "integrity": "sha512-izw9mVKFix6YSnC9eLgV6g1opl9DUlRio9ZNcq+Wu9Ujn2UwF+8Nl0B8nz22kEC+CTZCvinkxwJ0DeFbb6NwcQ==" }, "normalize-path": { "version": "3.0.0", @@ -10793,11 +10822,12 @@ } }, "qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "requires": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" } }, "queue-microtask": { @@ -11179,24 +11209,24 @@ "dev": true }, "side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "requires": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "requires": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" } }, "side-channel-map": { @@ -11607,9 +11637,9 @@ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "requires": { "escalade": "^3.2.0", From 73025cd5386cad7a3c959dc9adae90685592c31c Mon Sep 17 00:00:00 2001 From: Geoffrey Wu Date: Thu, 10 Sep 2026 23:52:03 -0400 Subject: [PATCH 5/7] fix ObjectId import for webpack and browser --- types.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/types.js b/types.js index 98b917fc2..d88f7ee86 100644 --- a/types.js +++ b/types.js @@ -1,5 +1,4 @@ -// eslint-disable-next-line no-unused-vars -import { ObjectId } from 'mongodb'; +/** @typedef {import('mongodb').ObjectId} ObjectId */ /** * @typedef {object} Question From cea3ce94394558e467f709c2347e97dab4ab4338 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:39:17 +0000 Subject: [PATCH 6/7] Initial plan From 8ea29d07415f8f104e67fc4f240d1acf12090e67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 11 Sep 2026 01:42:09 +0000 Subject: [PATCH 7/7] Keep multiplayer chat messages pinned at top Co-authored-by: geoffrey-wu <42471355+geoffrey-wu@users.noreply.github.com> --- client/play/mp/MultiplayerTossupBonusClient.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/client/play/mp/MultiplayerTossupBonusClient.js b/client/play/mp/MultiplayerTossupBonusClient.js index b47ccedc2..1f86a8c98 100644 --- a/client/play/mp/MultiplayerTossupBonusClient.js +++ b/client/play/mp/MultiplayerTossupBonusClient.js @@ -121,6 +121,11 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass this.sortPlayerListGroup(); } + endCurrentTossup ({ starred, tossup }) { + super.endCurrentTossup({ starred, tossup }); + moveChatMessagesToTop(); + } + confirmBan ({ targetId, targetUsername }) { if (targetId === this.USER_ID) { showAlert('You were banned from this room by the room owner.', () => window.location.replace('../')); @@ -286,6 +291,7 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass endCurrentBonus ({ bonus, lastPartRevealed, pointsPerPart, starred, teamId }) { super.endCurrentBonus({ bonus, starred }); + moveChatMessagesToTop(); if (lastPartRevealed) { const points = pointsPerPart.reduce((a, b) => a + b, 0); this.room.teams[teamId].bonusStats[points]++; @@ -437,6 +443,7 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass li.appendChild(i); document.getElementById('room-history').prepend(li); + moveChatMessagesToTop(); } logGiveAnswer ({ directive = null, givenAnswer, questionType, username }) { @@ -494,6 +501,7 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass } if (directive) { li.id = ''; } + moveChatMessagesToTop(); } lostBuzzerRace ({ username, userId }) { @@ -769,6 +777,14 @@ export const MultiplayerClientMixin = (ClientClass) => class extends ClientClass } }; +function moveChatMessagesToTop () { + const roomHistory = document.getElementById('room-history'); + const chatMessages = Array.from(roomHistory.getElementsByClassName('chat-message')); + chatMessages.reverse().forEach(chatMessage => { + roomHistory.prepend(chatMessage); + }); +} + function attachEventListeners (room, socket, client) { document.getElementById('toggle-distraction-free-mode').addEventListener('change', (event) => { client.distractionFreeMode = event.target.checked;