From 15c46ad6ba3605433202c83711ac80959c7fa94d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 01:44:35 +0000 Subject: [PATCH 01/11] Bump date-fns from 2.30.0 to 4.4.0 Bumps [date-fns](https://github.com/date-fns/date-fns) from 2.30.0 to 4.4.0. - [Release notes](https://github.com/date-fns/date-fns/releases) - [Commits](https://github.com/date-fns/date-fns/compare/v2.30.0...v4.4.0) --- updated-dependencies: - dependency-name: date-fns dependency-version: 4.4.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- client/package.json | 2 +- yarn.lock | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/client/package.json b/client/package.json index 9cdc5f6..78ea729 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,7 @@ "@mui/material": "^6.5.0", "@mui/styles": "^6.5.0", "clsx": "1.1.0", - "date-fns": "^2.16.1", + "date-fns": "^4.4.0", "history": "4.10.1", "material-ui-confirm": "^4.0.0", "prop-types": "15.8.1", diff --git a/yarn.lock b/yarn.lock index f270f0b..26a464e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -944,7 +944,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.29.7" "@babel/plugin-transform-react-pure-annotations" "^7.29.7" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.26.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.26.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768" integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== @@ -4053,12 +4053,10 @@ data-view-byte-offset@^1.0.1: es-errors "^1.3.0" is-data-view "^1.0.1" -date-fns@^2.16.1: - version "2.30.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" - integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== - dependencies: - "@babel/runtime" "^7.21.0" +date-fns@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.4.0.tgz#806539edf45c616b2b76b5f78b88c56ed3c7e036" + integrity sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w== dayjs@^1.10.4: version "1.11.21" From 224f515875a5d3b1efb03d2e5284d6192cffa5b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:23:53 +0000 Subject: [PATCH 02/11] Bump nodemon from 2.0.3 to 3.1.14 Bumps [nodemon](https://github.com/remy/nodemon) from 2.0.3 to 3.1.14. - [Release notes](https://github.com/remy/nodemon/releases) - [Commits](https://github.com/remy/nodemon/compare/v2.0.3...v3.1.14) --- updated-dependencies: - dependency-name: nodemon dependency-version: 3.1.14 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- server/package.json | 2 +- yarn.lock | 532 +++++++++++--------------------------------- 2 files changed, 129 insertions(+), 405 deletions(-) diff --git a/server/package.json b/server/package.json index 3490ae7..c98d7c2 100644 --- a/server/package.json +++ b/server/package.json @@ -53,6 +53,6 @@ "globals": "^17.7.0", "jest": "^30.4.2", "kill-port": "1.6.0", - "nodemon": "2.0.3" + "nodemon": "3.1.14" } } diff --git a/yarn.lock b/yarn.lock index 727f4f8..2abae5a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2238,11 +2238,6 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.50.tgz#6aaa311c3a90239f3ec21771ca7432a45131db99" integrity sha512-ydBWw0G6WFwWHzh9RK4B5c690UkreOG0llq0r+DaI7LgKgxigf8mhHzIPI3S0850g1BPkq/zpuCfrq4QFgUlTQ== -"@sindresorhus/is@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" - integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== - "@sinonjs/commons@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" @@ -2276,13 +2271,6 @@ resolved "https://registry.yarnpkg.com/@standard-schema/spec/-/spec-1.1.0.tgz#a79b55dbaf8604812f52d140b2c9ab41bc150bb8" integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w== -"@szmarczak/http-timer@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" - integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== - dependencies: - defer-to-connect "^1.0.1" - "@testing-library/dom@^10.4.1": version "10.4.1" resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95" @@ -2819,13 +2807,6 @@ ajv@^8.12.0, ajv@^8.6.0: json-schema-traverse "^1.0.0" require-from-string "^2.0.2" -ansi-align@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59" - integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== - dependencies: - string-width "^4.1.0" - ansi-escapes@^4.2.1, ansi-escapes@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -3301,20 +3282,6 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -boxen@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" - integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== - dependencies: - ansi-align "^3.0.0" - camelcase "^5.3.1" - chalk "^3.0.0" - cli-boxes "^2.2.0" - string-width "^4.1.0" - term-size "^2.1.0" - type-fest "^0.8.1" - widest-line "^3.1.0" - brace-expansion@^1.1.7: version "1.1.16" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.16.tgz#723d3a30c0558c225abc9fc479a73e14e26c3c2f" @@ -3403,19 +3370,6 @@ c12@3.3.4: pkg-types "^2.3.0" rc9 "^3.0.1" -cacheable-request@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" - integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^3.0.0" - lowercase-keys "^2.0.0" - normalize-url "^4.1.0" - responselike "^1.0.2" - cachedir@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" @@ -3481,14 +3435,6 @@ chalk@^2.1.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -3529,7 +3475,32 @@ chart.js@4.5.1: dependencies: "@kurkle/color" "^0.3.0" -chokidar@^3.2.2: +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@1.0.0-rc.12: + version "1.0.0-rc.12" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" + integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.0.1" + htmlparser2 "^8.0.1" + parse5 "^7.0.0" + parse5-htmlparser2-tree-adapter "^7.0.0" + +chokidar@^3.5.2: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -3551,11 +3522,6 @@ chokidar@^5.0.0: dependencies: readdirp "^5.0.0" -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== - ci-info@^3.2.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" @@ -3571,11 +3537,6 @@ cjs-module-lexer@^2.1.0: resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz#b3ca5101843389259ade7d88c77bd06ce55849ca" integrity sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ== -cli-boxes@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f" - integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== - cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -3621,13 +3582,6 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -clone-response@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" - integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== - dependencies: - mimic-response "^1.0.0" - clsx@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" @@ -3763,18 +3717,6 @@ confbox@^0.2.4: resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.4.tgz#592e7be71f882a4a874e3c88f0ac1ef6f7da1ce5" integrity sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ== -configstore@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" - integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== - dependencies: - dot-prop "^5.2.0" - graceful-fs "^4.1.2" - make-dir "^3.0.0" - unique-string "^2.0.0" - write-file-atomic "^3.0.0" - xdg-basedir "^4.0.0" - confusing-browser-globals@^1.0.9: version "1.0.11" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" @@ -3881,6 +3823,17 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== +css-select@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e" + integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + css-vendor@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" @@ -3889,6 +3842,11 @@ css-vendor@^2.0.8: "@babel/runtime" "^7.8.3" is-in-browser "^1.0.2" +css-what@^6.1.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" + integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== + css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -4031,14 +3989,14 @@ debug@2.6.9, debug@^2.6.9, debug@~2.6.9: dependencies: ms "2.0.0" -debug@4, debug@4.4.3, debug@4.x, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.4.3, debug@~4.4.1: +debug@4, debug@4.4.3, debug@4.x, debug@^4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.6, debug@^4.4.3, debug@~4.4.1: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== dependencies: ms "^2.1.3" -debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -4057,23 +4015,11 @@ decimal.js@^10.5.0: resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== -decompress-response@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" - integrity sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA== - dependencies: - mimic-response "^1.0.0" - dedent@^1.6.0: version "1.7.2" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.2.tgz#34e2264ab538301e27cf7b07bf2369c19baa8dd9" integrity sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA== -deep-extend@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" - integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== - deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -4089,11 +4035,6 @@ deepmerge@^4.2.2, deepmerge@^4.3.1: resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -defer-to-connect@^1.0.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" - integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== - define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -4211,12 +4152,35 @@ dom-helpers@^5.0.1, dom-helpers@^5.1.3: "@babel/runtime" "^7.8.7" csstype "^3.0.2" -dot-prop@^5.2.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" - integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== dependencies: - is-obj "^2.0.0" + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" dotenv@^17.3.1: version "17.4.2" @@ -4232,11 +4196,6 @@ dunder-proto@^1.0.0, dunder-proto@^1.0.1: es-errors "^1.3.0" gopd "^1.2.0" -duplexer3@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.5.tgz#0b5e4d7bad5de8901ea4440624c8e1d20099217e" - integrity sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA== - eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -4356,6 +4315,11 @@ engine.io@~6.6.0: engine.io-parser "~5.2.1" ws "~8.21.0" +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + entities@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" @@ -4526,11 +4490,6 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-goat@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" - integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== - escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -5334,14 +5293,7 @@ get-proto@^1.0.0, get-proto@^1.0.1: dunder-proto "^1.0.1" es-object-atoms "^1.0.0" -get-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" - integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== - dependencies: - pump "^3.0.0" - -get-stream@^5.0.0, get-stream@^5.1.0: +get-stream@^5.0.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== @@ -5434,13 +5386,6 @@ glob@^7.1.3, glob@^7.1.4: once "^1.3.0" path-is-absolute "^1.0.0" -global-dirs@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.1.0.tgz#e9046a49c806ff04d6c1825e196c8f0091e8df4d" - integrity sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ== - dependencies: - ini "1.3.7" - global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" @@ -5478,23 +5423,6 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -got@^9.6.0: - version "9.6.0" - resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" - integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== - dependencies: - "@sindresorhus/is" "^0.14.0" - "@szmarczak/http-timer" "^1.1.2" - cacheable-request "^6.0.0" - decompress-response "^3.3.0" - duplexer3 "^0.1.4" - get-stream "^4.1.0" - lowercase-keys "^1.0.1" - mimic-response "^1.0.1" - p-cancelable "^1.0.0" - to-readable-stream "^1.0.0" - url-parse-lax "^3.0.0" - graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -5556,11 +5484,6 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -has-yarn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" - integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== - has@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" @@ -5634,10 +5557,15 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -http-cache-semantics@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" - integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== +htmlparser2@^8.0.1: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" http-errors@~2.0.0, http-errors@~2.0.1: version "2.0.1" @@ -5761,11 +5689,6 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-lazy@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" - integrity sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A== - import-local@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" @@ -5797,21 +5720,11 @@ inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@1.3.7: - version "1.3.7" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84" - integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ== - ini@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -ini@~1.3.0: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" @@ -5943,13 +5856,6 @@ is-callable@^1.2.7: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-ci@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" - integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== - dependencies: - ci-info "^2.0.0" - is-core-module@^2.16.1, is-core-module@^2.16.2: version "2.16.2" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.2.tgz#3e07450a8080ebce3fbf0cac494f4d2ab324e082" @@ -6048,14 +5954,6 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g== -is-installed-globally@^0.3.1: - version "0.3.2" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141" - integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g== - dependencies: - global-dirs "^2.0.1" - is-path-inside "^3.0.1" - is-installed-globally@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -6079,11 +5977,6 @@ is-negative-zero@^2.0.3: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== -is-npm@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" - integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== - is-number-object@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" @@ -6102,12 +5995,7 @@ is-obj@^1.0.1: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== -is-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" - integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== - -is-path-inside@^3.0.1, is-path-inside@^3.0.2: +is-path-inside@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== @@ -6188,7 +6076,7 @@ is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: dependencies: which-typed-array "^1.1.16" -is-typedarray@^1.0.0, is-typedarray@~1.0.0: +is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== @@ -6218,11 +6106,6 @@ is-weakset@^2.0.3: call-bound "^1.0.3" get-intrinsic "^1.2.6" -is-yarn-global@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" - integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -6805,11 +6688,6 @@ jsesc@^3.0.2, jsesc@~3.1.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== -json-buffer@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" - integrity sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ== - json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -6966,13 +6844,6 @@ kareem@2.6.3: resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.6.3.tgz#23168ec8ffb6c1abfd31b7169a6fb1dd285992ac" integrity sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q== -keyv@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" - integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== - dependencies: - json-buffer "3.0.0" - keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -7014,13 +6885,6 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" -latest-version@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" - integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== - dependencies: - package-json "^6.3.0" - leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -7233,16 +7097,6 @@ loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4 dependencies: js-tokens "^3.0.0 || ^4.0.0" -lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" - integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - lru-cache@^10.2.0, lru-cache@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" @@ -7284,13 +7138,6 @@ magic-string@^0.30.21, magic-string@^0.30.3: dependencies: "@jridgewell/sourcemap-codec" "^1.5.5" -make-dir@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - make-dir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" @@ -7430,11 +7277,6 @@ mimic-function@^5.0.0: resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== -mimic-response@^1.0.0, mimic-response@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - min-indent@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" @@ -7445,7 +7287,7 @@ minimalistic-assert@^1.0.0: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@^10.1.1: +minimatch@^10.1.1, minimatch@^10.2.1: version "10.2.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== @@ -7644,21 +7486,21 @@ node-releases@^2.0.50: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.51.tgz#cdc08433577f5b32ad01694481726e22eeb54aef" integrity sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ== -nodemon@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.3.tgz#e9c64df8740ceaef1cb00e1f3da57c0a93ef3714" - integrity sha512-lLQLPS90Lqwc99IHe0U94rDgvjo+G9I4uEIxRG3evSLROcqQ9hwc0AxlSHKS4T1JW/IMj/7N5mthiN58NL/5kw== +nodemon@3.1.14: + version "3.1.14" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.14.tgz#8487ca379c515301d221ec007f27f24ecafa2b51" + integrity sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw== dependencies: - chokidar "^3.2.2" - debug "^3.2.6" + chokidar "^3.5.2" + debug "^4" ignore-by-default "^1.0.1" - minimatch "^3.0.4" - pstree.remy "^1.1.7" - semver "^5.7.1" + minimatch "^10.2.1" + pstree.remy "^1.1.8" + semver "^7.5.3" + simple-update-notifier "^2.0.0" supports-color "^5.5.0" touch "^3.1.0" - undefsafe "^2.0.2" - update-notifier "^4.0.0" + undefsafe "^2.0.5" normalize-package-data@^2.3.2: version "2.5.0" @@ -7675,11 +7517,6 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -normalize-url@^4.1.0: - version "4.5.1" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - notepack.io@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/notepack.io/-/notepack.io-3.0.1.tgz#2c2c9de1bd4e64a79d34e33c413081302a0d4019" @@ -7692,6 +7529,13 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + nwsapi@^2.2.16: version "2.2.24" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.24.tgz#f8927043d4c9b516abdebe804a32c8d1f9484d1f" @@ -7854,11 +7698,6 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" -p-cancelable@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" - integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== - p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" @@ -7916,16 +7755,6 @@ package-json-from-dist@^1.0.0: resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== -package-json@^6.3.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" - integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== - dependencies: - got "^9.6.0" - registry-auth-token "^4.0.0" - registry-url "^5.0.0" - semver "^6.2.0" - parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -7962,6 +7791,14 @@ parse-json@^5.0.0, parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz#b5a806548ed893a43e24ccb42fbb78069311e81b" + integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== + dependencies: + domhandler "^5.0.3" + parse5 "^7.0.0" + parse5@^7.0.0, parse5@^7.2.1: version "7.3.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05" @@ -8263,11 +8100,6 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== -prepend-http@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" - integrity sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA== - pretty-bytes@^5.3.0, pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -8366,7 +8198,7 @@ proxy-from-env@1.0.0: resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" integrity sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A== -pstree.remy@^1.1.7: +pstree.remy@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== @@ -8384,13 +8216,6 @@ punycode@^2.1.0, punycode@^2.3.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -pupa@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62" - integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== - dependencies: - escape-goat "^2.0.0" - pure-rand@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" @@ -8442,16 +8267,6 @@ rc9@^3.0.1: defu "^6.1.6" destr "^2.0.5" -rc@1.2.8, rc@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" - integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== - dependencies: - deep-extend "^0.6.0" - ini "~1.3.0" - minimist "^1.2.0" - strip-json-comments "~2.0.1" - react-cube-svg@^0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/react-cube-svg/-/react-cube-svg-0.2.3.tgz#05fabaf763a51b2ca040ad84491a6d0c981c77cf" @@ -8729,20 +8544,6 @@ regexpu-core@^6.3.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.2.1" -registry-auth-token@^4.0.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.2.tgz#f02d49c3668884612ca031419491a13539e21fac" - integrity sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg== - dependencies: - rc "1.2.8" - -registry-url@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" - integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== - dependencies: - rc "^1.2.8" - regjsgen@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" @@ -8840,13 +8641,6 @@ resolve@^2.0.0-next.5, resolve@^2.0.0-next.6: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -responselike@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" - integrity sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ== - dependencies: - lowercase-keys "^1.0.0" - rest-api-errors@^1.2.1: version "1.3.1" resolved "https://registry.yarnpkg.com/rest-api-errors/-/rest-api-errors-1.3.1.tgz#a206da021bcc908b716e2208063f09e052c49b40" @@ -9029,19 +8823,12 @@ scrambow@^1.8.1: dependencies: commander "^8.2.0" -semver-diff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" - integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== - dependencies: - semver "^6.3.0" - -"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.5.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.0.0, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: +semver@^6.1.2, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== @@ -9217,6 +9004,13 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +simple-update-notifier@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" + integrity sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w== + dependencies: + semver "^7.5.3" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -9463,7 +9257,7 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9643,11 +9437,6 @@ strip-json-comments@^3.0.1, strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strip-json-comments@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" - integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== - style-to-object@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46" @@ -9733,11 +9522,6 @@ tempy@^0.6.0: type-fest "^0.16.0" unique-string "^2.0.0" -term-size@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" - integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== - terser@^5.17.4: version "5.49.0" resolved "https://registry.yarnpkg.com/terser/-/terser-5.49.0.tgz#30b341fdf70cfc98486965125ae660fda8403670" @@ -9829,11 +9613,6 @@ tmpl@1.0.5: resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== -to-readable-stream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" - integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -10030,13 +9809,6 @@ typed-array-length@^1.0.7: possible-typed-array-names "^1.1.0" reflect.getprototypeof "^1.0.10" -typedarray-to-buffer@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" - integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== - dependencies: - is-typedarray "^1.0.0" - typescript@^4.9.4: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" @@ -10074,7 +9846,7 @@ unbox-primitive@^1.1.0: has-symbols "^1.1.0" which-boxed-primitive "^1.1.1" -undefsafe@^2.0.2: +undefsafe@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== @@ -10233,25 +10005,6 @@ update-browserslist-db@^1.2.3: escalade "^3.2.0" picocolors "^1.1.1" -update-notifier@^4.0.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" - integrity sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A== - dependencies: - boxen "^4.2.0" - chalk "^3.0.0" - configstore "^5.0.1" - has-yarn "^2.1.0" - import-lazy "^2.1.0" - is-ci "^2.0.0" - is-installed-globally "^0.3.1" - is-npm "^4.0.0" - is-yarn-global "^0.3.0" - latest-version "^5.0.0" - pupa "^2.0.1" - semver-diff "^3.1.1" - xdg-basedir "^4.0.0" - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -10259,13 +10012,6 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -url-parse-lax@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" - integrity sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ== - dependencies: - prepend-http "^2.0.0" - use-sync-external-store@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" @@ -10494,13 +10240,6 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -widest-line@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" - integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== - dependencies: - string-width "^4.0.0" - winston-transport@^4.3.0: version "4.9.0" resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.9.0.tgz#3bba345de10297654ea6f33519424560003b3bf9" @@ -10729,16 +10468,6 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -write-file-atomic@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" - integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== - dependencies: - imurmurhash "^0.1.4" - is-typedarray "^1.0.0" - signal-exit "^3.0.2" - typedarray-to-buffer "^3.1.5" - write-file-atomic@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" @@ -10759,11 +10488,6 @@ ws@^8.18.0, ws@~8.21.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951" integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== -xdg-basedir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" - integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== - xml-name-validator@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" From 44176ae583c3158caff2ff054725369404d73d7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 01:46:18 +0000 Subject: [PATCH 03/11] Bump clsx from 1.1.0 to 2.1.1 Bumps [clsx](https://github.com/lukeed/clsx) from 1.1.0 to 2.1.1. - [Release notes](https://github.com/lukeed/clsx/releases) - [Commits](https://github.com/lukeed/clsx/compare/v1.1.0...v2.1.1) --- updated-dependencies: - dependency-name: clsx dependency-version: 2.1.1 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- client/package.json | 4 ++-- yarn.lock | 13 ++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/client/package.json b/client/package.json index 78ea729..931e890 100644 --- a/client/package.json +++ b/client/package.json @@ -8,8 +8,8 @@ "@mui/icons-material": "^6.5.0", "@mui/material": "^6.5.0", "@mui/styles": "^6.5.0", - "clsx": "1.1.0", - "date-fns": "^4.4.0", + "clsx": "2.1.1", + "date-fns": "^4.4.0" "history": "4.10.1", "material-ui-confirm": "^4.0.0", "prop-types": "15.8.1", diff --git a/yarn.lock b/yarn.lock index 3fd7013..0a8688f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3582,21 +3582,16 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -clsx@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702" - integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA== +clsx@2.1.1, clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== clsx@^1.0.4: version "1.2.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== -clsx@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" - integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== - cluster-key-slot@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz#10ccb9ded0729464b6d2e7d714b100a2d1259d43" From 6e8cb700417af568d5ddd3940cb8bb5beeeba8e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:44:31 +0000 Subject: [PATCH 04/11] Bump eslint-plugin-import from 2.20.2 to 2.32.0 Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.20.2 to 2.32.0. - [Release notes](https://github.com/import-js/eslint-plugin-import/releases) - [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md) - [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.20.2...v2.32.0) --- updated-dependencies: - dependency-name: eslint-plugin-import dependency-version: 2.32.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- packages/scrambles/package.json | 2 +- yarn.lock | 194 ++------------------------------ 2 files changed, 13 insertions(+), 183 deletions(-) diff --git a/packages/scrambles/package.json b/packages/scrambles/package.json index 6a2c7aa..f541e84 100644 --- a/packages/scrambles/package.json +++ b/packages/scrambles/package.json @@ -24,7 +24,7 @@ "devDependencies": { "eslint": "6.8.0", "eslint-config-airbnb-base": "14.1.0", - "eslint-plugin-import": "2.20.2", + "eslint-plugin-import": "2.32.0", "jest": "^30.4.2" } } diff --git a/yarn.lock b/yarn.lock index 3fd7013..a54a25b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2910,7 +2910,7 @@ array-flatten@1.1.1: resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== -array-includes@^3.0.3, array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: +array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: version "3.1.9" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== @@ -2949,7 +2949,7 @@ array.prototype.findlastindex@^1.2.6: es-object-atoms "^1.1.1" es-shim-unscopables "^1.1.0" -array.prototype.flat@^1.2.1, array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== @@ -3730,11 +3730,6 @@ connect-mongo@^6.0.0: debug "^4.4.3" kruptein "3.0.8" -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" - integrity sha512-OKZnPGeMQy2RPaUIBPFFd71iNf4791H12MCRuVQDnzGRwCYNYmTDy5pdafo2SLAcEMKzTOQnLWG4QdcjeJUMEg== - content-disposition@~0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -3980,7 +3975,7 @@ dayjs@^1.10.4: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2" integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA== -debug@2.6.9, debug@^2.6.9, debug@~2.6.9: +debug@2.6.9, debug@~2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -4110,14 +4105,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - integrity sha512-lsGyRuYr4/PIB0txi+Fy2xOMI2dGaTguCaotzFGkVZuKR5usKfcRWIFKNM3QNrU7hh/+w2bwTW+ZeXPK5l8uVg== - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -4338,7 +4325,7 @@ environment@^1.0.0: resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.4" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== @@ -4517,7 +4504,7 @@ eslint-config-airbnb-base@14.1.0: object.assign "^4.1.0" object.entries "^1.1.1" -eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.9: +eslint-import-resolver-node@^0.3.9: version "0.3.10" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz#84ce3005abfc300588cf23bbac1aabec1fc6e8c1" integrity sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ== @@ -4526,32 +4513,14 @@ eslint-import-resolver-node@^0.3.2, eslint-import-resolver-node@^0.3.9: is-core-module "^2.16.1" resolve "^2.0.0-next.6" -eslint-module-utils@^2.12.1, eslint-module-utils@^2.4.1: +eslint-module-utils@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.14.0.tgz#611f8e1c6ceb29a93eb949e1cc670b8287764819" integrity sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig== dependencies: debug "^3.2.7" -eslint-plugin-import@2.20.2: - version "2.20.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.20.2.tgz#91fc3807ce08be4837141272c8b99073906e588d" - integrity sha512-FObidqpXrR8OnCh4iNsxy+WACztJLXAHBO5hK79T1Hc77PgQZkyDGA5Ag9xAvRpglvLNxhH/zSmZ70/pZ31dHg== - dependencies: - array-includes "^3.0.3" - array.prototype.flat "^1.2.1" - contains-path "^0.1.0" - debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.4.1" - has "^1.0.3" - minimatch "^3.0.4" - object.values "^1.1.0" - read-pkg-up "^2.0.0" - resolve "^1.12.0" - -eslint-plugin-import@^2.32.0: +eslint-plugin-import@2.32.0, eslint-plugin-import@^2.32.0: version "2.32.0" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz#602b55faa6e4caeaa5e970c198b5c00a37708980" integrity sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== @@ -5084,13 +5053,6 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-up@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ== - dependencies: - locate-path "^2.0.0" - find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" @@ -5421,7 +5383,7 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -5482,11 +5444,6 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -has@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" - integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== - hasha@5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" @@ -5538,11 +5495,6 @@ hono@^4.12.8: resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.30.tgz#4d57b051c66617b003b7ba318910cb5b79c4c7ac" integrity sha512-emn+JoJjrN9YTpRDS5it/UI2SO9BAE37T6I3d963RxcZ81G9A4pr2SZTEiiaiKbzx+NKRg5BZ89fCL7gCJCUog== -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - html-encoding-sniffer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" @@ -6109,11 +6061,6 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -6995,24 +6942,6 @@ listr2@^9.0.5: rfdc "^1.4.1" wrap-ansi "^9.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ== - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA== - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" @@ -7500,16 +7429,6 @@ nodemon@3.1.14: touch "^3.1.0" undefsafe "^2.0.5" -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -7600,7 +7519,7 @@ object.groupby@^1.0.3: define-properties "^1.2.1" es-abstract "^1.23.2" -object.values@^1.1.0, object.values@^1.1.6, object.values@^1.2.1: +object.values@^1.1.6, object.values@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== @@ -7696,13 +7615,6 @@ own-keys@^1.0.1: object-keys "^1.1.1" safe-push-apply "^1.0.0" -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -7717,13 +7629,6 @@ p-limit@^3.0.2, p-limit@^3.1.0: dependencies: yocto-queue "^0.1.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg== - dependencies: - p-limit "^1.1.0" - p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -7738,11 +7643,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww== - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -7772,13 +7672,6 @@ parse-entities@^2.0.0: is-decimal "^1.0.0" is-hexadecimal "^1.0.0" -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ== - dependencies: - error-ex "^1.2.0" - parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -7855,11 +7748,6 @@ passport@^0.7.0: pause "0.0.1" utils-merge "^1.0.1" -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -7913,13 +7801,6 @@ path-to-regexp@~0.1.12: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz#9b22ec16bc3ab88d05a0c7e369869421401ab17d" integrity sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA== -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ== - dependencies: - pify "^2.0.0" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -8021,7 +7902,7 @@ picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4, picomatch@^4.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab" integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A== -pify@^2.0.0, pify@^2.2.0: +pify@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== @@ -8417,23 +8298,6 @@ react@18.3.1: dependencies: loose-envify "^1.1.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w== - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA== - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - readable-stream@^3.1.1, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -8617,7 +8481,7 @@ resolve-pathname@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.11: +resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.11: version "1.22.12" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== @@ -8821,7 +8685,7 @@ scrambow@^1.8.1: dependencies: commander "^8.2.0" -"semver@2 || 3 || 4 || 5", semver@^5.5.0: +semver@^5.5.0: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== @@ -9133,32 +8997,6 @@ sparse-bitfield@^3.0.3: dependencies: memory-pager "^1.0.2" -spdx-correct@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" - integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" - integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.23" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz#b069e687b1291a32f126893ed76a27a745ee2133" - integrity sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw== - split2@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" @@ -10049,14 +9887,6 @@ valibot@1.2.0: resolved "https://registry.yarnpkg.com/valibot/-/valibot-1.2.0.tgz#8fc720d9e4082ba16e30a914064a39619b2f1d6f" integrity sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg== -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - value-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" From e3ce880b6f16c2a1351e7f5d034a011a9730d148 Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Tue, 14 Jul 2026 08:43:42 -0700 Subject: [PATCH 05/11] Add RaceSession PostgreSQL foundation Project normal-room sessions and expose development-only PostgreSQL solve history while preserving MongoDB read authority. --- README.md | 14 +- docs/data.md | 30 ++- server/api.js | 7 + server/api/solveHistory.js | 195 ++++++++++++++++++ server/api/solveHistory.test.js | 79 +++++++ server/features.js | 1 + server/features.test.js | 7 + server/metrics.js | 6 + server/models/metricEvent.js | 10 + server/postgres/dualWrite.js | 96 +++++++-- server/postgres/dualWrite.test.js | 2 +- .../migration.sql | 70 +++++++ server/prisma/schema.prisma | 56 ++++- server/raceSessions/service.js | 137 ++++++++++++ server/raceSessions/service.test.js | 66 ++++++ 15 files changed, 740 insertions(+), 36 deletions(-) create mode 100644 server/api/solveHistory.js create mode 100644 server/api/solveHistory.test.js create mode 100644 server/prisma/migrations/20260714120000_add_race_sessions/migration.sql create mode 100644 server/raceSessions/service.js create mode 100644 server/raceSessions/service.test.js diff --git a/README.md b/README.md index 5407740..cb9fde3 100644 --- a/README.md +++ b/README.md @@ -104,17 +104,19 @@ access codes, OAuth credentials, chat content, scramble text, or solve times. New MongoDB writes are mirrored into PostgreSQL without changing application reads. PostgreSQL receives public identity and preferences, rooms and -participant state, attempts, durable solve results, and sanitized analytics -events. OAuth access tokens are deliberately not copied. Writes use +participant state, RaceSession projections for normal rooms, attempts, durable +solve results, and sanitized analytics events. OAuth access tokens are deliberately not copied. Writes use deterministic UUIDs and upserts, so retries and future backfills are idempotent. Live room saves mirror only the attempts and results changed by that save; complete room snapshots are reserved -for explicit backfills. Changing a room event explicitly replaces that room's -PostgreSQL attempts so removed MongoDB attempts do not remain queryable. +for explicit backfills. Changing a normal room event ends its projected +RaceSession and starts a new one; earlier attempts and solves remain preserved +under their earlier session. Solve penalties use dedicated boolean columns rather than JSON so histories and -statistics remain compact and index-friendly. User solve history is indexed by -creation time and solve ID for stable cursor pagination. +statistics remain compact and index-friendly. Development-only +`GET /api/solve-history` reads PostgreSQL session-linked history for the +authenticated participant and is explicitly disabled in production. Set `POSTGRES_ENABLED=false` to disable mirroring. Production should set `PGHOST`, `PGDATABASE`, `PGUSER`, and `POSTGRES_PASSWORD`, or provide a diff --git a/docs/data.md b/docs/data.md index fcf99b4..f73789e 100644 --- a/docs/data.md +++ b/docs/data.md @@ -26,26 +26,36 @@ Prisma defines PostgreSQL in `server/prisma/schema.prisma`, using two schemas: | Schema | Contents | | --- | --- | -| `app` | users, rooms, participants, attempts, and solves | +| `app` | users, rooms, room/session participants, RaceSessions, attempts, and solves | | `analytics` | pseudonymous metric events | PostgreSQL receives normalized copies of users/preferences, rooms and -participant state, attempts, durable solves, and sanitized analytics. OAuth -access tokens are deliberately not mirrored. +participant state, normal-room RaceSession projections, attempts, durable +solves, and sanitized analytics. OAuth access tokens are deliberately not +mirrored. Existing PostgreSQL attempts and solves that predate the expansion +remain unlinked until the backfill/reconciliation operation assigns them. -Application reads do not use PostgreSQL yet. Setting `POSTGRES_ENABLED=false` -disables initialization and mirrors. PostgreSQL failures are logged, while API -and Socket.IO health report the service as `degraded` instead of unavailable. +Application reads remain MongoDB-backed except for the development-only solve +history experiment. Setting `POSTGRES_ENABLED=false` disables initialization +and mirrors. PostgreSQL failures are logged, while API and Socket.IO health +report the service as `degraded` instead of unavailable. ## Dual-Write Guarantees `server/postgres/dualWrite.js` derives stable UUIDs from Mongo/WCA identifiers and uses upserts so retries and future backfills remain idempotent. -Room saves collect changed attempts/results and mirror only that delta. An -explicit event change replaces that room's PostgreSQL attempts so attempts -removed from MongoDB do not remain queryable. Complete snapshots are reserved -for explicit backfill behavior. +Room saves collect changed attempts/results and mirror only that delta. A +normal-room event change ends the current projected RaceSession and creates a +new one, preserving the earlier session's attempts and solves. Complete +snapshots are reserved for explicit backfill behavior. + +`GET /api/solve-history` is a PostgreSQL-only, authenticated self-history +endpoint enabled only when `NODE_ENV=development`. It returns only +session-linked solves for a current non-banned room participant; hidden, +expired, and soft-deleted rooms remain readable to that participant. It never +falls back to MongoDB or selects access codes, passwords, membership lists, +moderation data, or email data. The mirror intentionally catches database failures and returns control to the MongoDB-backed request. Monitoring must surface mirror errors because users may diff --git a/server/api.js b/server/api.js index ad73f23..bb4c99a 100644 --- a/server/api.js +++ b/server/api.js @@ -7,6 +7,7 @@ const { updateUsername } = require('./username'); const createFriendsRouter = require('./api/friends'); const createNotificationsRouter = require('./api/notifications'); const createUsersRouter = require('./api/users'); +const { createSolveHistoryRouter } = require('./api/solveHistory'); const { isFeatureEnabled } = require('./features'); const { apiRateLimitOptions } = require('./middlewares/apiRateLimit'); @@ -36,6 +37,12 @@ module.exports = (app) => { res.json(req.user.toObject()); }); + if (isFeatureEnabled('solveHistory')) { + router.use('/solve-history', auth, createSolveHistoryRouter()); + } else { + router.use('/solve-history', createSolveHistoryRouter()); + } + router.put('/updateUsername', auth, async (req, res) => { try { const user = await updateUsername(User, req.user, req.body.username); diff --git a/server/api/solveHistory.js b/server/api/solveHistory.js new file mode 100644 index 0000000..4bea47c --- /dev/null +++ b/server/api/solveHistory.js @@ -0,0 +1,195 @@ +const express = require('express'); + +const { isFeatureEnabled } = require('../features'); +const { recordSolveHistoryRequest } = require('../metrics'); +const { query } = require('../postgres'); +const { stableId } = require('../postgres/dualWrite'); + +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 100; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +const invalidRequest = (code) => { + const error = new Error(code === 'invalid_cursor' ? 'Invalid cursor' : 'Invalid limit'); + error.statusCode = 400; + error.code = code; + return error; +}; + +const encodeCursor = ({ completedAt, id }) => Buffer.from(JSON.stringify({ + completedAt: new Date(completedAt).toISOString(), + id, +})).toString('base64url'); + +const decodeCursor = (value) => { + if (!value) { + return null; + } + if (typeof value !== 'string' || value.length > 512) { + throw invalidRequest('invalid_cursor'); + } + + try { + const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8')); + const completedAt = new Date(parsed.completedAt); + if (!parsed || typeof parsed.id !== 'string' || !UUID.test(parsed.id) + || Number.isNaN(completedAt.getTime())) { + throw new Error('invalid'); + } + return { completedAt, id: parsed.id }; + } catch { + throw invalidRequest('invalid_cursor'); + } +}; + +const parseLimit = (value) => { + if (value === undefined) { + return DEFAULT_LIMIT; + } + if (typeof value !== 'string' || !/^\d+$/.test(value)) { + throw invalidRequest('invalid_limit'); + } + const limit = Number(value); + if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIMIT) { + throw invalidRequest('invalid_limit'); + } + return limit; +}; + +const asScramble = (scrambles) => { + if (typeof scrambles !== 'string') { + return scrambles; + } + try { + return JSON.parse(scrambles); + } catch { + return scrambles; + } +}; + +const historyQuery = ` + SELECT + s.id AS solve_id, + s.time_ms, + s.dnf, + s.plus_two_penalty, + s.inspection_penalty, + s.auf_penalty, + s.source_created_at AS completed_at, + s.source_updated_at AS updated_at, + a.scrambles, + r.id AS room_id, + rs.id AS race_session_id, + rs.cube_event AS race_session_event + FROM app.solves s + JOIN app.attempts a ON a.id = s.attempt_id AND a.room_id = s.room_id + JOIN app.race_sessions rs ON rs.id = a.race_session_id AND rs.room_id = a.room_id + JOIN app.rooms r ON r.id = s.room_id + JOIN app.room_participants rp + ON rp.room_id = r.id AND rp.user_id = $1 AND rp.banned = false + WHERE s.user_id = $1 + AND ($2::timestamptz IS NULL OR (s.source_created_at, s.id) < ($2, $3::uuid)) + ORDER BY s.source_created_at DESC, s.id DESC + LIMIT $4 +`; + +const solveResponse = (row) => ({ + id: row.solve_id, + event: row.race_session_event, + timeMs: row.time_ms, + penalties: { + dnf: !!row.dnf, + plus2: !!row.plus_two_penalty, + inspection: !!row.inspection_penalty, + auf: !!row.auf_penalty, + }, + scramble: asScramble(row.scrambles), + room: { id: row.room_id }, + raceSession: { id: row.race_session_id, event: row.race_session_event }, + completedAt: new Date(row.completed_at).toISOString(), + updatedAt: new Date(row.updated_at).toISOString(), +}); + +const createSolveHistoryRouter = ({ + enabled = isFeatureEnabled('solveHistory'), + postgresQuery = query, + recordMetric = recordSolveHistoryRequest, + now = () => Date.now(), +} = {}) => { + const router = express.Router(); + + router.get('/', async (req, res) => { + const startedAt = now(); + let outcome = 'error'; + let count = 0; + try { + if (!enabled) { + outcome = 'feature_disabled'; + return res.status(404).json({ + code: 'feature_disabled', + message: 'This feature is not available', + }); + } + + const limit = parseLimit(req.query.limit); + const cursor = decodeCursor(req.query.cursor); + const numericId = Number(req.user && req.user.id); + if (!Number.isSafeInteger(numericId) || numericId < 1) { + outcome = 'unauthorized'; + return res.status(403).json({ code: 'unauthorized', message: 'Unauthorized' }); + } + + const result = await postgresQuery(historyQuery, [ + stableId('user', numericId), + cursor && cursor.completedAt, + cursor && cursor.id, + limit + 1, + ]); + if (!result) { + outcome = 'postgres_unavailable'; + return res.status(503).json({ + code: 'postgres_unavailable', + message: 'Solve history is temporarily unavailable', + }); + } + + const rows = result.rows || []; + const hasNextPage = rows.length > limit; + const page = rows.slice(0, limit); + count = page.length; + outcome = 'success'; + return res.json({ + solves: page.map(solveResponse), + nextCursor: hasNextPage ? encodeCursor({ + completedAt: page[page.length - 1].completed_at, + id: page[page.length - 1].solve_id, + }) : null, + }); + } catch (err) { + if (err.statusCode === 400) { + outcome = err.code; + return res.status(400).json({ code: err.code, message: err.message }); + } + outcome = 'postgres_unavailable'; + return res.status(503).json({ + code: 'postgres_unavailable', + message: 'Solve history is temporarily unavailable', + }); + } finally { + Promise.resolve(recordMetric({ + outcome, + count, + latencyMs: Math.max(0, now() - startedAt), + })).catch(() => {}); + } + }); + + return router; +}; + +module.exports = { + createSolveHistoryRouter, + decodeCursor, + encodeCursor, + parseLimit, +}; diff --git a/server/api/solveHistory.test.js b/server/api/solveHistory.test.js new file mode 100644 index 0000000..266c2c8 --- /dev/null +++ b/server/api/solveHistory.test.js @@ -0,0 +1,79 @@ +/** @jest-environment node */ +/* eslint-env jest */ + +jest.mock('../metrics', () => ({ recordSolveHistoryRequest: jest.fn() })); +jest.mock('../postgres', () => ({ query: jest.fn() })); +jest.mock('../postgres/dualWrite', () => ({ stableId: (kind, id) => `${kind}:${id}` })); + +const { recordSolveHistoryRequest } = require('../metrics'); +const { createSolveHistoryRouter, decodeCursor } = require('./solveHistory'); + +const response = () => { + const res = { json: jest.fn(), status: jest.fn() }; + res.status.mockReturnValue(res); + return res; +}; + +const handlerFor = (options) => createSolveHistoryRouter(options).stack[0].route.stack[0].handle; + +describe('solve history API', () => { + beforeEach(() => jest.clearAllMocks()); + + it('keeps production exposure explicitly disabled', async () => { + const res = response(); + await handlerFor({ enabled: false })({ query: {}, user: { id: 123 } }, res); + + expect(res.status).toHaveBeenCalledWith(404); + expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'feature_disabled' })); + }); + + it('returns only session-linked authorized PostgreSQL history with stable cursors', async () => { + const postgresQuery = jest.fn().mockResolvedValue({ rows: [{ + solve_id: '4d8ce8c4-8910-47e9-b39f-79e1280c1e3a', + time_ms: 12345, + dnf: false, + plus_two_penalty: true, + inspection_penalty: false, + auf_penalty: true, + scrambles: ['R U'], + room_id: 'ea1486ca-bce6-4c37-a4c9-1d32002c1e4a', + race_session_id: '121ac5d8-19df-463a-af08-8b7b3c2c94a3', + race_session_event: '333', + completed_at: new Date('2026-07-14T10:00:00.000Z'), + updated_at: new Date('2026-07-14T10:01:00.000Z'), + }] }); + const res = response(); + + await handlerFor({ enabled: true, postgresQuery })({ query: {}, user: { id: 123 } }, res); + + expect(postgresQuery.mock.calls[0][0]).toContain('rp.banned = false'); + expect(postgresQuery.mock.calls[0][0]).toContain('a.race_session_id'); + expect(postgresQuery.mock.calls[0][0]).not.toMatch(/email|access_code|password/i); + expect(res.json).toHaveBeenCalledWith({ + solves: [expect.objectContaining({ + event: '333', timeMs: 12345, scramble: ['R U'], + penalties: { dnf: false, plus2: true, inspection: false, auf: true }, + room: { id: 'ea1486ca-bce6-4c37-a4c9-1d32002c1e4a' }, + raceSession: { id: '121ac5d8-19df-463a-af08-8b7b3c2c94a3', event: '333' }, + })], + nextCursor: null, + }); + expect(recordSolveHistoryRequest).toHaveBeenCalledWith(expect.objectContaining({ + outcome: 'success', count: 1, + })); + }); + + it('returns 503 when PostgreSQL is unavailable and rejects malformed pagination', async () => { + const unavailable = response(); + await handlerFor({ enabled: true, postgresQuery: jest.fn().mockResolvedValue(null) })( + { query: {}, user: { id: 123 } }, unavailable, + ); + expect(unavailable.status).toHaveBeenCalledWith(503); + + const invalid = response(); + await handlerFor({ enabled: true })({ query: { cursor: 'not-a-cursor' }, user: { id: 123 } }, invalid); + expect(invalid.status).toHaveBeenCalledWith(400); + expect(invalid.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'invalid_cursor' })); + expect(() => decodeCursor('not-a-cursor')).toThrow('Invalid cursor'); + }); +}); diff --git a/server/features.js b/server/features.js index 895b23d..5d22b0d 100644 --- a/server/features.js +++ b/server/features.js @@ -1,5 +1,6 @@ const FEATURES = Object.freeze({ friends: process.env.NODE_ENV !== 'production', + solveHistory: ['development', 'dev'].includes(process.env.NODE_ENV), }); const isFeatureEnabled = (feature) => FEATURES[feature] === true; diff --git a/server/features.test.js b/server/features.test.js index ce4b932..8475f3e 100644 --- a/server/features.test.js +++ b/server/features.test.js @@ -21,4 +21,11 @@ describe('feature flags', () => { it('keeps friends available to local development', () => { expect(loadFeatures('development').isFeatureEnabled('friends')).toBe(true); }); + + it('keeps solve history development-only', () => { + expect(loadFeatures('development').isFeatureEnabled('solveHistory')).toBe(true); + expect(loadFeatures('dev').isFeatureEnabled('solveHistory')).toBe(true); + expect(loadFeatures('production').isFeatureEnabled('solveHistory')).toBe(false); + expect(loadFeatures('prod').isFeatureEnabled('solveHistory')).toBe(false); + }); }); diff --git a/server/metrics.js b/server/metrics.js index fc182c8..2c067cb 100644 --- a/server/metrics.js +++ b/server/metrics.js @@ -214,6 +214,12 @@ const createMetricRecorder = ({ ...roomProperties(room), roomSolveCount: countRoomSolves(room), }), + recordSolveHistoryRequest: ({ outcome, count, latencyMs }) => write({ + event: METRIC_EVENTS.SOLVE_HISTORY_REQUESTED, + historyOutcome: outcome, + historyCount: count, + latencyMs, + }), recordSocialOutcome: ({ userId, action, outcome }) => write({ event: METRIC_EVENTS.SOCIAL_ACTION, ...actorProperties({ userId }), diff --git a/server/models/metricEvent.js b/server/models/metricEvent.js index 87a0b11..4899c25 100644 --- a/server/models/metricEvent.js +++ b/server/models/metricEvent.js @@ -8,6 +8,7 @@ const METRIC_EVENTS = Object.freeze({ ROOM_JOIN_FAILED: 'room_join_failed', ROOM_LEFT: 'room_left', ROOM_RESULT_SUBMITTED: 'room_result_submitted', + SOLVE_HISTORY_REQUESTED: 'solve_history_requested', SOCIAL_ACTION: 'social_action', }); @@ -73,6 +74,15 @@ const MetricEvent = new mongoose.Schema({ type: Number, min: 0, }, + historyOutcome: String, + historyCount: { + type: Number, + min: 0, + }, + latencyMs: { + type: Number, + min: 0, + }, active: { type: Boolean, default: false, diff --git a/server/postgres/dualWrite.js b/server/postgres/dualWrite.js index 42a0cf7..afa699d 100644 --- a/server/postgres/dualWrite.js +++ b/server/postgres/dualWrite.js @@ -2,6 +2,7 @@ const { v4: uuidv4, v5: uuidv5 } = require('uuid'); const { query, withTransaction } = require('./index'); +const { ensureNormalSession, normalSession } = require('../raceSessions/service'); const DUAL_WRITE_NAMESPACE = uuidv5('https://letscube.net/postgres-dual-write', uuidv5.URL); @@ -283,9 +284,9 @@ const upsertRoomState = async (client, room, options = {}) => { INSERT INTO app.rooms ( id, mongo_id, name, cube_event, access_code, password_hash, room_type, owner_id, admin_id, require_revealed_identity, start_time, started, - next_solve_at, expires_at, twitch_channel, source_created_at, source_updated_at + next_solve_at, expires_at, twitch_channel, source_created_at, source_updated_at, kind ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18 ) ON CONFLICT (mongo_id) DO UPDATE SET name = EXCLUDED.name, @@ -293,8 +294,9 @@ const upsertRoomState = async (client, room, options = {}) => { access_code = EXCLUDED.access_code, password_hash = EXCLUDED.password_hash, room_type = EXCLUDED.room_type, - owner_id = CASE WHEN $18 THEN EXCLUDED.owner_id ELSE app.rooms.owner_id END, - admin_id = CASE WHEN $19 THEN EXCLUDED.admin_id ELSE app.rooms.admin_id END, + kind = EXCLUDED.kind, + owner_id = CASE WHEN $19 THEN EXCLUDED.owner_id ELSE app.rooms.owner_id END, + admin_id = CASE WHEN $20 THEN EXCLUDED.admin_id ELSE app.rooms.admin_id END, require_revealed_identity = EXCLUDED.require_revealed_identity, start_time = EXCLUDED.start_time, started = EXCLUDED.started, @@ -313,6 +315,7 @@ const upsertRoomState = async (client, room, options = {}) => { app.rooms.access_code, app.rooms.password_hash, app.rooms.room_type, + app.rooms.kind, app.rooms.owner_id, app.rooms.admin_id, app.rooms.require_revealed_identity, @@ -328,8 +331,9 @@ const upsertRoomState = async (client, room, options = {}) => { EXCLUDED.access_code, EXCLUDED.password_hash, EXCLUDED.room_type, - CASE WHEN $18 THEN EXCLUDED.owner_id ELSE app.rooms.owner_id END, - CASE WHEN $19 THEN EXCLUDED.admin_id ELSE app.rooms.admin_id END, + EXCLUDED.kind, + CASE WHEN $19 THEN EXCLUDED.owner_id ELSE app.rooms.owner_id END, + CASE WHEN $20 THEN EXCLUDED.admin_id ELSE app.rooms.admin_id END, EXCLUDED.require_revealed_identity, EXCLUDED.start_time, EXCLUDED.started, @@ -357,10 +361,33 @@ const upsertRoomState = async (client, room, options = {}) => { room.twitchChannel || null, room.createdAt || null, updatedAt, + room.type === 'grand_prix' ? 'competition' : 'normal', ownerKnown, adminKnown, ]); + let raceSessionId = null; + if (room.type === 'normal') { + const mongoRoomId = room._id.toString(); + const session = normalSession({ + id: stableId('race-session', mongoRoomId), + nextId: stableId('race-session', `${mongoRoomId}:${room.event}:${updatedAt.toISOString()}`), + roomId, + event: room.event, + sourceKey: `legacy-normal:${mongoRoomId}`, + nextSourceKey: `normal-session:${mongoRoomId}:${room.event}:${updatedAt.toISOString()}`, + sourceCreatedAt: room.createdAt || null, + sourceUpdatedAt: updatedAt, + started: !!room.started, + startTime: room.startTime || null, + nextSolveAt: room.nextSolveAt || null, + currentAttemptOrdinal: (room.attempts || []).length + ? (room.attempts[room.attempts.length - 1].id ?? room.attempts.length - 1) + : null, + }); + raceSessionId = await ensureNormalSession(client, session); + } + for (const user of participantUsers) { const wcaUserId = numericUserId(user); if (!knownUserIds.has(wcaUserId)) { @@ -407,11 +434,37 @@ const upsertRoomState = async (client, room, options = {}) => { mapValue(room.registered, wcaUserId), updatedAt, ]); + + if (raceSessionId) { + await client.query(` + INSERT INTO app.session_participants ( + race_session_id, user_id, eligible, competing, waiting_for, registered, + source_updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (race_session_id, user_id) DO UPDATE SET + eligible = EXCLUDED.eligible, + competing = EXCLUDED.competing, + waiting_for = EXCLUDED.waiting_for, + registered = EXCLUDED.registered, + source_updated_at = EXCLUDED.source_updated_at, + ingested_at = now() + WHERE app.session_participants.source_updated_at <= EXCLUDED.source_updated_at + `, [ + raceSessionId, + stableId('user', wcaUserId), + !mapValue(room.banned, wcaUserId), + mapValue(room.competing, wcaUserId), + mapValue(room.waitingFor, wcaUserId), + mapValue(room.registered, wcaUserId), + updatedAt, + ]); + } } return { knownUserIds, roomId, + raceSessionId, updatedAt, }; }; @@ -426,9 +479,9 @@ const upsertAttempt = async (client, room, roomState, attempt, attemptIndex) => await client.query(` INSERT INTO app.attempts ( - id, mongo_id, room_id, ordinal, cube_event, scrambles, + id, mongo_id, room_id, race_session_id, ordinal, cube_event, scrambles, source_created_at, source_updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (mongo_id) DO UPDATE SET ordinal = EXCLUDED.ordinal, cube_event = EXCLUDED.cube_event, @@ -452,6 +505,7 @@ const upsertAttempt = async (client, room, roomState, attempt, attemptIndex) => attemptId, attemptMongoId, roomState.roomId, + roomState.raceSessionId, ordinal, room.event, JSON.stringify(attempt.scrambles || []), @@ -478,13 +532,16 @@ const upsertSolve = async (client, roomState, attemptState, wcaUserId, result) = await client.query(` INSERT INTO app.solves ( id, attempt_id, room_id, user_id, time_ms, dnf, - inspection_penalty, auf_penalty, source_created_at, source_updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + plus_two_penalty, inspection_penalty, auf_penalty, submission_id, + source_created_at, source_updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (attempt_id, user_id) DO UPDATE SET time_ms = EXCLUDED.time_ms, dnf = EXCLUDED.dnf, + plus_two_penalty = EXCLUDED.plus_two_penalty, inspection_penalty = EXCLUDED.inspection_penalty, auf_penalty = EXCLUDED.auf_penalty, + submission_id = COALESCE(app.solves.submission_id, EXCLUDED.submission_id), source_updated_at = EXCLUDED.source_updated_at, ingested_at = now() WHERE app.solves.source_updated_at < EXCLUDED.source_updated_at @@ -493,13 +550,17 @@ const upsertSolve = async (client, roomState, attemptState, wcaUserId, result) = AND ROW( app.solves.time_ms, app.solves.dnf, + app.solves.plus_two_penalty, app.solves.inspection_penalty, - app.solves.auf_penalty + app.solves.auf_penalty, + app.solves.submission_id ) IS DISTINCT FROM ROW( EXCLUDED.time_ms, EXCLUDED.dnf, + EXCLUDED.plus_two_penalty, EXCLUDED.inspection_penalty, - EXCLUDED.auf_penalty + EXCLUDED.auf_penalty, + COALESCE(app.solves.submission_id, EXCLUDED.submission_id) ) ) `, [ @@ -509,8 +570,10 @@ const upsertSolve = async (client, roomState, attemptState, wcaUserId, result) = stableId('user', wcaUserId), result.time, !!penalties.DNF, + !!(penalties.plus2 || penalties['+2'] || penalties.PLUS2), !!penalties.inspection, !!penalties.AUF, + result.submissionId || null, resultCreatedAt, resultUpdatedAt, ]); @@ -526,11 +589,6 @@ const syncAttemptResults = async (client, roomState, attemptState, attempt, user const result = resultValue(attempt.results, userKey); if (result) { await upsertSolve(client, roomState, attemptState, wcaUserId, result); - } else { - await client.query(` - DELETE FROM app.solves - WHERE attempt_id = $1 AND user_id = $2 - `, [attemptState.attemptId, stableId('user', wcaUserId)]); } } }; @@ -592,7 +650,6 @@ const writeRoomChanges = async (client, room, changes) => { ); if (change.syncAllResults) { - await client.query('DELETE FROM app.solves WHERE attempt_id = $1', [attemptState.attemptId]); await syncAttemptResults( client, roomState, @@ -658,6 +715,9 @@ const mirrorMetricEvent = (event) => query(` activeUserCount: event.activeUserCount, roomSolveCount: event.roomSolveCount, durationMs: event.durationMs, + historyOutcome: event.historyOutcome, + historyCount: event.historyCount, + latencyMs: event.latencyMs, }, event.expiresAt, ]); diff --git a/server/postgres/dualWrite.test.js b/server/postgres/dualWrite.test.js index 6198edd..c7e5a10 100644 --- a/server/postgres/dualWrite.test.js +++ b/server/postgres/dualWrite.test.js @@ -115,7 +115,7 @@ describe('PostgreSQL dual writer', () => { const solveCall = client.query.mock.calls.find(([sql]) => sql.includes('INSERT INTO app.solves')); expect(solveCall[0]).toContain('inspection_penalty'); - expect(solveCall[1].slice(4, 8)).toEqual([-1, true, true, false]); + expect(solveCall[1].slice(4, 9)).toEqual([-1, true, false, true, false]); expect(solveCall[1]).not.toContain('room-access-code'); }); diff --git a/server/prisma/migrations/20260714120000_add_race_sessions/migration.sql b/server/prisma/migrations/20260714120000_add_race_sessions/migration.sql new file mode 100644 index 0000000..dd0a2f2 --- /dev/null +++ b/server/prisma/migrations/20260714120000_add_race_sessions/migration.sql @@ -0,0 +1,70 @@ +ALTER TABLE app.rooms + ADD COLUMN kind text NOT NULL DEFAULT 'normal', + ADD COLUMN competition_ref text; + +UPDATE app.rooms +SET kind = CASE WHEN room_type = 'grand_prix' THEN 'competition' ELSE 'normal' END; + +ALTER TABLE app.rooms + ADD CONSTRAINT rooms_kind_check CHECK (kind IN ('normal', 'competition')); + +CREATE TABLE app.race_sessions ( + id uuid PRIMARY KEY, + room_id uuid NOT NULL REFERENCES app.rooms(id) ON DELETE RESTRICT, + cube_event text NOT NULL, + race_format text NOT NULL, + scramble_source text NOT NULL, + status text NOT NULL CHECK (status IN ('ready', 'racing', 'paused', 'ended', 'cancelled')), + scheduled_start_at timestamptz, + started_at timestamptz, + ended_at timestamptz, + current_attempt_ordinal integer CHECK (current_attempt_ordinal >= 0), + next_solve_at timestamptz, + source_key text NOT NULL UNIQUE, + source_created_at timestamptz, + source_updated_at timestamptz NOT NULL, + ingested_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (id, room_id) +); + +CREATE INDEX race_sessions_room_status_idx + ON app.race_sessions (room_id, status, source_updated_at DESC); + +CREATE TABLE app.session_participants ( + race_session_id uuid NOT NULL REFERENCES app.race_sessions(id) ON DELETE RESTRICT, + user_id uuid NOT NULL REFERENCES app.users(id) ON DELETE RESTRICT, + eligible boolean NOT NULL DEFAULT true, + competing boolean NOT NULL DEFAULT false, + waiting_for boolean NOT NULL DEFAULT false, + registered boolean NOT NULL DEFAULT false, + source_updated_at timestamptz NOT NULL, + ingested_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (race_session_id, user_id) +); + +ALTER TABLE app.attempts + ADD COLUMN race_session_id uuid; + +ALTER TABLE app.attempts + ADD CONSTRAINT attempts_id_room_id_key UNIQUE (id, room_id), + ADD CONSTRAINT attempts_race_session_id_room_id_fkey + FOREIGN KEY (race_session_id, room_id) + REFERENCES app.race_sessions(id, room_id) + ON DELETE RESTRICT; + +CREATE INDEX attempts_race_session_ordinal_idx + ON app.attempts (race_session_id, ordinal, id); + +ALTER TABLE app.solves + ADD COLUMN plus_two_penalty boolean NOT NULL DEFAULT false, + ADD COLUMN submission_id text; + +ALTER TABLE app.solves + DROP CONSTRAINT solves_attempt_id_fkey, + ADD CONSTRAINT solves_attempt_id_room_id_fkey + FOREIGN KEY (attempt_id, room_id) + REFERENCES app.attempts(id, room_id) + ON DELETE RESTRICT; + +CREATE INDEX solves_user_session_history_idx + ON app.solves (user_id, source_created_at DESC, id DESC, attempt_id); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 5c23121..6de225f 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -20,6 +20,7 @@ model User { ownedRooms Room[] @relation("RoomOwner") administeredRooms Room[] @relation("RoomAdmin") roomParticipations RoomParticipant[] + sessionParticipations SessionParticipant[] solves Solve[] friendshipsAsLow FriendRelationship[] @relation("FriendshipLowUser") friendshipsAsHigh FriendRelationship[] @relation("FriendshipHighUser") @@ -122,11 +123,57 @@ model Room { participants RoomParticipant[] attempts Attempt[] solves Solve[] + raceSessions RaceSession[] + kind String @default("normal") + competitionRef String? @map("competition_ref") @@map("rooms") @@schema("app") } +model RaceSession { + id String @id @db.Uuid + roomId String @map("room_id") @db.Uuid + cubeEvent String @map("cube_event") + raceFormat String @map("race_format") + scrambleSource String @map("scramble_source") + status String + scheduledStartAt DateTime? @map("scheduled_start_at") @db.Timestamptz(6) + startedAt DateTime? @map("started_at") @db.Timestamptz(6) + endedAt DateTime? @map("ended_at") @db.Timestamptz(6) + currentAttemptOrdinal Int? @map("current_attempt_ordinal") + nextSolveAt DateTime? @map("next_solve_at") @db.Timestamptz(6) + sourceKey String @unique(map: "race_sessions_source_key_key") @map("source_key") + sourceCreatedAt DateTime? @map("source_created_at") @db.Timestamptz(6) + sourceUpdatedAt DateTime @map("source_updated_at") @db.Timestamptz(6) + ingestedAt DateTime @default(now()) @map("ingested_at") @db.Timestamptz(6) + room Room @relation(fields: [roomId], references: [id], onDelete: Restrict, onUpdate: NoAction) + participants SessionParticipant[] + attempts Attempt[] + + @@unique([id, roomId], map: "race_sessions_id_room_id_key") + @@index([roomId, status, sourceUpdatedAt(sort: Desc)], map: "race_sessions_room_status_idx") + @@map("race_sessions") + @@schema("app") +} + +model SessionParticipant { + raceSessionId String @map("race_session_id") @db.Uuid + userId String @map("user_id") @db.Uuid + eligible Boolean @default(true) + competing Boolean @default(false) + waitingFor Boolean @default(false) @map("waiting_for") + registered Boolean @default(false) + sourceUpdatedAt DateTime @map("source_updated_at") @db.Timestamptz(6) + ingestedAt DateTime @default(now()) @map("ingested_at") @db.Timestamptz(6) + raceSession RaceSession @relation(fields: [raceSessionId], references: [id], onDelete: Restrict, onUpdate: NoAction) + user User @relation(fields: [userId], references: [id], onDelete: Restrict, onUpdate: NoAction) + + @@id([raceSessionId, userId]) + @@map("session_participants") + @@schema("app") +} + model RoomParticipant { roomId String @map("room_id") @db.Uuid userId String @map("user_id") @db.Uuid @@ -149,6 +196,7 @@ model Attempt { id String @id @db.Uuid mongoId String @unique(map: "attempts_mongo_id_unique") @map("mongo_id") roomId String @map("room_id") @db.Uuid + raceSessionId String? @map("race_session_id") @db.Uuid ordinal Int cubeEvent String @map("cube_event") scrambles Json @db.JsonB @@ -156,9 +204,12 @@ model Attempt { sourceUpdatedAt DateTime @map("source_updated_at") @db.Timestamptz(6) ingestedAt DateTime @default(now()) @map("ingested_at") @db.Timestamptz(6) room Room @relation(fields: [roomId], references: [id], onDelete: Cascade, onUpdate: NoAction) + raceSession RaceSession? @relation(fields: [raceSessionId, roomId], references: [id, roomId], onDelete: Restrict, onUpdate: NoAction) solves Solve[] @@index([roomId, ordinal], map: "attempts_room_ordinal_idx") + @@index([raceSessionId, ordinal, id], map: "attempts_race_session_ordinal_idx") + @@unique([id, roomId], map: "attempts_id_room_id_key") @@map("attempts") @@schema("app") } @@ -170,17 +221,20 @@ model Solve { userId String @map("user_id") @db.Uuid timeMs Int @map("time_ms") dnf Boolean @default(false) + plusTwoPenalty Boolean @default(false) @map("plus_two_penalty") inspectionPenalty Boolean @default(false) @map("inspection_penalty") aufPenalty Boolean @default(false) @map("auf_penalty") + submissionId String? @map("submission_id") sourceCreatedAt DateTime @map("source_created_at") @db.Timestamptz(6) sourceUpdatedAt DateTime @map("source_updated_at") @db.Timestamptz(6) ingestedAt DateTime @default(now()) @map("ingested_at") @db.Timestamptz(6) - attempt Attempt @relation(fields: [attemptId], references: [id], onDelete: Cascade, onUpdate: NoAction) + attempt Attempt @relation(fields: [attemptId, roomId], references: [id, roomId], onDelete: Restrict, onUpdate: NoAction) room Room @relation(fields: [roomId], references: [id], onDelete: Cascade, onUpdate: NoAction) user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: NoAction) @@unique([attemptId, userId], map: "solves_attempt_id_user_id_key") @@index([userId, sourceCreatedAt(sort: Desc), id(sort: Desc)], map: "solves_user_created_idx") + @@index([userId, sourceCreatedAt(sort: Desc), id(sort: Desc), attemptId], map: "solves_user_session_history_idx") @@map("solves") @@schema("app") } diff --git a/server/raceSessions/service.js b/server/raceSessions/service.js new file mode 100644 index 0000000..a09b010 --- /dev/null +++ b/server/raceSessions/service.js @@ -0,0 +1,137 @@ +const ACTIVE_STATUSES = ['ready', 'racing', 'paused']; + +const sessionStatus = ({ started, nextSolveAt }) => { + if (started) { + return 'racing'; + } + if (nextSolveAt) { + return 'paused'; + } + return 'ready'; +}; + +const createRaceSession = async (client, session) => { + await client.query(` + INSERT INTO app.race_sessions ( + id, room_id, cube_event, race_format, scramble_source, status, + scheduled_start_at, started_at, ended_at, current_attempt_ordinal, + next_solve_at, source_key, source_created_at, source_updated_at + ) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14 + ) + ON CONFLICT (source_key) DO UPDATE SET + cube_event = EXCLUDED.cube_event, + race_format = EXCLUDED.race_format, + scramble_source = EXCLUDED.scramble_source, + status = EXCLUDED.status, + scheduled_start_at = EXCLUDED.scheduled_start_at, + started_at = COALESCE(app.race_sessions.started_at, EXCLUDED.started_at), + current_attempt_ordinal = EXCLUDED.current_attempt_ordinal, + next_solve_at = EXCLUDED.next_solve_at, + source_updated_at = EXCLUDED.source_updated_at, + ingested_at = now() + WHERE app.race_sessions.source_updated_at <= EXCLUDED.source_updated_at + `, [ + session.id, + session.roomId, + session.event, + session.format, + session.scrambleSource, + session.status, + session.scheduledStartAt || null, + session.startedAt || null, + session.endedAt || null, + session.currentAttemptOrdinal === undefined ? null : session.currentAttemptOrdinal, + session.nextSolveAt || null, + session.sourceKey, + session.sourceCreatedAt || null, + session.sourceUpdatedAt, + ]); + return session.id; +}; + +const endSession = async (client, sessionId, endedAt) => { + await client.query(` + UPDATE app.race_sessions + SET status = 'ended', + ended_at = COALESCE(ended_at, $2), + source_updated_at = GREATEST(source_updated_at, $2), + ingested_at = now() + WHERE id = $1 + `, [sessionId, endedAt]); +}; + +const normalSession = ({ + id, + nextId, + roomId, + event, + sourceKey, + nextSourceKey, + sourceCreatedAt, + sourceUpdatedAt, + started, + startTime, + nextSolveAt, + currentAttemptOrdinal, +}) => ({ + id, + nextId, + roomId, + event, + format: 'normal', + scrambleSource: 'legacy_room', + status: sessionStatus({ started, nextSolveAt }), + scheduledStartAt: startTime, + startedAt: started ? (startTime || sourceUpdatedAt) : null, + nextSolveAt, + currentAttemptOrdinal, + sourceKey, + nextSourceKey, + sourceCreatedAt, + sourceUpdatedAt, +}); + +const ensureNormalSession = async (client, desiredSession) => { + const active = await client.query(` + SELECT id, cube_event, source_key + FROM app.race_sessions + WHERE room_id = $1 AND status = ANY($2::text[]) + FOR UPDATE + `, [desiredSession.roomId, ACTIVE_STATUSES]); + const current = active.rows && active.rows[0]; + + if (current && current.cube_event === desiredSession.event) { + return createRaceSession(client, { + ...desiredSession, + id: current.id, + sourceKey: current.source_key, + }); + } + + if (current) { + await endSession(client, current.id, desiredSession.sourceUpdatedAt); + } + + return createRaceSession(client, current ? { + ...desiredSession, + id: desiredSession.nextId, + sourceKey: desiredSession.nextSourceKey, + } : desiredSession); +}; + +const createCompetitionSession = (client, session) => createRaceSession(client, { + ...session, + format: session.format || 'competition', + scrambleSource: session.scrambleSource || 'competition', + status: session.status || 'ready', +}); + +module.exports = { + ACTIVE_STATUSES, + createCompetitionSession, + createRaceSession, + ensureNormalSession, + normalSession, + sessionStatus, +}; diff --git a/server/raceSessions/service.test.js b/server/raceSessions/service.test.js new file mode 100644 index 0000000..465f319 --- /dev/null +++ b/server/raceSessions/service.test.js @@ -0,0 +1,66 @@ +/** @jest-environment node */ +/* eslint-env jest */ + +const { + createCompetitionSession, + ensureNormalSession, + normalSession, +} = require('./service'); + +const session = (overrides = {}) => normalSession({ + id: 'initial-session', + nextId: 'next-session', + roomId: 'room-id', + event: '333', + sourceKey: 'legacy-normal:room', + nextSourceKey: 'normal-session:room:333:next', + sourceCreatedAt: new Date('2026-07-14T10:00:00.000Z'), + sourceUpdatedAt: new Date('2026-07-14T10:00:00.000Z'), + started: false, + startTime: null, + nextSolveAt: null, + currentAttemptOrdinal: 0, + ...overrides, +}); + +describe('RaceSession service', () => { + it('creates the first normal session transactionally', async () => { + const client = { query: jest.fn().mockResolvedValueOnce({ rows: [] }).mockResolvedValue({}) }; + + await expect(ensureNormalSession(client, session())).resolves.toBe('initial-session'); + + expect(client.query).toHaveBeenNthCalledWith(1, expect.stringContaining('FOR UPDATE'), + expect.any(Array)); + expect(client.query).toHaveBeenNthCalledWith(2, expect.stringContaining('INSERT INTO app.race_sessions'), + expect.arrayContaining(['initial-session', 'room-id', '333'])); + }); + + it('ends a normal session before creating the deterministic next event session', async () => { + const client = { + query: jest.fn() + .mockResolvedValueOnce({ rows: [{ id: 'old-session', cube_event: '333', source_key: 'old' }] }) + .mockResolvedValue({}), + }; + + await expect(ensureNormalSession(client, session({ event: '222' }))).resolves.toBe('next-session'); + + expect(client.query).toHaveBeenNthCalledWith(2, expect.stringContaining("status = 'ended'"), + ['old-session', expect.any(Date)]); + expect(client.query).toHaveBeenNthCalledWith(3, expect.stringContaining('INSERT INTO app.race_sessions'), + expect.arrayContaining(['next-session', '222', 'normal-session:room:333:next'])); + }); + + it('creates competition sessions independently', async () => { + const client = { query: jest.fn().mockResolvedValue({}) }; + const base = { + roomId: 'competition-room', event: '333', sourceUpdatedAt: new Date(), sourceKey: 'competition:333', + }; + + await createCompetitionSession(client, { ...base, id: 'competition-333' }); + await createCompetitionSession(client, { ...base, id: 'competition-222', event: '222', sourceKey: 'competition:222' }); + + expect(client.query).toHaveBeenCalledTimes(2); + expect(client.query.mock.calls[0][1]).toEqual(expect.arrayContaining(['competition-333', '333', 'competition'])); + expect(client.query.mock.calls[1][1]).toEqual(expect.arrayContaining(['competition-222', '222', 'competition'])); + }); +}); From 8eec438b13cb35798adf3ebc58a82f592432769b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 01:46:48 +0000 Subject: [PATCH 06/11] Bump react-redux from 8.1.3 to 9.3.0 Bumps [react-redux](https://github.com/reduxjs/react-redux) from 8.1.3 to 9.3.0. - [Release notes](https://github.com/reduxjs/react-redux/releases) - [Changelog](https://github.com/reduxjs/react-redux/blob/master/CHANGELOG.md) - [Commits](https://github.com/reduxjs/react-redux/compare/v8.1.3...v9.3.0) --- updated-dependencies: - dependency-name: react-redux dependency-version: 9.3.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- client/package.json | 4 ++-- yarn.lock | 37 +++++++++++++------------------------ 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/client/package.json b/client/package.json index 931e890..8778245 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,7 @@ "@mui/material": "^6.5.0", "@mui/styles": "^6.5.0", "clsx": "2.1.1", - "date-fns": "^4.4.0" + "date-fns": "^4.4.0", "history": "4.10.1", "material-ui-confirm": "^4.0.0", "prop-types": "15.8.1", @@ -19,7 +19,7 @@ "react-dom": "18.3.1", "react-ga4": "^3.0.1", "react-markdown": "6.0.3", - "react-redux": "^8.1.3", + "react-redux": "^9.3.0", "react-router-dom": "^5.3.4", "react-twitch-embed": "^3.0.2", "react-virtualized": "^9.22.6", diff --git a/yarn.lock b/yarn.lock index 0a8688f..bfabd01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -944,7 +944,7 @@ "@babel/plugin-transform-react-jsx-development" "^7.29.7" "@babel/plugin-transform-react-pure-annotations" "^7.29.7" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.26.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.26.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.29.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768" integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== @@ -2418,13 +2418,6 @@ dependencies: "@types/unist" "^2" -"@types/hoist-non-react-statics@^3.3.1": - version "3.3.7" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz#306e3a3a73828522efa1341159da4846e7573a6c" - integrity sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g== - dependencies: - hoist-non-react-statics "^3.3.0" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" @@ -2582,10 +2575,10 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== -"@types/use-sync-external-store@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" - integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== +"@types/use-sync-external-store@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz#60be8d21baab8c305132eb9cb912ed497852aadc" + integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg== "@types/web-bluetooth@^0.0.20": version "0.0.20" @@ -5521,7 +5514,7 @@ history@4.10.1, history@^4.9.0: tiny-warning "^1.0.0" value-equal "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -8338,17 +8331,13 @@ react-markdown@6.0.3: unist-util-visit "^2.0.0" vfile "^4.0.0" -react-redux@^8.1.3: - version "8.1.3" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.1.3.tgz#4fdc0462d0acb59af29a13c27ffef6f49ab4df46" - integrity sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw== +react-redux@^9.3.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.3.0.tgz#a30113bb6d95c0a715d54dda4308d450fca6ce09" + integrity sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g== dependencies: - "@babel/runtime" "^7.12.1" - "@types/hoist-non-react-statics" "^3.3.1" - "@types/use-sync-external-store" "^0.0.3" - hoist-non-react-statics "^3.3.2" - react-is "^18.0.0" - use-sync-external-store "^1.0.0" + "@types/use-sync-external-store" "^0.0.6" + use-sync-external-store "^1.4.0" react-router-dom@^5.3.4: version "5.3.4" @@ -10005,7 +9994,7 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -use-sync-external-store@^1.0.0: +use-sync-external-store@^1.4.0: version "1.6.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== From 4f814c8cd9ba680b5793a4de83318c0c77d9f89e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:44:20 +0000 Subject: [PATCH 07/11] Bump eslint-config-airbnb-base from 14.1.0 to 15.0.0 Bumps [eslint-config-airbnb-base](https://github.com/airbnb/javascript) from 14.1.0 to 15.0.0. - [Commits](https://github.com/airbnb/javascript/compare/eslint-config-airbnb-base-v14.1.0...eslint-config-airbnb-base-v15.0.0) --- updated-dependencies: - dependency-name: eslint-config-airbnb-base dependency-version: 15.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/scrambles/package.json | 4 ++-- yarn.lock | 23 ++++++++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/scrambles/package.json b/packages/scrambles/package.json index f541e84..b6ec0a7 100644 --- a/packages/scrambles/package.json +++ b/packages/scrambles/package.json @@ -23,8 +23,8 @@ }, "devDependencies": { "eslint": "6.8.0", - "eslint-config-airbnb-base": "14.1.0", - "eslint-plugin-import": "2.32.0", + "eslint-config-airbnb-base": "15.0.0", + "eslint-plugin-import": "2.32.0" "jest": "^30.4.2" } } diff --git a/yarn.lock b/yarn.lock index 30566d5..0a5f6dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3705,7 +3705,7 @@ confbox@^0.2.4: resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.4.tgz#592e7be71f882a4a874e3c88f0ac1ef6f7da1ce5" integrity sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ== -confusing-browser-globals@^1.0.9: +confusing-browser-globals@^1.0.10: version "1.0.11" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== @@ -4483,14 +4483,15 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-config-airbnb-base@14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.1.0.tgz#2ba4592dd6843258221d9bff2b6831bd77c874e4" - integrity sha512-+XCcfGyCnbzOnktDVhwsCAx+9DmrzEmuwxyHUJpw+kqBVT744OUBrB09khgFKlK1lshVww6qXGsYPZpavoNjJw== +eslint-config-airbnb-base@15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236" + integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== dependencies: - confusing-browser-globals "^1.0.9" - object.assign "^4.1.0" - object.entries "^1.1.1" + confusing-browser-globals "^1.0.10" + object.assign "^4.1.2" + object.entries "^1.1.5" + semver "^6.3.0" eslint-import-resolver-node@^0.3.9: version "0.3.10" @@ -7466,7 +7467,7 @@ object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.4, object.assign@^4.1.7: +object.assign@^4.1.2, object.assign@^4.1.4, object.assign@^4.1.7: version "4.1.7" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== @@ -7478,7 +7479,7 @@ object.assign@^4.1.0, object.assign@^4.1.4, object.assign@^4.1.7: has-symbols "^1.1.0" object-keys "^1.1.1" -object.entries@^1.1.1, object.entries@^1.1.9: +object.entries@^1.1.5, object.entries@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== @@ -8674,7 +8675,7 @@ semver@^5.5.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== -semver@^6.1.2, semver@^6.3.1: +semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== From 27c3c7aaf1edf42b2471390b99ddb02bb8201afe Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Tue, 14 Jul 2026 08:49:38 -0700 Subject: [PATCH 08/11] Restore scrambles build tooling Fix the dependency separator and align ESLint with the newly updated Airbnb configuration so clean installs and pre-commit linting succeed. --- packages/scrambles/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/scrambles/package.json b/packages/scrambles/package.json index b6ec0a7..31b9438 100644 --- a/packages/scrambles/package.json +++ b/packages/scrambles/package.json @@ -22,9 +22,9 @@ "test:ci": "yarn test" }, "devDependencies": { - "eslint": "6.8.0", + "eslint": "8.57.0", "eslint-config-airbnb-base": "15.0.0", - "eslint-plugin-import": "2.32.0" + "eslint-plugin-import": "2.32.0", "jest": "^30.4.2" } } From f906f4e4d4f7fcab22a05cedef24efc43fe4d402 Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Tue, 14 Jul 2026 09:59:12 -0700 Subject: [PATCH 09/11] Add user-scoped feature flags Allow named feature previews to be enabled for selected WCA users while global production flags remain off. Use the gate for solve history so user 8184 can access the preview without exposing it to other users. --- .env.example | 2 ++ compose.yml | 1 + docs/development.md | 5 ++++ server/api/solveHistory.js | 7 +++-- server/api/solveHistory.test.js | 15 ++++++++++ server/features.js | 26 +++++++++++++++++ server/features.test.js | 51 ++++++++++++++++++++++++++++----- 7 files changed, 97 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 5b5a695..17cf5d3 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,8 @@ ROOM_RECONNECT_GRACE_MS=60000 GRAND_PRIX_ENABLED=false # Friend System routes remain disabled through the #188 launch gate. SOCIAL_FEATURES_ENABLED=false +# Comma-separated WCA user IDs approved for the solve-history preview. +FEATURE_SOLVE_HISTORY_USER_IDS= # Manual Compose commands use this tag. scripts/deploy.sh overrides it with # the full deployed commit SHA. APP_IMAGE_TAG=local diff --git a/compose.yml b/compose.yml index 4d00f2e..5d48b33 100644 --- a/compose.yml +++ b/compose.yml @@ -7,6 +7,7 @@ x-app-environment: &app-environment ROOM_RECONNECT_GRACE_MS: ${ROOM_RECONNECT_GRACE_MS:-60000} GRAND_PRIX_ENABLED: ${GRAND_PRIX_ENABLED:-false} SOCIAL_FEATURES_ENABLED: ${SOCIAL_FEATURES_ENABLED:-false} + FEATURE_SOLVE_HISTORY_USER_IDS: ${FEATURE_SOLVE_HISTORY_USER_IDS:-} METRICS_ENABLED: ${METRICS_ENABLED:-true} METRICS_RETENTION_DAYS: ${METRICS_RETENTION_DAYS:-90} METRICS_HASH_SECRET: ${METRICS_HASH_SECRET:-} diff --git a/docs/development.md b/docs/development.md index d28960a..c9ef81e 100644 --- a/docs/development.md +++ b/docs/development.md @@ -81,6 +81,11 @@ environment variables. Important groups are documented in `.env.example`: - CORS origins and metric retention; and - production TLS and backup paths. +An otherwise disabled feature that opts into user targeting can be scoped to selected WCA users with +`FEATURE__USER_IDS`, a comma-separated allowlist of numeric user IDs. +For example, `FEATURE_SOLVE_HISTORY_USER_IDS=8184` enables the solve-history +preview only for that signed-in user while its global production flag remains off. + `.env.example` is a template, not a file automatically sourced by host Node processes. Compose only passes variables referenced in its service environment; export any additional server override explicitly or add it to the relevant diff --git a/server/api/solveHistory.js b/server/api/solveHistory.js index 4bea47c..26f5048 100644 --- a/server/api/solveHistory.js +++ b/server/api/solveHistory.js @@ -1,6 +1,6 @@ const express = require('express'); -const { isFeatureEnabled } = require('../features'); +const { isFeatureEnabledForUser } = require('../features'); const { recordSolveHistoryRequest } = require('../metrics'); const { query } = require('../postgres'); const { stableId } = require('../postgres/dualWrite'); @@ -111,7 +111,7 @@ const solveResponse = (row) => ({ }); const createSolveHistoryRouter = ({ - enabled = isFeatureEnabled('solveHistory'), + enabled = (user) => isFeatureEnabledForUser('solveHistory', user && user.id), postgresQuery = query, recordMetric = recordSolveHistoryRequest, now = () => Date.now(), @@ -123,7 +123,8 @@ const createSolveHistoryRouter = ({ let outcome = 'error'; let count = 0; try { - if (!enabled) { + const featureEnabled = typeof enabled === 'function' ? enabled(req.user) : enabled; + if (!featureEnabled) { outcome = 'feature_disabled'; return res.status(404).json({ code: 'feature_disabled', diff --git a/server/api/solveHistory.test.js b/server/api/solveHistory.test.js index 266c2c8..aa28f4d 100644 --- a/server/api/solveHistory.test.js +++ b/server/api/solveHistory.test.js @@ -27,6 +27,21 @@ describe('solve history API', () => { expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ code: 'feature_disabled' })); }); + it('allows only the configured user when the feature is user-scoped', async () => { + const postgresQuery = jest.fn().mockResolvedValue({ rows: [] }); + const enabled = (user) => user && user.id === 8184; + + const allowed = response(); + await handlerFor({ enabled, postgresQuery })({ query: {}, user: { id: 8184 } }, allowed); + expect(postgresQuery).toHaveBeenCalledTimes(1); + expect(allowed.json).toHaveBeenCalledWith({ solves: [], nextCursor: null }); + + const blocked = response(); + await handlerFor({ enabled, postgresQuery })({ query: {}, user: { id: 8185 } }, blocked); + expect(blocked.status).toHaveBeenCalledWith(404); + expect(postgresQuery).toHaveBeenCalledTimes(1); + }); + it('returns only session-linked authorized PostgreSQL history with stable cursors', async () => { const postgresQuery = jest.fn().mockResolvedValue({ rows: [{ solve_id: '4d8ce8c4-8910-47e9-b39f-79e1280c1e3a', diff --git a/server/features.js b/server/features.js index 5d22b0d..cf58283 100644 --- a/server/features.js +++ b/server/features.js @@ -5,7 +5,33 @@ const FEATURES = Object.freeze({ const isFeatureEnabled = (feature) => FEATURES[feature] === true; +const userFeatureEnvironmentKey = (feature) => `FEATURE_${feature + .replace(/([a-z])([A-Z])/g, '$1_$2') + .toUpperCase()}_USER_IDS`; + +const parseUserIds = (value) => new Set((value || '') + .split(',') + .map((userId) => userId.trim()) + .filter((userId) => /^[1-9]\d*$/.test(userId))); + +const USER_FEATURES = Object.freeze(Object.fromEntries( + Object.keys(FEATURES).map((feature) => [ + feature, + parseUserIds(process.env[userFeatureEnvironmentKey(feature)]), + ]), +)); + +const isFeatureEnabledForUser = (feature, userId) => ( + isFeatureEnabled(feature) + || (typeof userId === 'number' && Number.isSafeInteger(userId) && userId > 0 + ? USER_FEATURES[feature]?.has(String(userId)) === true + : typeof userId === 'string' && /^[1-9]\d*$/.test(userId) + ? USER_FEATURES[feature]?.has(userId) === true + : false) +); + module.exports = { FEATURES, isFeatureEnabled, + isFeatureEnabledForUser, }; diff --git a/server/features.test.js b/server/features.test.js index 8475f3e..f77bcaf 100644 --- a/server/features.test.js +++ b/server/features.test.js @@ -1,31 +1,68 @@ /* eslint-env jest */ const originalNodeEnv = process.env.NODE_ENV; +const originalSolveHistoryUserIds = process.env.FEATURE_SOLVE_HISTORY_USER_IDS; +const originalFriendsUserIds = process.env.FEATURE_FRIENDS_USER_IDS; -const loadFeatures = (nodeEnv) => { +const loadFeatures = ({ nodeEnv, solveHistoryUserIds, friendsUserIds }) => { jest.resetModules(); process.env.NODE_ENV = nodeEnv; + if (solveHistoryUserIds === undefined) { + delete process.env.FEATURE_SOLVE_HISTORY_USER_IDS; + } else { + process.env.FEATURE_SOLVE_HISTORY_USER_IDS = solveHistoryUserIds; + } + if (friendsUserIds === undefined) { + delete process.env.FEATURE_FRIENDS_USER_IDS; + } else { + process.env.FEATURE_FRIENDS_USER_IDS = friendsUserIds; + } // eslint-disable-next-line global-require return require('./features'); }; afterEach(() => { process.env.NODE_ENV = originalNodeEnv; + if (originalSolveHistoryUserIds === undefined) { + delete process.env.FEATURE_SOLVE_HISTORY_USER_IDS; + } else { + process.env.FEATURE_SOLVE_HISTORY_USER_IDS = originalSolveHistoryUserIds; + } + if (originalFriendsUserIds === undefined) { + delete process.env.FEATURE_FRIENDS_USER_IDS; + } else { + process.env.FEATURE_FRIENDS_USER_IDS = originalFriendsUserIds; + } }); describe('feature flags', () => { it('keeps friends out of production', () => { - expect(loadFeatures('production').isFeatureEnabled('friends')).toBe(false); + expect(loadFeatures({ nodeEnv: 'production' }).isFeatureEnabled('friends')).toBe(false); }); it('keeps friends available to local development', () => { - expect(loadFeatures('development').isFeatureEnabled('friends')).toBe(true); + expect(loadFeatures({ nodeEnv: 'development' }).isFeatureEnabled('friends')).toBe(true); }); it('keeps solve history development-only', () => { - expect(loadFeatures('development').isFeatureEnabled('solveHistory')).toBe(true); - expect(loadFeatures('dev').isFeatureEnabled('solveHistory')).toBe(true); - expect(loadFeatures('production').isFeatureEnabled('solveHistory')).toBe(false); - expect(loadFeatures('prod').isFeatureEnabled('solveHistory')).toBe(false); + expect(loadFeatures({ nodeEnv: 'development' }).isFeatureEnabled('solveHistory')).toBe(true); + expect(loadFeatures({ nodeEnv: 'dev' }).isFeatureEnabled('solveHistory')).toBe(true); + expect(loadFeatures({ nodeEnv: 'production' }).isFeatureEnabled('solveHistory')).toBe(false); + expect(loadFeatures({ nodeEnv: 'prod' }).isFeatureEnabled('solveHistory')).toBe(false); + }); + + it('can enable a disabled feature for selected WCA users', () => { + const features = loadFeatures({ + nodeEnv: 'prod', + solveHistoryUserIds: '8184, 42, invalid, 0', + friendsUserIds: '8184', + }); + + expect(features.isFeatureEnabled('solveHistory')).toBe(false); + expect(features.isFeatureEnabledForUser('solveHistory', 8184)).toBe(true); + expect(features.isFeatureEnabledForUser('solveHistory', '42')).toBe(true); + expect(features.isFeatureEnabledForUser('solveHistory', 8185)).toBe(false); + expect(features.isFeatureEnabledForUser('friends', 8184)).toBe(true); + expect(features.isFeatureEnabledForUser('unknown', 8184)).toBe(false); }); }); From f310fdb0fe82a1b90bd80c27e890a275c64f0799 Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Tue, 14 Jul 2026 11:14:06 -0700 Subject: [PATCH 10/11] Fix normal room chat layout Make the chat rail own its width and collapse cleanly, and allow the local client origin to complete WCA login. --- client/src/components/Room/Normal/index.jsx | 50 +++++++++++++------- client/src/components/Room/Panels/Chat.jsx | 52 ++++++++------------- server/config/dev.json | 2 +- 3 files changed, 53 insertions(+), 51 deletions(-) diff --git a/client/src/components/Room/Normal/index.jsx b/client/src/components/Room/Normal/index.jsx index 465bc61..e757dc2 100644 --- a/client/src/components/Room/Normal/index.jsx +++ b/client/src/components/Room/Normal/index.jsx @@ -3,7 +3,6 @@ import clsx from 'clsx'; import PropTypes from 'prop-types'; import { makeStyles } from '@mui/styles'; import { connect } from 'react-redux'; -import Grid from '@mui/material/Grid'; import Paper from '@mui/material/Paper'; import BottomNavigation from '@mui/material/BottomNavigation'; import BottomNavigationAction from '@mui/material/BottomNavigationAction'; @@ -46,12 +45,30 @@ const useStyles = makeStyles((theme) => ({ }, }, container: { + display: 'flex', flexGrow: 1, flexWrap: 'nowrap', + minHeight: 0, }, - panel: { - flexGrow: 1, - transition: `display 5s ${theme.transitions.easing.easeInOut}`, + mainPanel: { + display: 'flex', + flex: '1 1 auto', + minWidth: 0, + }, + chatPanel: { + display: 'flex', + flex: '0 0 24rem', + minWidth: 0, + transition: theme.transitions.create('flex-basis'), + [theme.breakpoints.down('sm')]: { + flexBasis: '100%', + }, + }, + chatPanelClosed: { + flexBasis: '3rem', + [theme.breakpoints.down('sm')]: { + flexBasis: '100%', + }, }, backdrop: { zIndex: theme.zIndex.drawer + 1, @@ -73,6 +90,7 @@ const panels = [{ export const NormalRoom = ({ room, user }) => { const classes = useStyles(); const [currentPanel, setCurrentPanel] = useState(0); + const [chatOpen, setChatOpen] = useState(true); const isAdmin = () => room.admin && room.admin.id === user.id; @@ -91,27 +109,23 @@ export const NormalRoom = ({ room, user }) => { - - +
- - +
- - - + setChatOpen((isOpen) => !isOpen)} /> +
+
({ root: { display: 'flex', - flexGrow: 0, - flexShrink: 1, - width: '20rem', + flexGrow: 1, + minWidth: 0, flexDirection: 'column', height: '100%', - [theme.breakpoints.down('sm')]: { - width: '100%', - flexGrow: 1, - }, - }, - closed: { - flexGrow: 0, - width: 0, - position: 'relative', - border: 'none', }, messages: { display: 'flex', @@ -85,14 +74,8 @@ const useStyles = makeStyles((theme) => ({ 'user-select': 'text', }, toolbarClosed: { - position: 'absolute', - width: '10em', - right: 0, - }, - hiddenOnMobile: { - [theme.breakpoints.down('sm')]: { - display: 'none', - }, + justifyContent: 'center', + padding: 0, }, })); @@ -105,11 +88,10 @@ const UNKOWN_USER = { }; function Chat({ - dispatch, messages, users, user, + dispatch, messages, users, user, open, onToggle, }) { const classes = useStyles(); const [message, setMessage] = useState(''); - const [open, setOpen] = useState(true); const listRef = useRef(); const findUser = (id) => { @@ -157,9 +139,7 @@ function Chat({ return ( - - Chat - - setOpen(!open)}> - {open ? : } - + {open && ( + + Chat + + )} + {onToggle && ( + + {open ? : } + + )} )} > @@ -278,6 +262,8 @@ Chat.propTypes = { id: PropTypes.number, canJoinRoom: PropTypes.bool, }), + open: PropTypes.bool, + onToggle: PropTypes.func, }; Chat.defaultProps = { @@ -287,6 +273,8 @@ Chat.defaultProps = { id: undefined, canJoinRoom: false, }, + open: true, + onToggle: undefined, }; const mapStateToProps = (state) => ({ diff --git a/server/config/dev.json b/server/config/dev.json index 115e325..8caf1c6 100644 --- a/server/config/dev.json +++ b/server/config/dev.json @@ -22,6 +22,6 @@ "clientSecret": "example-secret" }, "cors": { - "origin": ["localhost", "192.168.1.18"] + "origin": ["http://localhost:3000", "localhost", "192.168.1.18"] } } From c63b1239ca0f43e24b6378a4b5452d12a2e67d38 Mon Sep 17 00:00:00 2001 From: Cailyn Sinclair Date: Wed, 15 Jul 2026 15:42:05 -0700 Subject: [PATCH 11/11] Enable user-scoped solve history preview Require authentication before checking solve-history availability so the existing production allowlist can authorize the requested user safely. --- README.md | 7 ++++--- docs/data.md | 11 ++++++----- server/api.js | 6 +----- server/api.test.js | 13 ++++++++++++- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index cb9fde3..39383a0 100644 --- a/README.md +++ b/README.md @@ -114,9 +114,10 @@ RaceSession and starts a new one; earlier attempts and solves remain preserved under their earlier session. Solve penalties use dedicated boolean columns rather than JSON so histories and -statistics remain compact and index-friendly. Development-only -`GET /api/solve-history` reads PostgreSQL session-linked history for the -authenticated participant and is explicitly disabled in production. +statistics remain compact and index-friendly. `GET /api/solve-history` reads +PostgreSQL session-linked history for the authenticated participant. It is +enabled in development and may be enabled for selected production users with +`FEATURE_SOLVE_HISTORY_USER_IDS`. Set `POSTGRES_ENABLED=false` to disable mirroring. Production should set `PGHOST`, `PGDATABASE`, `PGUSER`, and `POSTGRES_PASSWORD`, or provide a diff --git a/docs/data.md b/docs/data.md index f73789e..0e8f395 100644 --- a/docs/data.md +++ b/docs/data.md @@ -51,11 +51,12 @@ new one, preserving the earlier session's attempts and solves. Complete snapshots are reserved for explicit backfill behavior. `GET /api/solve-history` is a PostgreSQL-only, authenticated self-history -endpoint enabled only when `NODE_ENV=development`. It returns only -session-linked solves for a current non-banned room participant; hidden, -expired, and soft-deleted rooms remain readable to that participant. It never -falls back to MongoDB or selects access codes, passwords, membership lists, -moderation data, or email data. +endpoint enabled in development and optionally for selected production users +through `FEATURE_SOLVE_HISTORY_USER_IDS`. It returns only session-linked solves +for a current non-banned room participant; hidden, expired, and soft-deleted +rooms remain readable to that participant. It never falls back to MongoDB or +selects access codes, passwords, membership lists, moderation data, or email +data. The mirror intentionally catches database failures and returns control to the MongoDB-backed request. Monitoring must surface mirror errors because users may diff --git a/server/api.js b/server/api.js index bb4c99a..3a4a510 100644 --- a/server/api.js +++ b/server/api.js @@ -37,11 +37,7 @@ module.exports = (app) => { res.json(req.user.toObject()); }); - if (isFeatureEnabled('solveHistory')) { - router.use('/solve-history', auth, createSolveHistoryRouter()); - } else { - router.use('/solve-history', createSolveHistoryRouter()); - } + router.use('/solve-history', auth, createSolveHistoryRouter()); router.put('/updateUsername', auth, async (req, res) => { try { diff --git a/server/api.test.js b/server/api.test.js index bbc1944..bfef8ce 100644 --- a/server/api.test.js +++ b/server/api.test.js @@ -4,9 +4,10 @@ jest.mock('./models', () => ({ User: { findOne: jest.fn() }, })); -jest.mock('./middlewares/auth.js', () => (req, res, next) => next()); +jest.mock('./middlewares/auth.js', () => jest.fn((req, res, next) => next())); const { User } = require('./models'); +const auth = require('./middlewares/auth.js'); const router = require('./api')(); const updateUsernameHandler = router.stack @@ -74,3 +75,13 @@ describe('username API responses', () => { })); }); }); + +describe('solve history API access', () => { + it('requires authentication before checking the feature flag', () => { + const authLayer = router.stack.find((layer) => ( + String(layer.regexp).includes('solve-history') && layer.handle === auth + )); + + expect(authLayer).toBeDefined(); + }); +});