diff --git a/.env.production b/.env.production index 4bc8765d..8b0a7a3d 100644 --- a/.env.production +++ b/.env.production @@ -3,4 +3,6 @@ NEXT_PUBLIC_API_URL=/api/ NEXT_PUBLIC_SLACK_CLIENT_ID=10831824934.7404945710466 NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-1BFJYBDC76 -NEXT_PUBLIC_RECAPTCHA_KEY=6Le63OUqAAAAABxxDrbaU9OywDLLHqutVwbw7a9d \ No newline at end of file +NEXT_PUBLIC_RECAPTCHA_KEY=6Le63OUqAAAAABxxDrbaU9OywDLLHqutVwbw7a9d + +ENV_FILE=.env.production \ No newline at end of file diff --git a/.env.test b/.env.test index 3eae1f85..29d9257d 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,7 @@ -NEXT_PUBLIC_API_URL=http://localhost:3000/api +NEXTAUTH_URL=http://localhost:3000/api/ +NEXTAUTH_SECRET=testsecret + +NEXT_PUBLIC_API_URL=/api/ DEVELOPER_EMAILS=["test@gmail.com"] @@ -6,4 +9,14 @@ TOA_URL=https://example.com TOA_APP_ID=123 TOA_KEY=456 -DEFAULT_IMAGE=https://example.com/default.jpg \ No newline at end of file +API_URL=/api/ +API_KEY=gearboxiscool + +DEFAULT_IMAGE=https://example.com/default.jpg + +BASE_URL_FOR_PLAYWRIGHT=http://localhost:3000/ +ENABLE_TEST_SIGNIN_ROUTE=true +FALLBACK_MONGODB_URI=mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.5.10 +ENV_FILE=.env.test + +DB=playwright_tests diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4f83cda2..01b9e600 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ version: 2 updates: - package-ecosystem: "npm" # See documentation for possible values directory: "/" # Location of package manifests - assignees: ["renatodellosso", "BanEvading"] + assignees: ["Tr01ler"] schedule: day: "monday" interval: "weekly" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 211da336..987b7490 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,3 +66,9 @@ jobs: - name: Lint run: npm run lint + + e2e_test: + uses: ./.github/workflows/e2e_test.yml + permissions: + contents: read + pull-requests: write diff --git a/.github/workflows/e2e_test.yml b/.github/workflows/e2e_test.yml new file mode 100644 index 00000000..e79336ca --- /dev/null +++ b/.github/workflows/e2e_test.yml @@ -0,0 +1,71 @@ +name: Playwright Tests +on: [workflow_dispatch, workflow_call] +jobs: + e2e_tests: + timeout-minutes: 60 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Make sure to require each shard in GitHub! + shardIndex: [1, 2, 3, 4] + shardTotal: [4] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.12.0 + with: + mongodb-version: "8.0" + + - name: Run Playwright tests + run: npx cross-env NODE_ENV=test playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} + + - name: Upload blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v4 + with: + name: blob-report-${{ matrix.shardIndex }} + path: blob-report + retention-days: 1 + + merge_reports: + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() }} + needs: [e2e_tests] + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + path: all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./all-blob-reports + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 14 diff --git a/.github/workflows/increment_version.yml b/.github/workflows/increment_version.yml index a021286e..46ca6d3d 100644 --- a/.github/workflows/increment_version.yml +++ b/.github/workflows/increment_version.yml @@ -10,7 +10,7 @@ on: jobs: increment: runs-on: ubuntu-latest - if: + if: steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 40dfa349..445a7479 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,11 @@ next-env.d.ts # PWA public/sw.js -public/swe-worker* \ No newline at end of file +public/swe-worker* + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1249c1a7..f12f8f3c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,7 @@ { "recommendations": [ - "ms-azuretools.vscode-docker", - "formulahendry.docker-explorer", "esbenp.prettier-vscode", "github.vscode-github-actions", - "mongodb.mongodb-vscode", "pmneo.tsimporter", "austenc.tailwind-docs", "bradlc.vscode-tailwindcss" diff --git a/LICENSE.md b/LICENSE.md index cbe5ad16..11976172 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -67,90 +67,88 @@ Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. - Section 1 -- Definitions. - a. Adapted Material means material subject to Copyright and Similar - Rights that is derived from or based upon the Licensed Material - and in which the Licensed Material is translated, altered, - arranged, transformed, or otherwise modified in a manner requiring - permission under the Copyright and Similar Rights held by the - Licensor. For purposes of this Public License, where the Licensed - Material is a musical work, performance, or sound recording, - Adapted Material is always produced where the Licensed Material is - synched in timed relation with a moving image. - - b. Adapter's License means the license You apply to Your Copyright - and Similar Rights in Your contributions to Adapted Material in - accordance with the terms and conditions of this Public License. - - c. BY-NC-SA Compatible License means a license listed at - creativecommons.org/compatiblelicenses, approved by Creative - Commons as essentially the equivalent of this Public License. - - d. Copyright and Similar Rights means copyright and/or similar rights - closely related to copyright including, without limitation, - performance, broadcast, sound recording, and Sui Generis Database - Rights, without regard to how the rights are labeled or - categorized. For purposes of this Public License, the rights - specified in Section 2(b)(1)-(2) are not Copyright and Similar - Rights. - - e. Effective Technological Measures means those measures that, in the - absence of proper authority, may not be circumvented under laws - fulfilling obligations under Article 11 of the WIPO Copyright - Treaty adopted on December 20, 1996, and/or similar international - agreements. - - f. Exceptions and Limitations means fair use, fair dealing, and/or - any other exception or limitation to Copyright and Similar Rights - that applies to Your use of the Licensed Material. - - g. License Elements means the license attributes listed in the name - of a Creative Commons Public License. The License Elements of this - Public License are Attribution, NonCommercial, and ShareAlike. - - h. Licensed Material means the artistic or literary work, database, - or other material to which the Licensor applied this Public - License. - - i. Licensed Rights means the rights granted to You subject to the - terms and conditions of this Public License, which are limited to - all Copyright and Similar Rights that apply to Your use of the - Licensed Material and that the Licensor has authority to license. - - j. Licensor means the individual(s) or entity(ies) granting rights - under this Public License. - - k. NonCommercial means not primarily intended for or directed towards - commercial advantage or monetary compensation. For purposes of - this Public License, the exchange of the Licensed Material for - other material subject to Copyright and Similar Rights by digital - file-sharing or similar means is NonCommercial provided there is - no payment of monetary compensation in connection with the - exchange. - - l. Share means to provide material to the public by any means or - process that requires permission under the Licensed Rights, such - as reproduction, public display, public performance, distribution, - dissemination, communication, or importation, and to make material - available to the public including in ways that members of the - public may access the material from a place and at a time - individually chosen by them. - - m. Sui Generis Database Rights means rights other than copyright - resulting from Directive 96/9/EC of the European Parliament and of - the Council of 11 March 1996 on the legal protection of databases, - as amended and/or succeeded, as well as other essentially - equivalent rights anywhere in the world. - - n. You means the individual or entity exercising the Licensed Rights - under this Public License. Your has a corresponding meaning. - +a. Adapted Material means material subject to Copyright and Similar +Rights that is derived from or based upon the Licensed Material +and in which the Licensed Material is translated, altered, +arranged, transformed, or otherwise modified in a manner requiring +permission under the Copyright and Similar Rights held by the +Licensor. For purposes of this Public License, where the Licensed +Material is a musical work, performance, or sound recording, +Adapted Material is always produced where the Licensed Material is +synched in timed relation with a moving image. + +b. Adapter's License means the license You apply to Your Copyright +and Similar Rights in Your contributions to Adapted Material in +accordance with the terms and conditions of this Public License. + +c. BY-NC-SA Compatible License means a license listed at +creativecommons.org/compatiblelicenses, approved by Creative +Commons as essentially the equivalent of this Public License. + +d. Copyright and Similar Rights means copyright and/or similar rights +closely related to copyright including, without limitation, +performance, broadcast, sound recording, and Sui Generis Database +Rights, without regard to how the rights are labeled or +categorized. For purposes of this Public License, the rights +specified in Section 2(b)(1)-(2) are not Copyright and Similar +Rights. + +e. Effective Technological Measures means those measures that, in the +absence of proper authority, may not be circumvented under laws +fulfilling obligations under Article 11 of the WIPO Copyright +Treaty adopted on December 20, 1996, and/or similar international +agreements. + +f. Exceptions and Limitations means fair use, fair dealing, and/or +any other exception or limitation to Copyright and Similar Rights +that applies to Your use of the Licensed Material. + +g. License Elements means the license attributes listed in the name +of a Creative Commons Public License. The License Elements of this +Public License are Attribution, NonCommercial, and ShareAlike. + +h. Licensed Material means the artistic or literary work, database, +or other material to which the Licensor applied this Public +License. + +i. Licensed Rights means the rights granted to You subject to the +terms and conditions of this Public License, which are limited to +all Copyright and Similar Rights that apply to Your use of the +Licensed Material and that the Licensor has authority to license. + +j. Licensor means the individual(s) or entity(ies) granting rights +under this Public License. + +k. NonCommercial means not primarily intended for or directed towards +commercial advantage or monetary compensation. For purposes of +this Public License, the exchange of the Licensed Material for +other material subject to Copyright and Similar Rights by digital +file-sharing or similar means is NonCommercial provided there is +no payment of monetary compensation in connection with the +exchange. + +l. Share means to provide material to the public by any means or +process that requires permission under the Licensed Rights, such +as reproduction, public display, public performance, distribution, +dissemination, communication, or importation, and to make material +available to the public including in ways that members of the +public may access the material from a place and at a time +individually chosen by them. + +m. Sui Generis Database Rights means rights other than copyright +resulting from Directive 96/9/EC of the European Parliament and of +the Council of 11 March 1996 on the legal protection of databases, +as amended and/or succeeded, as well as other essentially +equivalent rights anywhere in the world. + +n. You means the individual or entity exercising the Licensed Rights +under this Public License. Your has a corresponding meaning. Section 2 -- Scope. - a. License grant. +a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, @@ -211,7 +209,7 @@ Section 2 -- Scope. the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). - b. Other rights. +b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, @@ -233,13 +231,12 @@ Section 2 -- Scope. the Licensed Material is used other than for NonCommercial purposes. - Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. - a. Attribution. +a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: @@ -280,7 +277,7 @@ following conditions. information required by Section 3(a)(1)(A) to the extent reasonably practicable. - b. ShareAlike. +b. ShareAlike. In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. @@ -299,69 +296,66 @@ following conditions. Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. - Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: - a. for the avoidance of doubt, Section 2(a)(1) grants You the right - to extract, reuse, reproduce, and Share all or a substantial - portion of the contents of the database for NonCommercial purposes - only; +a. for the avoidance of doubt, Section 2(a)(1) grants You the right +to extract, reuse, reproduce, and Share all or a substantial +portion of the contents of the database for NonCommercial purposes +only; - b. if You include all or a substantial portion of the database - contents in a database in which You have Sui Generis Database - Rights, then the database in which You have Sui Generis Database - Rights (but not its individual contents) is Adapted Material, - including for purposes of Section 3(b); and +b. if You include all or a substantial portion of the database +contents in a database in which You have Sui Generis Database +Rights, then the database in which You have Sui Generis Database +Rights (but not its individual contents) is Adapted Material, +including for purposes of Section 3(b); and - c. You must comply with the conditions in Section 3(a) if You Share - all or a substantial portion of the contents of the database. +c. You must comply with the conditions in Section 3(a) if You Share +all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. - Section 5 -- Disclaimer of Warranties and Limitation of Liability. - a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE - EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS - AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF - ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, - IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, - WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR - PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, - ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT - KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT - ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. - - b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE - TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, - NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, - INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, - COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR - USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN - ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR - DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR - IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. - - c. The disclaimer of warranties and limitation of liability provided - above shall be interpreted in a manner that, to the extent - possible, most closely approximates an absolute disclaimer and - waiver of all liability. - +a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE +EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS +AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF +ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, +IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, +WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, +ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT +KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT +ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + +b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE +TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, +NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, +INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, +COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR +USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN +ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR +DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR +IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + +c. The disclaimer of warranties and limitation of liability provided +above shall be interpreted in a manner that, to the extent +possible, most closely approximates an absolute disclaimer and +waiver of all liability. Section 6 -- Term and Termination. - a. This Public License applies for the term of the Copyright and - Similar Rights licensed here. However, if You fail to comply with - this Public License, then Your rights under this Public License - terminate automatically. +a. This Public License applies for the term of the Copyright and +Similar Rights licensed here. However, if You fail to comply with +this Public License, then Your rights under this Public License +terminate automatically. - b. Where Your right to use the Licensed Material has terminated under - Section 6(a), it reinstates: +b. Where Your right to use the Licensed Material has terminated under +Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the @@ -373,47 +367,45 @@ Section 6 -- Term and Termination. right the Licensor may have to seek remedies for Your violations of this Public License. - c. For the avoidance of doubt, the Licensor may also offer the - Licensed Material under separate terms or conditions or stop - distributing the Licensed Material at any time; however, doing so - will not terminate this Public License. - - d. Sections 1, 5, 6, 7, and 8 survive termination of this Public - License. +c. For the avoidance of doubt, the Licensor may also offer the +Licensed Material under separate terms or conditions or stop +distributing the Licensed Material at any time; however, doing so +will not terminate this Public License. +d. Sections 1, 5, 6, 7, and 8 survive termination of this Public +License. Section 7 -- Other Terms and Conditions. - a. The Licensor shall not be bound by any additional or different - terms or conditions communicated by You unless expressly agreed. - - b. Any arrangements, understandings, or agreements regarding the - Licensed Material not stated herein are separate from and - independent of the terms and conditions of this Public License. +a. The Licensor shall not be bound by any additional or different +terms or conditions communicated by You unless expressly agreed. +b. Any arrangements, understandings, or agreements regarding the +Licensed Material not stated herein are separate from and +independent of the terms and conditions of this Public License. Section 8 -- Interpretation. - a. For the avoidance of doubt, this Public License does not, and - shall not be interpreted to, reduce, limit, restrict, or impose - conditions on any use of the Licensed Material that could lawfully - be made without permission under this Public License. - - b. To the extent possible, if any provision of this Public License is - deemed unenforceable, it shall be automatically reformed to the - minimum extent necessary to make it enforceable. If the provision - cannot be reformed, it shall be severed from this Public License - without affecting the enforceability of the remaining terms and - conditions. - - c. No term or condition of this Public License will be waived and no - failure to comply consented to unless expressly agreed to by the - Licensor. - - d. Nothing in this Public License constitutes or may be interpreted - as a limitation upon, or waiver of, any privileges and immunities - that apply to the Licensor or You, including from the legal - processes of any jurisdiction or authority. +a. For the avoidance of doubt, this Public License does not, and +shall not be interpreted to, reduce, limit, restrict, or impose +conditions on any use of the Licensed Material that could lawfully +be made without permission under this Public License. + +b. To the extent possible, if any provision of this Public License is +deemed unenforceable, it shall be automatically reformed to the +minimum extent necessary to make it enforceable. If the provision +cannot be reformed, it shall be severed from this Public License +without affecting the enforceability of the remaining terms and +conditions. + +c. No term or condition of this Public License will be waived and no +failure to comply consented to unless expressly agreed to by the +Licensor. + +d. Nothing in this Public License constitutes or may be interpreted +as a limitation upon, or waiver of, any privileges and immunities +that apply to the Licensor or You, including from the legal +processes of any jurisdiction or authority. ======================================================================= diff --git a/README.md b/README.md index e05a572b..fe8c3973 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Gearbox: Intuitive, Powerful Scouting at [4026.org](https://4026.org) The final incarnation of Scout Janssen. - Rewritten fully in Typescript, written to be easy to maintain and modular. Features full feature parity with SJ2, whilst remaining simpler, faster and cooler. +Used by 190+ teams worldwide to collect 380,000+ datapoints across 230+ competitions. ## Features @@ -32,10 +32,10 @@ Features full feature parity with SJ2, whilst remaining simpler, faster and cool - Node.js - NPM - A MongoDB instance - - We use Atlas + - We use Mongo Atlas - An SSL certificate saved as `certs/key.pem` and `certs/cert.pem` - Can be generated with OpenSSL -- Secrets: +- Secrets in a `.env` file (see [`environment.d.ts`](environment.d.ts) for a full list): - A Blue Alliance API key - An Orange Alliance API key - A Google OAuth client ID and secret @@ -67,18 +67,84 @@ See the [Gearbox-Terraform](https://github.com/Decatur-Robotics/Gearbox-Terrafor #### Tests -1. Run `npm run test` +Gearbox has both unit tests (via Jest) and E2E tests (via Playwright). + +Unit tests are run with `npm run test`. +E2E tests are run with `npm run e2e`. #### Scripts -There's a few scripts in the /scripts folder that can be run with `npx tsx scripts/.ts`. +There's a few scripts in the `/scripts` folder that can be run with `npx tsx scripts/.ts`. ## Contributing -You've made it past set up and are ready to contibure to the future of scouting - here's how. +You've made it past set up and are ready to contribute to the future of scouting - here's how. We recommend you start with issues labelled `good first issue` to get a feel for the codebase. Fork the repo (unless you're part of Decatur Robotics, in which case make a new branch) and then make a pull request to the main branch. We'll review it and, if it looks good, merge it. +## Other Repositories + +Our Terraform code is in a separate repository, [Gearbox-Terraform](https://github.com/Decatur-Robotics/Gearbox-Terraform). + +We've also developed several packages that we use (available through NPM): + +- [mongo-anywhere](https://github.com/Decatur-Robotics/mongo-anywhere) - Provides dependency injection and mocks for MongoDB. Gearbox has wrappers around this package's `DbInterface` types. +- [unified-api](https://github.com/Decatur-Robotics/unified-api) - Provides handling and structure for API routes. +- [unified-api-nextjs](https://github.com/Decatur-Robotics/unified-api-nextjs) - Provides types and templates for Next.js API routes. +- [omit-call-signature](https://github.com/Decatur-Robotics/omit-call-signature) - Provides a type for removing call signatures from another type and a type for removing constructor signatures from a class type. Gearbox doesn't directly use this package, but the `unified-api` package does. + +## Tools Used + +### Codebase + +- Typescript +- Next.js +- NextAuth +- MongoDB + +### Testing + +- Jest +- Playwright + +### Dev Tools + +- GitHub Actions +- Prettier +- ESLint + +### Hosting & Infrastructure + +See the [Gearbox-Terraform](https://github.com/Decatur-Robotics/Gearbox-Terraform) repository for more details. + +- Terraform (stored in the [Gearbox-Terraform](https://github.com/Decatur-Robotics/Gearbox-Terraform) repository) +- HashiCorp Managed Terraform (to apply the Terraform code) +- AWS ECS +- AWS S3 (to store secrets) +- Cloudflare (for DNS) +- MongoDB Atlas +- Docker +- GitHub Actions +- GitHub Container Registry + +### External APIs + +- The Blue Alliance API (for match data) +- The Orange Alliance API (for match data) +- Rollbar (for error tracking and deployment notifications) +- Resend (for email sending) +- Google Analytics +- Google OAuth (for authentication) +- Slack OAuth (for authentication) + +## History + +Gearbox is internally known as Scout Janssen 3 (SJ3). It is the third iteration of the Scout Janssen project, which started in 2019. A brief timeline of the project is as follows: + +- 2019-2020: [Scout Janssen 1](https://github.com/Decatur-Robotics/Scout-Janssen), developed by team members Hayden, Keon, and Carter. Used a Django backend with a plain HTML/CSS/JS frontend. +- 2020-2023: [Scout Janssen 2](https://github.com/Decatur-Robotics/Scout-Janssen-2), developed by Theo, Hayden, and Carter with help from Avyn and Renato. Unlike other iterations, SJ2 used GraphQL, Redis, and Socket.io. +- 2023-present: Gearbox (SJ3), developed by Theo, Renato, Davis, and Colin. + ## Contibutors diff --git a/components/Avatar.tsx b/components/Avatar.tsx index 7455a912..82fe40ad 100644 --- a/components/Avatar.tsx +++ b/components/Avatar.tsx @@ -3,7 +3,7 @@ import { levelToClassName } from "@/lib/Xp"; import { BsGearFill } from "react-icons/bs"; export default function Avatar(props: { - user?: { image: string | undefined; level: number; admin?: boolean }; + user?: { image: string | undefined; level?: number; admin?: boolean }; scale?: string | undefined; // Use "scale-75" for 75% scale, etc. imgHeightOverride?: string | undefined; showLevel?: boolean | undefined; @@ -13,6 +13,7 @@ export default function Avatar(props: { className?: string | undefined; online?: boolean; gearSize?: number; + altText?: string; }) { const { session, status } = useCurrentSession(); const user = props.user ?? session?.user; @@ -24,8 +25,8 @@ export default function Avatar(props: {
- {(props.showLevel ?? true) && ( -
+ {props.showLevel && ( +
LVL: {user?.level}
)} @@ -34,12 +35,12 @@ export default function Avatar(props: { > {"Avatar"}
{admin ? ( -
+
) : ( diff --git a/components/Card.tsx b/components/Card.tsx index b584664a..7ae27ac0 100644 --- a/components/Card.tsx +++ b/components/Card.tsx @@ -14,6 +14,7 @@ export default function Card(props: CardProps) { return (
{color ? ( diff --git a/components/Container.tsx b/components/Container.tsx index 0cc63f42..e9bb660a 100644 --- a/components/Container.tsx +++ b/components/Container.tsx @@ -317,7 +317,7 @@ export default function Container(props: ContainerProps) { "w-16 h-16 btn btn-ghost " + (selected ? "border-2 border-accent" : "border-2") } - key={team._id.toString()} + key={team._id?.toString()} onClick={() => { setSelectedTeamIndex(index); }} diff --git a/components/EditAvatarModal.tsx b/components/EditAvatarModal.tsx new file mode 100644 index 00000000..8ba8e85e --- /dev/null +++ b/components/EditAvatarModal.tsx @@ -0,0 +1,68 @@ +import { useState } from "react"; +import Card from "./Card"; +import ClientApi from "@/lib/api/ClientApi"; +import Avatar from "./Avatar"; +import Image from "next/image"; +import { Session } from "inspector/promises"; +import toast from "react-hot-toast"; + +const api = new ClientApi(); +export default function EditAvatarModal(props: { + currentImg: string; + close: () => void; +}) { + const [newAvatar, setNewAvatar] = useState(props.currentImg); + + async function updateAvatar() { + toast.promise( + api.changePFP(newAvatar).then(() => location.reload()), + { + loading: "Updating profile picture...", + success: "Successfully updated profile picture!", + error: "Failed to update profile picture!", + }, + ); + } + + return ( + + +
+ +
+ + setNewAvatar(e.target.value)} + placeholder="Enter new avatar url" + /> +
+ + +
+
+
+ ); +} diff --git a/components/SignInMenu.tsx b/components/SignInMenu.tsx index 1c9ae53b..66d17c34 100644 --- a/components/SignInMenu.tsx +++ b/components/SignInMenu.tsx @@ -59,7 +59,13 @@ function SignInCard() {

Sign In

{error &&

{error}

} -

Choose a login provider

+

Choose a login provider

+ + You currently have to sign-in + using either your{" "} + original sign-in method or + your email. +
+
+
+ {myMatches + .filter((matchData) => showSubmittedReports || !matchData.completed) + .map((matchData, index) => ( + + ))} +
+ + + ); +} diff --git a/components/competition/CompHeaderCard.tsx b/components/competition/CompHeaderCard.tsx index 2222446d..012fd8ca 100644 --- a/components/competition/CompHeaderCard.tsx +++ b/components/competition/CompHeaderCard.tsx @@ -1,13 +1,31 @@ import { NotLinkedToTba } from "@/lib/client/ClientUtils"; -import { Competition } from "@/lib/Types"; +import { Competition, Match, Report } from "@/lib/Types"; +import { useState } from "react"; import { BiExport } from "react-icons/bi"; +import { FaCalendarDay } from "react-icons/fa"; import { MdAutoGraph, MdQueryStats, MdCoPresent } from "react-icons/md"; +import ViewMatchesModal from "../ViewMatchesModal"; +import { User } from "../../lib/Types"; export default function CompHeaderCard({ comp, + matches, + reports, + user, + matchPathway, }: { comp: Competition | undefined; + matches: Match[]; + reports: Report[]; + user: User | null; + matchPathway: string; }) { + const [viewMatches, setViewMatches] = useState(false); + + async function toggleViewMatches() { + setViewMatches(!viewMatches); + } + return (
@@ -36,8 +54,24 @@ export default function CompHeaderCard({ > Pit Stats +
+
+ {viewMatches && user && ( + + )}
); } diff --git a/components/competition/InsightsAndSettingsCard.tsx b/components/competition/InsightsAndSettingsCard.tsx index 5062d365..0cfd21b9 100644 --- a/components/competition/InsightsAndSettingsCard.tsx +++ b/components/competition/InsightsAndSettingsCard.tsx @@ -64,7 +64,7 @@ export default function InsightsAndSettingsCard(props: { const exportAsCsv = async () => { setExportPending(true); - const res = await api.exportCompAsCsv(comp?._id!).catch((e) => { + const res = await api.exportCompDataAsCsv(comp?._id!).catch((e) => { console.error(e); return { csv: undefined }; }); @@ -82,6 +82,31 @@ export default function InsightsAndSettingsCard(props: { setExportPending(false); }; + async function exportScheduleAsCsv() { + setExportPending(true); + + const res = await api.exportCompScheduleAsCsv(comp?._id!).catch((e) => { + console.error(e); + return { csv: undefined }; + }); + + if (!res) { + console.error("failed to export"); + } + + if (res.csv) { + download( + `${comp?.name ?? "Competition"}Schedule.csv`, + res.csv, + "text/csv", + ); + } else { + console.error("No CSV data returned from server"); + } + + setExportPending(false); + } + const createMatch = async () => { try { await api.createMatch( @@ -255,6 +280,18 @@ export default function InsightsAndSettingsCard(props: { "Export Scouting Data as CSV" )} +
; reports: Report[]; pitReports: Pitreport[]; subjectiveReports: SubjectiveReport[]; @@ -138,13 +139,12 @@ export default function TeamPage(props: { [key: number]: SubjectiveReport[]; }>({}); - const teamNumbers = Array.from( - new Set([ - ...Object.keys(teamReports), - ...Object.keys(pitReports), - ...Object.keys(teamSubjectiveReports), - ]), - ); + const teamNumbers = props.teams; + [ + ...Object.keys(teamReports), + ...Object.keys(pitReports), + ...Object.keys(teamSubjectiveReports), + ].forEach((team) => teamNumbers.add(Number(team))); const [selectedTeam, setSelectedTeam] = useState(); const selectedReports = teamReports[selectedTeam ? selectedTeam : 0]; @@ -212,9 +212,9 @@ export default function TeamPage(props: { }); // Find teams not in team ranking - const missingTeams = teamNumbers.filter( - (team) => !teamRanking.includes(team), - ); + const missingTeams = Array.from(teamNumbers) + .filter((team) => !teamRanking.includes(team.toString())) + .map((team) => team.toString()); return (
diff --git a/environment.d.ts b/environment.d.ts index 77881c1f..ee2020d9 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -56,6 +56,10 @@ declare global { DEPLOY_ID: string; + BASE_URL_FOR_PLAYWRIGHT: string | undefined; + ENABLE_TEST_SIGNIN_ROUTE: string | undefined; + FALLBACK_MONGODB_URI: string | undefined; + NODE_ENV: "development" | "production"; } } diff --git a/eslint.config.mjs b/eslint.config.mjs index e0462f2b..e8345844 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,7 +20,7 @@ const config = [ }, }, { - // Ignores has to go in its own config object + // Ignores has to go in its own config object ignores: ["coverage/**/*", ".next/**/*"], }, ]; diff --git a/index.ts b/index.ts index c88f6195..7b0a3eeb 100644 --- a/index.ts +++ b/index.ts @@ -8,22 +8,26 @@ import { createServer as createServerHttp, } from "http"; import Logger from "./lib/client/Logger"; -import { configDotenv } from "dotenv"; -import getRollbar from "./lib/client/RollbarUtils"; -import reportDeploymentToRollbar from "./lib/reportDeploymentToRollbar"; +import { loadEnvConfig } from "@next/env"; +import getRollbar, { + reportDeploymentToRollbar, +} from "./lib/client/RollbarUtils"; -configDotenv(); +const projectDir = process.cwd(); +loadEnvConfig(projectDir); const logger = new Logger(["STARTUP"]); logger.log("Starting server..."); -const dev = process.env.NODE_ENV !== "production"; +const mode = process.env.NODE_ENV; -logger.debug("Constants set"); +logger.info("Constants set. Using mode:", mode); const useHttps = - existsSync("./certs/key.pem") && existsSync("./certs/cert.pem"); + mode !== "test" && + existsSync("./certs/key.pem") && + existsSync("./certs/cert.pem"); const httpsOptions = useHttps ? { @@ -32,10 +36,13 @@ const httpsOptions = useHttps } : {}; -const port = useHttps ? 443 : 80; +const port = useHttps ? 443 : mode == "test" ? 3000 : 80; logger.debug(`Using port ${port}`); -const app = next({ dev, port }); +const app = next({ + dev: mode == "development", + port, +}); const handle = app.getRequestHandler(); logger.debug("App preparing..."); diff --git a/jest.config.ts b/jest.config.ts index e16b603b..9bca3a92 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -143,7 +143,7 @@ const config: Config = { // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test - setupFiles: ["/lib/testutils/setup.ts"], + setupFiles: ["/lib/testutils/JestSetup.ts"], // A list of paths to modules that run some code to configure or set up the testing framework before each test // setupFilesAfterEnv: [], @@ -164,10 +164,7 @@ const config: Config = { // testLocationInResults: false, // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], + testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(test).[tj]s?(x)"], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ diff --git a/lib/CompetitionHandling.ts b/lib/CompetitionHandling.ts index 8ea481b5..75ee044c 100644 --- a/lib/CompetitionHandling.ts +++ b/lib/CompetitionHandling.ts @@ -10,7 +10,7 @@ import { League, } from "./Types"; import { ObjectId } from "bson"; -import { rotateArray } from "./client/ClientUtils"; +import { rotateArray, shuffleArray } from "./client/ClientUtils"; import { games } from "./games"; import { GameId } from "./client/GameId"; import CollectionId from "./client/CollectionId"; @@ -29,6 +29,7 @@ export function generateSchedule( matchCount: number, robotsPerMatch: number, ) { + scouters = shuffleArray(scouters); const schedule = []; for (let i = 0; i < matchCount; i++) { const subjectiveScouter = diff --git a/lib/Enums.ts b/lib/Enums.ts index ccd7a6f0..79c45916 100644 --- a/lib/Enums.ts +++ b/lib/Enums.ts @@ -109,3 +109,19 @@ export namespace ReefscapeEnums { Shallow = "Shallow", } } + +export namespace DecodeEnums { + export enum EndgameParkStatus { + No = "No", + Partial = "Partial", + Full = "Full", + TwoBotPark = "Two Bot Park", + } + + export enum AutoCapabilities { + NoAuto = "No Auto", + MovePastStart = "Move Past Start", + ScoreOneArtifact = "Score One Artifact", + ScoreMultipleArtifacts = "Score Multiple Artifacts", + } +} diff --git a/lib/Layout.ts b/lib/Layout.ts index 01ef80d1..841b9625 100644 --- a/lib/Layout.ts +++ b/lib/Layout.ts @@ -10,6 +10,7 @@ import { IntoTheDeepEnums, FtcDrivetrain, ReefscapeEnums, + DecodeEnums, } from "./Enums"; import { PitReportData, QuantData, Pitreport, Report, League } from "./Types"; @@ -217,6 +218,8 @@ export function keyToType( ReefscapeEnums.Climbing, ReefscapeEnums.DriveThroughDeepCage, ReefscapeEnums.EndgameClimbStatus, + DecodeEnums.AutoCapabilities, + DecodeEnums.EndgameParkStatus, ]; if (key === "Defense") return Defense; @@ -231,6 +234,9 @@ export function keyToType( if (key == "DriveThroughDeepCage") return ReefscapeEnums.DriveThroughDeepCage; if (key == "EndgameClimbStatus") return ReefscapeEnums.EndgameClimbStatus; + if (key == "EndgameParkStatusDecode") return DecodeEnums.EndgameParkStatus; + if (key == "AutoAbilities") return DecodeEnums.AutoCapabilities; + for (const e of enums) { if (Object.values(e).includes(exampleData[key])) return e; } diff --git a/lib/MongoDB.ts b/lib/MongoDB.ts index d196dca9..f1ae7e1a 100644 --- a/lib/MongoDB.ts +++ b/lib/MongoDB.ts @@ -11,22 +11,29 @@ import { default as BaseMongoDbInterface } from "mongo-anywhere/MongoDbInterface import CachedDbInterface from "./client/dbinterfaces/CachedDbInterface"; import { cacheOptions } from "./client/dbinterfaces/CachedDbInterface"; import { findObjectBySlugLookUp } from "./slugToId"; +import { loadEnvConfig } from "@next/env"; -if (!process.env.MONGODB_URI) { +let uri = process.env.MONGODB_URI ?? process.env.FALLBACK_MONGODB_URI; + +if (!uri) { // Necessary to allow connections from files running outside of Next - require("dotenv").config(); + const projectDir = process.cwd(); + loadEnvConfig(projectDir); + + uri = process.env.MONGODB_URI ?? process.env.FALLBACK_MONGODB_URI; - if (!process.env.MONGODB_URI) - console.error('Invalid/Missing environment variable: "MONGODB_URI"'); + if (!uri) + console.warn( + 'Invalid/Missing environment variables: "MONGODB_URI", "FALLBACK_MONGODB_URI". Using default connection string.', + ); } -const uri = process.env.MONGODB_URI ?? "mongodb://localhost:27017"; const options: MongoClientOptions = { maxPoolSize: 3 }; let client; let clientPromise: Promise; -if (!global.clientPromise) { +if (uri && !global.clientPromise) { client = new MongoClient(uri, options); global.clientPromise = client.connect(); } @@ -37,21 +44,19 @@ export { clientPromise }; export async function getDatabase( useCache: boolean = true, ): Promise { - if (!global.interface) { - await clientPromise; + if (global.interface) return global.interface; // Return the existing instance if already created - const mongo = new MongoDBInterface(clientPromise); + await clientPromise; - const dbInterface = useCache - ? new CachedDbInterface(mongo, cacheOptions) - : mongo; - await dbInterface.init(); - global.interface = dbInterface; + const mongo = new MongoDBInterface(clientPromise); - return dbInterface; - } + const dbInterface = useCache + ? new CachedDbInterface(mongo, cacheOptions) + : mongo; + await dbInterface.init(); + global.interface = dbInterface; - return global.interface; + return dbInterface; } export class MongoDBInterface diff --git a/lib/ResendUtils.ts b/lib/ResendUtils.ts index a2a0f468..8efd88fd 100644 --- a/lib/ResendUtils.ts +++ b/lib/ResendUtils.ts @@ -11,10 +11,11 @@ export interface ResendInterface { } export class ResendUtils implements ResendInterface { - private static resend: Resend; + private static resend: Resend | undefined; constructor() { - ResendUtils.resend ??= new Resend(process.env.SMTP_PASSWORD); + if (process.env.SMTP_PASSWORD) + ResendUtils.resend ??= new Resend(process.env.SMTP_PASSWORD); } async createContact(rawUser: NextAuthUser) { @@ -31,7 +32,7 @@ export class ResendUtils implements ResendInterface { const nameParts = user.name?.split(" "); - const res = await ResendUtils.resend.contacts.create({ + const res = await ResendUtils.resend?.contacts.create({ email: user.email, firstName: nameParts[0], lastName: nameParts.length > 1 ? nameParts[1] : "", @@ -39,7 +40,7 @@ export class ResendUtils implements ResendInterface { audienceId: process.env.RESEND_AUDIENCE_ID, }); - if (!res.data?.id) { + if (!res?.data?.id) { console.error("Failed to create contact for", user.email); console.error(res); return; @@ -64,7 +65,7 @@ export class ResendUtils implements ResendInterface { return; } - ResendUtils.resend.emails.send({ + ResendUtils.resend?.emails.send({ from: "Gearbox Server ", to: JSON.parse(process.env.DEVELOPER_EMAILS), // Environment variables are always strings, so we need to parse it subject, diff --git a/lib/TheBlueAlliance.ts b/lib/TheBlueAlliance.ts index c5c3baf9..3fe3456a 100644 --- a/lib/TheBlueAlliance.ts +++ b/lib/TheBlueAlliance.ts @@ -155,6 +155,8 @@ export namespace TheBlueAlliance { } async request(suburl: string): Promise { + if (!this.apiKey) return; + var res = await fetch(this.baseUrl + suburl, { method: "GET", headers: { @@ -334,7 +336,7 @@ export namespace TheBlueAlliance { async allCompetitionsToPairings(year: number) { var allCompetitions = await this.req.getEvents(year); var pairings: CompetitonNameIdPair[] = []; - allCompetitions.forEach((comp) => { + allCompetitions?.forEach((comp) => { pairings.push({ name: comp.name, tbaId: comp.key }); }); diff --git a/lib/Types.ts b/lib/Types.ts index 32cf938f..8127ae02 100644 --- a/lib/Types.ts +++ b/lib/Types.ts @@ -32,6 +32,9 @@ export interface Session extends NextAuthSession { _id: string; sessionToken: string; userId: ObjectId; + /** + * Should actually be a Date + */ expires: string; } diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index 96bc0685..25fc2f5c 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -21,15 +21,16 @@ import { LeaderboardTeam, LinkedList, } from "@/lib/Types"; -import { NotLinkedToTba, removeDuplicates } from "../client/ClientUtils"; +import { + NotLinkedToTba, + removeDuplicates, + toDict, +} from "../client/ClientUtils"; import { addXp, deleteComp, deleteSeason, generatePitReports, - getSeasonFromComp, - getTeamFromMatch, - getTeamFromReport, onTeam, ownsTeam, } from "./ApiUtils"; @@ -51,14 +52,14 @@ import { RequestHelper } from "unified-api"; import { createNextRoute, NextApiTemplate } from "unified-api-nextjs"; import { Report } from "../Types"; import Logger from "../client/Logger"; -import getRollbar, { RollbarInterface } from "../client/RollbarUtils"; const requestHelper = new RequestHelper( process.env.NEXT_PUBLIC_API_URL ?? "", // Replace undefined when env is not present (ex: for testing builds) - (url) => - toast.error( - `Failed API request: ${url}. If this is an error, please contact the developers.`, - ), + (url, error) => { + const msg = `Failed API request: ${url}. Details: ${error}`; + console.error(msg); + toast.error(msg); + }, ); const logger = new Logger(["API"]); @@ -213,7 +214,7 @@ export default class ClientApi extends NextApiTemplate { if (number <= 0) { return res.status(200).send(undefined); } - + console.log("Getting autofill data for team:", number, league); res .status(200) .send( @@ -630,7 +631,7 @@ export default class ClientApi extends NextApiTemplate { competitionReports = createNextRoute< [string, boolean, boolean], - Report[], + { quantReports: Report[]; pitReports: { [team: number]: Pitreport[] } }, ApiDependencies, { team: Team; comp: Competition } >({ @@ -656,19 +657,51 @@ export default class ClientApi extends NextApiTemplate { if (usePublicData && !comp.publicData) usedComps.push(comp); - const reports = ( - await db.findObjects(CollectionId.Reports, { - match: { $in: usedComps.flatMap((m) => m.matches) }, - submitted: submitted ? true : { $exists: true }, - }) - ) - // Filter out comments from other competitions - .map((report) => - comp.matches.includes(report.match) - ? report - : { ...report, data: { ...report.data, comments: "" } }, - ); - return res.status(200).send(reports); + const [reports, pitReports] = await Promise.all([ + ( + await db.findObjects(CollectionId.Reports, { + match: { $in: usedComps.flatMap((m) => m.matches) }, + submitted: submitted ? true : { $exists: true }, + }) + ) + // Filter out comments from other competitions + .map((report) => + comp.matches.includes(report.match) + ? report + : { ...report, data: { ...report.data, comments: "" } }, + ), + ( + await db.findObjects(CollectionId.PitReports, { + _id: { + $in: usedComps + .flatMap((m) => m.pitReports) + .map((id) => new ObjectId(id)), + }, + submitted: submitted ? true : { $exists: true }, + }) + ) + .map((pitReport) => + comp.pitReports.includes(pitReport._id!.toString()) + ? pitReport + : ({ + ...pitReport, + data: { ...pitReport.data, comments: "" }, + } as Pitreport), + ) + .reduce( + (dict, pitReport) => { + dict[pitReport.teamNumber] ??= []; + dict[pitReport.teamNumber].push(pitReport); + return dict; + }, + {} as { [team: number]: Pitreport[] }, + ), + ]); + + return res.status(200).send({ + quantReports: reports, + pitReports: pitReports, + }); }, }); @@ -696,6 +729,28 @@ export default class ClientApi extends NextApiTemplate { }, }); + getTeamsAtComp = createNextRoute< + [string], + number[], + ApiDependencies, + { team: Team; comp: Competition } + >({ + isAuthorized: (req, res, deps, [compId]) => + AccessLevels.IfOnTeamThatOwnsComp(req, res, deps, compId), + handler: async (req, res, { db: dbPromise }, { team, comp }, [compId]) => { + const db = await dbPromise; + + const pitReports = await db.findObjects(CollectionId.PitReports, { + _id: { + $in: comp.pitReports.map((pitReportId) => new ObjectId(pitReportId)), + }, + }); + return res + .status(200) + .send(pitReports.map((pitReport) => pitReport.teamNumber)); + }, + }); + matchReports = createNextRoute< [string], Report[], @@ -1069,7 +1124,7 @@ export default class ClientApi extends NextApiTemplate { }, }); - exportCompAsCsv = createNextRoute< + exportCompDataAsCsv = createNextRoute< [string], { csv: string }, ApiDependencies, @@ -1129,6 +1184,93 @@ export default class ClientApi extends NextApiTemplate { }, }); + exportCompScheduleAsCsv = createNextRoute< + [string], + { csv: string }, + ApiDependencies, + { team: Team; comp: Competition } + >({ + isAuthorized: (req, res, deps, [compId]) => + AccessLevels.IfCompOwner(req, res, deps, compId), + handler: async ( + req, + res, + { db: dbPromise, userPromise }, + { team, comp }, + [compId], + ) => { + const db = await dbPromise; + + const matches = await db.findObjects(CollectionId.Matches, { + _id: { $in: comp.matches.map((matchId) => new ObjectId(matchId)) }, + }); + const reports = await db.findObjects(CollectionId.Reports, { + match: { $in: matches.map((match) => match?._id?.toString()) }, + }); + + if (reports.length == 0) { + return res + .status(200) + .send({ error: "No reports found for competition" }); + } + + const users = await db.findObjects(CollectionId.Users, { + _id: { + $in: reports + .map((r) => r.user) + .concat(matches.map((m) => m.subjectiveScouter)) + .flat() + .map((id) => new ObjectId(id)), + }, + }); + + const reportsById = toDict(reports); + const usersById = toDict(users); + + interface Row { + matchNumber: string; + quantScouters: string[]; + subjectiveScouter: string; + } + + const rows: Row[] = [ + // Headers + { + matchNumber: "Match #", + quantScouters: matches[0].reports.map( + (_, index) => `Scouter ${index + 1}`, + ), + subjectiveScouter: "Subjective Scouter", + }, + ]; + + for (const match of matches) { + rows.push({ + matchNumber: match.number.toString(), + quantScouters: match.reports.map((id) => + reportsById[id].user ? usersById[reportsById[id].user].name! : "", + ), + subjectiveScouter: match.subjectiveScouter + ? usersById[match.subjectiveScouter].name! + : "", + }); + } + + const headers = Object.values(rows[0]).flat(); + + let csv = ""; + for (const row of rows) { + csv += + Object.values(row) + .flat() + .map((str) => str.replace(",", "")) + .join(",") + "\n"; + } + + res.status(200).send({ csv }); + }, + }); + teamCompRanking = createNextRoute< [string, number], { place: number | string; max: number | string }, @@ -2567,4 +2709,55 @@ export default class ClientApi extends NextApiTemplate { return res.status(200).send({ result: "success" }); }, }); + + /** + * Creates a user and session, and then returns the session token. Used in E2E tests. + */ + testSignIn = createNextRoute< + [], + { + sessionToken: string; + user: User; + }, + ApiDependencies, + void + >({ + isAuthorized: () => + Promise.resolve({ + authorized: process.env.ENABLE_TEST_SIGNIN_ROUTE === "true", + authData: undefined, + }), + handler: async (req, res, { db: dbPromise }, authData, args) => { + const db = await dbPromise; + + const user = await db.addObject( + CollectionId.Users, + new User( + "Test User", + "test@gmail.com", + process.env.DEFAULT_IMAGE, + false, + await GenerateSlug(db, CollectionId.Users, "Test User"), + [], + [], + undefined, + 0, + 0, + ), + ); + + const session = await db.addObject(CollectionId.Sessions, { + sessionToken: crypto.randomUUID().toString(), + userId: user._id as unknown as ObjectId, + expires: new Date( + Date.now() + 7 * 24 * 60 * 60 * 1000, + ) as unknown as string, // 1 week expiration + }); + + return res.status(200).send({ + sessionToken: session.sessionToken, + user, + }); + }, + }); } diff --git a/lib/client/ClientUtils.ts b/lib/client/ClientUtils.ts index e0f85d77..10f471ca 100644 --- a/lib/client/ClientUtils.ts +++ b/lib/client/ClientUtils.ts @@ -1,3 +1,6 @@ +import { Pitreport } from "../Types"; +import { MostCommonValue } from "./StatsMath"; + export function getIdsInProgressFromTimestamps(timestamps: { [id: string]: string; }) { @@ -151,3 +154,37 @@ export function promisify( export async function wait(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export function mergePitReports(reports: Pitreport[]) { + if (reports.length === 0) { + throw new Error("Cannot merge 0 pit reports"); + } + + if (reports.length === 1) { + return reports[0]; + } + + const dataKeys = removeDuplicates( + reports.reduce( + (acc, report) => [...acc, ...Object.keys(report.data!)], + [] as string[], + ), + ); + + const newReport = { + teamNumber: reports[0].teamNumber, + data: dataKeys.reduce( + (acc, key) => { + acc[key] = MostCommonValue(key, reports); + return acc; + }, + {} as Record, + ), + } as Pitreport; + + for (const report of reports) { + if (report.data?.comments) newReport.data!.comments = report.data.comments; + } + + return newReport; +} diff --git a/lib/client/GameId.ts b/lib/client/GameId.ts index d2f946cc..2dcbdb15 100644 --- a/lib/client/GameId.ts +++ b/lib/client/GameId.ts @@ -3,6 +3,7 @@ export enum GameId { CenterStage = "CenterStage", IntoTheDeep = "IntoTheDeep", Reefscape = "Reefscape", + Decode = "Decode", } export const defaultGameId = GameId.Reefscape; diff --git a/lib/client/RollbarUtils.ts b/lib/client/RollbarUtils.ts index 984f3d1b..e2bd0fc1 100644 --- a/lib/client/RollbarUtils.ts +++ b/lib/client/RollbarUtils.ts @@ -13,6 +13,14 @@ export interface RollbarInterface { export default function getRollbar(): RollbarInterface { if (global.rollbar) return global.rollbar; + if (!process.env.ROLLBAR_TOKEN) { + return { + error: (...args: any[]) => console.error(...args), + warn: (...args: any[]) => console.warn(...args), + info: (...args: any[]) => console.info(...args), + debug: (...args: any[]) => console.debug(...args), + }; + } const rollbar = new Rollbar({ accessToken: process.env.ROLLBAR_TOKEN, @@ -23,3 +31,34 @@ export default function getRollbar(): RollbarInterface { global.rollbar = rollbar; return rollbar; } + +export function reportDeploymentToRollbar() { + const deployId = process.env.DEPLOY_ID; + + if (!deployId) { + getRollbar().error("Missing deployId in environment variables"); + return; + } + + if (!process.env.ROLLBAR_TOKEN) { + console.warn("ROLLBAR_TOKEN is not set. Cannot report deployment."); + return; + } + + const url = "https://api.rollbar.com/api/1/deploy/" + deployId; + const options = { + method: "PATCH", + headers: { + accept: "application/json", + "content-type": "application/json", + "X-Rollbar-Access-Token": process.env.ROLLBAR_TOKEN, + }, + body: JSON.stringify({ + status: "succeeded", + }), + }; + + fetch(url, options) + .then(() => console.log("Deployment reported to Rollbar")) + .catch((err) => getRollbar().error(err)); +} diff --git a/lib/client/StatsMath.ts b/lib/client/StatsMath.ts index 8464971e..61b49d71 100644 --- a/lib/client/StatsMath.ts +++ b/lib/client/StatsMath.ts @@ -6,11 +6,16 @@ export const AmpAutoPoints = 2; export const AmpTeleopPoints = 1; export const TrapPoints = 5; +export const ArtifactPoints = 3; +export const MotifArtifactPoints = 5; +export const OverflowArtifactPoints = 1; +export const DepotArtifactPoints = 1; + type Selector = ((r: T) => number) | (keyof T & string); function getSelection( selector: Selector, - report: Report, + report: Record, ) { return typeof selector === "string" ? report.data[selector] @@ -53,13 +58,13 @@ export function NumericalTotal( */ export function MostCommonValue( selector: Selector, - reports: Report[], + objects: Record[], ) { // Get a list of all values of the specified field let values: string[] = []; - reports?.forEach((report) => { + objects?.forEach((report) => { const val = getSelection(selector, report); - values.push((val as any).toString?.() ?? JSON.stringify(val)); + values.push((val as any)?.toString?.() ?? JSON.stringify(val)); }); // Count the occurrences of each value @@ -146,3 +151,33 @@ export function ComparativePercentMulti( return results; } + +//Takes a list of Quantitative reports and a stat and returns the minimum value recorded for said stat +export function GetMinimum( + quantitativeReports: Report[], + stat: string, +) { + if (!quantitativeReports) return 0; + let minimum = quantitativeReports[0].data[stat]; + for (let repo of quantitativeReports) { + if (repo.data[stat] < minimum) { + minimum = repo.data[stat]; + } + } + return minimum; +} + +//Takes a list of Quantitative reports and a stat and returns the maximum value recorded for said stat +export function GetMaximum( + quantitativeReports: Report[], + stat: string, +) { + if (!quantitativeReports) return 0; + let maximum = 0; + for (let repo of quantitativeReports) { + if (repo.data[stat] > maximum) { + maximum = repo.data[stat]; + } + } + return maximum; +} diff --git a/lib/dev/FakeData.ts b/lib/dev/FakeData.ts index 9782f082..c9e71d49 100644 --- a/lib/dev/FakeData.ts +++ b/lib/dev/FakeData.ts @@ -9,8 +9,8 @@ import { ObjectId } from "bson"; import CollectionId from "../client/CollectionId"; import DbInterface from "../client/dbinterfaces/DbInterface"; -const firstNameMaleURL = "https://www.randomlists.com/data/names-male.json"; -const firstNameFemaleURL = "https://www.randomlists.com/data/names-female.json"; +const firstNameMaleURL = "https://www.randomlists.com/male-names"; +const firstNameFemaleURL = "https://www.randomlists.com/female-names"; var cachedFirstNames: string[] = []; var cachedLastNames: string[] = []; @@ -43,7 +43,7 @@ export async function fakeUser( db: DbInterface, teamId: ObjectId | undefined, ): Promise { - const name = await randomName(); + const name = String(Math.random() * 101); const user = new User( name, "totallyrealemail@gmail.com", diff --git a/lib/games.ts b/lib/games.ts index fff434e3..839e2a4b 100644 --- a/lib/games.ts +++ b/lib/games.ts @@ -1,6 +1,7 @@ import { Dot } from "@/components/stats/Heatmap"; import { CenterStageEnums, + DecodeEnums, Defense, FrcDrivetrain, IntakeTypes, @@ -21,14 +22,21 @@ import { GameId } from "./client/GameId"; import { AmpAutoPoints, AmpTeleopPoints, + ArtifactPoints, BooleanAverage, + DepotArtifactPoints, + GetMinimum, MostCommonValue, + MotifArtifactPoints, NumericalTotal, + OverflowArtifactPoints, Round, SpeakerAutoPoints, SpeakerTeleopPoints, TrapPoints, } from "./client/StatsMath"; +import { report } from "process"; +import { GetMaximum } from "./client/StatsMath"; function getBaseBadges( pitReport: Pitreport | undefined, @@ -1323,23 +1331,76 @@ namespace Reefscape { const statsLayout: StatsLayout = { sections: { Auto: [ - { key: "AutoMovedPastStaringLine", label: "Avg Auto Moves Past Start" }, { key: "AutoCoralScoredLevelOne", label: "Avg Amt Of Coral Scored Level One Auto", }, + { + label: "> Min Auto L1 Coral", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelOne"); + }, + }, + { + label: "> Max Auto L1 Coral", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelOne"); + }, + }, { key: "AutoCoralScoredLevelTwo", label: "Avg Amt Of Coral Scored Level Two Auto", }, + { + label: "> Min Auto L2 Coral", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelTwo"); + }, + }, + { + label: "> Max Auto L2 Coral", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelTwo"); + }, + }, { key: "AutoCoralScoredLevelThree", label: "Avg Amt Of Coral Scored Level Three Auto", }, + { + label: "> Min Auto L3 Coral", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "AutoCoralScoredLevelThree", + ); + }, + }, + { + label: "> Max Auto L3 Coral", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "AutoCoralScoredLevelThree", + ); + }, + }, { key: "AutoCoralScoredLevelFour", label: "Avg Amt Of Coral Scored Level Four Auto", }, + { + label: "> Min Auto L4 Coral", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoCoralScoredLevelFour"); + }, + }, + { + label: "> Max Auto L4 Coral", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoCoralScoredLevelFour"); + }, + }, { label: "Avg Auto Coral", get(pitData, quantitativeReports) { @@ -1362,33 +1423,140 @@ namespace Reefscape { key: "AutoAlgaeRemovedFromReef", label: "Avg Amt of Algae Removed From Reef", }, + { + label: "> Min Algae Removed From Reef", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoAlgaeRemovedFromReef"); + }, + }, + { + label: "> Max Algae Removed From Reef", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoAlgaeRemovedFromReef"); + }, + }, { key: "AutoAlgaeScoredProcessor", label: "Avg Amt of Algae Scored Processor Auto", }, + { + label: "> Min Algae Scored In Processor", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoAlgaeScoredProcessor"); + }, + }, + { + label: "> Max Algae Scored In Processor", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoAlgaeScoredProcessor"); + }, + }, { key: "AutoAlgaeScoredNet", label: "Avg Amt of Algae Scored Net Auto", }, + { + label: "> Min Algae Scored In Net", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoAlgaeScoredNet"); + }, + }, + { + label: "> Max Algae Scored In Net", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoAlgaeScoredNet"); + }, + }, ], Teleop: [ - { key: "GroundIntake", label: "Has Ground Intake?" }, { key: "TeleopCoralScoredLevelOne", label: "Avg Amt Of Coral Scored Level One Teleop", }, + { + label: "> Min L1 Coral Scored", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopCoralScoredLevelOne", + ); + }, + }, + { + label: "> Max L1 Coral Scored", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopCoralScoredLevelOne", + ); + }, + }, { key: "TeleopCoralScoredLevelTwo", label: "Avg Amt Of Coral Scored Level Two Teleop", }, + { + label: "> Min L2 Coral Scored", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopCoralScoredLevelTwo", + ); + }, + }, + { + label: "> Max L2 Coral Scored", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopCoralScoredLevelTwo", + ); + }, + }, { key: "TeleopCoralScoredLevelThree", label: "Avg Amt Of Coral Scored Level Three Teleop", }, + { + label: "> Min L3 Coral Scored", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopCoralScoredLevelThree", + ); + }, + }, + { + label: "> Max L3 Coral Scored", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopCoralScoredLevelThree", + ); + }, + }, { key: "TeleopCoralScoredLevelFour", label: "Avg Amt Of Coral Scored Level Four Teleop", }, + { + label: "> Min L4 Coral Scored", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopCoralScoredLevelFour", + ); + }, + }, + { + label: "> Max L4 Coral Scored", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopCoralScoredLevelFour", + ); + }, + }, { label: "Avg Teleop Coral", get(pitData, quantitativeReports) { @@ -1411,14 +1579,62 @@ namespace Reefscape { key: "TeleopAlgaeRemovedFromReef", label: "Avg Amt of Algae Removed From Reef", }, + { + label: "> Min Algae Removed From Reef", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopAlgaeRemovedFromReef", + ); + }, + }, + { + label: "> Max Algae Removed From Reef", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopAlgaeRemovedFromReef", + ); + }, + }, { key: "TeleopAlgaeScoredProcessor", label: "Avg Amt of Algae Scored Processor Teleop", }, + { + label: "> Min Algae Scored In Processor", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopAlgaeScoredProcessor", + ); + }, + }, + { + label: "> Max Algae Scored In Processor", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopAlgaeScoredProcessor", + ); + }, + }, { key: "TeleopAlgaeScoredNet", label: "Avg Amt of Algae Scored Net Teleop", }, + { + label: "> Min Algae Scored In Net", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "TeleopAlgaeScoredNet"); + }, + }, + { + label: "> Max Algae Scored In Net", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "TeleopAlgaeScoredNet"); + }, + }, ], }, getGraphDots: function ( @@ -1638,7 +1854,9 @@ namespace Reefscape { break; } - totalPoints += report.TeleopCoralScoredLevelOne * 2; + totalPoints += + (report.TeleopCoralScoredLevelOne + report.TeleopAlgaeScoredProcessor) * + 2; totalPoints += (report.AutoCoralScoredLevelOne + report.TeleopCoralScoredLevelTwo) * 3; totalPoints += @@ -1649,9 +1867,7 @@ namespace Reefscape { 4; totalPoints += report.TeleopCoralScoredLevelFour * 5; totalPoints += - (report.AutoAlgaeScoredProcessor + - report.TeleopAlgaeScoredProcessor + - report.AutoCoralScoredLevelThree) * + (report.AutoAlgaeScoredProcessor + report.AutoCoralScoredLevelThree) * 6; totalPoints += report.AutoCoralScoredLevelFour * 7; } @@ -1677,9 +1893,383 @@ namespace Reefscape { ); } +namespace Decode { + export class QuantitativeData extends QuantData { + AutoMovedPastStartingLine: boolean = false; + + AutoArtifactsClassified: number = 0; + AutoOverflowArtifacts: number = 0; + AutoMotifArtifacts: number = 0; + + TeleopArtifactsClassified: number = 0; + TeleopOverflowArtifacts: number = 0; + TeleopMotifArtifacts: number = 0; + TeleopDepotArtifacts: number = 0; + + EndgameParkStatusDecode: DecodeEnums.EndgameParkStatus = + DecodeEnums.EndgameParkStatus.No; + EndgameDefense: Defense = Defense.None; + } + + export class PitData extends PitReportData { + CanScoreClassifier: boolean = false; + CanScoreDepot: boolean = false; + CanOpenGate: boolean = false; + CanParkWithOtherBots: boolean = false; + + ArtifactsScoredAuto: number = 0; + AutoAccountsForMotif: boolean = false; + AutoAbilities: DecodeEnums.AutoCapabilities = + DecodeEnums.AutoCapabilities.NoAuto; + } + + const pitReportLayout: FormLayoutProps = { + Capabilities: [ + { key: "CanScoreClassifier", label: "Can Score Classifier?" }, + { key: "CanScoreDepot", label: "Can Score Depot?" }, + { key: "CanOpenGate", label: "Can Score Gate?" }, + { key: "CanParkWithOtherBots", label: "Can Park With Other Bots?" }, + ], + Auto: [ + { key: "ArtifactsScoredAuto", label: "Average Auto Artifacts" }, + { key: "AutoAccountsForMotif", label: "Auto Accounts For Motif?" }, + { key: "AutoAbilities", label: "Other Auto Scoring Capabilities" }, + ], + }; + + const quantitativeReportLayout: FormLayoutProps = { + Auto: [ + { key: "AutoMovedPastStart", label: "Moved Past Starting line" }, + [ + [ + { + key: "AutoArtifactsClassified", + label: "Artifacts Classified (Auto)", + }, + ], + [{ key: "AutoOverflowArtifacts", label: "Overflow Artifacts (Auto)" }], + [{ key: "AutoMotifArtifacts", label: "Motif Artifacts (Auto)" }], + ], + ], + Teleop: [ + [ + [ + { + key: "TeleopArtifactsClassified", + label: "Artifacts Classified (Teleop)", + }, + { + key: "TeleopOverflowArtifacts", + label: "Overflow Artifacts (Teleop)", + }, + ], + [ + { + key: "TeleopMotifArtifacts", + label: "Motif Artifacts (Teleop)", + }, + { + key: "TeleopDepotArtifacts", + label: "Depot Artifacts (Teleop)", + }, + ], + ], + ], + Endgame: ["Defense", "EndgameParkStatusDecode"], + }; + + const statsLayout: StatsLayout = { + sections: { + Auto: [ + { + key: "AutoArtifactsClassified", + label: "Average Amt Of Artifacts Classified Auto", + }, + { + label: "> Min Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoArtifactsClassified"); + }, + }, + { + label: "> Max Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoArtifactsClassified"); + }, + }, + { + key: "AutoOverflowArtifacts", + label: "Average Amt Of Overflow Artifacts Classified Auto", + }, + { + label: "> Min Overflow Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoOverflowArtifacts"); + }, + }, + { + label: "> Max Overflow Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoOverflowArtifacts"); + }, + }, + { + key: "AutoMotifArtifacts", + label: "Average Amt Of Motif Artifacts Classified Auto", + }, + { + label: "> Min Motif Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "AutoMotifArtifacts"); + }, + }, + { + label: "> Max Motif Artifacts Classified Auto", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "AutoMotifArtifacts"); + }, + }, + ], + Teleop: [ + { + key: "TeleopArtifactsClassified", + label: "Average Amt Of Artifacts Classified Teleop", + }, + { + label: "> Min Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMinimum( + quantitativeReports!, + "TeleopArtifactsClassified", + ); + }, + }, + { + label: "> Max Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMaximum( + quantitativeReports!, + "TeleopArtifactsClassified", + ); + }, + }, + { + key: "TeleopOverflowArtifacts", + label: "Average Amt Of Overflow Artifacts Classified Teleop", + }, + { + label: "> Min Overflow Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "TeleopOverflowArtifacts"); + }, + }, + { + label: "> Max Overflow Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "TeleopOverflowArtifacts"); + }, + }, + { + key: "TeleopMotifArtifacts", + label: "Average Amt Of Motif Artifacts Classified Teleop", + }, + { + label: "> Min Motif Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "TeleopMotifArtifacts"); + }, + }, + { + label: "> Max Motif Artifacts Classified Teleop", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "TeleopMotifArtifacts"); + }, + }, + { + key: "TeleopDepotArtifacts", + label: "Average Amt of Artifacts In Depot Teleop", + }, + { + label: "> Min Depot Artifacts Teleop", + get(pitData, quantitativeReports) { + return GetMinimum(quantitativeReports!, "TeleopDepotArtifacts"); + }, + }, + { + label: "> Max Depot Artifacts Teleop", + get(pitData, quantitativeReports) { + return GetMaximum(quantitativeReports!, "TeleopDepotArtifacts"); + }, + }, + ], + }, + getGraphDots: function ( + quantitativeReports: Report[], + pitReport?: Pitreport | undefined, + ): Dot[] { + return []; + }, + }; + + const pitStatsLayout: PitStatsLayout = { + overallSlideStats: [ + { label: "Artifacts Scored Auto", key: "ArtifactsScoredAuto" }, + ], + individualSlideStats: [ + { + label: "Average Auto Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) { + return 0; + } + + const Artifacts = + NumericalTotal("AutoArtifactsClassified", quantitativeReports) * + ArtifactPoints; + + const MotifArtifacts = + NumericalTotal("AutoMotifArtifacts", quantitativeReports) * + MotifArtifactPoints; + + const OverflowArtifacts = + NumericalTotal("AutoOverflowArtifacts", quantitativeReports) * + OverflowArtifactPoints; + + return ( + (Artifacts + MotifArtifacts + OverflowArtifacts) / + quantitativeReports.length + ); + }, + }, + { + label: "Average Teleop Points", + get: ( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + ) => { + if (!quantitativeReports) { + return 0; + } + + const Artifacts = + NumericalTotal("TeleopArtifactsClassified", quantitativeReports) * + ArtifactPoints; + + const MotifArtifacts = + NumericalTotal("TeleopMotifArtifacts", quantitativeReports) * + MotifArtifactPoints; + + const OverflowArtifacts = + NumericalTotal("TeleopOverflowArtifacts", quantitativeReports) * + OverflowArtifactPoints; + + const DepotArtifacts = + NumericalTotal("TeleopDepotArtifacts", quantitativeReports) * + DepotArtifactPoints; + + return ( + (Artifacts + MotifArtifacts + OverflowArtifacts + DepotArtifacts) / + quantitativeReports.length + ); + }, + }, + ], + robotCapabilities: [ + { key: "CanScoreClassifier", label: "Can Score Classifier?" }, + { key: "CanScoreDepot", label: "Can Score Depot?" }, + { key: "CanOpenGate", label: "Can Open Gate?" }, + { key: "CanParkWithOtherBots", label: "Can Park With Other Bots" }, + ], + graphStat: { + label: "TeleopArtifactsClassified", + key: "TeleopArtifactsClassified", + }, + }; + + function getBadges( + pitReport: Pitreport | undefined, + quantitativeReports: Report[] | undefined, + card: boolean, + ) { + const badges: Badge[] = getBaseBadges(pitReport, quantitativeReports); + + if (pitReport?.data?.CanOpenGate) + badges.push({ text: "Can Open Gate", color: "info" }); + if (pitReport?.data?.CanScoreDepot) + badges.push({ text: "Can Score Depot", color: "secondary" }); + if (pitReport?.data?.CanScoreClassifier) + badges.push({ text: "Can Score Classifier", color: "primary" }); + if (pitReport?.data?.CanParkWithOtherBots) + badges.push({ text: "Can Double Park", color: "success" }); + + if ( + !(pitReport?.data?.CanScoreDepot || pitReport?.data?.CanScoreClassifier) + ) + badges.push({ text: "Cannot Score", color: "warning" }); + + return badges; + } + + function getAvgPoints(reports: Report[] | undefined) { + if (!reports) return 0; + + let totalPoints = 0; + + for (const report of reports.map((r) => r.data)) { + switch (report.EndgameParkStatus) { + case DecodeEnums.EndgameParkStatus.No: + break; + case DecodeEnums.EndgameParkStatus.Partial: + totalPoints += 5; + break; + case DecodeEnums.EndgameParkStatus.Full: + totalPoints += 10; + break; + case DecodeEnums.EndgameParkStatus.TwoBotPark: + totalPoints += 20; + break; + } + + totalPoints += + (report.AutoArtifactsClassified + report.TeleopArtifactsClassified) * + ArtifactPoints; + totalPoints += + (report.AutoMotifArtifacts + report.TeleopMotifArtifacts) * + MotifArtifactPoints; + totalPoints += + (report.AutoOverflowArtifacts + report.TeleopOverflowArtifacts) * + OverflowArtifactPoints; + totalPoints += report.TeleopDepotArtifacts * DepotArtifactPoints; + } + + return totalPoints / Math.max(reports.length, 1); + } + + export const game = new Game( + "Decode", + 2026, + League.FTC, + QuantitativeData, + PitData, + pitReportLayout, + quantitativeReportLayout, + statsLayout, + pitStatsLayout, + "Decode", + "https://info.firstinspires.org/hs-fs/hubfs/2026%20Season/Season%20Assets/first_age_ftc_decode_logo_vertical_rgb_fullcolor.png?width=237&height=348&name=first_age_ftc_decode_logo_vertical_rgb_fullcolor.png", + "invert", + getBadges, + getAvgPoints, + ); +} + export const games: { [id in GameId]: Game } = Object.freeze({ [GameId.Reefscape]: Reefscape.game, [GameId.IntoTheDeep]: IntoTheDeep.game, [GameId.Crescendo]: Crescendo.game, [GameId.CenterStage]: CenterStage.game, + [GameId.Decode]: Decode.game, }); diff --git a/lib/reportDeploymentToRollbar.ts b/lib/reportDeploymentToRollbar.ts deleted file mode 100644 index cfe4e577..00000000 --- a/lib/reportDeploymentToRollbar.ts +++ /dev/null @@ -1,27 +0,0 @@ -import getRollbar from "./client/RollbarUtils"; - -export default function reportDeploymentToRollbar() { - const deployId = process.env.DEPLOY_ID; - - if (!deployId) { - getRollbar().error("Missing gitSha or deployId in environment variables"); - return; - } - - const url = "https://api.rollbar.com/api/1/deploy/" + deployId; - const options = { - method: "PATCH", - headers: { - accept: "application/json", - "content-type": "application/json", - "X-Rollbar-Access-Token": process.env.ROLLBAR_TOKEN, - }, - body: JSON.stringify({ - status: "succeeded", - }), - }; - - fetch(url, options) - .then(() => console.log("Deployment reported to Rollbar")) - .catch((err) => getRollbar().error(err)); -} diff --git a/lib/testutils/JestSetup.ts b/lib/testutils/JestSetup.ts new file mode 100644 index 00000000..61989856 --- /dev/null +++ b/lib/testutils/JestSetup.ts @@ -0,0 +1,4 @@ +import { loadEnvConfig } from "@next/env"; + +const projectDir = process.cwd(); +loadEnvConfig(projectDir); diff --git a/lib/testutils/PlaywrightSetup.ts b/lib/testutils/PlaywrightSetup.ts new file mode 100644 index 00000000..d45ecea6 --- /dev/null +++ b/lib/testutils/PlaywrightSetup.ts @@ -0,0 +1,6 @@ +import { loadEnvConfig } from "@next/env"; + +export default function setup() { + const projectDir = process.cwd(); + loadEnvConfig(projectDir); +} diff --git a/lib/testutils/TestUtils.ts b/lib/testutils/TestUtils.ts index d7f80896..74dce5e5 100644 --- a/lib/testutils/TestUtils.ts +++ b/lib/testutils/TestUtils.ts @@ -17,6 +17,8 @@ import { ResendInterface } from "../ResendUtils"; import { SlackInterface } from "../SlackClient"; import { NextResponse } from "unified-api-nextjs"; import { RollbarInterface } from "../client/RollbarUtils"; +import { BrowserContext, Page } from "@playwright/test"; +import ClientApi from "../api/ClientApi"; export class TestRes extends NextResponse { status = jest.fn((code) => this); @@ -160,3 +162,71 @@ export async function createTestDocuments(db: DbInterface) { return { report, subjectiveReport, match, pitReport, comp, season, team }; } + +export namespace PlaywrightUtils { + export function getTestClientApi() { + const api = new ClientApi(); + + // Relative requests don't work in Playwright apparently + if ( + process.env.BASE_URL_FOR_PLAYWRIGHT && + !api.requestHelper.baseUrl.startsWith(process.env.BASE_URL_FOR_PLAYWRIGHT) + ) { + api.requestHelper.baseUrl = + process.env.BASE_URL_FOR_PLAYWRIGHT + api.requestHelper.baseUrl; + } + + return api; + } + + /** + * Will reload the page + */ + export async function signUp(page: Page) { + const { sessionToken, user } = await getTestClientApi().testSignIn(); + + if (!sessionToken || !user) { + throw new Error("Failed to sign in"); + } + + await signIn(page, sessionToken); + + return { + sessionToken, + user, + }; + } + + /** + * Will reload the page + */ + export async function signIn(page: Page, sessionToken: string) { + await page.context().addCookies([ + { + name: "next-auth.session-token", + value: sessionToken, + path: "/", + domain: "localhost", + sameSite: "Lax", + httpOnly: true, + secure: true, + expires: Math.floor(Date.now() / 1000) + 60 * 60 * 24, // 1 day expiration + }, + ]); + + // It sometimes requires a reload and a fetch to get sign ins to register + await getUser(page); + await page.reload(); + } + + export async function getUser(page: Page) { + const res = await page.context().request.get("/api/auth/session"); + + if (res.ok()) { + const { user } = await res.json(); + return user as User; + } else { + throw new Error("Failed to get user"); + } + } +} diff --git a/lib/testutils/setup.ts b/lib/testutils/setup.ts deleted file mode 100644 index 967c95da..00000000 --- a/lib/testutils/setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as dotenv from "dotenv"; - -dotenv.config({ path: ".env.test" }); diff --git a/package-lock.json b/package-lock.json index 3ee4c646..9d3d3dc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,72 +1,74 @@ { "name": "sj3", - "version": "1.2.23", + "version": "1.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.2.23", + "version": "1.3.5", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", "@next-auth/mongodb-adapter": "^1.1.3", - "@serwist/next": "^9.0.11", + "@serwist/next": "^9.0.13", "@types/http-proxy": "^1.17.15", "@types/react-dom": "18.3.1", "@types/socket.io-client": "^3.0.0", "@yudiel/react-qr-scanner": "^2.2.1", - "bootstrap": "^5.3.3", + "bootstrap": "^5.3.5", "browser-image-compression": "^2.0.2", "bson": "^5.0.0", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "eslint": "9.18.0", - "eslint-config-next": "15.2.2", - "formidable": "^3.5.2", - "jose": "^6.0.8", + "eslint-config-next": "15.2.4", + "formidable": "^3.5.4", + "jose": "^6.0.10", "levenary": "^1.1.1", - "minimongo": "^6.19.0", - "mongo-anywhere": "^1.1.11", + "minimongo": "^7.0.0", + "mongo-anywhere": "^1.1.15", "mongodb": "^5.0.0", - "next": "^15.2.3", + "next": "^15.2.6", "next-auth": "^4.24.11", "next-seo": "^6.6.0", + "nodemailer": "^7.0.12", "omit-call-signature": "^1.0.15", "react": "18.3.1", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.10.9", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "react-ga4": "^2.1.0", "react-google-recaptcha-v3": "^1.10.1", "react-hot-toast": "^2.5.1", - "react-icons": "^5.4.0", + "react-icons": "^5.5.0", "react-p5": "^1.4.1", "react-qr-code": "^2.0.15", - "resend": "^4.1.2", + "resend": "^4.3.0", "rollbar": "^2.26.4", "string-similarity-js": "^2.1.4", "ts-node": "^10.9.2", "tsx": "^4.19.3", - "typescript": "5.7.3", - "unified-api-nextjs": "^1.0.9" + "typescript": "5.8.3", + "unified-api-nextjs": "^1.1.3" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.17.0", + "@eslint/js": "^9.24.0", "@jest/globals": "^29.7.0", + "@playwright/test": "^1.52.0", "@types/formidable": "^3.4.5", "@types/jest": "^29.5.14", - "@types/node": "^22.13.0", + "@types/node": "^22.19.3", "@types/react": "^18.3.8", "autoprefixer": "^10.4.21", "cross-env": "^7.0.3", "daisyui": "^4.12.22", "jest": "^29.7.0", "postcss": "^8.5.3", - "prettier": "3.5.0", + "prettier": "3.5.3", "serwist": "^9.0.11", "tailwindcss": "^3.4.17", "ts-jest": "^29.2.5" @@ -106,13 +108,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -132,6 +136,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -337,19 +342,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -364,109 +371,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/types": "^7.28.5" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -709,14 +635,15 @@ } }, "node_modules/@babel/template": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -753,14 +680,14 @@ } }, "node_modules/@babel/types": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -792,6 +719,16 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", @@ -1256,105 +1193,567 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", - "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", - "dependencies": { - "@eslint/core": "^0.10.0", - "levn": "^0.4.1" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "license": "Apache-2.0", + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12.22" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", - "license": "Apache-2.0", + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=18.18" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -1450,10 +1849,11 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -1908,8 +2308,7 @@ "node_modules/@kurkle/color": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", - "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", - "peer": true + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.9", @@ -1930,15 +2329,15 @@ } }, "node_modules/@next/env": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.3.tgz", - "integrity": "sha512-a26KnbW9DFEUsSxAxKBORR/uD9THoYoKbkpFywMN/AFvboTt94b8+g/07T8J6ACsdLag8/PDU60ov4rPxRAixw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", + "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.2.tgz", - "integrity": "sha512-1+BzokFuFQIfLaRxUKf2u5In4xhPV7tUgKcK53ywvFl6+LXHWHpFkcV7VNeKlyQKUotwiq4fy/aDNF9EiUp4RQ==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.2.4.tgz", + "integrity": "sha512-O8ScvKtnxkp8kL9TpJTTKnMqlkZnS+QxwoQnJwPGBxjBbzd6OVVPEJ5/pMNrktSyXQD/chEfzfFzYLM6JANOOQ==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" @@ -1973,9 +2372,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.3.tgz", - "integrity": "sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz", + "integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==", "cpu": [ "arm64" ], @@ -1989,9 +2388,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.3.tgz", - "integrity": "sha512-pVwKvJ4Zk7h+4hwhqOUuMx7Ib02u3gDX3HXPKIShBi9JlYllI0nU6TWLbPT94dt7FSi6mSBhfc2JrHViwqbOdw==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz", + "integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==", "cpu": [ "x64" ], @@ -2005,9 +2404,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.3.tgz", - "integrity": "sha512-50ibWdn2RuFFkOEUmo9NCcQbbV9ViQOrUfG48zHBCONciHjaUKtHcYFiCwBVuzD08fzvzkWuuZkd4AqbvKO7UQ==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz", + "integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==", "cpu": [ "arm64" ], @@ -2021,9 +2420,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.3.tgz", - "integrity": "sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz", + "integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==", "cpu": [ "arm64" ], @@ -2037,9 +2436,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.3.tgz", - "integrity": "sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.7.tgz", + "integrity": "sha512-hvXcZvCaaEbCZcVzcY7E1uXN9xWZfFvkNHwbe/n4OkRhFWrs1J1QV+4U1BN06tXLdaS4DazEGXwgqnu/VMcmqw==", "cpu": [ "x64" ], @@ -2053,9 +2452,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.3.tgz", - "integrity": "sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz", + "integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==", "cpu": [ "x64" ], @@ -2069,9 +2468,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.3.tgz", - "integrity": "sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz", + "integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==", "cpu": [ "arm64" ], @@ -2085,9 +2484,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.3.tgz", - "integrity": "sha512-gHYS9tc+G2W0ZC8rBL+H6RdtXIyk40uLiaos0yj5US85FNhbFEndMA2nW3z47nzOWiSvXTZ5kBClc3rD0zJg0w==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz", + "integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==", "cpu": [ "x64" ], @@ -2100,6 +2499,18 @@ "node": ">= 10" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2132,11 +2543,6 @@ "node": ">= 8" } }, - "node_modules/@one-ini/wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", - "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==" - }, "node_modules/@panva/hkdf": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", @@ -2145,6 +2551,15 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -2154,10 +2569,28 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -2194,12 +2627,13 @@ "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" }, "node_modules/@react-email/render": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz", - "integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz", + "integrity": "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==", + "license": "MIT", "dependencies": { "html-to-text": "9.0.5", - "js-beautify": "^1.14.11", + "prettier": "3.5.3", "react-promise-suspense": "0.3.4" }, "engines": { @@ -2278,6 +2712,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" @@ -2287,15 +2722,16 @@ } }, "node_modules/@serwist/build": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@serwist/build/-/build-9.0.11.tgz", - "integrity": "sha512-mYBBpm6hWN40J/Sj2aNncVgl/o7Ch/CfyHHl3XvEecbowRuJNv8BfIch+Tvwlfd1cqBqNc65kCYvCL2XJms8DA==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@serwist/build/-/build-9.2.3.tgz", + "integrity": "sha512-UU38GDsTerzoCRDIT5v62W/CcTMLfGZm/tAa+u8XLBU0y0f2aJ2GCfsHnI1eXhEuWvt4Y7Imx/uG2ww2KWIBWQ==", + "license": "MIT", "dependencies": { "common-tags": "1.8.2", - "glob": "10.4.5", + "glob": "10.5.0", "pretty-bytes": "6.1.1", "source-map": "0.8.0-beta.0", - "zod": "3.23.8" + "zod": "4.1.12" }, "engines": { "node": ">=18.0.0" @@ -2310,17 +2746,19 @@ } }, "node_modules/@serwist/build/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@serwist/build/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -2340,6 +2778,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2354,6 +2793,8 @@ "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "license": "BSD-3-Clause", "dependencies": { "whatwg-url": "^7.0.0" }, @@ -2365,6 +2806,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", "dependencies": { "punycode": "^2.1.0" } @@ -2372,12 +2814,14 @@ "node_modules/@serwist/build/node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" }, "node_modules/@serwist/build/node_modules/whatwg-url": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", @@ -2385,17 +2829,18 @@ } }, "node_modules/@serwist/next": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@serwist/next/-/next-9.0.11.tgz", - "integrity": "sha512-Aa26qgJxnaxbGK8JYAWqAPeDcyfu0xjYUPpvSfc3d9GrIl9G9cdflkcsvcbmJGnOb3Oc4tHN4wncYO6cIKb2wQ==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@serwist/next/-/next-9.2.3.tgz", + "integrity": "sha512-sbStIaen8H6ZhNCtfxAuZ2haujJe8nPFRBiq5KQFYYi5iyFrDGLpYICmRTBP6Oc1pQBmsAqbBZzIjjC60d+7zg==", + "license": "MIT", "dependencies": { - "@serwist/build": "9.0.11", - "@serwist/webpack-plugin": "9.0.11", - "@serwist/window": "9.0.11", - "chalk": "5.3.0", - "glob": "10.4.5", - "serwist": "9.0.11", - "zod": "3.23.8" + "@serwist/build": "9.2.3", + "@serwist/webpack-plugin": "9.2.3", + "@serwist/window": "9.2.3", + "chalk": "5.6.2", + "glob": "10.5.0", + "serwist": "9.2.3", + "zod": "4.1.12" }, "engines": { "node": ">=18.0.0" @@ -2411,17 +2856,19 @@ } }, "node_modules/@serwist/next/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@serwist/next/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -2430,9 +2877,10 @@ } }, "node_modules/@serwist/next/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -2452,6 +2900,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2463,13 +2912,14 @@ } }, "node_modules/@serwist/webpack-plugin": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@serwist/webpack-plugin/-/webpack-plugin-9.0.11.tgz", - "integrity": "sha512-ZTxVJqv7nKlPWmhC3oMt7tZc0ssnd7SYr6L8zZLLF7ltJUub9TMyh1K1zL5TJtsr8DyfALNFPwPA/s2yjYSk+w==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@serwist/webpack-plugin/-/webpack-plugin-9.2.3.tgz", + "integrity": "sha512-y1Ag1a7LARo/LtPVRSrfGO5PG4BZdc1NiMGAq+CGlBH/PJa7RQimo4iq9C94pjxusEsP4yHnsDQ06+GVTQ6gww==", + "license": "MIT", "dependencies": { - "@serwist/build": "9.0.11", + "@serwist/build": "9.2.3", "pretty-bytes": "6.1.1", - "zod": "3.23.8" + "zod": "4.1.12" }, "engines": { "node": ">=18.0.0" @@ -2488,12 +2938,13 @@ } }, "node_modules/@serwist/window": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/@serwist/window/-/window-9.0.11.tgz", - "integrity": "sha512-hUJVNxZbqfnJbUTfuJew7SR3f1KBIv/4nwmP1YRxjR6EMthUfN4bVSLzWLW8/u3h5bNaVh7sOhopUVC8m1HTCQ==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@serwist/window/-/window-9.2.3.tgz", + "integrity": "sha512-6bK01QAaFI+oXTmDSQHvsw0kTTNlj79PFkox+0Z9XYNqTVLoXY9C6BFmZNvxNxEinOqCSm7cms4I5V54E9/1XA==", + "license": "MIT", "dependencies": { "@types/trusted-types": "2.0.7", - "serwist": "9.0.11" + "serwist": "9.2.3" }, "peerDependencies": { "typescript": ">=5.0.0" @@ -2533,11 +2984,6 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2939,12 +3385,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz", - "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==", + "version": "22.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz", + "integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==", "license": "MIT", + "peer": true, "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/p5": { @@ -3012,7 +3459,8 @@ "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" }, "node_modules/@types/warning": { "version": "3.0.3", @@ -3063,19 +3511,12 @@ "react-dom": "^17 || ^18 || ^19" } }, - "node_modules/abbrev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", - "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3627,9 +4068,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", + "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", "funding": [ { "type": "github", @@ -3640,6 +4081,7 @@ "url": "https://opencollective.com/bootstrap" } ], + "license": "MIT", "peerDependencies": { "@popperjs/core": "^2.11.8" } @@ -3650,9 +4092,10 @@ "integrity": "sha512-N/toHA87JcmAHPqU8Qt7YnhFK6W2WUpdq5M1k/JqLdTqtts7sHEMZhFjFWTvvR2poKF7Qki0qknhIPIr5I7TIQ==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3697,6 +4140,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -3745,17 +4189,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -4004,20 +4437,6 @@ "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4034,17 +4453,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4069,6 +4477,7 @@ "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -4078,15 +4487,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, "node_modules/console-polyfill": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/console-polyfill/-/console-polyfill-0.3.0.tgz", @@ -4201,7 +4601,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true }, "node_modules/culori": { "version": "3.3.0", @@ -4317,15 +4718,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/decache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/decache/-/decache-3.1.0.tgz", - "integrity": "sha512-p7D6wJ5EJFFq1CcF2lu1XeqKFLBob8jRQGNAvFLTsV3CbSKBl3VtliAVlUIGz2i9H6kEFnI2Amaft5ZopIG2Fw==", - "optional": true, - "dependencies": { - "find": "^0.2.4" - } - }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -4413,9 +4805,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "optional": true, "engines": { @@ -4492,6 +4884,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -4510,12 +4903,14 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -4530,6 +4925,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -4540,9 +4936,10 @@ } }, "node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -4554,61 +4951,14 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/editorconfig": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", - "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", - "dependencies": { - "@one-ini/wasm": "0.1.1", - "commander": "^10.0.0", - "minimatch": "9.0.1", - "semver": "^7.5.3" - }, - "bin": { - "editorconfig": "bin/editorconfig" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/editorconfig/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "engines": { - "node": ">=14" - } - }, - "node_modules/editorconfig/node_modules/minimatch": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz", - "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" } }, "node_modules/ejs": { @@ -4686,6 +5036,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -4927,6 +5278,7 @@ "version": "9.18.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4982,12 +5334,12 @@ } }, "node_modules/eslint-config-next": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.2.tgz", - "integrity": "sha512-g34RI7RFS4HybYFwGa/okj+8WZM+/fy+pEM+aqRQoVvM4gQhKrd4wIEddKmlZfWD75j8LTwB5zwkmNv3DceH1A==", + "version": "15.2.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.2.4.tgz", + "integrity": "sha512-v4gYjd4eYIme8qzaJItpR5MMBXJ0/YV07u7eb50kEnlEmX7yhOjdUdzz70v4fiINYRjLf8X8TbogF0k7wlz6sA==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.2.2", + "@next/eslint-plugin-next": "15.2.4", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -5100,6 +5452,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.17.0.tgz", "integrity": "sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.17.0", "@typescript-eslint/types": "8.17.0", @@ -5199,9 +5552,9 @@ } }, "node_modules/eslint-config-next/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5306,6 +5659,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.8", @@ -5499,6 +5853,15 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", + "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", @@ -5744,10 +6107,11 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -5775,15 +6139,6 @@ "node": ">=8" } }, - "node_modules/find": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/find/-/find-0.2.9.tgz", - "integrity": "sha512-7a4/LCiInB9xYMnAUEjLilL9FKclwbwK7VlXw+h5jMvT2TDFeYFCHM24O1XdnC/on/hx8mxVO3FTQkyHZnOghQ==", - "optional": true, - "dependencies": { - "traverse-chain": "~0.1.0" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5849,16 +6204,33 @@ "node": "*" } }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/formidable": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.2.tgz", - "integrity": "sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==", + "version": "3.5.4", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", + "integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==", "license": "MIT", "dependencies": { + "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", - "hexoid": "^2.0.0", "once": "^1.4.0" }, + "engines": { + "node": ">=14.0.0" + }, "funding": { "url": "https://ko-fi.com/tunnckoCore/commissions" } @@ -6216,15 +6588,6 @@ "node": ">= 0.4" } }, - "node_modules/hexoid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-2.0.0.tgz", - "integrity": "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -6243,6 +6606,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", "dependencies": { "@selderee/plugin-htmlparser2": "^0.11.0", "deepmerge": "^4.3.1", @@ -6265,6 +6629,7 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", @@ -6296,9 +6661,10 @@ } }, "node_modules/idb": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.0.tgz", - "integrity": "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw==" + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", + "license": "ISC" }, "node_modules/idb-wrapper": { "version": "1.7.2", @@ -6373,11 +6739,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -6399,10 +6760,14 @@ "loose-envify": "^1.0.0" } }, - "node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==" + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } }, "node_modules/is-array-buffer": { "version": "3.0.4", @@ -6931,6 +7296,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -7823,14 +8189,15 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "devOptional": true, + "peer": true, "bin": { "jiti": "bin/jiti.js" } }, "node_modules/jose": { - "version": "6.0.8", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.8.tgz", - "integrity": "sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==", + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.10.tgz", + "integrity": "sha512-skIAxZqcMkOrSwjJvplIPYrlXGpxTPnro2/QWTDCxAdWQrSTV5/KqspMWmi5WAx5+ULswASJiZ0a+1B/Lxt9cw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -7841,75 +8208,6 @@ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" }, - "node_modules/js-beautify": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz", - "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==", - "dependencies": { - "config-chain": "^1.1.13", - "editorconfig": "^1.0.4", - "glob": "^10.3.3", - "js-cookie": "^3.0.5", - "nopt": "^7.2.0" - }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/js-beautify/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/js-beautify/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-beautify/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "engines": { - "node": ">=14" - } - }, "node_modules/js-sha1": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/js-sha1/-/js-sha1-0.6.0.tgz", @@ -7921,9 +8219,9 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8062,6 +8360,7 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", "funding": { "url": "https://ko-fi.com/killymxi" } @@ -8148,7 +8447,8 @@ "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", @@ -8272,9 +8572,10 @@ } }, "node_modules/minimongo": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/minimongo/-/minimongo-6.19.0.tgz", - "integrity": "sha512-DF2FLEJ6vTJSz7QsWrm2FMq2UR2yowwXPKhgQ69mxT5KOm/bYcrVv1RDw3zyeeYw/I7a/2MkiRWyV1EglniONA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/minimongo/-/minimongo-7.0.0.tgz", + "integrity": "sha512-kFF0oXLr8GKv4mZ2HCMNSwNjpDsGtXr5WfdnBcYwzSAvgS3HNf2UMbbhrBDG5ElYJ0im7QAQxFkSj8S66g0cvg==", + "license": "LGPLv3", "dependencies": { "@turf/boolean-crosses": "^6.0.1", "@turf/boolean-point-in-polygon": "^6.0.1", @@ -8305,9 +8606,9 @@ } }, "node_modules/mongo-anywhere": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.1.11.tgz", - "integrity": "sha512-wM5FMS7sj6vZAEw9XaRaWFYqNeA8slKcQxxWdhyN4a8xGBrOxN2U4wF33kuAkYTgqsdy5fxS3NSF4KkQAUq5Zg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.1.15.tgz", + "integrity": "sha512-wN6E/jN0lae5EqAeaAaE5fdUdb+ZchZKib3FWGOOOQUYZvTv2ino9Aii3GK+uIMxNdnm6N//B0bEGEeCNbBy1g==", "dependencies": { "bson": "^5.0.0", "minimongo": "^6.19.0", @@ -8315,10 +8616,38 @@ "node-cache": "^5.1.2" } }, + "node_modules/mongo-anywhere/node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "license": "MIT" + }, + "node_modules/mongo-anywhere/node_modules/minimongo": { + "version": "6.19.0", + "resolved": "https://registry.npmjs.org/minimongo/-/minimongo-6.19.0.tgz", + "integrity": "sha512-DF2FLEJ6vTJSz7QsWrm2FMq2UR2yowwXPKhgQ69mxT5KOm/bYcrVv1RDw3zyeeYw/I7a/2MkiRWyV1EglniONA==", + "license": "LGPLv3", + "dependencies": { + "@turf/boolean-crosses": "^6.0.1", + "@turf/boolean-point-in-polygon": "^6.0.1", + "@turf/boolean-within": "^6.0.1", + "@turf/distance": "^6.5.0", + "@turf/helpers": "^6.5.0", + "@turf/intersect": "^6.1.3", + "@turf/nearest-point-on-line": "^6.5.0", + "async": "^1.4.2", + "bowser": "^0.7.1", + "idb-wrapper": "^1.4.1", + "jquery": "^3.6.0", + "js-sha1": "^0.6.0", + "lodash": "^4.0.0" + } + }, "node_modules/mongodb": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "peer": true, "dependencies": { "bson": "^5.5.0", "mongodb-connection-string-url": "^2.6.0", @@ -8403,15 +8732,14 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/next": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/next/-/next-15.2.3.tgz", - "integrity": "sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==", + "version": "15.5.7", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", + "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", "license": "MIT", + "peer": true, "dependencies": { - "@next/env": "15.2.3", - "@swc/counter": "0.1.3", + "@next/env": "15.5.7", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -8423,19 +8751,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.2.3", - "@next/swc-darwin-x64": "15.2.3", - "@next/swc-linux-arm64-gnu": "15.2.3", - "@next/swc-linux-arm64-musl": "15.2.3", - "@next/swc-linux-x64-gnu": "15.2.3", - "@next/swc-linux-x64-musl": "15.2.3", - "@next/swc-win32-arm64-msvc": "15.2.3", - "@next/swc-win32-x64-msvc": "15.2.3", - "sharp": "^0.33.5" + "@next/swc-darwin-arm64": "15.5.7", + "@next/swc-darwin-x64": "15.5.7", + "@next/swc-linux-arm64-gnu": "15.5.7", + "@next/swc-linux-arm64-musl": "15.5.7", + "@next/swc-linux-x64-gnu": "15.5.7", + "@next/swc-linux-x64-musl": "15.5.7", + "@next/swc-win32-arm64-msvc": "15.5.7", + "@next/swc-win32-x64-msvc": "15.5.7", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -8457,10 +8785,11 @@ } }, "node_modules/next-auth": { - "version": "4.24.11", - "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", - "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "version": "4.24.13", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz", + "integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==", "license": "ISC", + "peer": true, "dependencies": { "@babel/runtime": "^7.20.13", "@panva/hkdf": "^1.0.2", @@ -8473,9 +8802,9 @@ "uuid": "^8.3.2" }, "peerDependencies": { - "@auth/core": "0.34.2", - "next": "^12.2.5 || ^13 || ^14 || ^15", - "nodemailer": "^6.6.5", + "@auth/core": "0.34.3", + "next": "^12.2.5 || ^13 || ^14 || ^15 || ^16", + "nodemailer": "^7.0.7", "react": "^17.0.2 || ^18 || ^19", "react-dom": "^17.0.2 || ^18 || ^19" }, @@ -8566,30 +8895,15 @@ } }, "node_modules/nodemailer": { - "version": "6.9.16", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", - "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz", + "integrity": "sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==", "license": "MIT-0", - "optional": true, "peer": true, "engines": { "node": ">=6.0.0" } }, - "node_modules/nopt": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", - "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8912,6 +9226,7 @@ "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", "dependencies": { "leac": "^0.6.0", "peberminta": "^0.9.0" @@ -8974,6 +9289,7 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", "funding": { "url": "https://ko-fi.com/killymxi" } @@ -9081,6 +9397,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/polygon-clipping": { "version": "0.15.7", "resolved": "https://registry.npmjs.org/polygon-clipping/-/polygon-clipping-0.15.7.tgz", @@ -9118,6 +9481,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -9246,6 +9610,7 @@ "version": "10.24.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -9271,10 +9636,9 @@ } }, "node_modules/prettier": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz", - "integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==", - "dev": true, + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -9290,6 +9654,7 @@ "version": "6.1.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "license": "MIT", "engines": { "node": "^14.13.1 || >=16.0.0" }, @@ -9337,11 +9702,6 @@ "react": ">=0.14.0" } }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -9425,6 +9785,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9482,12 +9843,13 @@ } }, "node_modules/react-chartjs-2": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", - "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", "peerDependencies": { "chart.js": "^4.1.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-dnd": { @@ -9532,6 +9894,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -9575,9 +9938,10 @@ } }, "node_modules/react-icons": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", - "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", "peerDependencies": { "react": "*" } @@ -9616,6 +9980,7 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^2.0.1" } @@ -9623,7 +9988,8 @@ "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "license": "MIT" }, "node_modules/react-qr-code": { "version": "2.0.15", @@ -9790,19 +10156,6 @@ "resolved": "https://registry.npmjs.org/request-ip/-/request-ip-3.3.0.tgz", "integrity": "sha512-cA6Xh6e0fDBBBwH77SLJaJPBmD3nWVAcF9/XAcsrIHdjhFzFiB5aNQFytdjCGPezU3ROwrR11IddKAM08vohxA==" }, - "node_modules/request/node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, "node_modules/request/node_modules/uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -9822,12 +10175,12 @@ } }, "node_modules/resend": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/resend/-/resend-4.1.2.tgz", - "integrity": "sha512-km0btrAj/BqIaRlS+SoLNMaCAUUWEgcEvZpycfVvoXEwAHCxU+vp/ikxPgKRkyKyiR2iDcdUq5uIBTDK9oSSSQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-4.3.0.tgz", + "integrity": "sha512-4OBHeusMVSl0vcba2J3AaGzdZ1SXAAhX/Wkcwobe16AHmlW9h3li8wG62Fhvlsc61e+wlQoxcwJZP6WrBTbghQ==", "license": "MIT", "dependencies": { - "@react-email/render": "1.0.1" + "@react-email/render": "1.0.6" }, "engines": { "node": ">=18" @@ -9911,9 +10264,10 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" }, "node_modules/rollbar": { - "version": "2.26.4", - "resolved": "https://registry.npmjs.org/rollbar/-/rollbar-2.26.4.tgz", - "integrity": "sha512-JKmrj6riYm9ZPJisgxljgH4uCsvjMHDHXrinDF7aAFaP+eoF51HomVPtLcDTYLsrJ568aKVNLUhedFajONBwSg==", + "version": "2.26.5", + "resolved": "https://registry.npmjs.org/rollbar/-/rollbar-2.26.5.tgz", + "integrity": "sha512-4Of0ALl5+CU2glyDy5dWMRRy9Ty81DrY2r46ucbqjtCikbgHoWJNGXbQUWpDaLxsc8Q71LT/yj1bPb9NHbJIFQ==", + "license": "MIT", "dependencies": { "async": "~3.2.3", "console-polyfill": "0.3.0", @@ -9922,9 +10276,6 @@ "lru-cache": "~2.2.1", "request-ip": "~3.3.0", "source-map": "^0.5.7" - }, - "optionalDependencies": { - "decache": "^3.0.5" } }, "node_modules/rollbar/node_modules/lru-cache": { @@ -10037,6 +10388,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", "dependencies": { "parseley": "^0.12.0" }, @@ -10045,9 +10397,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10057,11 +10409,12 @@ } }, "node_modules/serwist": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/serwist/-/serwist-9.0.11.tgz", - "integrity": "sha512-oUuyOuIFZoj+ZYdwWnPYVsndOw5CoGsTy106mHJ2wxjnz01TrDhg2l26yrsoJek1qT/Doi0pzknYLLXTIFs5og==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/serwist/-/serwist-9.2.3.tgz", + "integrity": "sha512-menorUH09SpLvU7R25TvG0TuQ12MUDpQ5NKGohbtREF8YI+0D++Abn59XCmvvMp2n740rBizIkeqWq0m6U+++g==", + "license": "MIT", "dependencies": { - "idb": "8.0.0" + "idb": "8.0.3" }, "peerDependencies": { "typescript": ">=5.0.0" @@ -10104,16 +10457,16 @@ } }, "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -10122,25 +10475,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -10190,23 +10548,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -10258,15 +10599,16 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -10357,14 +10699,6 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -10637,19 +10971,21 @@ } }, "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -10802,15 +11138,6 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10845,12 +11172,6 @@ "node": ">=12" } }, - "node_modules/traverse-chain": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", - "integrity": "sha512-up6Yvai4PYKhpNp5PkYtx50m3KbwQrqDwbuZP/ItyL64YEWHAvH6Md83LFLV/GRSk/BoUVwwgUzX6SOQSbsfAg==", - "optional": true - }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -10933,6 +11254,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11130,9 +11452,11 @@ } }, "node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11175,25 +11499,26 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" }, "node_modules/unified-api": { - "version": "1.0.19", - "resolved": "https://registry.npmjs.org/unified-api/-/unified-api-1.0.19.tgz", - "integrity": "sha512-8fQ/fnOTHtEzNTQPPQ+vlFP3jq4vje+7D8adn1yh2TETIkEvgaVvxFbfG4BKyYyp1ZRJxvLH42kYz14WlU8/6A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unified-api/-/unified-api-1.1.4.tgz", + "integrity": "sha512-g9ATByEgoQq2HVrF2X4DYCPW2xqSw+xBh3n6SoqCXwuN6fj6P31lFViaK1yHu9iy2mTHHdQW6IApj+KKNrbShg==", "dependencies": { "omit-call-signature": "^1.0.16" } }, "node_modules/unified-api-nextjs": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/unified-api-nextjs/-/unified-api-nextjs-1.0.10.tgz", - "integrity": "sha512-EkW0g3shBuLkbCkX/I3uQHq+ug31rzsHNGEXnOZc2UHypaD5OfQjKV5irwwGtc0iucwYzkDvKKd8aX6dgXXTkQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/unified-api-nextjs/-/unified-api-nextjs-1.1.3.tgz", + "integrity": "sha512-6y9eMJgJArjJ+K9XpTx2IXic1HQFWYyixPnwZUe07zH1DEMTbq57oOrPHon8kqYT2PUbbyy2P47JjmXkaGxNjA==", "dependencies": { "next": "^15.1.2", - "unified-api": "^1.0.19" + "unified-api": "^1.1.4" } }, "node_modules/update-browserslist-db": { @@ -11664,9 +11989,10 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 9500f4bd..41c2f309 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.23", + "version": "1.3.5", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", @@ -10,6 +10,8 @@ "start": "cross-env NODE_ENV=production npx tsx index.ts", "restart": "next build && cross-env NODE_ENV=production npx tsx index.ts", "test": "jest", + "e2e": "cross-env NODE_ENV=test playwright test", + "e2e-start-server": "cross-env NODE_ENV=test next build && cross-env NODE_ENV=test npx tsx index.ts", "docker-build": "docker build -t gearbox .", "docker-start": "docker run -i -t -p 80:80 gearbox", "docker-prune": "docker container prune", @@ -19,63 +21,65 @@ }, "dependencies": { "@next-auth/mongodb-adapter": "^1.1.3", - "@serwist/next": "^9.0.11", + "@serwist/next": "^9.0.13", "@types/http-proxy": "^1.17.15", "@types/react-dom": "18.3.1", "@types/socket.io-client": "^3.0.0", "@yudiel/react-qr-scanner": "^2.2.1", - "bootstrap": "^5.3.3", + "bootstrap": "^5.3.5", "browser-image-compression": "^2.0.2", "bson": "^5.0.0", "dependencies": "^0.0.1", - "dotenv": "^16.4.7", + "dotenv": "^16.5.0", "eslint": "9.18.0", - "eslint-config-next": "15.2.2", - "formidable": "^3.5.2", - "jose": "^6.0.8", + "eslint-config-next": "15.2.4", + "formidable": "^3.5.4", + "jose": "^6.0.10", "levenary": "^1.1.1", - "minimongo": "^6.19.0", - "mongo-anywhere": "^1.1.11", + "minimongo": "^7.0.0", + "mongo-anywhere": "^1.1.15", "mongodb": "^5.0.0", - "next": "^15.2.3", + "next": "^15.2.6", "next-auth": "^4.24.11", "next-seo": "^6.6.0", + "nodemailer": "^7.0.12", "omit-call-signature": "^1.0.15", "react": "18.3.1", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.10.9", - "react-chartjs-2": "^5.2.0", + "react-chartjs-2": "^5.3.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "18.3.1", "react-ga4": "^2.1.0", "react-google-recaptcha-v3": "^1.10.1", "react-hot-toast": "^2.5.1", - "react-icons": "^5.4.0", + "react-icons": "^5.5.0", "react-p5": "^1.4.1", "react-qr-code": "^2.0.15", - "resend": "^4.1.2", + "resend": "^4.3.0", "rollbar": "^2.26.4", "string-similarity-js": "^2.1.4", "ts-node": "^10.9.2", "tsx": "^4.19.3", - "typescript": "5.7.3", - "unified-api-nextjs": "^1.0.9" + "typescript": "5.8.3", + "unified-api-nextjs": "^1.1.3" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "^9.17.0", + "@eslint/js": "^9.24.0", "@jest/globals": "^29.7.0", + "@playwright/test": "^1.52.0", "@types/formidable": "^3.4.5", "@types/jest": "^29.5.14", - "@types/node": "^22.13.0", + "@types/node": "^22.19.3", "@types/react": "^18.3.8", "autoprefixer": "^10.4.21", "cross-env": "^7.0.3", "daisyui": "^4.12.22", "jest": "^29.7.0", "postcss": "^8.5.3", - "prettier": "3.5.0", + "prettier": "3.5.3", "serwist": "^9.0.11", "tailwindcss": "^3.4.17", "ts-jest": "^29.2.5" diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx index 91442621..ecb101e1 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/index.tsx @@ -186,7 +186,7 @@ export default function CompetitionIndex({ if (!silent) setLoadingReports(true); - let newReports: Report[] = await api.competitionReports( + let { quantReports: newReports } = await api.competitionReports( comp?._id!, false, false, @@ -405,7 +405,13 @@ export default function CompetitionIndex({ >
- + { - const newReports = (await api.competitionReports( + const { quantReports: newReports } = await api.competitionReports( comp._id!, true, usePublicData, - )) as Report[]; + ); const rankings = await api.compRankings(comp.tbaId!); diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index 6f342c26..6bee5996 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -1,5 +1,5 @@ import ClientApi from "@/lib/api/ClientApi"; -import { NotLinkedToTba } from "@/lib/client/ClientUtils"; +import { mergePitReports, NotLinkedToTba } from "@/lib/client/ClientUtils"; import { defaultGameId } from "@/lib/client/GameId"; import { Competition, @@ -41,6 +41,9 @@ export default function Stats(props: { >(props.subjectiveReports); const [page, setPage] = useState(0); const [usePublicData, setUsePublicData] = useState(true); + const [fetchedTeamNumbers, setFetchedTeamNumbers] = useState([]); + const [externalPitReportCount, setExternalPitReportCount] = useState(0); + const [internalPitReportCount, setInternalPitReportCount] = useState(0); useEffect(() => { const i = setInterval(() => { @@ -58,22 +61,39 @@ export default function Stats(props: { const promises = [ api .competitionReports(props.competition._id!, true, usePublicData) - .then((data) => setReports(data)), - pitReports.length === 0 && - props.competition._id && - api.getPitReports(props.competition._id).then((data) => { - setPitReports(data); + .then((data) => { + setReports(data.quantReports); + + const newPitReports: Pitreport[] = []; + let internalPitReportCount = 0, + externalPitReportCount = 0; + for (const teamPitReports of Object.values(data.pitReports)) { + newPitReports.push(mergePitReports(teamPitReports)); + + teamPitReports.forEach((r) => { + if (props.competition.pitReports.includes(r._id!.toString())) { + internalPitReportCount++; + } else { + externalPitReportCount++; + } + }); + } + + setPitReports(newPitReports); + setInternalPitReportCount(internalPitReportCount); + setExternalPitReportCount(externalPitReportCount); }), api .getSubjectiveReportsForComp(props.competition._id!) .then(setSubjectiveReports), + api.getTeamsAtComp(props.competition._id!).then(setFetchedTeamNumbers), ].flat(); await Promise.all(promises); setUpdate(Date.now()); setUpdating(false); - }, [pitReports.length, props.competition._id, usePublicData]); + }, [props.competition._id, props.competition.pitReports, usePublicData]); useEffect(() => { resync(); @@ -85,14 +105,15 @@ export default function Stats(props: { subjectiveReports.forEach((r) => Object.keys(r.robotComments).forEach((c) => teams.add(+c)), ); //+str converts to number + fetchedTeamNumbers.forEach((t) => teams.add(t)); - let internalReportCount = 0, - externalReportCount = 0; + let internalQuantReportCount = 0, + externalQuantReportCount = 0; reports.forEach((r) => { if (props.competition.matches.includes(r.match)) { - internalReportCount++; + internalQuantReportCount++; } else { - externalReportCount++; + externalQuantReportCount++; } }); @@ -110,12 +131,17 @@ export default function Stats(props: { > {usePublicData ? (
- Using public data ({internalReportCount} internal reports +{" "} - {externalReportCount} external reports) + Using public data ({internalQuantReportCount} internal quant + reports + {externalQuantReportCount} external quant reports,{" "} + {internalPitReportCount} internal pit reports +{" "} + {externalPitReportCount} external pit reports, and{" "} + {subjectiveReports.length} subjective reports)
) : (
- Not using public data ({internalReportCount} internal reports) + Not using public data ({internalQuantReportCount} internal + reports, {internalPitReportCount} internal pit reports,{" "} + {subjectiveReports.length} subjective reports)
)}
(Click to toggle)
@@ -170,6 +196,7 @@ export default function Stats(props: { {page === 0 && ( - Delete Season +

+ Delete +
Season +

)}
diff --git a/pages/createTeam.tsx b/pages/createTeam.tsx index 0859d0ac..70c03fed 100644 --- a/pages/createTeam.tsx +++ b/pages/createTeam.tsx @@ -100,7 +100,10 @@ export default function CreateTeam() { >
+ {editingAvatar && ( + + )} ); } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..66c9923d --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,84 @@ +import { defineConfig, devices } from "@playwright/test"; +import { loadEnvConfig } from "@next/env"; + +const projectDir = process.cwd(); +loadEnvConfig(projectDir); + +const baseURL = process.env.BASE_URL_FOR_PLAYWRIGHT; // Default base URL for tests + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests/e2e", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 5 : 1, + repeatEach: process.env.CI ? 10 : 1, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 4 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? "blob" : "html", + + globalSetup: require.resolve("./lib/testutils/PlaywrightSetup"), + + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + video: "retain-on-failure", // Record video only for failed tests + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"], baseURL }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"], baseURL }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"], baseURL }, + }, + + /* Test against mobile viewports. */ + { + name: "Mobile Chrome", + use: { ...devices["Pixel 5"], baseURL }, + }, + { + name: "Mobile Safari", + use: { ...devices["iPhone 12"], baseURL }, + }, + + /* Test against branded browsers. */ + { + name: "Microsoft Edge", + use: { ...devices["Desktop Edge"], channel: "msedge", baseURL }, + }, + { + name: "Google Chrome", + use: { ...devices["Desktop Chrome"], channel: "chrome", baseURL }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "npm run e2e-start-server", + url: baseURL, + reuseExistingServer: !process.env.CI, + timeout: 5 * 60 * 1000, // 5 minutes, + stdout: "pipe", + }, +}); diff --git a/public/fields/DecodeBlue.png b/public/fields/DecodeBlue.png new file mode 100644 index 00000000..4ece8d32 Binary files /dev/null and b/public/fields/DecodeBlue.png differ diff --git a/public/fields/DecodeRed.png b/public/fields/DecodeRed.png new file mode 100644 index 00000000..4ece8d32 Binary files /dev/null and b/public/fields/DecodeRed.png differ diff --git a/styles/globals.css b/styles/globals.css index d1cd3c0e..df011e23 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -2,27 +2,26 @@ @tailwind components; @tailwind utilities; - @layer components { - .background-grid { - background-size: 50px 50px; - background-image: - linear-gradient(to right, white 1px, transparent 1px), - linear-gradient(to bottom, white 1px, transparent 1px); - } + .background-grid { + background-size: 50px 50px; + background-image: + linear-gradient(to right, white 1px, transparent 1px), + linear-gradient(to bottom, white 1px, transparent 1px); + } } @keyframes float { 0% { - box-shadow: 0 5px 15px 0px rgba(0,0,0,0.6); + box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6); transform: translatey(0px); } 50% { - box-shadow: 0 25px 15px 0px rgba(0,0,0,0.2); + box-shadow: 0 25px 15px 0px rgba(0, 0, 0, 0.2); transform: translatey(-20px); } 100% { - box-shadow: 0 5px 15px 0px rgba(0,0,0,0.6); + box-shadow: 0 5px 15px 0px rgba(0, 0, 0, 0.6); transform: translatey(0px); } } @@ -56,4 +55,4 @@ border-radius: 10px; } -*/ \ No newline at end of file +*/ diff --git a/tests/e2e/index.spec.ts b/tests/e2e/index.spec.ts new file mode 100644 index 00000000..c6c0cecb --- /dev/null +++ b/tests/e2e/index.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from "@playwright/test"; + +test("Has title", async ({ page }) => { + await page.goto("/"); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Gearbox/i); +}); + +test("Has Gearbox header", async ({ page }) => { + await page.goto("/"); + + // Expect an h1 "to contain" a substring. + await expect( + page.getByRole("heading", { name: "Gearbox" }).first(), + ).toBeVisible(); +}); + +test("Has get started link", async ({ page }) => { + await page.goto("/"); + + // Click the get started link. + await expect(page.getByRole("link", { name: "Get started" })).toHaveText( + "Get Started", + ); +}); + +test("Has build time", async ({ page }) => { + await page.goto("/"); + + await expect(page.getByText(/Build Time:/i).first()).toBeVisible(); + + expect( + await page + .getByText(/Build Time:/i) + .first() + .textContent() + .then((text) => { + const timeString = text!.split(": ")[1]; + const time = new Date(timeString); + + return time.getTime(); + }), + ).toBeGreaterThan(new Date().getTime() - 60 * 15 * 1000); +}); + +test("Has link to Decatur Robotics website", async ({ page }) => { + await page.goto("/"); + + await expect( + page.locator("a[href='https://www.decaturrobotics.org/our-team']"), + ).toBeVisible(); +}); diff --git a/tests/e2e/misc.spec.ts b/tests/e2e/misc.spec.ts new file mode 100644 index 00000000..2dbe3fae --- /dev/null +++ b/tests/e2e/misc.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; +import { PlaywrightUtils } from "@/lib/testutils/TestUtils"; + +test("Sign up function signs up", async ({ page, context }) => { + const { user } = await PlaywrightUtils.signUp(page); + + const sessionToken = await context + .cookies() + .then( + (cookies) => + cookies.find((cookie) => cookie.name === "next-auth.session-token") + ?.value, + ); + + expect(sessionToken).toBeDefined(); + expect(sessionToken).not.toBe(""); + + const foundUser = await PlaywrightUtils.getUser(page); + if (foundUser) foundUser.id = user.id; // ID mismatches are normal + + expect(foundUser).toEqual(user as any); +}); diff --git a/tests/e2e/profile.spec.ts b/tests/e2e/profile.spec.ts new file mode 100644 index 00000000..5150bd14 --- /dev/null +++ b/tests/e2e/profile.spec.ts @@ -0,0 +1,134 @@ +import Card from "@/components/Card"; +import { PlaywrightUtils } from "@/lib/testutils/TestUtils"; +import { test, expect } from "@playwright/test"; + +const poSans = + '"https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSSZk-p-TpJAKV0GsfBa3EJPWWjQPxcVTB5Rg&s"'; +test("Displays sign in page when not signed in", async ({ page }) => { + await page.goto("/profile"); + + await expect( + page.getByRole("heading", { name: /sign in/i }).first(), + ).toBeVisible(); +}); + +test("Displays user information when signed in", async ({ page }) => { + const { user } = await PlaywrightUtils.signUp(page); + + await page.goto("/profile"); + + await expect( + page.getByRole("heading", { name: /sign in/i }).first(), + ).not.toBeVisible(); + + await expect(page.getByText(user.email!)).toBeVisible(); + await expect(page.getByText(user.slug!)).toBeVisible(); + await expect(page.getByText(new RegExp(user.name!))).toBeVisible(); +}); + +test.describe("Edit user name", () => { + test("Allows user to edit their name", async ({ page }) => { + await PlaywrightUtils.signUp(page); + + await page.goto("/profile"); + + const editButton = page.getByTestId("edit-name-button"); + await editButton.click(); + + const nameInput = page.getByPlaceholder(/new name/i); + await expect(nameInput).toBeVisible(); + + await nameInput.fill("New Name"); + await editButton.click(); + + await expect(page.getByText("New Name")).toBeVisible(); + }); +}); + +test.describe("Edit Avatar", () => { + test("Edit Avatar button displays popup", async ({ page }) => { + await PlaywrightUtils.signUp(page); + + await page.goto("/profile"); + + const editAvatarButton = page.getByRole("button", { name: "Edit Avatar" }); + await editAvatarButton.click(); + + const editAvatarPopup = page.getByTitle("Edit Avatar"); + await expect(editAvatarPopup).toBeVisible(); + }); + + test("Cancel button closes popup", async ({ page }) => { + await PlaywrightUtils.signUp(page); + + await page.goto("/profile"); + + //Edit Avatar button + await page.getByRole("button", { name: "Edit Avatar" }).click(); + + //Cancel button + await page.getByRole("button", { name: "Cancel" }).click(); + + //Edit Avatar popup + await expect(page.getByTitle("Edit Avatar")).not.toBeVisible(); + }); + + test("Preview Image recognizes image in field", async ({ page }) => { + await PlaywrightUtils.signUp(page); + + await page.goto("/profile"); + + //Edit Avatar button + await page.getByRole("button", { name: "Edit Avatar" }).click(); + + //Enter avatar url input + await page.getByPlaceholder("Enter new avatar url").fill(poSans); + + //Avatar preview + await expect(page.getByAltText("New Avatar")).toHaveAttribute( + "src", + poSans, + ); + }); + + test("Save button saves new avatar", async ({ page }) => { + await PlaywrightUtils.signUp(page); + + await page.goto("/profile"); + + //Edit Avatar button + await page.getByRole("button", { name: "Edit Avatar" }).click(); + + //Enter avatar url input + await page.getByPlaceholder("Enter new avatar url").fill(poSans); + + //Save button + await page.getByRole("button", { name: "Save" }).click(); + + //Url of user's avatar + await expect(page.getByAltText("Avatar").first()).toHaveAttribute( + "src", + poSans, + ); + }); + + test("Cancel button does not save new avatar", async ({ page }) => { + const currentAvatar = (await PlaywrightUtils.signUp(page)).user.image; + + await page.goto("/profile"); + + //Edit Avatar button + await page.getByRole("button", { name: "Edit Avatar" }).click(); + + //Enter avatar url input + await page.getByPlaceholder("Enter new avatar url").fill(poSans); + + //Cancel button + await page.getByRole("button", { name: "Cancel" }).click(); + + await expect(page.getByAltText("Avatar").first()).toHaveAttribute( + "src", + currentAvatar, + ); + }); +}); diff --git a/tests/lib/CompetitionHandling.test.ts b/tests/unit/lib/CompetitionHandling.test.ts similarity index 99% rename from tests/lib/CompetitionHandling.test.ts rename to tests/unit/lib/CompetitionHandling.test.ts index 8e38fc33..6b1e7162 100644 --- a/tests/lib/CompetitionHandling.test.ts +++ b/tests/unit/lib/CompetitionHandling.test.ts @@ -13,8 +13,8 @@ import { Match, Report, Team, + MatchType, } from "@/lib/Types"; -import { MatchType } from "../../lib/Types"; import { ObjectId } from "bson"; import { GameId } from "@/lib/client/GameId"; diff --git a/tests/lib/DbInterfaceAuthAdapter.test.ts b/tests/unit/lib/DbInterfaceAuthAdapter.test.ts similarity index 100% rename from tests/lib/DbInterfaceAuthAdapter.test.ts rename to tests/unit/lib/DbInterfaceAuthAdapter.test.ts diff --git a/tests/lib/Layout.test.ts b/tests/unit/lib/Layout.test.ts similarity index 100% rename from tests/lib/Layout.test.ts rename to tests/unit/lib/Layout.test.ts diff --git a/tests/lib/TheOrangeAlliance.test.ts b/tests/unit/lib/TheOrangeAlliance.test.ts similarity index 100% rename from tests/lib/TheOrangeAlliance.test.ts rename to tests/unit/lib/TheOrangeAlliance.test.ts diff --git a/tests/lib/Types.test.ts b/tests/unit/lib/Types.test.ts similarity index 100% rename from tests/lib/Types.test.ts rename to tests/unit/lib/Types.test.ts diff --git a/tests/lib/Utils.test.ts b/tests/unit/lib/Utils.test.ts similarity index 100% rename from tests/lib/Utils.test.ts rename to tests/unit/lib/Utils.test.ts diff --git a/tests/lib/Xp.test.ts b/tests/unit/lib/Xp.test.ts similarity index 100% rename from tests/lib/Xp.test.ts rename to tests/unit/lib/Xp.test.ts diff --git a/tests/lib/api/AccessLevels.test.ts b/tests/unit/lib/api/AccessLevels.test.ts similarity index 100% rename from tests/lib/api/AccessLevels.test.ts rename to tests/unit/lib/api/AccessLevels.test.ts diff --git a/tests/lib/api/ApiUtils.test.ts b/tests/unit/lib/api/ApiUtils.test.ts similarity index 100% rename from tests/lib/api/ApiUtils.test.ts rename to tests/unit/lib/api/ApiUtils.test.ts diff --git a/tests/lib/api/ClientApi.test.ts b/tests/unit/lib/api/ClientApi.test.ts similarity index 95% rename from tests/lib/api/ClientApi.test.ts rename to tests/unit/lib/api/ClientApi.test.ts index 24b6c6ad..a10478de 100644 --- a/tests/lib/api/ClientApi.test.ts +++ b/tests/unit/lib/api/ClientApi.test.ts @@ -1107,3 +1107,58 @@ describe(`${ClientApi.name}.${api.changeTeamNumberForReport.name}`, () => { ); }); }); + +describe(`${ClientApi.name}.${api.testSignIn.name}`, () => { + test("Returns user", async () => { + const { db, res } = await getTestApiUtils(); + + await api.testSignIn.handler(...(await getTestApiParams(res, { db }, []))); + + const { user } = res.send.mock.calls[0][0] as { + user: User; + }; + + expect(user).toBeDefined(); + expect(user._id).toBeDefined(); + expect( + await db.findObjectById(CollectionId.Users, new ObjectId(user._id!)), + ).toEqual(user); + }); + + test("Returns valid sessionToken", async () => { + const { db, res } = await getTestApiUtils(); + + await api.testSignIn.handler(...(await getTestApiParams(res, { db }, []))); + + const { sessionToken } = res.send.mock.calls[0][0] as { + sessionToken: string; + }; + + expect(sessionToken).toBeDefined(); + expect(sessionToken).not.toBe(""); + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + expect(session).toBeDefined(); + }); + + test("Session has correct userId", async () => { + const { db, res } = await getTestApiUtils(); + + await api.testSignIn.handler(...(await getTestApiParams(res, { db }, []))); + + const { user, sessionToken } = res.send.mock.calls[0][0] as { + user: User; + sessionToken: string; + }; + + expect(sessionToken).toBeDefined(); + + const session = await db.findObject(CollectionId.Sessions, { + sessionToken, + }); + expect(session).toBeDefined(); + expect(session?.userId).toEqual(user._id); + }); +}); diff --git a/tests/lib/client/ClientUtils.test.ts b/tests/unit/lib/client/ClientUtils.test.ts similarity index 72% rename from tests/lib/client/ClientUtils.test.ts rename to tests/unit/lib/client/ClientUtils.test.ts index e91fdeef..01d93949 100644 --- a/tests/lib/client/ClientUtils.test.ts +++ b/tests/unit/lib/client/ClientUtils.test.ts @@ -1,5 +1,6 @@ import { camelCaseToTitleCase, + mergePitReports, promisify, removeDuplicates, removeWhitespaceAndMakeLowerCase, @@ -7,6 +8,8 @@ import { toDict, wait, } from "@/lib/client/ClientUtils"; +import { Motors } from "@/lib/Enums"; +import { Pitreport } from "@/lib/Types"; test(camelCaseToTitleCase.name, () => { expect(camelCaseToTitleCase("notLinkedToTba")).toBe("Not Linked To Tba"); @@ -113,3 +116,69 @@ describe(wait.name, () => { } }); }); + +describe(mergePitReports.name, () => { + test("Takes the most common value for each key in data", () => { + const reports: Pitreport[] = [ + { + teamNumber: 1, + data: { + motorType: Motors.CIMs, + }, + } as any, + { + teamNumber: 1, + data: { + motorType: Motors.CIMs, + }, + } as any, + { + teamNumber: 1, + data: { + motorType: Motors.Falcons, + }, + } as any, + ]; + + const merged = mergePitReports(reports); + + expect(merged).toEqual({ + teamNumber: 1, + data: { + motorType: Motors.CIMs, + }, + }); + }); + + test("Merges comments when only one report has a comment", () => { + const reports: Pitreport[] = [ + { + teamNumber: 1, + data: { + comments: "", + }, + } as any, + { + teamNumber: 1, + data: { + comments: "", + }, + } as any, + { + teamNumber: 1, + data: { + comments: "comment 3", + }, + } as any, + ]; + + const merged = mergePitReports(reports); + + expect(merged).toEqual({ + teamNumber: 1, + data: { + comments: "comment 3", + }, + }); + }); +}); diff --git a/tests/lib/client/InputVerification.test.ts b/tests/unit/lib/client/InputVerification.test.ts similarity index 100% rename from tests/lib/client/InputVerification.test.ts rename to tests/unit/lib/client/InputVerification.test.ts diff --git a/tests/lib/client/Picklist.test.ts b/tests/unit/lib/client/Picklist.test.ts similarity index 100% rename from tests/lib/client/Picklist.test.ts rename to tests/unit/lib/client/Picklist.test.ts diff --git a/tests/lib/client/StatsMath.test.ts b/tests/unit/lib/client/StatsMath.test.ts similarity index 100% rename from tests/lib/client/StatsMath.test.ts rename to tests/unit/lib/client/StatsMath.test.ts diff --git a/tests/lib/dev/FakeData.test.ts b/tests/unit/lib/dev/FakeData.test.ts similarity index 100% rename from tests/lib/dev/FakeData.test.ts rename to tests/unit/lib/dev/FakeData.test.ts diff --git a/tests/lib/slugToId.test.ts b/tests/unit/lib/slugToId.test.ts similarity index 100% rename from tests/lib/slugToId.test.ts rename to tests/unit/lib/slugToId.test.ts