diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..526c8a3
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+*.sh text eol=lf
\ No newline at end of file
diff --git a/.github/workflows/delete_all_workflow_runs.sh b/.github/workflows/delete_all_workflow_runs.sh
new file mode 100755
index 0000000..8dd8e80
--- /dev/null
+++ b/.github/workflows/delete_all_workflow_runs.sh
@@ -0,0 +1,11 @@
+REPO="MeasureOneCodeTwice/Finus"
+
+# Optional: count runs first
+gh api --paginate "/repos/$REPO/actions/runs?per_page=100" --jq '.workflow_runs[].id' | wc -l
+
+# Delete everything
+gh api --paginate "/repos/$REPO/actions/runs?per_page=100" --jq '.workflow_runs[].id' \
+| while read -r RUN_ID; do
+ echo "Deleting run $RUN_ID"
+ gh api -X DELETE "/repos/$REPO/actions/runs/$RUN_ID" >/dev/null
+ done
diff --git a/.github/workflows/docker_compose_publish.yml b/.github/workflows/docker_compose_publish.yml
new file mode 100644
index 0000000..c2193a5
--- /dev/null
+++ b/.github/workflows/docker_compose_publish.yml
@@ -0,0 +1,49 @@
+on:
+ workflow_call:
+ inputs:
+ dockerfile-dir:
+ type: string
+ required: true
+ DOCKER_HUB_USERNAME:
+ type: string
+ required: true
+
+ secrets:
+ DOCKER_HUB_PASSWORD:
+ required: true
+
+jobs:
+ publish-compose:
+ runs-on: ubuntu-24.04
+ defaults:
+ run:
+ working-directory: ${{ inputs.dockerfile-dir }}
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ - uses: docker/setup-buildx-action@000d75d273dec231f74115df50b6ee04c9b25e55
+ - uses: docker/login-action@0d33dcdeedfe8f116d005eaa548b93b404f61fa8
+ with:
+ username: ${{ inputs.DOCKER_HUB_USERNAME }}
+ password: ${{ secrets.DOCKER_HUB_PASSWORD }}
+ - run: |
+ BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
+ RELEASE_VERSION=$(echo $BRANCH_NAME | sed "s|release\/||g")
+
+ echo RELEASE_VERSION=$RELEASE_VERSION > .env
+ echo DOCKER_HUB_USERNAME=$DOCKER_HUB_USERNAME >> .env
+
+ echo "Building images..."
+ docker compose build
+ echo "Pushing images..."
+ docker compose push
+
+ echo RELEASE_VERSION=latest > .env
+ echo DOCKER_HUB_USERNAME=$DOCKER_HUB_USERNAME >> .env
+ echo "Re-building with lastest tag..."
+ docker compose build
+ echo "Pushing images..."
+ docker compose push
+
+ env:
+ DOCKER_HUB_USERNAME: ${{ inputs.DOCKER_HUB_USERNAME }}
+
diff --git a/.github/workflows/publish_backend_images.yml b/.github/workflows/publish_backend_images.yml
new file mode 100644
index 0000000..269a21a
--- /dev/null
+++ b/.github/workflows/publish_backend_images.yml
@@ -0,0 +1,27 @@
+name: "Publish Images"
+
+on:
+ push:
+ branches: [ "release/**" ]
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ publish-backend-images:
+ uses: ./.github/workflows/docker_compose_publish.yml
+ with:
+ dockerfile-dir: app/server
+ DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME }}
+ secrets:
+ DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
+
+ publish-frontend-images:
+ name: "Publish frontend images"
+ uses: ./.github/workflows/docker_compose_publish.yml
+ with:
+ dockerfile-dir: app/client
+ DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME }}
+ secrets:
+ DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }}
+
+
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
new file mode 100644
index 0000000..93a0fb6
--- /dev/null
+++ b/.github/workflows/run_tests.yml
@@ -0,0 +1,20 @@
+name: "Run tests"
+
+on:
+ pull_request:
+ branches: [ "*" ]
+ push:
+ branches: [ "main", "development", "release/*" ]
+ # Allows you to run this workflow manually from the Actions tab
+ workflow_dispatch:
+
+jobs:
+ run-backend-tests:
+ runs-on: ubuntu-24.04
+ defaults:
+ run:
+ working-directory: app/server
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ - name: Run tests
+ run: ./run_tests.sh
diff --git a/.gitignore b/.gitignore
index b7faf40..e2e8776 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,12 +135,8 @@ celerybeat.pid
*.sage.py
# Environments
-.env
-.envrc
.venv
-env/
venv/
-ENV/
env.bak/
venv.bak/
@@ -205,3 +201,7 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/
+app/server/services/api-gateway/bun.lock
+
+# stryker files, these are ran with node and break git error checking - mutation testing for JS
+.stryker-tmp
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..acf6d3b
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "python-envs.defaultEnvManager": "ms-python.python:system"
+}
diff --git a/README.md b/README.md
index e221936..2e13c46 100644
--- a/README.md
+++ b/README.md
@@ -1,120 +1,26 @@
-# COMP-4350-Finus Team 6
+# About
+Our project provides users with basic personal financial tracking and analytics via a web-based dashboard.
-#### Members:
-Roman Tebel, Jackie Mei, Deep Patel, Hoang Huy Truong, Logan Decock, Dylan Prabagaran
+We target users past the age of 16, who are looking to gain better awareness of their financial situation. Our UI prioritizes intuitive design and meaningful analytics.
-# Project Summary and Vision
-Our project provides users with basic personal financial tracking and analytics on a web-based dashboard.
+# Getting started
-#### Target Audience
-Our project targets users past the age of 16, who are looking to gain better awareness of their financial situation. Because of a broad range, our UI will prioritize intuitive design and meaningful analytics.
+#### Installation
+Our project is completely dockerized, so our sole dependency is docker.
-#### Core Problem
-Financial education varies highly among the general population, and some people find themselves in difficult situations because of their ignorance when it comes to savings and expenditures.
+Our microservices are located in `app/server`.
+run `docker compose up` in this directory to start run the backend.
-#### Key Benefit
-This project will serve as a means to make users more situationally aware of their finances, and an easier portal to personal finance compared to Excel spreadsheets. Our project would provide the user with everything they would need to get started after a simple interactive setup. This will alleviate the common headache of formatting, processing and visualizing personal finances for the less technical users.
+The webserver is located in `app/client`. The same command starts the webserver.
-## Core Functional Features
- 1. User Authentication. This is a standard sign up and sign in system, which will be implemented via JTW. Our fallback is to rely on cookies should JTW fail.
- 2. User Data Input. At the minimum, users will be able to manually input their expenses and savings. Ideally, users would upload CSV files of bank statements, as we can’t get access to an API of any Canadian bank (we’re not a commercial body).
- 3. Visualization Dashboard. After processing user data, our app will visualize what users spend their money on and present statistics of their finances over time in various charts.
- 4. Financial Goals. Users will be able to declare goals for savings and track them over time.
- 5. Projections. With enough data, the app will produce possible outcomes for where users will end up over a period of time. This will visualize compounding savings and how expenses can affect the users long-term.
- 6. Stock and FOREX Tracking. By using an open-source library to skim through Yahoo Finance and Oanda API, we will showcase users a minimalized view of the stock and foreign currency exchange markets. Users would be able to search for instruments, pin them, and view their history over time.
- 7. Stretch goal: Machine Learning Integration. Through the Python submodule on the server-side, we would boost our projections accuracy and could provide the user recommendations on how they could decrease spendings. With ML, we would also automatically classify user expenses from bank statements.
- 8. Stretch goal: Collaborative Budgets. Users would collaborate with other users and combine their expenses/savings for all features.
+#### Usage
+Once you have both ends of the project running you can access the website via http://localhost.
+# Architecture
+Finus was designed with a microservice architecture.
+You can find each service in the `app/server/services` folder.
-## Technologies
- - JTW for user authentication
- - Typescript for all JS.
- - React.js relevant graph libraries for client-side UI in a browser page
- - Node.js for server-side application
- - Express.js for API and some server logic
- - SQLite or MySQL for database
- - Potentially Python for machine learning and heavier data processing on the server.
-
-## User Stories
-
-### 1. Visualization Dashboard:
-#### User Story 1: As a user, I want to see the state of my income, investments, savings and expenses in order to better understand my financial situation.
-#### Acceptance Criteria:
- - The user is presented with a dashboard, which contains statistical graphs
- - At least one of the graphs visualizes expenses over time
- - At least one of the graphs visualizes the savings contribution over time
- - At least one of the graphs shows the relationship between total income and where all of that money ends up going
-
-#### User Story 2: As a user, I want to see how I’ve kept up with a proposed budget over a period of time so that I can learn if I need to fix anything about my financial habits.
-#### Acceptance Criteria:
- - Users are proposed a feasible budget plan
- - The budget plan takes into account the user’s income and expenses
- - The budget prioritizes saving money
- - The user is shown how closely they match the budget and whether they overspent within a defined period of time
-
-### 2. User Data Input:
-#### User Story 1: As a user, I want multiple ways to input my financial data so that I can explain my financial situation with more precision.
-#### Acceptance Criteria:
- - Users can enter data regarding finances manually
- - Users can also enter data regarding finances semi-automatically, by letting the app parse a CSV bank statement
- - Users are able to set up profiles, which incorporate all of their finances
- - Users can create categories for investments, incomes and expenses and assign transactions into those categories
- - When bank statements are parsed, transactions are compared against all the historical transactions within certain categories in order to decide which category they fall under
-
-#### User Story 2: As a user I want to be able to edit transactions in order to correct personal mistakes and ambiguous bank statements.
-#### Acceptance Criteria:
- - Users can edit the transaction category for any historical transaction
- - Users can edit the name of any historical transaction
- - Users can edit the financial value associated with any historical transaction
-
-### 3. Projections:
-#### User Story 1: As a user who has debt I want to input financial data related to it, and I want to see when I would be able to pay it all off with minimum contributions. I need to know this in order to see what debt I should focus on paying off first.
-#### Acceptance Criteria:
- - Users can input various kinds of debt like loans and mortgages
- - Certain kinds of debt have additional input associated with them like amortization
- - Users are presented with a projection of how much they will owe at every specified period of time, given minimum payments
-
-#### User story 2: As a user who wishes to save money, I want to see how my savings will compound over time in various accounts so that I can plan for my future.
-#### Acceptance Criteria:
- - Users can input financial information about their savings accounts
- - Users can specify the kind of account they’re using for savings
- - Users are presented with a graph that visualizes how their savings compound over time
- - The projected savings graph shows both worst-case and best-case growth based on historical economic values
-
-### 4.Financial Goals:
-#### User Story 1: As a user I want to specify a financial goal such as decreased spending or saving a certain amount of money by a certain date. I need this to keep myself on track with future plans.
-#### Acceptance Criteria:
- - User budget is aligned with the selected financial goal
- - User budget recommends changes to user’s financial habits should the user stray away from their financial goal
-
-### 5. User Authentication:
-#### User Story 1: As a user I want to create an account so that all my data can be loaded from various supported devices
-#### Acceptance Criteria:
- - Users can create an account with an email and a password
- - User data is associated with an account under a certain name
- - Users can log into their account from multiple supported devices (Desktop only)
- - Users can log out of their account, and log in with a different account on the same machine
-
-### 6. Stock and FOREX Tracking:
-#### User story 1: As a user I want to view the current state of the stock and the foreign currency exchange markets so that I can learn if certain stocks are performing really well.
-#### Acceptance criteria:
- - Users can search for stocks and currency pairs on the dashboard
- - Users can view historical data of a stock or a currency pair
- - Users can pin instruments
- - Pinned instruments always appear on the dashboard with a minimalised view of their historical data
-
-### 7. Stretch goal: Collaborative Budgets:
-#### User story 1: As a user who has family and friends, I want to add other profiles under my account so that other people can see their finances and we can make plans together for the future.
-#### Acceptance criteria:
- - Users can specify if some of their profiles should be treated as combined
- - Users are presented with a secondary collective budget if they have combined profiles
- - The collaborative budget takes into account multiple profiles
- - The collaborative budget has its own financial goals which it tries to follow
-
-### 8. Stretch goal: Machine Learning Integration:
-#### User story 1: As a user I want a more accurate prediction of my finances over time in order to have better situational awareness.
-#### Acceptance criteria:
- - Projections for user savings are assisted with statistical ML algorithms
- - Classification of transactions is assisted with classification ML algorithms based on semantic embeddings (through SBERT, as an example)
-
+
+#### More
+You can read more about our project's features and goals [here](documentation/product_vision.md).
diff --git a/app/client/.env b/app/client/.env
new file mode 100644
index 0000000..aefb610
--- /dev/null
+++ b/app/client/.env
@@ -0,0 +1,6 @@
+#THIS IS NOT .gitignored'D!
+#DON'T PUT REAL SECRETS IN HERE!!!
+
+#docker-file
+DOCKER_HUB_USERNAME=measureonecodetwice
+RELEASE_VERSION=local
diff --git a/app/client/.gitignore b/app/client/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/app/client/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/app/client/Dockerfile b/app/client/Dockerfile
new file mode 100644
index 0000000..42e73fa
--- /dev/null
+++ b/app/client/Dockerfile
@@ -0,0 +1,19 @@
+FROM oven/bun:1.3.9-slim AS builder
+
+WORKDIR /app
+
+COPY package.json bun.lockb ./
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+ bun install
+
+COPY tsconfig.* .
+COPY vite.config.ts .
+COPY index.* .
+COPY public ./public
+COPY src ./src
+
+RUN bun run build
+
+FROM nginx:1.29.5-alpine3.23-perl
+COPY --from=builder /app/dist /data/www
+COPY nginx.conf /etc/nginx/nginx.conf
diff --git a/app/client/Dockerfile.base b/app/client/Dockerfile.base
new file mode 100644
index 0000000..92b7333
--- /dev/null
+++ b/app/client/Dockerfile.base
@@ -0,0 +1,15 @@
+FROM oven/bun:1.3.9-slim
+
+WORKDIR /app
+
+COPY package.json bun.lockb ./
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+ bun install
+
+COPY tsconfig.* .
+COPY vite.config.ts .
+COPY index.* .
+COPY public ./public
+COPY src ./src
+
+ENTRYPOINT [ "/bin/bash", "-c", "exit" ]
diff --git a/app/client/Dockerfile.mutation.test b/app/client/Dockerfile.mutation.test
new file mode 100644
index 0000000..92de251
--- /dev/null
+++ b/app/client/Dockerfile.mutation.test
@@ -0,0 +1,10 @@
+FROM oven/bun:1
+
+WORKDIR /app
+
+COPY bun.lockb package.json ./
+RUN bun add -g @stryker-mutator/core @stryker-mutator/typescript-checker
+
+COPY . .
+
+CMD ["bun","run", "mutate"]
diff --git a/app/client/Dockerfile.test b/app/client/Dockerfile.test
new file mode 100644
index 0000000..e7d096f
--- /dev/null
+++ b/app/client/Dockerfile.test
@@ -0,0 +1,8 @@
+FROM local/finus-webserver-base:latest
+
+COPY test ./test
+COPY vitest.config.ts .
+
+ENV CI=true
+
+ENTRYPOINT ["bun", "run", "vitest", "--run"]
diff --git a/app/client/bun.lock b/app/client/bun.lock
new file mode 100644
index 0000000..993dbe8
--- /dev/null
+++ b/app/client/bun.lock
@@ -0,0 +1,1276 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "finus-client",
+ "dependencies": {
+ "@tailwindcss/vite": "^4.1.18",
+ "@types/react-router-dom": "^5.3.3",
+ "axios": "^1.13.5",
+ "chart.js": "^4.5.1",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.563.0",
+ "papaparse": "^5.5.3",
+ "radix-ui": "^1.4.3",
+ "react": "^19.2.0",
+ "react-chartjs-2": "^5.3.1",
+ "react-dom": "^19.2.0",
+ "react-icons": "^5.5.0",
+ "react-pro-sidebar": "^1.1.0",
+ "react-router-dom": "^7.13.0",
+ "recharts": "^3.7.0",
+ "tailwind-merge": "^3.4.0",
+ "tailwindcss": "^4.1.18",
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@stryker-mutator/core": "^9.6.0",
+ "@stryker-mutator/vitest-runner": "^9.6.0",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^25.3.5",
+ "@types/papaparse": "^5.3.16",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react-swc": "^4.2.2",
+ "@vitest/coverage-v8": "^4.0.18",
+ "@vitest/ui": "^4.0.18",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "prettier": "3.8.1",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.5",
+ "vitest": "^4.0.18",
+ },
+ },
+ },
+ "overrides": {
+ "vite": "npm:rolldown-vite@7.2.5",
+ },
+ "packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
+
+ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.0", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ=="],
+
+ "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
+
+ "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
+
+ "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
+
+ "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="],
+
+ "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA=="],
+
+ "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
+
+ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
+
+ "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="],
+
+ "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="],
+
+ "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="],
+
+ "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
+
+ "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
+
+ "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
+
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
+
+ "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
+
+ "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
+
+ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+
+ "@emotion/babel-plugin": ["@emotion/babel-plugin@11.13.5", "", { "dependencies": { "@babel/helper-module-imports": "^7.16.7", "@babel/runtime": "^7.18.3", "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/serialize": "^1.3.3", "babel-plugin-macros": "^3.1.0", "convert-source-map": "^1.5.0", "escape-string-regexp": "^4.0.0", "find-root": "^1.1.0", "source-map": "^0.5.7", "stylis": "4.2.0" } }, "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ=="],
+
+ "@emotion/cache": ["@emotion/cache@11.14.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0", "@emotion/sheet": "^1.4.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "stylis": "4.2.0" } }, "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA=="],
+
+ "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="],
+
+ "@emotion/is-prop-valid": ["@emotion/is-prop-valid@1.4.0", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="],
+
+ "@emotion/memoize": ["@emotion/memoize@0.9.0", "", {}, "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="],
+
+ "@emotion/react": ["@emotion/react@11.14.0", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2", "@emotion/weak-memoize": "^0.4.0", "hoist-non-react-statics": "^3.3.1" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA=="],
+
+ "@emotion/serialize": ["@emotion/serialize@1.3.3", "", { "dependencies": { "@emotion/hash": "^0.9.2", "@emotion/memoize": "^0.9.0", "@emotion/unitless": "^0.10.0", "@emotion/utils": "^1.4.2", "csstype": "^3.0.2" } }, "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA=="],
+
+ "@emotion/sheet": ["@emotion/sheet@1.4.0", "", {}, "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="],
+
+ "@emotion/styled": ["@emotion/styled@11.14.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/is-prop-valid": "^1.3.0", "@emotion/serialize": "^1.3.3", "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", "@emotion/utils": "^1.4.2" }, "peerDependencies": { "@emotion/react": "^11.0.0-rc.0", "react": ">=16.8.0" } }, "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw=="],
+
+ "@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="],
+
+ "@emotion/use-insertion-effect-with-fallbacks": ["@emotion/use-insertion-effect-with-fallbacks@1.2.0", "", { "peerDependencies": { "react": ">=16.8.0" } }, "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg=="],
+
+ "@emotion/utils": ["@emotion/utils@1.4.2", "", {}, "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="],
+
+ "@emotion/weak-memoize": ["@emotion/weak-memoize@0.4.0", "", {}, "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="],
+
+ "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+ "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
+
+ "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
+
+ "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="],
+
+ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@inquirer/ansi": ["@inquirer/ansi@2.0.4", "", {}, "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg=="],
+
+ "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="],
+
+ "@inquirer/confirm": ["@inquirer/confirm@6.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ=="],
+
+ "@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="],
+
+ "@inquirer/editor": ["@inquirer/editor@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/external-editor": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA=="],
+
+ "@inquirer/expand": ["@inquirer/expand@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ=="],
+
+ "@inquirer/external-editor": ["@inquirer/external-editor@2.0.4", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA=="],
+
+ "@inquirer/figures": ["@inquirer/figures@2.0.4", "", {}, "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ=="],
+
+ "@inquirer/input": ["@inquirer/input@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ=="],
+
+ "@inquirer/number": ["@inquirer/number@4.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA=="],
+
+ "@inquirer/password": ["@inquirer/password@5.0.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A=="],
+
+ "@inquirer/prompts": ["@inquirer/prompts@8.3.2", "", { "dependencies": { "@inquirer/checkbox": "^5.1.2", "@inquirer/confirm": "^6.0.10", "@inquirer/editor": "^5.0.10", "@inquirer/expand": "^5.0.10", "@inquirer/input": "^5.0.10", "@inquirer/number": "^4.0.10", "@inquirer/password": "^5.0.10", "@inquirer/rawlist": "^5.2.6", "@inquirer/search": "^4.1.6", "@inquirer/select": "^5.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w=="],
+
+ "@inquirer/rawlist": ["@inquirer/rawlist@5.2.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w=="],
+
+ "@inquirer/search": ["@inquirer/search@4.1.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ=="],
+
+ "@inquirer/select": ["@inquirer/select@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA=="],
+
+ "@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
+
+ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
+
+ "@oxc-project/runtime": ["@oxc-project/runtime@0.97.0", "", {}, "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w=="],
+
+ "@oxc-project/types": ["@oxc-project/types@0.97.0", "", {}, "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ=="],
+
+ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
+
+ "@popperjs/core": ["@popperjs/core@2.11.8", "", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
+
+ "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
+
+ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
+
+ "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
+
+ "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
+
+ "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
+
+ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
+
+ "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="],
+
+ "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
+
+ "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
+
+ "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
+
+ "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
+
+ "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
+
+ "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
+
+ "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
+
+ "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
+
+ "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
+
+ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
+
+ "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
+
+ "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
+
+ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
+
+ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
+
+ "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
+
+ "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
+
+ "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
+
+ "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
+
+ "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
+
+ "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
+
+ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
+
+ "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
+
+ "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
+
+ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
+
+ "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
+
+ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
+
+ "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
+
+ "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
+
+ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
+
+ "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
+
+ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
+
+ "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
+
+ "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
+
+ "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
+
+ "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
+
+ "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
+
+ "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
+
+ "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+ "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
+ "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
+
+ "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
+
+ "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
+
+ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+ "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
+
+ "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
+
+ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
+
+ "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
+
+ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
+
+ "@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
+
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "x64" }, "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.50", "", { "os": "freebsd", "cpu": "x64" }, "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm" }, "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.50", "", { "os": "none", "cpu": "arm64" }, "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA=="],
+
+ "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.50", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "arm64" }, "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA=="],
+
+ "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "ia32" }, "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "x64" }, "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
+
+ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
+
+ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
+
+ "@stryker-mutator/api": ["@stryker-mutator/api@9.6.0", "", { "dependencies": { "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "tslib": "~2.8.0", "typed-inject": "~5.0.0" } }, "sha512-kJEEwOVoWDXGEIXuM+9efT6LSJ7nyxnQQvjEoKg8GSZXbDUjfD0tqA0aBD06U1SzQLKCM7ffjgPffr154MHZKw=="],
+
+ "@stryker-mutator/core": ["@stryker-mutator/core@9.6.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/instrumenter": "9.6.0", "@stryker-mutator/util": "9.6.0", "ajv": "~8.18.0", "chalk": "~5.6.0", "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.6.0", "execa": "~9.6.0", "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", "minimatch": "~10.2.4", "mutation-server-protocol": "~0.4.0", "mutation-testing-elements": "3.7.2", "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "npm-run-path": "~6.0.0", "progress": "~2.0.3", "rxjs": "~7.8.1", "semver": "^7.6.3", "source-map": "~0.7.4", "tree-kill": "~1.2.2", "tslib": "2.8.1", "typed-inject": "~5.0.0", "typed-rest-client": "~2.2.0" }, "bin": { "stryker": "bin/stryker.js" } }, "sha512-oSbw01l6HXHt0iW9x5fQj7yHGGT8ZjCkXSkI7Bsu0juO7Q6vRMXk7XcvKpCBgRgzKXi1osg8+iIzj7acHuxepQ=="],
+
+ "@stryker-mutator/instrumenter": ["@stryker-mutator/instrumenter@9.6.0", "", { "dependencies": { "@babel/core": "~7.29.0", "@babel/generator": "~7.29.0", "@babel/parser": "~7.29.0", "@babel/plugin-proposal-decorators": "~7.29.0", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/preset-typescript": "~7.28.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/util": "9.6.0", "angular-html-parser": "~10.4.0", "semver": "~7.7.0", "tslib": "2.8.1", "weapon-regex": "~1.3.2" } }, "sha512-tWdRYfm9LF4Go7cNOos0xEIOEnN7ZOSj38rfXvGZS9IINlvYBrBCl2xcz/67v6l5A7xksMWWByZRIq2bgdnnUg=="],
+
+ "@stryker-mutator/util": ["@stryker-mutator/util@9.6.0", "", {}, "sha512-gw7fJOFNHEj9inAEOodD9RrrMEMhZmWJ46Ww/kDJAXlSsBBmdwCzeomNLngmLTvgp14z7Tfq85DHYwvmNMdOxA=="],
+
+ "@stryker-mutator/vitest-runner": ["@stryker-mutator/vitest-runner@9.6.0", "", { "dependencies": { "@stryker-mutator/api": "9.6.0", "@stryker-mutator/util": "9.6.0", "tslib": "~2.8.0" }, "peerDependencies": { "@stryker-mutator/core": "9.6.0", "vitest": ">=2.0.0" } }, "sha512-/zyELz5jTDAiH0Hr23G6KSnBFl9XV+vn0T0qUAk4sPqJoP5NVm9jjpgt9EBACS/VTkVqSvXqBid4jmESPx11Sg=="],
+
+ "@swc/core": ["@swc/core@1.15.11", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.25" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.11", "@swc/core-darwin-x64": "1.15.11", "@swc/core-linux-arm-gnueabihf": "1.15.11", "@swc/core-linux-arm64-gnu": "1.15.11", "@swc/core-linux-arm64-musl": "1.15.11", "@swc/core-linux-x64-gnu": "1.15.11", "@swc/core-linux-x64-musl": "1.15.11", "@swc/core-win32-arm64-msvc": "1.15.11", "@swc/core-win32-ia32-msvc": "1.15.11", "@swc/core-win32-x64-msvc": "1.15.11" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w=="],
+
+ "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg=="],
+
+ "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA=="],
+
+ "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.11", "", { "os": "linux", "cpu": "arm" }, "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg=="],
+
+ "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA=="],
+
+ "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w=="],
+
+ "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ=="],
+
+ "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw=="],
+
+ "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA=="],
+
+ "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw=="],
+
+ "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.11", "", { "os": "win32", "cpu": "x64" }, "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw=="],
+
+ "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
+
+ "@swc/types": ["@swc/types@0.1.25", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="],
+
+ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="],
+
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+
+ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
+
+ "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
+
+ "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
+
+ "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
+
+ "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
+
+ "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
+
+ "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="],
+
+ "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
+
+ "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/history": ["@types/history@4.7.11", "", {}, "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
+
+ "@types/papaparse": ["@types/papaparse@5.5.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA=="],
+
+ "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],
+
+ "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@types/react-router": ["@types/react-router@5.1.20", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*" } }, "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q=="],
+
+ "@types/react-router-dom": ["@types/react-router-dom@5.3.3", "", { "dependencies": { "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router": "*" } }, "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw=="],
+
+ "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
+
+ "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.2.3", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7" } }, "sha512-QIluDil2prhY1gdA3GGwxZzTAmLdi8cQ2CcuMW4PB/Wu4e/1pzqrwhYWVd09LInCRlDUidQjd0B70QWbjWtLxA=="],
+
+ "@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="],
+
+ "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
+
+ "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
+
+ "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
+
+ "@vitest/ui": ["@vitest/ui@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "fflate": "^0.8.2", "flatted": "^3.3.3", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "vitest": "4.0.18" } }, "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ=="],
+
+ "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
+
+ "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
+
+ "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+
+ "aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.12", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="],
+
+ "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="],
+
+ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
+
+ "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
+
+ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001767", "", {}, "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ=="],
+
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
+ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
+
+ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
+
+ "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
+
+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
+ "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
+
+ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
+
+ "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
+
+ "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
+
+ "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
+
+ "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
+
+ "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
+
+ "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
+
+ "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
+
+ "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
+
+ "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
+
+ "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
+
+ "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
+
+ "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
+
+ "dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.283", "", {}, "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w=="],
+
+ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
+
+ "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="],
+
+ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
+
+ "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="],
+
+ "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
+
+ "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
+
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
+
+ "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
+
+ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+
+ "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
+
+ "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
+
+ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+
+ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
+
+ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+
+ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
+
+ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
+
+ "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
+
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
+
+ "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
+
+ "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
+
+ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
+
+ "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
+
+ "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
+
+ "json-rpc-2.0": ["json-rpc-2.0@1.7.1", "", {}, "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg=="],
+
+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
+
+ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
+
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="],
+
+ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
+
+ "minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "mutation-server-protocol": ["mutation-server-protocol@0.4.1", "", { "dependencies": { "zod": "^4.1.12" } }, "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g=="],
+
+ "mutation-testing-elements": ["mutation-testing-elements@3.7.2", "", {}, "sha512-i7X2Q4X5eYon72W2QQ9HND7plVhQcqTnv+Xc3KeYslRZSJ4WYJoal8LFdbWm7dKWLNE0rYkCUrvboasWzF3MMA=="],
+
+ "mutation-testing-metrics": ["mutation-testing-metrics@3.7.2", "", { "dependencies": { "mutation-testing-report-schema": "3.7.2" } }, "sha512-ichXZSC4FeJbcVHYOWzWUhNuTJGogc0WiQol8lqEBrBSp+ADl3fmcZMqrx0ogInEUiImn+A8JyTk6uh9vd25TQ=="],
+
+ "mutation-testing-report-schema": ["mutation-testing-report-schema@3.7.2", "", {}, "sha512-fN5M61SDzIOeJyatMOhGPLDOFz5BQIjTNPjo4PcHIEUWrejO4i4B5PFuQ/2l43709hEsTxeiXX00H73WERKcDw=="],
+
+ "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
+
+ "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
+
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "papaparse": ["papaparse@5.5.3", "", {}, "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
+
+ "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
+
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+ "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
+
+ "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
+
+ "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
+
+ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
+
+ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
+
+ "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
+
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "react-chartjs-2": ["react-chartjs-2@5.3.1", "", { "peerDependencies": { "chart.js": "^4.1.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A=="],
+
+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+
+ "react-icons": ["react-icons@5.5.0", "", { "peerDependencies": { "react": "*" } }, "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw=="],
+
+ "react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
+
+ "react-pro-sidebar": ["react-pro-sidebar@1.1.0", "", { "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@popperjs/core": "^2.11.6", "classnames": "^2.3.2" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-rdRJ4PeMsqWq9n69AmF6et6qCbhCF1KEBgjAH8vIiLxE1k5fMxtRYo0k4asxW8qpIH6sqahiMxrxVVoObv8orQ=="],
+
+ "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
+
+ "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
+
+ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
+
+ "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="],
+
+ "react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="],
+
+ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
+
+ "recharts": ["recharts@3.7.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew=="],
+
+ "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="],
+
+ "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
+
+ "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="],
+
+ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
+
+ "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
+ "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
+
+ "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
+
+ "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
+
+ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+ "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
+
+ "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
+
+ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
+
+ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
+
+ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
+
+ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
+
+ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
+
+ "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "typed-inject": ["typed-inject@5.0.0", "", {}, "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA=="],
+
+ "typed-rest-client": ["typed-rest-client@2.2.0", "", { "dependencies": { "des.js": "^1.1.0", "js-md4": "^0.3.2", "qs": "^6.14.1", "tunnel": "0.0.6", "underscore": "^1.12.1" } }, "sha512-/e2Rk9g20N0r44kaQLb3v6QGuryOD8SPb53t43Y5kqXXA+SqWuU7zLiMxetw61jNn/JFrxTdr5nPDhGY/eTNhQ=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="],
+
+ "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
+
+ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+
+ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
+
+ "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
+
+ "vite": ["rolldown-vite@7.2.5", "", { "dependencies": { "@oxc-project/runtime": "0.97.0", "fdir": "^6.5.0", "lightningcss": "^1.30.2", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.50", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA=="],
+
+ "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
+
+ "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
+
+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
+ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
+
+ "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
+
+ "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+
+ "@eslint/eslintrc/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
+
+ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+ "@eslint/eslintrc/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+
+ "@reduxjs/toolkit/immer": ["immer@11.1.4", "", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
+
+ "@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@types/papaparse/@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
+
+ "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
+
+ "cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
+
+ "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "eslint/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
+
+ "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
+ "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
+ "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="],
+
+ "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+ "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="],
+
+ "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
+
+ "@types/papaparse/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+ "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "@eslint/eslintrc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+ }
+}
diff --git a/app/client/bun.lockb b/app/client/bun.lockb
new file mode 100755
index 0000000..77f69e2
Binary files /dev/null and b/app/client/bun.lockb differ
diff --git a/app/client/components.json b/app/client/components.json
new file mode 100644
index 0000000..c3085d6
--- /dev/null
+++ b/app/client/components.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "rtl": false,
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {}
+}
diff --git a/app/client/docker-compose.test.yml b/app/client/docker-compose.test.yml
new file mode 100644
index 0000000..e54856d
--- /dev/null
+++ b/app/client/docker-compose.test.yml
@@ -0,0 +1,21 @@
+services:
+ webserver-base:
+ image: local/finus-webserver-base:latest
+ build:
+ dockerfile: Dockerfile.base
+
+ user-input:
+ build:
+ dockerfile: Dockerfile.test
+ depends_on:
+ - webserver-base
+
+ mutation-tests:
+ build:
+ context: .
+ dockerfile: Dockerfile.mutation.test
+ depends_on:
+ - webserver-base
+ volumes:
+ - .:/app/client
+ working_dir: /app/client
diff --git a/app/client/docker-compose.yml b/app/client/docker-compose.yml
new file mode 100644
index 0000000..9dcbd8b
--- /dev/null
+++ b/app/client/docker-compose.yml
@@ -0,0 +1,7 @@
+services:
+ webserver:
+ image: $DOCKER_HUB_USERNAME/finus-webserver:$RELEASE_VERSION
+ build:
+ dockerfile: Dockerfile
+ ports:
+ - "80:80"
diff --git a/app/client/eslint.config.js b/app/client/eslint.config.js
new file mode 100644
index 0000000..75d3c46
--- /dev/null
+++ b/app/client/eslint.config.js
@@ -0,0 +1,23 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+import { defineConfig, globalIgnores } from "eslint/config";
+
+export default defineConfig([
+ globalIgnores(["dist"]),
+ {
+ files: ["**/*.{ts,tsx}"],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+]);
diff --git a/app/client/index.html b/app/client/index.html
new file mode 100644
index 0000000..829f295
--- /dev/null
+++ b/app/client/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ finus-client
+
+
+
+
+
+
diff --git a/app/client/nginx.conf b/app/client/nginx.conf
new file mode 100644
index 0000000..f587d14
--- /dev/null
+++ b/app/client/nginx.conf
@@ -0,0 +1,14 @@
+#used for running in docker image. For dev use `bun run dev`
+events {}
+http {
+ include /etc/nginx/mime.types;
+ types {
+ text/javascript .js;
+ }
+ server {
+ location / {
+ root /data/www;
+ try_files $uri $uri/ /index.html;
+ }
+ }
+}
diff --git a/app/client/package-lock.json b/app/client/package-lock.json
new file mode 100644
index 0000000..c57d9ea
--- /dev/null
+++ b/app/client/package-lock.json
@@ -0,0 +1,6096 @@
+{
+ "name": "finus-client",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "finus-client",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tailwindcss/vite": "^4.1.18",
+ "@types/react-router-dom": "^5.3.3",
+ "axios": "^1.13.5",
+ "chart.js": "^4.5.1",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.563.0",
+ "radix-ui": "^1.4.3",
+ "react": "^19.2.0",
+ "react-chartjs-2": "^5.3.1",
+ "react-dom": "^19.2.0",
+ "react-icons": "^5.5.0",
+ "react-pro-sidebar": "^1.1.0",
+ "react-router-dom": "^7.13.0",
+ "recharts": "^3.7.0",
+ "tailwind-merge": "^3.4.0",
+ "tailwindcss": "^4.1.18"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/node": "^25.2.0",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react-swc": "^4.2.2",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "prettier": "3.8.1",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "npm:rolldown-vite@7.2.5"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+ "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
+ "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.1.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
+ "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
+ "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.13.5",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
+ "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/serialize": "^1.3.3",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
+ "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/sheet": "^1.4.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
+ "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/memoize": "^0.9.0"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
+ "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.14.0",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
+ "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/cache": "^11.14.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2",
+ "@emotion/weak-memoize": "^0.4.0",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
+ "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.2",
+ "@emotion/memoize": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
+ "@emotion/utils": "^1.4.2",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
+ "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.14.1",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
+ "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/is-prop-valid": "^1.3.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
+ "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
+ "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "license": "MIT"
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.4",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.5",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.4",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.5"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "license": "MIT"
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxc-project/runtime": {
+ "version": "0.97.0",
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.97.0",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.3",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accessible-icon": {
+ "version": "1.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.12",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-alert-dialog": {
+ "version": "1.1.15",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-aspect-ratio": {
+ "version": "1.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.10",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-checkbox": {
+ "version": "1.3.3",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collapsible": {
+ "version": "1.1.12",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context-menu": {
+ "version": "2.2.16",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dialog": {
+ "version": "1.1.15",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-direction": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.11",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-guards": {
+ "version": "1.1.3",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-focus-scope": {
+ "version": "1.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-form": {
+ "version": "0.1.8",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-label": "2.1.7",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-hover-card": {
+ "version": "1.1.15",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-label": {
+ "version": "2.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menubar": {
+ "version": "1.1.16",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-navigation-menu": {
+ "version": "1.2.14",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-one-time-password-field": {
+ "version": "0.1.8",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-password-toggle-field": {
+ "version": "0.1.3",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-is-hydrated": "0.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popover": {
+ "version": "1.1.15",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.9",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.5",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-radio-group": {
+ "version": "1.3.8",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-scroll-area": {
+ "version": "1.2.10",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.7",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slider": {
+ "version": "1.3.6",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tabs": {
+ "version": "1.1.13",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.15",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle": {
+ "version": "1.1.10",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toggle-group": {
+ "version": "1.1.11",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toolbar": {
+ "version": "1.1.11",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-separator": "1.1.7",
+ "@radix-ui/react-toggle-group": "1.1.11"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.8",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-previous": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "license": "MIT"
+ },
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
+ "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.4",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
+ "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-beta.50",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.50.tgz",
+ "integrity": "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.50.tgz",
+ "integrity": "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.50.tgz",
+ "integrity": "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.50.tgz",
+ "integrity": "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.50.tgz",
+ "integrity": "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.50.tgz",
+ "integrity": "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.50.tgz",
+ "integrity": "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.0.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.50.tgz",
+ "integrity": "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-ia32-msvc": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.0.0-beta.50.tgz",
+ "integrity": "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-beta.50",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.50.tgz",
+ "integrity": "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
+ "node_modules/@swc/core": {
+ "version": "1.15.11",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3",
+ "@swc/types": "^0.1.25"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/swc"
+ },
+ "optionalDependencies": {
+ "@swc/core-darwin-arm64": "1.15.11",
+ "@swc/core-darwin-x64": "1.15.11",
+ "@swc/core-linux-arm-gnueabihf": "1.15.11",
+ "@swc/core-linux-arm64-gnu": "1.15.11",
+ "@swc/core-linux-arm64-musl": "1.15.11",
+ "@swc/core-linux-x64-gnu": "1.15.11",
+ "@swc/core-linux-x64-musl": "1.15.11",
+ "@swc/core-win32-arm64-msvc": "1.15.11",
+ "@swc/core-win32-ia32-msvc": "1.15.11",
+ "@swc/core-win32-x64-msvc": "1.15.11"
+ },
+ "peerDependencies": {
+ "@swc/helpers": ">=0.5.17"
+ },
+ "peerDependenciesMeta": {
+ "@swc/helpers": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@swc/core-darwin-arm64": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.11.tgz",
+ "integrity": "sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-darwin-x64": {
+ "version": "1.15.11",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm-gnueabihf": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.11.tgz",
+ "integrity": "sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-gnu": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.11.tgz",
+ "integrity": "sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-arm64-musl": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.11.tgz",
+ "integrity": "sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-gnu": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.11.tgz",
+ "integrity": "sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-linux-x64-musl": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.11.tgz",
+ "integrity": "sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-arm64-msvc": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.11.tgz",
+ "integrity": "sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-ia32-msvc": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.11.tgz",
+ "integrity": "sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/core-win32-x64-msvc": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.11.tgz",
+ "integrity": "sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@swc/counter": {
+ "version": "0.1.3",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/@swc/types": {
+ "version": "0.1.25",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@swc/counter": "^0.1.3"
+ }
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.1.18",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "enhanced-resolve": "^5.18.3",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.30.2",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.1.18"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss": {
+ "version": "1.30.2",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.30.2",
+ "lightningcss-darwin-arm64": "1.30.2",
+ "lightningcss-darwin-x64": "1.30.2",
+ "lightningcss-freebsd-x64": "1.30.2",
+ "lightningcss-linux-arm-gnueabihf": "1.30.2",
+ "lightningcss-linux-arm64-gnu": "1.30.2",
+ "lightningcss-linux-arm64-musl": "1.30.2",
+ "lightningcss-linux-x64-gnu": "1.30.2",
+ "lightningcss-linux-x64-musl": "1.30.2",
+ "lightningcss-win32-arm64-msvc": "1.30.2",
+ "lightningcss-win32-x64-msvc": "1.30.2"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
+ "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
+ "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
+ "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
+ "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
+ "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
+ "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
+ "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
+ "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
+ "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.2",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
+ "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/node/node_modules/lightningcss/node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.2",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.1.18",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.18",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.18",
+ "@tailwindcss/oxide-darwin-x64": "4.1.18",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.18",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.18",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.18",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
+ "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
+ "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.1.18",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
+ "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
+ "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
+ "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
+ "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
+ "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
+ "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
+ "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.1.0",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
+ "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.1.18",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
+ "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.1.18",
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.1.18",
+ "@tailwindcss/oxide": "4.1.18",
+ "tailwindcss": "4.1.18"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/history": {
+ "version": "4.7.11",
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.2.0",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.10",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "devOptional": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@types/react-router": {
+ "version": "5.1.20",
+ "license": "MIT",
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-router-dom": {
+ "version": "5.3.3",
+ "license": "MIT",
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*",
+ "@types/react-router": "*"
+ }
+ },
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/type-utils": "8.54.0",
+ "@typescript-eslint/utils": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.54.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.54.0",
+ "@typescript-eslint/types": "^8.54.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0",
+ "@typescript-eslint/utils": "8.54.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.54.0",
+ "@typescript-eslint/tsconfig-utils": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/visitor-keys": "8.54.0",
+ "debug": "^4.4.3",
+ "minimatch": "^9.0.5",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.54.0",
+ "@typescript-eslint/types": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.54.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react-swc": {
+ "version": "4.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.2",
+ "@swc/core": "^1.15.11"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4 || ^5 || ^6 || ^7"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-hidden": {
+ "version": "1.2.6",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.5",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+ "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/babel-plugin-macros": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
+ "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "cosmiconfig": "^7.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">=10",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.19",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001767",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.283",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.19.0",
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-toolkit": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz",
+ "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "license": "ISC"
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "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",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "license": "MIT"
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.31.1",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.31.1",
+ "lightningcss-darwin-arm64": "1.31.1",
+ "lightningcss-darwin-x64": "1.31.1",
+ "lightningcss-freebsd-x64": "1.31.1",
+ "lightningcss-linux-arm-gnueabihf": "1.31.1",
+ "lightningcss-linux-arm64-gnu": "1.31.1",
+ "lightningcss-linux-arm64-musl": "1.31.1",
+ "lightningcss-linux-x64-gnu": "1.31.1",
+ "lightningcss-linux-x64-musl": "1.31.1",
+ "lightningcss-win32-arm64-msvc": "1.31.1",
+ "lightningcss-win32-x64-msvc": "1.31.1"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
+ "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
+ "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.31.1",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
+ "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
+ "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
+ "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
+ "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
+ "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
+ "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
+ "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
+ "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "0.563.0",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/radix-ui": {
+ "version": "1.4.3",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-accessible-icon": "1.1.7",
+ "@radix-ui/react-accordion": "1.2.12",
+ "@radix-ui/react-alert-dialog": "1.1.15",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-aspect-ratio": "1.1.7",
+ "@radix-ui/react-avatar": "1.1.10",
+ "@radix-ui/react-checkbox": "1.3.3",
+ "@radix-ui/react-collapsible": "1.1.12",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-context-menu": "2.2.16",
+ "@radix-ui/react-dialog": "1.1.15",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-dropdown-menu": "2.1.16",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-form": "0.1.8",
+ "@radix-ui/react-hover-card": "1.1.15",
+ "@radix-ui/react-label": "2.1.7",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-menubar": "1.1.16",
+ "@radix-ui/react-navigation-menu": "1.2.14",
+ "@radix-ui/react-one-time-password-field": "0.1.8",
+ "@radix-ui/react-password-toggle-field": "0.1.3",
+ "@radix-ui/react-popover": "1.1.15",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-progress": "1.1.7",
+ "@radix-ui/react-radio-group": "1.3.8",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-scroll-area": "1.2.10",
+ "@radix-ui/react-select": "2.2.6",
+ "@radix-ui/react-separator": "1.1.7",
+ "@radix-ui/react-slider": "1.3.6",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-switch": "1.2.6",
+ "@radix-ui/react-tabs": "1.1.13",
+ "@radix-ui/react-toast": "1.2.15",
+ "@radix-ui/react-toggle": "1.1.10",
+ "@radix-ui/react-toggle-group": "1.1.11",
+ "@radix-ui/react-toolbar": "1.1.11",
+ "@radix-ui/react-tooltip": "1.2.8",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-escape-keydown": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-chartjs-2": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz",
+ "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-icons": {
+ "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": "*"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
+ "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/react-pro-sidebar": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/react-pro-sidebar/-/react-pro-sidebar-1.1.0.tgz",
+ "integrity": "sha512-rdRJ4PeMsqWq9n69AmF6et6qCbhCF1KEBgjAH8vIiLxE1k5fMxtRYo0k4asxW8qpIH6sqahiMxrxVVoObv8orQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/react": "^11.10.5",
+ "@emotion/styled": "^11.10.5",
+ "@popperjs/core": "^2.11.6",
+ "classnames": "^2.3.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/react-redux": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
+ "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.13.0",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.0",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/recharts": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz",
+ "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "1.x.x || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-beta.50",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.97.0",
+ "@rolldown/pluginutils": "1.0.0-beta.50"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-beta.50",
+ "@rolldown/binding-darwin-arm64": "1.0.0-beta.50",
+ "@rolldown/binding-darwin-x64": "1.0.0-beta.50",
+ "@rolldown/binding-freebsd-x64": "1.0.0-beta.50",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50",
+ "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.50",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwind-merge": {
+ "version": "3.4.0",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.1.18",
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "license": "0BSD"
+ },
+ "node_modules/tw-animate-css": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Wombosvideo"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.54.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.54.0",
+ "@typescript-eslint/parser": "8.54.0",
+ "@typescript-eslint/typescript-estree": "8.54.0",
+ "@typescript-eslint/utils": "8.54.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/vite": {
+ "name": "rolldown-vite",
+ "version": "7.2.5",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/runtime": "0.97.0",
+ "fdir": "^6.5.0",
+ "lightningcss": "^1.30.2",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rolldown": "1.0.0-beta.50",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "esbuild": "^0.25.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/app/client/package.json b/app/client/package.json
new file mode 100644
index 0000000..c2815c3
--- /dev/null
+++ b/app/client/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "finus-client",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "mutate": "stryker run"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.1.18",
+ "@types/react-router-dom": "^5.3.3",
+ "axios": "^1.13.5",
+ "chart.js": "^4.5.1",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.563.0",
+ "papaparse": "^5.5.3",
+ "radix-ui": "^1.4.3",
+ "react": "^19.2.0",
+ "react-chartjs-2": "^5.3.1",
+ "react-dom": "^19.2.0",
+ "react-icons": "^5.5.0",
+ "react-pro-sidebar": "^1.1.0",
+ "react-router-dom": "^7.13.0",
+ "recharts": "^3.7.0",
+ "tailwind-merge": "^3.4.0",
+ "tailwindcss": "^4.1.18"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@stryker-mutator/core": "^9.6.0",
+ "@stryker-mutator/vitest-runner": "^9.6.0",
+ "@testing-library/dom": "^10.4.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^25.3.5",
+ "@types/papaparse": "^5.3.16",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react-swc": "^4.2.2",
+ "@vitest/coverage-v8": "^4.0.18",
+ "@vitest/ui": "^4.0.18",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "prettier": "3.8.1",
+ "tw-animate-css": "^1.4.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.5",
+ "vitest": "^4.0.18"
+ },
+ "overrides": {
+ "vite": "npm:rolldown-vite@7.2.5"
+ }
+}
diff --git a/app/client/public/vite.svg b/app/client/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/app/client/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/App.css b/app/client/src/App.css
new file mode 100644
index 0000000..03b7a0c
--- /dev/null
+++ b/app/client/src/App.css
@@ -0,0 +1,261 @@
+.auth-shell {
+ position: relative;
+ min-height: 100vh;
+ overflow: hidden;
+}
+
+.auth-glow {
+ position: absolute;
+ width: 28rem;
+ height: 28rem;
+ border-radius: 50%;
+ opacity: 0.25;
+ filter: blur(90px);
+ pointer-events: none;
+ animation: float 9s ease-in-out infinite;
+}
+
+.auth-glow-left {
+ background: radial-gradient(
+ circle,
+ #18cc5f 0%,
+ #0d4d26 70%,
+ transparent 100%
+ );
+ top: -8rem;
+ left: -8rem;
+}
+
+.auth-glow-right {
+ background: radial-gradient(
+ circle,
+ #27a552 0%,
+ #0f411d 65%,
+ transparent 100%
+ );
+ right: -8rem;
+ bottom: -8rem;
+ animation-delay: 1.2s;
+}
+
+.auth-layout {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.auth-panel {
+ width: min(26rem, 100%);
+ background: rgba(9, 16, 11, 0.9);
+ border: 1px solid rgba(75, 255, 122, 0.25);
+ border-radius: 1.2rem;
+ box-shadow: 0 18px 45px rgba(0, 0, 0, 0.45);
+ padding: 2rem;
+ backdrop-filter: blur(6px);
+ animation: rise-in 0.5s ease-out;
+}
+
+.auth-panel-signup {
+ width: min(34rem, 100%);
+}
+
+.dashboard-panel {
+ text-align: center;
+}
+
+.auth-tag {
+ display: inline-block;
+ margin: 0;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.16em;
+ padding: 0.25rem 0.55rem;
+ border-radius: 999px;
+ color: #94ffb4;
+ border: 1px solid rgba(148, 255, 180, 0.45);
+ background: rgba(53, 177, 95, 0.12);
+}
+
+.auth-title {
+ margin: 1rem 0 0.4rem;
+ color: #d7ffe5;
+ font-size: clamp(1.55rem, 2vw + 1rem, 2rem);
+}
+
+.auth-copy {
+ margin: 0;
+ color: #9acaa9;
+ line-height: 1.45;
+}
+
+.auth-copy strong {
+ color: #d7ffe5;
+}
+
+.auth-form {
+ margin-top: 1.4rem;
+ display: grid;
+ gap: 0.75rem;
+}
+
+.auth-signup-form {
+ gap: 1rem;
+}
+
+.auth-group {
+ margin: 0;
+ padding: 0.9rem;
+ border: 1px solid rgba(82, 164, 106, 0.35);
+ border-radius: 0.9rem;
+ background: rgba(8, 22, 12, 0.55);
+}
+
+.auth-group legend {
+ color: #98d8ac;
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ padding: 0 0.35rem;
+}
+
+.auth-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.8rem 0.9rem;
+}
+
+.auth-field {
+ display: grid;
+ gap: 0.35rem;
+}
+
+.auth-field-full {
+ grid-column: 1 / -1;
+}
+
+.auth-form label {
+ color: #a8e0b9;
+ font-size: 0.86rem;
+}
+
+.auth-form input {
+ width: 100%;
+ border: 1px solid rgba(82, 164, 106, 0.5);
+ border-radius: 0.65rem;
+ background: #06100a;
+ color: #deffe9;
+ padding: 0.68rem 0.8rem;
+ outline: none;
+ transition:
+ border-color 0.2s,
+ box-shadow 0.2s;
+ font-size: 0.95rem;
+}
+
+.auth-form input::placeholder {
+ color: #79a588;
+}
+
+.auth-form input:focus {
+ border-color: #4af07f;
+ box-shadow: 0 0 0 3px rgba(74, 240, 127, 0.18);
+}
+
+.auth-error {
+ margin: 0.2rem 0 0;
+ color: #ff7f8f;
+ font-size: 0.9rem;
+}
+
+.auth-button {
+ width: 100%;
+ margin-top: 0.5rem;
+ border: 0;
+ border-radius: 0.7rem;
+ padding: 0.78rem 1rem;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ cursor: pointer;
+ color: #041108;
+ background: linear-gradient(135deg, #53ff8c, #17ac4d);
+ transition:
+ transform 0.2s,
+ box-shadow 0.2s;
+}
+
+.auth-button:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 8px 18px rgba(42, 174, 85, 0.35);
+}
+
+.auth-button:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+}
+
+.auth-button:active {
+ transform: translateY(0);
+}
+
+.auth-switch {
+ margin: 1rem 0 0;
+ color: #8eb79d;
+ font-size: 0.93rem;
+}
+
+.auth-switch a {
+ color: #8dffaf;
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.auth-switch a:hover {
+ text-decoration: underline;
+}
+
+.dash-hover-scrollbar {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(8, 22, 12, 0.55) rgba(8, 22, 12, 0.55);
+}
+
+.dash-hover-scrollbar:hover {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(34, 197, 94, 0.3) rgba(8, 22, 12, 0.55);
+}
+
+@media (max-width: 640px) {
+ .auth-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .auth-field-full {
+ grid-column: auto;
+ }
+}
+
+@keyframes rise-in {
+ from {
+ opacity: 0;
+ transform: translateY(14px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes float {
+ 0%,
+ 100% {
+ transform: translateY(0px);
+ }
+ 50% {
+ transform: translateY(-18px);
+ }
+}
diff --git a/app/client/src/App.tsx b/app/client/src/App.tsx
new file mode 100644
index 0000000..c13df41
--- /dev/null
+++ b/app/client/src/App.tsx
@@ -0,0 +1,237 @@
+import { useState } from "react";
+import {
+ BrowserRouter as Router,
+ Navigate,
+ Route,
+ Routes,
+} from "react-router-dom";
+import "./App.css";
+import LoginPage from "./pages/LoginPage";
+import SignUpPage from "./pages/SignUpPage";
+import type { AuthSession, AuthUser, AuthApiResponse } from "./types/authTypes";
+import DashboardPage from "./pages/DashboardPage.tsx";
+import MarketsPage from "./pages/MarketsPage";
+import AppLayout from "./components/AppLayout.tsx";
+import ProjectionPage from "./pages/ProjectionPage.tsx";
+import { BASE_URL, SESSION_STORAGE_KEY } from "@/utils/constants";
+
+function isValidAuthUser(value: unknown): value is AuthUser {
+ if (!value || typeof value !== "object") {
+ return false;
+ }
+
+ return typeof (value as { email?: unknown }).email === "string";
+}
+
+function loadSession(): AuthSession | null {
+ const raw = localStorage.getItem(SESSION_STORAGE_KEY);
+ if (!raw) {
+ return null;
+ }
+
+ try {
+ const parsed = JSON.parse(raw) as AuthSession;
+ if (!parsed.token || !isValidAuthUser(parsed.user)) {
+ return null;
+ }
+
+ return parsed;
+ } catch {
+ return null;
+ }
+}
+
+function saveSession(session: AuthSession) {
+ localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
+}
+
+function clearSession() {
+ localStorage.removeItem(SESSION_STORAGE_KEY);
+}
+
+function decodeTokenClaims(token: string): Record | null {
+ const parts = token.split(".");
+ if (parts.length < 2) {
+ return null;
+ }
+
+ try {
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
+ const json = atob(base64);
+ const parsed = JSON.parse(json) as Record;
+ return parsed;
+ } catch {
+ return null;
+ }
+}
+
+function resolveUserFromToken(
+ token: string,
+ fallbackUser: Partial,
+): AuthUser {
+ const claims = decodeTokenClaims(token);
+
+ const emailFromToken =
+ claims && typeof claims.email === "string" ? claims.email : undefined;
+ const nameFromToken =
+ claims && typeof claims.name === "string" ? claims.name : undefined;
+ const firstNameFromToken =
+ claims && typeof claims.first_name === "string"
+ ? claims.first_name
+ : undefined;
+ const lastNameFromToken =
+ claims && typeof claims.last_name === "string"
+ ? claims.last_name
+ : undefined;
+ const subFromToken =
+ claims && typeof claims.sub === "string" ? Number(claims.sub) : undefined;
+
+ return {
+ id: Number.isFinite(subFromToken) ? subFromToken : fallbackUser.id,
+ email: emailFromToken ?? fallbackUser.email ?? "",
+ name: nameFromToken ?? fallbackUser.name,
+ first_name: firstNameFromToken ?? fallbackUser.first_name,
+ last_name: lastNameFromToken ?? fallbackUser.last_name,
+ age: fallbackUser.age,
+ };
+}
+
+async function requestAuth(
+ path: string,
+ payload: Record,
+): Promise {
+ try {
+ const response = await fetch(`${BASE_URL}${path}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const data = (await response
+ .json()
+ .catch(() => null)) as AuthApiResponse | null;
+ if (!data) {
+ return { ok: false, error: "Invalid response from auth server." };
+ }
+
+ if (response.status >= 400) {
+ return {
+ ok: false,
+ error: data.error ?? "Authentication failed.",
+ };
+ }
+
+ return { ...data, ok: true };
+ } catch {
+ return { ok: false, error: "Unable to connect to auth server." };
+ }
+}
+
+function App() {
+ const [session, setSession] = useState(() =>
+ loadSession(),
+ );
+
+ function handleAuthSuccess(token: string, fallbackUser: Partial) {
+ const nextSession: AuthSession = {
+ token,
+ user: resolveUserFromToken(token, fallbackUser),
+ };
+
+ saveSession(nextSession);
+ setSession(nextSession);
+ }
+
+ function handleLogout() {
+ clearSession();
+ setSession(null);
+ }
+
+ return (
+
+
+
+
+
+
+
+ }
+ />
+
+ ) : (
+
+ )
+ }
+ />
+
+ ) : (
+
+ )
+ }
+ />
+
+ {session && (
+ }>
+ }
+ />
+ }
+ />
+
+ )}
+
+ {session && (
+ }>
+ }
+ />
+
+ )}
+
+ } />
+
+ {/**Code below is only used for dashboard development purposes */}
+ {/*} />
+ {console.log("Logout clicked")}}/>} >
+ } />
+ */}
+
+
+
+ );
+}
+
+export default App;
diff --git a/app/client/src/api/Account.ts b/app/client/src/api/Account.ts
new file mode 100644
index 0000000..417984c
--- /dev/null
+++ b/app/client/src/api/Account.ts
@@ -0,0 +1,83 @@
+import type { updateResponse } from "../types/responseTypes";
+import type { Account } from "../types/AccountType";
+
+import { instance } from "./config";
+// import { data } from "react-router-dom";
+
+//Sends a request to get different accounts the user has
+export async function getUserAccounts(type?: string): Promise {
+ try {
+ const response = await instance.get(
+ `/api/accounts${type ? `?type=${type}` : ""}`,
+ );
+
+ //determine if we were able to retrieve user's data
+ if (response.status === 200 || response.status === 201) {
+ // console.log("Account created:", response.data);
+ return response.data;
+ } else {
+ console.error("Failed to create account", response.status);
+ throw new Error(`Failed to create account: ${response.statusText}`);
+ }
+ } catch (error) {
+ console.error("Error creating account:", error);
+ throw error;
+ }
+}
+
+//Sends a post request to create a user account
+export async function postUserAccount(
+ newAccount: Account,
+): Promise {
+ try {
+ const response = await instance.post(`/api/accounts`, newAccount);
+ if (response.status === 200 || response.status === 201) {
+ console.log("Account created:", response.data);
+ return newAccount;
+ } else {
+ console.error("Failed to create account", response.status);
+ throw new Error(`Failed to create account: ${response.statusText}`);
+ }
+ } catch (error) {
+ console.error("Error creating account:", error);
+ throw error;
+ }
+}
+
+//Updates the users account
+export async function putUserAccount(
+ newAccount: Account,
+): Promise {
+ try {
+ const response = await instance.put(`/api/accounts`, newAccount);
+
+ if (response.status === 200) {
+ console.log("Account updated:", response.data);
+ return response.data;
+ } else {
+ console.error("Failed to update account", response.status);
+ throw new Error(`Failed to update account: ${response.statusText}`);
+ }
+ } catch (error) {
+ console.error("Error updating account:", error);
+ throw error;
+ }
+}
+
+export async function deleteUserAccount(accountId: number): Promise {
+ try {
+ const id = Number(accountId);
+ const response = await instance.delete(`/api/accounts?id=${id}`);
+
+ if (response.status === 200) {
+ console.log("Account deleted successfully");
+ return true;
+ } else {
+ console.error("Failed to delete account", response.status);
+ return false;
+ }
+ } catch (error) {
+ console.error("Error deleting account:", error);
+ return false;
+ }
+}
diff --git a/app/client/src/api/AuthAPI.ts b/app/client/src/api/AuthAPI.ts
new file mode 100644
index 0000000..09a3652
--- /dev/null
+++ b/app/client/src/api/AuthAPI.ts
@@ -0,0 +1,35 @@
+import type { AuthApiResponse } from "@/types/authTypes";
+import { BASE_URL } from "@/utils/constants";
+
+export async function requestAuth(
+ path: string,
+ payload: Record,
+): Promise {
+ try {
+ const response = await fetch(`${BASE_URL}${path}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+
+ const data = (await response
+ .json()
+ .catch(() => null)) as AuthApiResponse | null;
+ if (!data) {
+ return { ok: false, error: "Invalid response from auth server." };
+ }
+
+ if (response.status >= 400) {
+ return {
+ ok: false,
+ error: data.error ?? "Authentication failed.",
+ };
+ }
+
+ return { ...data, ok: true };
+ } catch {
+ return { ok: false, error: "Unable to connect to auth server." };
+ }
+}
diff --git a/app/client/src/api/DashboardAPI.ts b/app/client/src/api/DashboardAPI.ts
new file mode 100644
index 0000000..b17285c
--- /dev/null
+++ b/app/client/src/api/DashboardAPI.ts
@@ -0,0 +1,383 @@
+import type { ChartData } from "chart.js";
+import type { SankeyData } from "recharts/types/chart/Sankey";
+import type { Transaction } from "@/types/Transaction";
+import { instance } from "./config";
+import type { BudgetWithExpenditure } from "@/types/BudgetWithExpenditure";
+import type { SnapshotData } from "@/types/AggregatedSnapshot";
+import type { Account, MinimizedAccount } from "@/types/AccountType";
+
+async function getTransactions(): Promise {
+ try {
+ const response = await instance.get(`/table/transactions`);
+
+ if (response.status !== 200) {
+ throw new Error(
+ `Failed to fetch transactions table data: ${response.statusText}`,
+ );
+ }
+ if (!response.data) {
+ return null;
+ }
+ const output: Transaction[] = [];
+ for (let i = 0; i < response.data.length; i++) {
+ let formattedDate = response.data[i]["date"];
+ if (formattedDate) {
+ formattedDate = formattedDate.split("T")[0]; //database stores transacitons as datetime so split to get date and disregard time
+ }
+
+ output.push({
+ id: response.data[i]["id"],
+ financialAccount_id: response.data[i]["financialAccount_id"],
+ amount: response.data[i]["amount"],
+ category: response.data[i]["category"],
+ date: formattedDate,
+ sender: response.data[i]["sender"],
+ recipient: response.data[i]["recipient"],
+ description: response.data[i]["description"],
+ account_name: response.data[i]["account_name"],
+ });
+ }
+
+ return output;
+ } catch (error) {
+ console.error("Error fetching transactions data:", error);
+ throw error;
+ }
+}
+
+//deletes a single transaciton based on id - returns true if successful, false otherwise
+export async function deleteTransaction(
+ transactionId: number,
+): Promise {
+ try {
+ const response = await instance.delete(
+ `/table/transactions?tid=${transactionId}`,
+ );
+ if (response.status !== 200) {
+ console.error("Failed to delete transaction", response.status);
+ return false;
+ }
+ return true;
+ } catch (error) {
+ console.error("Error deleting transaction:", error);
+ return false;
+ }
+}
+
+//updates a single transaciton based on id - returns true if successful, false otherwise
+export async function updateTransaction(
+ transaction: Transaction,
+): Promise {
+ try {
+ const response = await instance.patch(
+ `/table/transactions?tid=${transaction.id}`,
+ transaction,
+ );
+
+ if (response.status !== 200) {
+ console.error("Failed to update transaction", response.status);
+ return false;
+ }
+
+ return true;
+ } catch (error) {
+ console.error("Error updating transaction:", error);
+ return false;
+ }
+}
+
+//creates a single transaciton - returns the created transaction with id if successful, throws error otherwise
+export async function createTransaction(
+ transaction: Transaction,
+): Promise {
+ try {
+ const response = await instance.post(
+ `/table/transactions?fid=${transaction.financialAccount_id}`,
+ transaction,
+ );
+ if (response.status !== 200) {
+ throw new Error(`Failed to create transaction: ${response.statusText}`);
+ }
+ return response.data;
+ } catch (error) {
+ console.error("Error creating transaction:", error);
+ throw error;
+ }
+}
+
+//gets a map of account ids to account names for a user id
+export async function getAccountIdsForUser(): Promise<
+ MinimizedAccount[] | null
+> {
+ try {
+ const response = await instance.get(`/table/transactions/accounts`);
+
+ if (response.status !== 200) {
+ throw new Error(
+ `Failed to fetch account IDs for user: ${response.statusText}`,
+ );
+ }
+
+ if (!response.data || !Array.isArray(response.data)) {
+ return null;
+ }
+
+ //directly map the array
+ const output: MinimizedAccount[] = response.data.map(
+ (account: MinimizedAccount) => ({
+ id: account.id,
+ name: account.name,
+ }),
+ );
+
+ return output;
+ } catch (error) {
+ console.error("Error fetching account IDs for user:", error);
+ throw error;
+ }
+}
+
+export async function getSnapshotData(): Promise {
+ try {
+ const response = await instance.get("/table/snapshot");
+ if (response.status !== 200) {
+ throw new Error(`Failed to fetch snapshot data: ${response.statusText}`);
+ }
+ return response.data;
+ } catch (error) {
+ console.error("Error fetching snapshot data:", error);
+ throw error;
+ }
+}
+
+//API for calling for various charts below
+
+// Budget with actual expenditure and proposed budgets - returns category, budgetAmount, actualAmount
+async function getBudgetWithExpenditure(
+ period: string,
+): Promise {
+ try {
+ const response = await instance.get(
+ `/charts/budget-expenditure?period=${period}`,
+ );
+ if (response.status !== 200) {
+ throw new Error(
+ `Failed to fetch budget with actual expenditure: ${response.statusText}`,
+ );
+ }
+ if (!response.data.categories) {
+ return null;
+ }
+ const categories = response.data.categories || [];
+ const budgetAmounts = response.data.budgetAmounts || [];
+ const actualAmounts = response.data.actualAmounts || [];
+ const output: BudgetWithExpenditure[] = [];
+
+ for (let i = 0; i < categories.length; i++) {
+ output.push({
+ id: `budget-${i}-${Date.now()}`, // This will be the same for all items if called once
+ category: categories[i],
+ budgetAmount: budgetAmounts[i],
+ actualAmount: actualAmounts[i],
+ });
+ }
+
+ return output;
+ } catch (error) {
+ console.error("Error fetching budget with actual expenditure:", error);
+ throw error;
+ }
+}
+
+// Expenses over time chart
+// Accepted time periods are "w" for weekly, "m" for monthly, and "y" for yearly - the backend will handle the units
+async function getExpensesChartData(
+ period: string,
+): Promise | null> {
+ try {
+ if (!["w", "m", "y"].includes(period)) {
+ throw new Error(
+ "Invalid period. Must be 'weekly', 'monthly', or 'yearly'.",
+ );
+ }
+ const response = await instance.get(`/charts/expenses?period=${period}`);
+ if (response.status !== 200) {
+ throw new Error(
+ `Failed to fetch expenses chart data: ${response.statusText}`,
+ );
+ }
+ if (!response.data) {
+ return null;
+ }
+ return {
+ labels: response.data["labels"],
+ datasets: [
+ {
+ label: response.data["datasets"][0]["label"],
+ data: response.data["datasets"][0]["data"],
+ borderColor: "rgb(254, 103, 48)",
+ backgroundColor: "rgba(255, 47, 47, 0.5)",
+ },
+ ],
+ };
+ } catch (error) {
+ console.error("Error fetching expenses chart data:", error);
+ throw error;
+ }
+}
+
+// Savings contribution chart
+async function getSavingsContribChartData(
+ period: string,
+): Promise | null> {
+ try {
+ if (!["w", "m", "y"].includes(period)) {
+ throw new Error(
+ "Invalid period. Must be 'weekly', 'monthly', or 'yearly'.",
+ );
+ }
+ const response = await instance.get(`/charts/savings?period=${period}`);
+ if (response.status !== 200) {
+ throw new Error(
+ `Failed to fetch savings contribution chart data: ${response.statusText}`,
+ );
+ }
+
+ if (response && response.data.datasets) {
+ return {
+ labels: response.data["labels"],
+ datasets: [
+ {
+ label: response.data["datasets"][0]["label"],
+ data: response.data["datasets"][0]["data"],
+ borderColor: "rgb(68, 255, 21)",
+ backgroundColor: "rgba(68, 255, 21, 0.5)",
+ },
+ ],
+ };
+ } else {
+ return null;
+ }
+ } catch (error) {
+ console.error("Error fetching savings contribution chart data:", error);
+ throw error;
+ }
+}
+
+// Income flow - sankey chart data
+async function getIncomeFlowChartData(
+ period: string,
+): Promise {
+ try {
+ if (!["w", "m", "y"].includes(period)) {
+ throw new Error(
+ "Invalid period. Must be 'weekly', 'monthly', or 'yearly'.",
+ );
+ }
+ const response = await instance.get(`/charts/incomeflow?period=${period}`);
+ if (response.status !== 200) {
+ throw new Error(
+ `Failed to fetch income flow chart data: ${response.statusText}`,
+ );
+ }
+ if (!response.data) {
+ return null;
+ }
+ return response.data;
+ } catch (error) {
+ console.error("Error fetching income flow chart data:", error);
+ throw error;
+ }
+}
+
+export async function getUserAccounts(): Promise {
+ try {
+ const response = await instance.get(`/api/accounts`);
+
+ if (response.status !== 200) {
+ throw new Error(`Failed to fetch accounts: ${response.statusText}`);
+ }
+ if (!response.data) {
+ return null;
+ }
+
+ const output: Account[] = [];
+ for (let i = 0; i < response.data.length; i++) {
+ output.push({
+ id: response.data[i]["id"],
+ name: response.data[i]["name"],
+ type: response.data[i]["type"],
+ balance: response.data[i]["balance"],
+ value: response.data[i]["value"] || response.data[i]["balance"],
+ subtype: response.data[i]["subtype"],
+ last_updated: response.data[i]["last_updated"],
+ });
+ }
+
+ return output;
+ } catch (error) {
+ console.error("Error fetching accounts:", error);
+ throw error;
+ }
+}
+
+//create a new account
+export async function createAccount(
+ accountData: Omit,
+): Promise {
+ try {
+ const response = await instance.post(`/api/accounts`, accountData);
+
+ if (response.status !== 200) {
+ throw new Error(`Failed to create account: ${response.statusText}`);
+ }
+
+ return response.data;
+ } catch (error) {
+ console.error("Error creating account:", error);
+ throw error;
+ }
+}
+
+//update an existing account
+export async function updateAccount(
+ accountData: Partial & { id: number },
+): Promise {
+ try {
+ const response = await instance.put(
+ `/api/accounts/${accountData.id}`,
+ accountData,
+ );
+
+ if (response.status !== 200) {
+ throw new Error(`Failed to update account: ${response.statusText}`);
+ }
+
+ return response.data;
+ } catch (error) {
+ console.error("Error updating account:", error);
+ throw error;
+ }
+}
+
+//delete an account
+export async function deleteAccount(accountId: number): Promise {
+ try {
+ const response = await instance.delete(`/api/accounts/${accountId}`);
+
+ if (response.status !== 200) {
+ throw new Error(`Failed to delete account: ${response.statusText}`);
+ }
+ } catch (error) {
+ console.error("Error deleting account:", error);
+ throw error;
+ }
+}
+
+export {
+ getBudgetWithExpenditure,
+ getTransactions,
+ getExpensesChartData,
+ getSavingsContribChartData,
+ getIncomeFlowChartData,
+};
diff --git a/app/client/src/api/Debt.ts b/app/client/src/api/Debt.ts
new file mode 100644
index 0000000..afbf575
--- /dev/null
+++ b/app/client/src/api/Debt.ts
@@ -0,0 +1,61 @@
+import type { DebtPayoffResponse } from "../types/responseTypes";
+// import type { AuthSession } from "@/types/authTypes";
+import type { Account } from "@/types/AccountType";
+import type { projectionDebtRequest } from "@/types/requestTypes";
+import { instance } from "./config";
+
+// const requestUrl = "http://localhost:3000/api/debts";
+
+//Sends a request to get different debts the user has
+export async function getDebt(): Promise {
+ try {
+ //Sends a http request and waits for a response
+ // const response = await fetch(requestUrl, {
+ // method: "GET",
+ // headers: { Authorization: `Bearer ${session.token}` },
+ // });
+ const response = await instance.get(`/api/debts`);
+
+ //Determine if we were able to retrieve user's data
+ if (response.status !== 200) {
+ //Failed to retrieve user data, return empty array
+ console.error("Error: Failed to retrieve users debts", response.status);
+ return [];
+ }
+
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+}
+
+//Post request, even tho it says get in the function
+export async function getDebtProjection(
+ // session: AuthSession,
+ request: projectionDebtRequest,
+): Promise {
+ // const url = "http://localhost:3000/predict-debt-payoff";
+
+ try {
+ //Create post request and wait for response
+ // const response = await fetch(url, {
+ // method: "POST",
+ // headers: {
+ // "content-type": "application/json",
+ // Authorization: `Bearer ${session.token}`,
+ // },
+ // body: JSON.stringify(request),
+ // });
+ const response = await instance.post(`/predict-debt-payoff`, request);
+ //Determine if our post was a success
+ if (response.status !== 200) {
+ console.error(response.status);
+ }
+
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+}
diff --git a/app/client/src/api/GoalsAPI.ts b/app/client/src/api/GoalsAPI.ts
new file mode 100644
index 0000000..4a024d0
--- /dev/null
+++ b/app/client/src/api/GoalsAPI.ts
@@ -0,0 +1,41 @@
+import type { Goal } from "../types/goals.ts";
+import { instance } from "./config";
+
+async function fetchGoals(): Promise {
+ const response = await instance.get("/goals");
+ if (response.status !== 200) {
+ throw new Error(`Failed to fetch goals data: ${response.statusText}`);
+ }
+ return response.data;
+}
+
+async function createGoal(goal: Partial): Promise {
+ const response = await instance.post("/goals", goal);
+ if (response.status !== 201) {
+ // 201 Created is standard for POST
+ throw new Error(`Failed to create goal: ${response.statusText}`);
+ }
+ return response.data;
+}
+
+async function updateGoal(
+ goalId: string,
+ updates: Partial,
+): Promise {
+ const response = await instance.patch(`/goals?gid=${goalId}`, updates);
+ if (response.status !== 200) {
+ throw new Error(`Failed to edit goal: ${response.statusText}`);
+ }
+
+ return response.data;
+}
+
+async function deleteGoal(goalId: string): Promise {
+ const response = await instance.delete(`/goals?gid=${goalId}`);
+ if (response.status !== 204) {
+ // 204 No Content is standard for DELETE
+ throw new Error(`Failed to delete goal: ${response.statusText}`);
+ }
+}
+
+export { fetchGoals, createGoal, updateGoal, deleteGoal };
diff --git a/app/client/src/api/Income.ts b/app/client/src/api/Income.ts
new file mode 100644
index 0000000..dff2958
--- /dev/null
+++ b/app/client/src/api/Income.ts
@@ -0,0 +1,126 @@
+import type { updateResponse } from "../types/responseTypes";
+import { type Income } from "../types/IncomeType";
+import type { AuthSession } from "@/types/authTypes";
+import { BASE_URL } from "@/utils/constants";
+
+const requestUrl = `${BASE_URL}/api/income`;
+
+export async function getIncome(session: AuthSession): Promise {
+ try {
+ const response = await fetch(requestUrl, {
+ method: "GET",
+ headers: { Authorization: `Bearer ${session.token}` },
+ });
+
+ if (!response.ok) {
+ console.error(response.status);
+ alert("Failed to retrieve income");
+ return [];
+ } else {
+ return response.json();
+ }
+ } catch (error) {
+ console.log(error);
+ throw error;
+ }
+}
+
+export async function postIncome(
+ session: AuthSession,
+ name: string,
+ income: number,
+ description: string,
+): Promise {
+ try {
+ const response = await fetch(requestUrl, {
+ method: "POST",
+ headers: {
+ "content-type": "application/json",
+ Authorization: `Bearer ${session.token}`,
+ },
+
+ body: JSON.stringify({
+ name: name,
+ income: income,
+ description: description,
+ }),
+ });
+
+ if (response.ok) {
+ alert("Created income " + name);
+ } else {
+ alert("Failed to create income " + name);
+ console.error(response.status);
+ }
+
+ return response.json();
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+}
+
+export async function putIncome(
+ session: AuthSession,
+ name: string,
+ income: number,
+ description: string,
+) {
+ try {
+ const response = await fetch(requestUrl, {
+ method: "POST",
+ headers: {
+ "content-type": "applicaton/json",
+ Authorization: `Bearer ${session.token}`,
+ },
+ credentials: "include",
+ body: JSON.stringify({
+ name: name,
+ income: income,
+ description: description,
+ }),
+ });
+
+ if (response.ok) {
+ alert("Created income " + name);
+ } else {
+ alert("Failed to create income " + name);
+ console.error(response.status);
+ }
+
+ return response.ok;
+ } catch (error) {
+ console.log(error);
+ throw error;
+ }
+}
+
+export async function deleteIncome(
+ session: AuthSession,
+ selectedIncome: Income,
+) {
+ const content = JSON.stringify({ id: selectedIncome.id });
+ try {
+ //Create delete request to delete the income
+ const response = await fetch(requestUrl, {
+ method: "DELETE",
+ headers: {
+ "content-type": "/application/json",
+ Authorization: `Bearer ${session.token}`,
+ },
+ body: content,
+ });
+
+ if (response.ok) {
+ alert("Income " + selectedIncome.name + " has been deleted");
+ } else {
+ alert("Failed to delete income " + selectedIncome.name);
+ console.error(response.status);
+ }
+
+ return response.ok;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+}
diff --git a/app/client/src/api/Market.ts b/app/client/src/api/Market.ts
new file mode 100644
index 0000000..df430cb
--- /dev/null
+++ b/app/client/src/api/Market.ts
@@ -0,0 +1,88 @@
+import axios from "axios";
+import { instance } from "@/api/config";
+import type {
+ MarketHistoryPeriod,
+ MarketHistoryPoint,
+ MarketInstrument,
+} from "@/types/Market";
+import { getIntervalForPeriod } from "@/utils/market";
+
+type MarketQuoteResponse = Pick<
+ MarketInstrument,
+ "symbol" | "price" | "change" | "changePercent" | "timestamp"
+>;
+
+type MarketHistoryResponse = {
+ symbol: string;
+ period: MarketHistoryPeriod;
+ interval: "5m" | "15m" | "1d" | "1wk" | "1mo";
+ points: MarketHistoryPoint[];
+};
+
+function toMarketApiError(error: unknown, fallbackMessage: string): Error {
+ if (axios.isAxiosError(error)) {
+ const detail = error.response?.data?.detail;
+ if (typeof detail === "string" && detail.length > 0) {
+ return new Error(detail);
+ }
+ }
+
+ return new Error(fallbackMessage);
+}
+
+async function searchMarkets(query: string): Promise {
+ try {
+ const response = await instance.get("/markets/search", {
+ params: { q: query },
+ });
+
+ if (response.status !== 200) {
+ throw new Error(`Failed to search markets: ${response.statusText}`);
+ }
+
+ return response.data as MarketInstrument[];
+ } catch (error) {
+ throw toMarketApiError(error, "Unable to search market instruments.");
+ }
+}
+
+async function getMarketQuote(symbol: string): Promise {
+ try {
+ const response = await instance.get("/markets/quote", {
+ params: { symbol },
+ });
+
+ if (response.status !== 200) {
+ throw new Error(`Failed to load market quote: ${response.statusText}`);
+ }
+
+ return response.data as MarketQuoteResponse;
+ } catch (error) {
+ throw toMarketApiError(error, "Unable to load market quote.");
+ }
+}
+
+async function getMarketHistory(
+ symbol: string,
+ period: MarketHistoryPeriod,
+): Promise {
+ try {
+ const response = await instance.get("/markets/history", {
+ params: {
+ symbol,
+ period,
+ interval: getIntervalForPeriod(period),
+ },
+ });
+
+ if (response.status !== 200) {
+ throw new Error(`Failed to load market history: ${response.statusText}`);
+ }
+
+ return response.data as MarketHistoryResponse;
+ } catch (error) {
+ throw toMarketApiError(error, "Unable to load market history.");
+ }
+}
+
+export { getMarketHistory, getMarketQuote, searchMarkets };
diff --git a/app/client/src/api/Saving.ts b/app/client/src/api/Saving.ts
new file mode 100644
index 0000000..45412a2
--- /dev/null
+++ b/app/client/src/api/Saving.ts
@@ -0,0 +1,67 @@
+import { type Account } from "@/types/AccountType";
+// import { type AuthSession } from "@/types/authTypes";
+import type { projectionSavingRequest } from "@/types/requestTypes";
+import { type savingProjectionResponseData } from "@/types/responseTypes";
+// const requestUrl = "http://localhost:3000/api/savings";
+import { instance } from "./config";
+
+//Sends a request to get different debts the user has
+export async function getSaving(): Promise {
+ try {
+ //Sends a http request and waits for a response
+ // const response = await fetch(requestUrl, {
+ // method: "GET",
+ // headers: { Authorization: `Bearer ${session.token}` },
+ // });
+ const response = await instance.get(`/api/savings`);
+
+ //Determine if we were able to retrieve user's data
+ if (response.status !== 200) {
+ //Failed to retrieve user data, return empty array
+ console.error("Error: Failed to retrieve users debts", response.status);
+ return [];
+ }
+
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+}
+
+//Post request, even tho it says get in the function
+export async function getSavingProjection(
+ // session: AuthSession,
+ request: projectionSavingRequest,
+): Promise {
+ // const url = "http://localhost:3000/compound-interest";
+
+ try {
+ //Create post request and wait for response
+ // const response = await fetch(url, {
+ // method: "POST",
+ // headers: {
+ // "content-type": "application/json",
+ // Authorization: `Bearer ${session.token}`,
+ // },
+ // body: JSON.stringify(request),
+ // });
+ console.log("sent over: ", JSON.stringify(request));
+ const response = await instance.post(
+ `/compound-interest`,
+ JSON.stringify(request),
+ );
+ console.log("response from server: ", response);
+
+ //Determine if our post was a success
+ if (response.status !== 200) {
+ console.error(response.status);
+ return [];
+ }
+
+ return response.data;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+}
diff --git a/app/client/src/api/Transaction.ts b/app/client/src/api/Transaction.ts
new file mode 100644
index 0000000..eff3752
--- /dev/null
+++ b/app/client/src/api/Transaction.ts
@@ -0,0 +1,23 @@
+// import type { AuthSession } from "@/types/authTypes";
+// import type { updateResponse } from "../types/responseTypes";
+import type { Transaction } from "../types/Transaction";
+import type { TransactionDraft } from "@/utils/ConvertTransaction";
+import { instance } from "./config";
+
+
+// POST /api/transactions/csvTransaction
+export async function uploadCsvTransactions(
+ // session: AuthSession,
+ financialAccount_id: number,
+ transactions: TransactionDraft[],
+): Promise<{
+ inserted: number;
+ skipped: number;
+ transactions: Transaction[];
+}> {
+ const response = await instance.post(
+ `/api/transactions/csvTransaction`,
+ JSON.stringify({ financialAccount_id, transactions }),
+ );
+ return response.data;
+}
diff --git a/app/client/src/api/config.ts b/app/client/src/api/config.ts
new file mode 100644
index 0000000..c274ec3
--- /dev/null
+++ b/app/client/src/api/config.ts
@@ -0,0 +1,31 @@
+import axios from "axios";
+import { BASE_URL } from "@/utils/constants";
+import { loadSession } from "@/utils/storage";
+
+// Create an Axios instance with default configuration. This instance is used to request protected endpoints only.
+const instance = axios.create({
+ baseURL: BASE_URL,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ withCredentials: true, // Include cookies for authentication
+});
+const session = loadSession();
+const accessToken = session ? `Bearer ${session.token}` : "";
+instance.defaults.headers.common["Authorization"] = accessToken;
+
+//updates the token on each user request in case it has changed
+instance.interceptors.request.use(
+ (config) => {
+ const session = loadSession();
+ if (session?.token) {
+ config.headers.Authorization = `Bearer ${session.token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ },
+);
+
+export { instance };
diff --git a/app/client/src/assets/react.svg b/app/client/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/app/client/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/client/src/components/AccountCard.tsx b/app/client/src/components/AccountCard.tsx
new file mode 100644
index 0000000..0d53f4a
--- /dev/null
+++ b/app/client/src/components/AccountCard.tsx
@@ -0,0 +1,61 @@
+type AccountCardProps = {
+ title: string;
+ amount: string;
+ trend?: "positive" | "negative" | "neutral"; // for tracking performance at a glance instead of a budget
+ isLoading?: boolean;
+};
+export default function AccountCard({
+ title,
+ amount,
+ trend,
+ isLoading,
+}: AccountCardProps) {
+ const getAmountColor = () => {
+ if (isLoading) return "text-gray-500";
+ if (trend === "positive") return "text-green-400";
+ if (trend === "negative") return "text-red-400";
+ if (trend === "neutral") return "text-white";
+
+ //auto-detect from amount string
+ if (amount.includes("-") || amount.startsWith("-")) return "text-red-400";
+ if (
+ amount !== "Loading..." &&
+ amount !== "$0" &&
+ !amount.includes("Loading")
+ ) {
+ return "text-green-400";
+ }
+ return "text-white";
+ };
+ return (
+
+ {/* subtle glow effect */}
+
+
+
+ {title}
+
+
+
{amount}
+ {isLoading && (
+
+ )}
+
+ );
+}
diff --git a/app/client/src/components/AccountForm.tsx b/app/client/src/components/AccountForm.tsx
new file mode 100644
index 0000000..578df72
--- /dev/null
+++ b/app/client/src/components/AccountForm.tsx
@@ -0,0 +1,264 @@
+import React, { useState } from "react";
+import "./userForm.css";
+import { postUserAccount, putUserAccount } from "../api/Account.ts";
+import { type Account } from "../types/AccountType.ts";
+import { validateAccountForm } from "../utils/ValidateForms.ts";
+import {
+ handleCurrencyChange,
+ handleCurrencyBlur,
+} from "../utils/handleInput.ts";
+import { accountCategory } from "@/enum/AccountCategory.ts";
+import type { AuthSession } from "@/types/authTypes.ts";
+
+interface popupProp {
+ toggle: () => void;
+ session: AuthSession;
+ setAccount?: (account: Account) => void;
+ addAccount?: (account: Account) => void;
+ edit: boolean;
+ selectedAccount?: Account;
+}
+
+type typeofAccount = keyof typeof accountCategory;
+
+export default function PopupForm({
+ toggle,
+ session,
+ setAccount,
+ addAccount,
+ edit,
+ selectedAccount,
+}: popupProp) {
+ //Submits the account data
+
+ const handleSubmit = () => {
+ let subtype = undefined;
+ let interest = undefined;
+ const accountBalance = Number(balance);
+
+ if (accountType === accountCategory.SAVING) {
+ subtype = formInput.subType;
+ }
+
+ if (
+ accountType === accountCategory.SAVING ||
+ accountType === accountCategory.DEBT
+ ) {
+ interest = formInput.interest;
+ }
+
+ //Check account before validating b/c account can be undefined
+ //Determine if form input for an account is valid
+ if (
+ accountType &&
+ validateAccountForm(
+ formInput.name,
+ accountCategory[accountType],
+ accountBalance,
+ undefined,
+ subtype,
+ interest,
+ )
+ ) {
+ const newAccount: Account = {
+ id: 0,
+ name: formInput.name,
+ type: accountType,
+ balance: accountBalance,
+ subtype: subtype,
+ value: 0,
+ last_updated: new Date(),
+ };
+ console.log(newAccount);
+
+ //Determine if we editing an account info
+ if (edit && selectedAccount) {
+ newAccount.id = selectedAccount.id;
+
+ try {
+ //Put request to update the account
+ putUserAccount(session, newAccount).then((response) => {
+ //Determine if sucessfully updated the account
+ if (response && response.lastUpdated && setAccount) {
+ newAccount.last_updated = response.lastUpdated;
+ newAccount.balance = Number(newAccount.balance.toFixed(2));
+ setAccount(newAccount);
+ }
+ });
+ } catch {
+ alert("Failed to update Account");
+ }
+ } else {
+ try {
+ //Send a post request to create
+ postUserAccount(session, newAccount).then((response) => {
+ //When creating new account, id and lastupdated is returned for that account
+ if (response && response.id) {
+ newAccount.id = response.id;
+ }
+
+ if (response.lastUpdated) {
+ newAccount.last_updated = response.lastUpdated;
+
+ //Check if addAcount is undefined
+ if (addAccount) {
+ addAccount(newAccount);
+ }
+ }
+ });
+ } catch {
+ alert("Failed to create account " + newAccount.name);
+ }
+ }
+
+ toggle();
+ } else {
+ alert("Please fill out the form");
+ }
+ };
+
+ const handleChange = (
+ event: React.ChangeEvent,
+ ) => {
+ setFormInput({ ...formInput, [event.target.name]: event.target.value });
+ };
+
+ //Handles the bank statement
+ /*const handleFile = (event:React.ChangeEvent) => {
+
+ if(event.target && event.target.files && event.target.files[0]){
+ setFile(event.target.files[0])
+ } else {
+ setFile(undefined)
+ }
+ }*/
+
+ const [formInput, setFormInput] = useState(() => {
+ if (edit && selectedAccount) {
+ return {
+ name: selectedAccount.name,
+ subType: selectedAccount.subtype || "",
+ interest: 0,
+ };
+ } else {
+ //Default value
+ return {
+ name: "",
+ subType: "",
+ interest: 0,
+ };
+ }
+ });
+ //const [file, setFile] = useState(undefined)
+ const [balance, setBalance] = useState(() => {
+ if (edit && selectedAccount) {
+ return selectedAccount.balance.toString();
+ } else {
+ //Default value
+ return "";
+ }
+ });
+
+ const [accountType, setAccountType] = useState(
+ () => {
+ if (edit && selectedAccount) {
+ return selectedAccount.type as typeofAccount;
+ } else {
+ //Default value
+ return undefined;
+ }
+ },
+ );
+
+ const accountCat: typeofAccount[] = Object.keys(
+ accountCategory,
+ ) as typeofAccount[];
+
+ return (
+ <>
+
+
+ {edit ?
Edit Account
:
Create Account
}
+
+
+
+
+
+
+
+
+
+
+
handleCurrencyChange(event, setBalance)}
+ onBlur={(event) => handleCurrencyBlur(event, balance, setBalance)}
+ placeholder="0.00"
+ />
+
+
+ {accountType === "DEBT" || accountType === "SAVING" ? (
+ <>
+
+
+
+ >
+ ) : null}
+
+ {accountType === "SAVING" ? (
+ <>
+
+
+
+ >
+ ) : null}
+
+
+
+ {edit ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/app/client/src/components/AccountList.tsx b/app/client/src/components/AccountList.tsx
new file mode 100644
index 0000000..2b8f3c1
--- /dev/null
+++ b/app/client/src/components/AccountList.tsx
@@ -0,0 +1,553 @@
+import { useEffect, useState, useMemo } from "react";
+import {
+ getUserAccounts,
+ postUserAccount,
+ putUserAccount,
+ deleteUserAccount,
+} from "../api/Account";
+import { type Account } from "../types/AccountType";
+import { AiOutlinePlus, AiOutlineClose, AiOutlineSearch } from "react-icons/ai";
+import { FiEdit2 } from "react-icons/fi";
+import NoItemState from "./NoItemState";
+import type { AuthSession } from "@/types/authTypes";
+import {
+ handleCurrencyBlur,
+ handleCurrencyChange,
+ handleListCurrencyChange,
+ handleListCurrencyBlur,
+} from "@/utils/handleInput";
+
+interface AccountListProps {
+ session: AuthSession;
+}
+
+interface EditableAccount extends Account {
+ isExpanded?: boolean;
+}
+
+function AccountList({ session }: AccountListProps) {
+ const [accounts, setAccounts] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [showAddForm, setShowAddForm] = useState(false);
+ const [editingValues, setEditingValues] = useState<{
+ [key: number]: Partial;
+ }>({});
+
+ // Fetch accounts on mount
+ useEffect(() => {
+ const fetchAccounts = async () => {
+ try {
+ setIsLoading(true);
+ const data = await getUserAccounts();
+ setAccounts((data || []).map((acc) => ({ ...acc, isExpanded: false })));
+ } catch (error) {
+ console.error("Error fetching accounts:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchAccounts();
+ }, [session]);
+
+ // Filter accounts based on search
+ const filteredAccounts = useMemo(() => {
+ if (!searchTerm.trim()) return accounts;
+ const term = searchTerm.toLowerCase();
+ return accounts.filter(
+ (acc) =>
+ acc.name.toLowerCase().includes(term) ||
+ acc.type.toLowerCase().includes(term) ||
+ (acc.subtype && acc.subtype.toLowerCase().includes(term)),
+ );
+ }, [accounts, searchTerm]);
+
+ // format helpers
+ const formatCurrency = (amount: number | string): string => {
+ //convert to number if it's a string
+ const numAmount = typeof amount === "string" ? parseFloat(amount) : amount;
+
+ //check if conversion was successful
+ if (isNaN(numAmount)) {
+ return "$0.00";
+ }
+
+ if (numAmount < 0) {
+ return `-$${Math.abs(numAmount).toFixed(2)}`;
+ }
+ return `$${numAmount.toFixed(2)}`;
+ };
+
+ const formatDate = (dateStr: string): string => {
+ if (!dateStr) return "N/A";
+ return new Date(dateStr).toLocaleDateString();
+ };
+
+ // expand/collapse row
+ const toggleExpand = (accountId: number) => {
+ setAccounts((prev) =>
+ prev.map((acc) =>
+ acc.id === accountId ? { ...acc, isExpanded: !acc.isExpanded } : acc,
+ ),
+ );
+
+ const account = accounts.find((a) => a.id === accountId);
+ if (account && !editingValues[accountId]) {
+ setEditingValues((prev) => ({
+ ...prev,
+ [accountId]: {
+ name: account.name,
+ type: account.type,
+ balance: account.balance,
+ subtype: account.subtype,
+ },
+ }));
+ }
+ };
+
+ // handle field changes
+ const handleFieldChange = (
+ accountId: number,
+ field: string,
+ value: string | number,
+ ) => {
+ setEditingValues((prev) => ({
+ ...prev,
+ [accountId]: {
+ ...prev[accountId],
+ [field]: value,
+ },
+ }));
+ };
+
+ // save edited account
+ const handleSaveEdit = async (accountId: number) => {
+ const updates = editingValues[accountId];
+ if (!updates) return;
+
+ try {
+ const updatedAccount = await putUserAccount({
+ ...updates,
+ balance: Number(updates.balance),
+ id: accountId,
+ } as Account);
+ if (updatedAccount) {
+ setAccounts((prev) =>
+ prev.map((acc) =>
+ acc.id === accountId
+ ? { ...acc, ...updates, isExpanded: false }
+ : acc,
+ ),
+ );
+ setEditingValues((prev) => {
+ const newState = { ...prev };
+ delete newState[accountId];
+ return newState;
+ });
+ }
+ } catch (error) {
+ console.error("Failed to update account:", error);
+ alert("Failed to update account");
+ }
+ };
+
+ // delete account
+ const handleDelete = async (accountId: number) => {
+ if (
+ !confirm(
+ "Are you sure you want to delete this account? This will also delete all associated transactions.",
+ )
+ )
+ return;
+
+ try {
+ const accountToDelete = accounts.find((a) => a.id === accountId);
+ if (accountToDelete) {
+ const success = await deleteUserAccount(accountToDelete.id);
+ if (success) {
+ setAccounts((prev) => prev.filter((acc) => acc.id !== accountId));
+ }
+ }
+ } catch (error) {
+ console.error("Failed to delete account:", error);
+ alert("Failed to delete account");
+ }
+ };
+
+ // new account state
+ const [newAccount, setNewAccount] = useState({
+ name: "",
+ type: "",
+ subtype: "",
+ });
+
+ const [newBalance, setNewBalance] = useState("");
+
+ const handleAddAccount = async () => {
+ if (!newAccount.name || !newAccount.type) {
+ alert("Please fill in required fields (name and type)");
+ return;
+ }
+
+ try {
+ const balance = Number(newBalance);
+ const created = await postUserAccount({
+ id: 0,
+ name: newAccount.name,
+ type: newAccount.type,
+ subtype: newAccount.subtype || null,
+ balance: balance,
+ value: balance,
+ last_updated: new Date().toISOString(),
+ } as Account);
+
+ setAccounts(
+ (prev) =>
+ [{ ...created, isExpanded: false }, ...prev] as EditableAccount[],
+ );
+ setNewAccount({ name: "", type: "", subtype: "" });
+ setNewBalance("");
+ setShowAddForm(false);
+ } catch (error) {
+ console.error("Failed to create account:", error);
+ alert("Failed to create account");
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (accounts.length === 0 && !showAddForm) {
+ return (
+
+ 💰
}
+ />
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header with search and add button */}
+
+
+
+
setSearchTerm(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 bg-black/50 border border-green-500/20 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-green-500"
+ />
+
+
+
+
+ {/* Add Account Form */}
+ {showAddForm && (
+
+
+
New Account
+
+
+
+
+ setNewAccount({ ...newAccount, name: e.target.value })
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white"
+ />
+
+ {newAccount.type === "savings" && (
+
+ )}
+ {newAccount.type === "credit_card" && (
+
+ )}
+ handleCurrencyChange(e, setNewBalance)}
+ onBlur={(e) => handleCurrencyBlur(e, newBalance, setNewBalance)}
+ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white"
+ />
+
+
+
+
+
+ )}
+
+ {/* Accounts Table */}
+
+ {/* Header */}
+
+
Account Name
+
Type
+
Balance
+
Last Updated
+
+
+
+ {/* Body */}
+
+
+ {filteredAccounts.map((account, index) => {
+ const isExpanded = account.isExpanded;
+ const editValues = editingValues[account.id] || {};
+
+ return (
+
+ {/* Main Row */}
+
toggleExpand(account.id)}
+ >
+
+ {account.name}
+
+
+ {account.type ? account.type.replace("_", " ") : ""}
+
+
+ {formatCurrency(account.balance)}
+
+
+ {formatDate(account.last_updated)}
+
+
+
+
+
+
+ {/* Expanded Edit Row */}
+ {isExpanded && (
+
+
+
+
+
+ handleFieldChange(
+ account.id,
+ "name",
+ e.target.value,
+ )
+ }
+ className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+
+
+
+
+ {account.subtype && (
+
+
+ {account.type === "savings" && (
+
+ )}
+ {account.type === "credit_card" && (
+
+ )}
+
+ )}
+
+
+
+ handleListCurrencyChange(
+ e,
+ handleFieldChange,
+ account.id,
+ "balance",
+ )
+ }
+ onBlur={(e) =>
+ handleListCurrencyBlur(
+ e,
+ handleFieldChange,
+ account.id,
+ "balance",
+ )
+ }
+ className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+
+
+
+
+
+ )}
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+export default AccountList;
diff --git a/app/client/src/components/AccountListCard.tsx b/app/client/src/components/AccountListCard.tsx
new file mode 100644
index 0000000..4fc5111
--- /dev/null
+++ b/app/client/src/components/AccountListCard.tsx
@@ -0,0 +1,71 @@
+import { deleteUserAccount } from "../api/Account.ts";
+import { useState } from "react";
+import AccountPopup from "./AccountForm.tsx";
+import { type Account } from "../types/AccountType.ts";
+import type { AuthSession } from "@/types/authTypes.ts";
+
+interface cardProp {
+ account: Account;
+ session: AuthSession;
+ setAccount: (editAccount: Account) => void;
+ removeAccount: (removeAccount: Account) => void;
+}
+
+export default function Card({
+ account,
+ session,
+ setAccount,
+ removeAccount,
+}: cardProp) {
+ //Stores the value that toggles thhe account popup form
+ const [seen, setSeen] = useState(false);
+
+ const toggle = () => {
+ setSeen(!seen);
+ };
+
+ //Deletes the user account from the server and then remove it from the list
+ const deleteAccount = () => {
+ try {
+ //Send a request to delete user account
+ deleteUserAccount(session, account).then((result) => {
+ //Sucessfully deleted account
+ if (result) {
+ removeAccount(account);
+ }
+ });
+ } catch {
+ alert("Failed to delete account " + account.name);
+ }
+ };
+
+ return (
+ <>
+
+
{account.name}
+
+
+
+
+ {account.type + " " + (account.subtype ? " " + account.type : "")}
+
+
{Number(account.balance).toFixed(2)}
+
+
+
+
+
+
+
+ {seen ? (
+
+ ) : null}
+ >
+ );
+}
diff --git a/app/client/src/components/AppLayout.tsx b/app/client/src/components/AppLayout.tsx
new file mode 100644
index 0000000..67a647f
--- /dev/null
+++ b/app/client/src/components/AppLayout.tsx
@@ -0,0 +1,208 @@
+import { useEffect, useRef, useState } from "react";
+import { Outlet } from "react-router-dom";
+import { IoCheckmarkCircleOutline, IoReorderThreeSharp } from "react-icons/io5";
+import NavBar from "./NavBar";
+import { SIDEBAR_WIDTH } from "../utils/constants";
+import GoalsPanel from "./GoalsPanel";
+import type { Goal } from "../types/goals.ts";
+import {
+ fetchGoals,
+ createGoal,
+ updateGoal,
+ deleteGoal,
+} from "../api/GoalsAPI.ts";
+import LoadingSpinner from "@/components/LoadingSpinner";
+
+type AppLayoutProps = {
+ onLogout: () => void;
+};
+export default function AppLayout({ onLogout }: AppLayoutProps) {
+ const [isNavBarOpen, setIsNavBarOpen] = useState(false);
+ const [isGoalsPanelOpen, setIsGoalsPanelOpen] = useState(false);
+ const closeTimeoutRef = useRef(null);
+ const [goals, setGoals] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ loadGoals();
+ }, []);
+
+ const loadGoals = async () => {
+ try {
+ setIsLoading(true);
+ const data = await fetchGoals();
+ setGoals(data);
+ } catch (error) {
+ console.error("Failed to load goals:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleAddGoal = async () => {
+ // need a modal or a form for this placeholder
+ const newGoal: Partial = {
+ type: "reduce_spending",
+ name: "New Goal",
+ category: "Unknown",
+ target: 100,
+ current_amount: 0,
+ period: "m",
+ };
+
+ try {
+ const created = await createGoal(newGoal);
+ setGoals([...goals, created]);
+ } catch (error) {
+ console.error("Failed to create goal:", error);
+ }
+ };
+
+ const handleEditGoal = async (goalId: string, updates: Partial) => {
+ try {
+ const updated = await updateGoal(goalId, updates);
+ // console.log("Updated goal:", updated);
+ setGoals(goals.map((g) => (g.id === goalId ? updated : g)));
+ } catch (error) {
+ console.error("Failed to update goal:", error);
+ }
+ };
+
+ const handleDeleteGoal = async (goalId: string) => {
+ try {
+ await deleteGoal(goalId);
+ setGoals(goals.filter((g) => g.id !== goalId));
+ } catch (error) {
+ console.error("Failed to delete goal:", error);
+ }
+ };
+
+ const handleOpenPanel = () => {
+ if (closeTimeoutRef.current) {
+ clearTimeout(closeTimeoutRef.current);
+ }
+ setIsGoalsPanelOpen(true);
+ };
+
+ const handleClosePanel = () => {
+ closeTimeoutRef.current = setTimeout(() => {
+ setIsGoalsPanelOpen(false);
+ }, 200); //200ms delay before the goals panel closes
+ };
+
+ function toggleSidebar() {
+ setIsNavBarOpen((prev) => !prev);
+ }
+
+ return (
+
+
+
+
+ {isLoading ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ {isNavBarOpen && (
+
setIsNavBarOpen(false)}
+ onLogout={onLogout}
+ />
+ )}
+
+
+
+
+
+ );
+}
diff --git a/app/client/src/components/BudgetExpenditureChart.tsx b/app/client/src/components/BudgetExpenditureChart.tsx
new file mode 100644
index 0000000..aacae6f
--- /dev/null
+++ b/app/client/src/components/BudgetExpenditureChart.tsx
@@ -0,0 +1,126 @@
+import { getBudgetWithExpenditure } from "@/api/DashboardAPI";
+import type { ChartData, ChartOptions } from "chart.js";
+import { useEffect, useState } from "react";
+import { Bar } from "react-chartjs-2";
+import { Wallet } from "lucide-react";
+import NoItemState from "./NoItemState";
+
+function BudgetExpenditureChart() {
+ const [chartData, setChartData] = useState | null>(null);
+ const [selectedPeriod, setSelectedPeriod] = useState<"w" | "m" | "y">("m");
+
+ const chartOptions: ChartOptions<"bar"> = {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: "top",
+ labels: {
+ font: {
+ size: 16,
+ weight: "bold",
+ },
+ padding: 50,
+ },
+ },
+ },
+ backgroundColor: "rgb(255, 255, 255, 0.8)",
+ scales: {
+ x: {
+ ticks: {
+ font: {
+ size: 14,
+ weight: "bold", // Set the font weight to bold
+ },
+ },
+ },
+ y: {
+ beginAtZero: true,
+ ticks: {
+ font: {
+ size: 14,
+ weight: "bold", // Set the font weight to bold
+ },
+ },
+ },
+ },
+ };
+ useEffect(() => {
+ const fetchData = async (period: "w" | "m" | "y") => {
+ const data = await getBudgetWithExpenditure(period);
+ if (!data) return;
+ const labels = data.map((item) => formatCategoryLabel(item.category));
+ const budgetAmounts = data.map((item) => item.budgetAmount);
+ const expenditureAmounts = data.map((item) => item.actualAmount);
+ //console.log("Received data:", data);
+ const renderedData: ChartData<"bar"> = {
+ labels,
+ datasets: [
+ {
+ label: "Budget Amount",
+ data: budgetAmounts,
+ backgroundColor: "rgba(34, 250, 94, 0.55)",
+ borderColor: "rgba(34, 197, 94, 1)",
+ borderWidth: 1,
+ },
+ {
+ label: "Actual Spending Amount",
+ data: expenditureAmounts,
+ backgroundColor: "rgba(239, 68, 68, 0.55)",
+ borderColor: "rgba(239, 68, 68, 1)",
+ borderWidth: 1,
+ },
+ ],
+ };
+ setChartData(renderedData);
+ };
+ fetchData(selectedPeriod);
+ }, [selectedPeriod]);
+
+ const periodButtons = [
+ { key: "w", label: "Week" },
+ { key: "m", label: "Month" },
+ { key: "y", label: "Year" },
+ ].map(({ key, label }) => (
+
+ ));
+
+ const budgetExpenditureSection = chartData ? (
+
+
+ {periodButtons}
+
+
Budget vs Expenditure
+
+
+ ) : (
+ }
+ />
+ );
+ return budgetExpenditureSection;
+}
+
+//small function to format category labels for budget chart
+//split on underscore, capitalize first letter of each word, then join with space
+function formatCategoryLabel(category: string): string {
+ return category
+ .split("_")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(" ");
+}
+
+export default BudgetExpenditureChart;
diff --git a/app/client/src/components/CSVImportModal.tsx b/app/client/src/components/CSVImportModal.tsx
new file mode 100644
index 0000000..10c0553
--- /dev/null
+++ b/app/client/src/components/CSVImportModal.tsx
@@ -0,0 +1,73 @@
+import { useState } from "react";
+import { AiOutlineClose } from "react-icons/ai";
+import CsvUpload from "./csvread/CsvUpload";
+// import type { AuthSession } from "@/types/authTypes";
+import type { Transaction } from "@/types/Transaction";
+
+interface CSVImportModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSuccess: (newTransactions: Transaction[]) => void;
+ accountId: number;
+}
+
+function CSVImportModal({
+ isOpen,
+ onClose,
+ onSuccess,
+ accountId,
+}: CSVImportModalProps) {
+ const [importComplete, setImportComplete] = useState(false);
+
+ if (!isOpen) return null;
+
+ const handleImported = (newTransactions: Transaction[]) => {
+ onSuccess(newTransactions);
+ setImportComplete(true);
+ };
+
+ const handleClose = () => {
+ setImportComplete(false);
+ onClose();
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
+ Import Transactions from CSV
+
+
+
+
+ {/* Body */}
+
+ {importComplete ? (
+
+
+ ✓ Import Complete!
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
+
+export default CSVImportModal;
diff --git a/app/client/src/components/DashboardChartSection.tsx b/app/client/src/components/DashboardChartSection.tsx
new file mode 100644
index 0000000..c906ef5
--- /dev/null
+++ b/app/client/src/components/DashboardChartSection.tsx
@@ -0,0 +1,278 @@
+import { useEffect, useState } from "react";
+import { Line, Bar } from "react-chartjs-2";
+import LoadingSpinner from "@/components/LoadingSpinner";
+import SankeyChart from "@/components/SankeyChart";
+import {
+ getExpensesChartData,
+ getSavingsContribChartData,
+ getIncomeFlowChartData,
+} from "@/api/DashboardAPI";
+import type { SankeyData } from "recharts/types/chart/Sankey";
+import type { ChartData, ChartOptions } from "chart.js";
+import NoTransactionReport from "./NoTransactionReport";
+import { MdOutlineSavings } from "react-icons/md";
+import { MdOutlinePayment } from "react-icons/md";
+import { TrendingDown } from "lucide-react";
+
+function DashboardChartSection() {
+ //Active chart state - this just determines which chart is displayed in the holder - change this later to potentially load up all charts at once if latency is good
+ const [activeChart, setActiveChart] = useState<
+ "expenses" | "savings" | "income"
+ >("expenses");
+
+ //Active chart period state - this determines over what timeframe the chart is displayed - weekly, monthly, or yearly - default to monthly
+ const [selectedPeriod, setSelectedPeriod] = useState<"w" | "m" | "y">("m");
+
+ //Chart data state - this is used to determine whether to put a spinner in place of a chart while data is being fetched from API
+ const [expensesData, setExpensesData] = useState | null>(
+ null,
+ );
+ const [savingsData, setsavingsData] = useState | null>(
+ null,
+ );
+ const [incomeData, setIncomeData] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const fetchExpensesData = async () => {
+ setIsLoading(true);
+ try {
+ const data = await getTestExpensesData(selectedPeriod);
+ setExpensesData(data);
+ } catch (error) {
+ console.error("Failed to fetch expenses data:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ async function fetchSavingsData() {
+ setIsLoading(true);
+ try {
+ const data = await getTestSavingsContribData(selectedPeriod);
+ console.log("data", data);
+ setsavingsData(data);
+ } catch (error) {
+ console.error("Failed to fetch savings data:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ async function fetchIncomeData() {
+ setIsLoading(true);
+ try {
+ const data = await getTestIncomeFlowData(selectedPeriod);
+ setIncomeData(data);
+ } catch (error) {
+ console.error("Failed to fetch income data:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ switch (activeChart) {
+ case "expenses":
+ fetchExpensesData();
+ break;
+ case "savings":
+ fetchSavingsData();
+ break;
+ case "income":
+ fetchIncomeData();
+ break;
+ default:
+ fetchExpensesData();
+ }
+ }, [activeChart, selectedPeriod]);
+
+ //Temporary test data for expenses - delete once API works. This is just to test graph components
+ const getTestExpensesData = async (
+ period: "w" | "m" | "y",
+ ): Promise | null> => {
+ //Try to reach API first, get synthetic data if fails
+ try {
+ const response = await getExpensesChartData(period);
+ return response;
+ } catch (error) {
+ console.error("Error fetching expenses chart data:", error);
+ }
+ return null;
+ };
+
+ const getTestSavingsContribData = async (
+ period: "w" | "m" | "y",
+ ): Promise | null> => {
+ try {
+ const response = await getSavingsContribChartData(period);
+ console.log("Received savings contribution chart data:", response);
+ return response;
+ } catch (error) {
+ console.error("Error fetching savings contribution chart data:", error);
+ }
+ return null;
+ };
+
+ const getTestIncomeFlowData = async (
+ period: "w" | "m" | "y",
+ ): Promise => {
+ try {
+ const response = await getIncomeFlowChartData(period);
+ //console.log("Received income flow chart data:", response);
+ return response;
+ } catch (error) {
+ console.error("Error fetching income flow chart data:", error);
+ }
+ //console.log("Using placeholder income flow chart data");
+ return null;
+ };
+
+ const expensesBarOptions: ChartOptions<"bar"> = {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: "top",
+ },
+ title: {
+ display: true,
+ text: "Expenses Over Time",
+ font: {
+ size: 24,
+ weight: "bold",
+ },
+ },
+ },
+ };
+
+ const savingsLineOptions: ChartOptions<"line"> = {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: "top",
+ },
+ title: {
+ display: true,
+ text: "Savings Contributions Over Time",
+ font: {
+ size: 24,
+ weight: "bold",
+ },
+ },
+ },
+ };
+
+ //Renders a chart based on activeChart - puts a spinner in place while data is being fetched or if no data is available for any reason
+ const renderChart = () => {
+ switch (activeChart) {
+ case "expenses":
+ if (isLoading || !expensesData) {
+ return ;
+ }
+ return ;
+ case "savings":
+ if (isLoading || !savingsData) {
+ return ;
+ }
+ return ;
+ case "income":
+ if (isLoading || !incomeData) {
+ return ;
+ }
+ return ;
+ default:
+ /* figure out a default case in case of an error - can have a placeholder or a spinner chart (loading spinner*/
+ return ;
+ }
+ };
+
+ const transactionButtons = [
+ { key: "expenses", label: "Expenses" },
+ { key: "savings", label: "Savings" },
+ { key: "income", label: "Income Flow" },
+ ].map(({ key, label }) => (
+
+ ));
+ const periodButtons = [
+ { key: "w", label: "Week" },
+ { key: "m", label: "Month" },
+ { key: "y", label: "Year" },
+ ].map(({ key, label }) => (
+
+ ));
+
+ let noTransactionReport = null;
+ if (!isLoading) {
+ if (activeChart === "expenses" && !expensesData) {
+ noTransactionReport = (
+ }
+ title="No Expenses Available"
+ description="There is no data available for the selected chart type and period."
+ />
+ );
+ } else if (activeChart === "savings" && !savingsData) {
+ noTransactionReport = (
+ }
+ title="No Savings Available"
+ description="There is no data available for the selected chart type and period."
+ />
+ );
+ } else if (activeChart === "income" && !incomeData) {
+ noTransactionReport = (
+ }
+ title="No Income Available"
+ description="There is no data available for the selected chart type and period."
+ />
+ );
+ }
+ }
+ return (
+ <>
+
+
+ {transactionButtons}
+
+
+ {noTransactionReport || (
+
+
+ {/* This chart is here just to test all the graph components */}
+
+ {periodButtons}
+
+ {renderChart()}
+
+
+ )}
+ >
+ );
+}
+
+export default DashboardChartSection;
diff --git a/app/client/src/components/GoalsPanel.tsx b/app/client/src/components/GoalsPanel.tsx
new file mode 100644
index 0000000..d36247b
--- /dev/null
+++ b/app/client/src/components/GoalsPanel.tsx
@@ -0,0 +1,405 @@
+// This is just a goals panel that gets populated by user goals - for simplicity there are 5 goals per user for now
+import type { Goal, GoalsPanelProps } from "../types/goals.ts";
+import { useState, useEffect, useRef, useReducer } from "react";
+
+//define the reducer - this is necessary because we need to track the edits in state even if the panel is closed
+interface EditValueBase {
+ name: string;
+ category: string;
+ target: number;
+}
+
+interface EditValueReduceSpending extends EditValueBase {
+ type: "reduce_spending";
+ period: "m" | "w";
+}
+
+interface EditValueSave extends EditValueBase {
+ type: "save";
+ period?: "m";
+}
+
+type EditValue = EditValueReduceSpending | EditValueSave;
+type EditValuesState = Record;
+
+type EditValuesAction =
+ | {
+ type: "INIT_GOAL";
+ payload: { goalId: string; goal: Goal };
+ }
+ | {
+ type: "UPDATE_FIELD";
+ payload: {
+ goalId: string;
+ field: string;
+ value: string | number;
+ };
+ };
+
+function editValuesReducer(
+ state: EditValuesState,
+ action: EditValuesAction,
+): EditValuesState {
+ switch (action.type) {
+ case "INIT_GOAL": {
+ const { goalId, goal } = action.payload;
+ if (state[goalId]) return state;
+
+ const baseValues = {
+ name: goal.name || "",
+ category: goal.category || "",
+ target: goal.target || 0,
+ };
+
+ if (goal.type === "reduce_spending") {
+ return {
+ ...state,
+ [goalId]: {
+ ...baseValues,
+ type: "reduce_spending",
+ period: goal.period || "m",
+ },
+ };
+ } else {
+ // type === "save"
+ return {
+ ...state,
+ [goalId]: {
+ ...baseValues,
+ type: "save",
+ period: "m",
+ },
+ };
+ }
+ }
+
+ case "UPDATE_FIELD": {
+ const { goalId, field, value } = action.payload;
+ const current = state[goalId];
+ if (!current) return state;
+
+ // Handle field updates with type safety
+ if (field === "type") {
+ // When type changes, we need to restructure the edit value
+ if (value === "reduce_spending") {
+ return {
+ ...state,
+ [goalId]: {
+ name: current.name,
+ category: current.category,
+ target: current.target,
+ type: "reduce_spending",
+ period: "m", // default period
+ } as EditValueReduceSpending,
+ };
+ } else {
+ return {
+ ...state,
+ [goalId]: {
+ name: current.name,
+ category: current.category,
+ target: current.target,
+ type: "save",
+ period: "m",
+ } as EditValueSave,
+ };
+ }
+ }
+
+ // For other fields, preserve the type
+ if (field === "period" && current.type === "reduce_spending") {
+ return {
+ ...state,
+ [goalId]: {
+ ...current,
+ period: value as "m" | "w",
+ },
+ };
+ }
+
+ // Generic field update
+ return {
+ ...state,
+ [goalId]: {
+ ...current,
+ [field]: value,
+ },
+ };
+ }
+
+ default:
+ return state;
+ }
+}
+
+function GoalsPanel({
+ goals,
+ onAddGoal,
+ onEditGoal,
+ onDeleteGoal,
+ maxGoals = 5,
+}: GoalsPanelProps) {
+ const [expandedGoalId, setExpandedGoalId] = useState(null);
+ const [editValues, dispatch] = useReducer(editValuesReducer, {});
+
+ const initializedGoals = useRef>(new Set());
+
+ //initialize edit values when a goal expands
+ useEffect(() => {
+ if (expandedGoalId) {
+ const goal = goals.find((g: Goal) => g.id === expandedGoalId);
+ if (goal && !initializedGoals.current.has(expandedGoalId)) {
+ initializedGoals.current.add(expandedGoalId);
+ dispatch({
+ type: "INIT_GOAL",
+ payload: { goalId: expandedGoalId, goal },
+ });
+ }
+ }
+ }, [expandedGoalId, goals]);
+
+ //helper to format currency
+ const formatAmount = (amount: number): string => {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "CAD",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(amount);
+ };
+
+ //helper to get goal type, only the reduce_spending type has a monthly or a weekly period, savings are eternal
+ const getGoalDescription = (goal: Goal): string => {
+ if (goal.type === "reduce_spending") {
+ return `Spending limit • ${goal.period === "m" ? "Monthly" : "Weekly"}`;
+ } else {
+ return `Savings Goal`;
+ }
+ };
+
+ //toggle expanded view of a goal
+ const handleGoalClick = (goalId: string) => {
+ setExpandedGoalId(expandedGoalId === goalId ? null : goalId);
+ };
+
+ //helper to handle field changes
+ const handleFieldChange = (
+ goalId: string,
+ field: string,
+ value: string | number,
+ ) => {
+ dispatch({ type: "UPDATE_FIELD", payload: { goalId, field, value } });
+ };
+
+ const handleApplyEdits = (goalId: string) => {
+ const updates = editValues[goalId];
+ if (updates) {
+ onEditGoal(goalId, updates);
+ setExpandedGoalId(null);
+ }
+ };
+
+ //saves the edits in state even of the panel closes
+ const hasUnsavedChanges = (goalId: string): boolean => {
+ const goal = goals.find((g: Goal) => g.id === goalId);
+ const edits = editValues[goalId];
+ if (!goal || !edits) return false;
+
+ return (
+ goal.name !== edits.name ||
+ goal.category !== edits.category ||
+ goal.type !== edits.type ||
+ goal.period !== edits.period ||
+ goal.target !== edits.target
+ );
+ };
+
+ return (
+
+
+
Your Goals
+
+ {goals.length} / {maxGoals}
+
+
+
+
+ {goals.map((goal: Goal) => {
+ const progress = goal.progress_percentage;
+ const isExpanded = expandedGoalId === goal.id;
+ const currentEdits = editValues[goal.id];
+ const hasChanges = hasUnsavedChanges(goal.id);
+
+ const progressBarColor =
+ goal.type === "reduce_spending" ? "bg-red-500" : "bg-green-500";
+
+ return (
+
handleGoalClick(goal.id)}
+ >
+
+
+ {goal.name} - {goal.category}
+
+
+ {formatAmount(goal.current_amount)} /{" "}
+ {formatAmount(goal.target)}
+
+
+
+
+
+
+ {getGoalDescription(goal)}
+
+
+ {/* expanded details with editable fields */}
+ {isExpanded && currentEdits && (
+
+ {/* goal name */}
+
+ Goal Name:
+
+ handleFieldChange(goal.id, "name", e.target.value)
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-2 py-1 text-sm text-white w-48"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+ {/* category */}
+
+ Category:
+
+ handleFieldChange(goal.id, "category", e.target.value)
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-2 py-1 text-sm text-white w-48"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+ {/* goal type */}
+
+ Goal Type:
+
+
+
+ {/* period (only for reduce_spending) */}
+ {currentEdits.type === "reduce_spending" && (
+
+ Period:
+
+
+ )}
+
+ {/* target amount */}
+
+
+ Target Amount:
+
+
+ handleFieldChange(
+ goal.id,
+ "target",
+ parseFloat(e.target.value) || 0,
+ )
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-2 py-1 text-sm text-white w-32 text-right"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+ {/* action buttons */}
+
+
+
+
+
+ )}
+
+ );
+ })}
+
+ {/* add goal button */}
+ {goals.length < maxGoals && (
+
+ )}
+
+
+ );
+}
+
+export default GoalsPanel;
diff --git a/app/client/src/components/Header.tsx b/app/client/src/components/Header.tsx
new file mode 100644
index 0000000..81f7fdd
--- /dev/null
+++ b/app/client/src/components/Header.tsx
@@ -0,0 +1,34 @@
+import { useState } from "react";
+import { VscThreeBars } from "react-icons/vsc";
+import { IoPersonCircleSharp } from "react-icons/io5";
+import NavBar from "./NavBar";
+function Header() {
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
+ const toggleSidebar = () => {
+ setIsSidebarOpen((prev) => !prev);
+ };
+
+ return (
+ <>
+ {isSidebarOpen && (
+ setIsSidebarOpen(false)}
+ onLogout={() => setIsSidebarOpen(false)} // Add the onLogout prop
+ />
+ )}
+
+ >
+ );
+}
+
+export default Header;
diff --git a/app/client/src/components/IncomeCard.tsx b/app/client/src/components/IncomeCard.tsx
new file mode 100644
index 0000000..55bdac3
--- /dev/null
+++ b/app/client/src/components/IncomeCard.tsx
@@ -0,0 +1,67 @@
+import { useState } from "react";
+import { deleteIncome } from "../api/Income";
+import { type Income } from "../types/IncomeType";
+import IncomePopup from "./IncomeForm";
+import type { AuthSession } from "@/types/authTypes";
+
+interface cardProp {
+ income: Income;
+ session: AuthSession;
+ setIncome: (editIncome: Income) => void;
+ removeIncome: (removeIncome: Income) => void;
+}
+
+export default function Card({
+ session,
+ income,
+ setIncome,
+ removeIncome,
+}: cardProp) {
+ const [seen, setSeen] = useState(false);
+
+ const toggle = () => {
+ setSeen(!seen);
+ };
+
+ //Deletes the user income from the server and then remove it from the list
+ const deleteUserIncome = () => {
+ try {
+ //Send a request to delete user's income
+ deleteIncome(session, income).then((result) => {
+ //Sucessfully deleted income
+ if (result) {
+ removeIncome(income);
+ }
+ });
+ } catch {
+ alert("Failed to delete income " + income.name);
+ }
+ };
+
+ return (
+ <>
+
+
{income.name}
+
+
{income.income}
+
+
{income.description}
+
+
+
+
+
+
+
+
+ {seen ? (
+
+ ) : null}
+ >
+ );
+}
diff --git a/app/client/src/components/IncomeForm.tsx b/app/client/src/components/IncomeForm.tsx
new file mode 100644
index 0000000..9881759
--- /dev/null
+++ b/app/client/src/components/IncomeForm.tsx
@@ -0,0 +1,131 @@
+import { useState } from "react";
+import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput";
+import "./userForm.css";
+import { postIncome, putIncome } from "../api/Income";
+import { validateIncomeForm } from "../utils/ValidateForms";
+import { type Income } from "../types/IncomeType";
+import type { AuthSession } from "@/types/authTypes";
+
+interface popupProp {
+ toggle: () => void;
+ session: AuthSession;
+ setIncome?: (income: Income) => void;
+ addIncome?: (income: Income) => void;
+ edit: boolean;
+ selectedIncome?: Income;
+}
+
+export default function PopupForm({
+ toggle,
+ session,
+ setIncome,
+ addIncome,
+ edit,
+ selectedIncome,
+}: popupProp) {
+ const handleSubmit = () => {
+ //Convert to number
+ const inputAmount = Number(amount);
+
+ if (validateIncomeForm(name, Number(amount))) {
+ if (edit) {
+ putIncome(session, name, inputAmount, description).then((result) => {
+ if (result && selectedIncome && setIncome) {
+ const updateIncome: Income = {
+ id: selectedIncome.id,
+ name: name,
+ income: inputAmount,
+ description: description,
+ };
+ setIncome(updateIncome);
+ }
+ });
+ } else {
+ postIncome(session, name, inputAmount, description).then((data) => {
+ const newIncome: Income = {
+ id: 0,
+ name: name,
+ income: inputAmount,
+ description: description,
+ };
+
+ //Determine if response gave back an id for the new created income
+ if (data && data.id) {
+ newIncome.id = data.id;
+
+ //Check if add income is defined
+ if (addIncome) {
+ addIncome(newIncome);
+ }
+ }
+ });
+ }
+ toggle();
+ } else {
+ alert("Please fillout the income form");
+ }
+ };
+
+ const [name, setName] = useState("");
+ const [amount, setAmount] = useState("");
+ const [description, setDescription] = useState("");
+
+ if (edit && selectedIncome) {
+ setName(selectedIncome.name);
+ setAmount(selectedIncome.toString());
+ setDescription(selectedIncome.description);
+ }
+
+ return (
+ <>
+
+
+ {edit ?
Edit Income
:
Create Income
}
+
+
+
+
setName(event.target.value)}
+ />
+
+
+
+
handleCurrencyChange(event, setAmount)}
+ onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)}
+ />
+
+
+
+
setDescription(event.target.value)}
+ />
+
+
+
+
+ {edit ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ >
+ );
+}
diff --git a/app/client/src/components/IncomeList.tsx b/app/client/src/components/IncomeList.tsx
new file mode 100644
index 0000000..3b874c7
--- /dev/null
+++ b/app/client/src/components/IncomeList.tsx
@@ -0,0 +1,73 @@
+import { useState } from "react";
+import { getIncome } from "../api/Income";
+import { type Income } from "../types/IncomeType";
+import IncomePopup from "./IncomeForm";
+import IncomeCard from "./IncomeCard";
+import type { AuthSession } from "@/types/authTypes";
+
+interface listProp {
+ session: AuthSession;
+}
+
+export default function IncomeList({ session }: listProp) {
+ const [userIncomes, setUsersIncomes] = useState([]);
+ const [seen, setSeen] = useState(false);
+
+ //Try to get the account's transaction from the server
+ try {
+ getIncome(session).then((incomes) => {
+ //Determine if we acquired the accounts transaction
+ if (incomes) {
+ setUsersIncomes(incomes);
+ }
+ });
+ } catch {
+ alert("Failed to get user income");
+ }
+
+ //Adds income to the list
+ const addIncome = (newIncome: Income) => {
+ setUsersIncomes((userIncomes) => [...userIncomes, newIncome]);
+ };
+
+ //Removes any income from the list thats shares the same id
+ const removeIncome = (remove: Income) => {
+ setUsersIncomes((userIncomes) =>
+ userIncomes.filter((incomes) => incomes.id !== remove.id),
+ );
+ };
+
+ const toggle = () => {
+ setSeen(!seen);
+ };
+
+ return (
+ <>
+
+
+ {userIncomes.map((income) => (
+
+ ))}
+
+
+
+
+
+
+
+ {seen ? (
+
+ ) : null}
+ >
+ );
+}
diff --git a/app/client/src/components/Layout.tsx b/app/client/src/components/Layout.tsx
new file mode 100644
index 0000000..d3312db
--- /dev/null
+++ b/app/client/src/components/Layout.tsx
@@ -0,0 +1,13 @@
+import { Outlet } from "react-router-dom";
+import Header from "./Header";
+
+function Layout() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default Layout;
diff --git a/app/client/src/components/LoadingSpinner.tsx b/app/client/src/components/LoadingSpinner.tsx
new file mode 100644
index 0000000..c1734d1
--- /dev/null
+++ b/app/client/src/components/LoadingSpinner.tsx
@@ -0,0 +1,7 @@
+export default function LoadingSpinner() {
+ return (
+
+ );
+}
diff --git a/app/client/src/components/MarketDashboardSection.tsx b/app/client/src/components/MarketDashboardSection.tsx
new file mode 100644
index 0000000..c7e78a3
--- /dev/null
+++ b/app/client/src/components/MarketDashboardSection.tsx
@@ -0,0 +1,388 @@
+import { useEffect, useMemo, useState } from "react";
+
+import { getMarketHistory, getMarketQuote, searchMarkets } from "@/api/Market";
+import PinnedInstrumentsSection from "@/components/market/PinnedInstrumentsSection";
+import MarketSearchBar from "@/components/market/MarketSearchBar";
+import MarketSearchResultsPanel from "@/components/market/MarketSearchResultsPanel";
+import SelectedInstrumentPanel from "@/components/market/SelectedInstrumentPanel";
+import {
+ fromPinnedInstrument,
+ toPinnedInstrument,
+} from "@/components/market/marketSectionHelpers";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import type {
+ MarketHistoryPeriod,
+ MarketHistoryPoint,
+ MarketInstrument,
+ MarketInstrumentType,
+} from "@/types/Market";
+import { mergeInstrumentQuote, toChartSeries } from "@/utils/market";
+import {
+ loadPinnedMarkets,
+ PINNED_MARKETS_CLEARED_EVENT,
+ savePinnedMarkets,
+} from "@/utils/marketStorage";
+
+function isMarketInstrumentType(value: unknown): value is MarketInstrumentType {
+ return value === "stock" || value === "forex";
+}
+
+function normalizeMarketInstrument(value: unknown): MarketInstrument | null {
+ if (!value || typeof value !== "object") {
+ return null;
+ }
+
+ const item = value as Partial;
+ if (typeof item.symbol !== "string" || typeof item.name !== "string") {
+ return null;
+ }
+
+ return {
+ symbol: item.symbol,
+ displaySymbol:
+ typeof item.displaySymbol === "string" ? item.displaySymbol : item.symbol,
+ name: item.name,
+ type: isMarketInstrumentType(item.type) ? item.type : "stock",
+ currency: typeof item.currency === "string" ? item.currency : "USD",
+ exchange: typeof item.exchange === "string" ? item.exchange : undefined,
+ price: typeof item.price === "number" ? item.price : null,
+ change: typeof item.change === "number" ? item.change : null,
+ changePercent:
+ typeof item.changePercent === "number" ? item.changePercent : null,
+ timestamp: typeof item.timestamp === "number" ? item.timestamp : null,
+ };
+}
+
+export default function MarketDashboardSection() {
+ const [searchTerm, setSearchTerm] = useState("");
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
+ const [searchResults, setSearchResults] = useState([]);
+ const [searchLoading, setSearchLoading] = useState(false);
+ const [searchError, setSearchError] = useState(null);
+ const [selectedPeriod, setSelectedPeriod] =
+ useState("6mo");
+ const [selectedHistory, setSelectedHistory] = useState(
+ [],
+ );
+ const [historyLoading, setHistoryLoading] = useState(false);
+ const [historyError, setHistoryError] = useState(null);
+ const [pinnedInstruments, setPinnedInstruments] = useState<
+ MarketInstrument[]
+ >(() => loadPinnedMarkets().map(fromPinnedInstrument));
+ const [hydratingPinnedSymbols, setHydratingPinnedSymbols] = useState<
+ Record
+ >({});
+ const [hydratedPinnedSymbols, setHydratedPinnedSymbols] = useState<
+ Record
+ >({});
+ const [selectedInstrument, setSelectedInstrument] =
+ useState(() => {
+ const [firstPinned] = loadPinnedMarkets();
+ return firstPinned ? fromPinnedInstrument(firstPinned) : null;
+ });
+
+ const pinnedSymbols = useMemo(
+ () => new Set(pinnedInstruments.map((instrument) => instrument.symbol)),
+ [pinnedInstruments],
+ );
+ const selectedChartData = useMemo(
+ () => toChartSeries(selectedHistory),
+ [selectedHistory],
+ );
+ const selectedSymbol = selectedInstrument?.symbol ?? null;
+
+ useEffect(() => {
+ const timeoutId = window.setTimeout(() => {
+ setDebouncedSearchTerm(searchTerm.trim());
+ }, 250);
+
+ return () => window.clearTimeout(timeoutId);
+ }, [searchTerm]);
+
+ useEffect(() => {
+ function handlePinnedMarketsCleared() {
+ const nextPinned = loadPinnedMarkets().map(fromPinnedInstrument);
+ setPinnedInstruments(nextPinned);
+ setHydratingPinnedSymbols({});
+ setHydratedPinnedSymbols({});
+ setSelectedInstrument((current) => {
+ if (nextPinned.length > 0) {
+ return nextPinned[0];
+ }
+
+ if (current) {
+ const refreshedSelection = searchResults.find(
+ (instrument) => instrument.symbol === current.symbol,
+ );
+ return refreshedSelection ?? searchResults[0] ?? null;
+ }
+
+ return searchResults[0] ?? null;
+ });
+ }
+
+ window.addEventListener(
+ PINNED_MARKETS_CLEARED_EVENT,
+ handlePinnedMarketsCleared,
+ );
+
+ return () => {
+ window.removeEventListener(
+ PINNED_MARKETS_CLEARED_EVENT,
+ handlePinnedMarketsCleared,
+ );
+ };
+ }, [searchResults]);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ async function loadSearchResults() {
+ setSearchLoading(true);
+ setSearchError(null);
+
+ try {
+ const payload = await searchMarkets(debouncedSearchTerm);
+ const nextResults = payload
+ .map(normalizeMarketInstrument)
+ .filter((item): item is MarketInstrument => item !== null);
+
+ if (cancelled) {
+ return;
+ }
+
+ setSearchResults(nextResults);
+ setSelectedInstrument((current) => {
+ if (!current) {
+ return nextResults[0] ?? null;
+ }
+
+ const refreshedSelection = nextResults.find(
+ (instrument) => instrument.symbol === current.symbol,
+ );
+ return refreshedSelection ?? current;
+ });
+ } catch (error) {
+ if (cancelled) {
+ return;
+ }
+
+ setSearchResults([]);
+ setSearchError(
+ error instanceof Error
+ ? error.message
+ : "Unable to load market instruments right now.",
+ );
+ } finally {
+ if (!cancelled) {
+ setSearchLoading(false);
+ }
+ }
+ }
+
+ void loadSearchResults();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [debouncedSearchTerm]);
+
+ function syncInstrumentQuote(
+ symbol: string,
+ quote: Pick<
+ MarketInstrument,
+ "price" | "change" | "changePercent" | "timestamp"
+ >,
+ ) {
+ setSearchResults((current) =>
+ current.map((instrument) =>
+ instrument.symbol === symbol
+ ? mergeInstrumentQuote(instrument, quote)
+ : instrument,
+ ),
+ );
+ setPinnedInstruments((current) =>
+ current.map((instrument) =>
+ instrument.symbol === symbol
+ ? mergeInstrumentQuote(instrument, quote)
+ : instrument,
+ ),
+ );
+ setSelectedInstrument((current) =>
+ current && current.symbol === symbol
+ ? mergeInstrumentQuote(current, quote)
+ : current,
+ );
+ }
+
+ useEffect(() => {
+ if (!selectedSymbol) {
+ setSelectedHistory([]);
+ setHistoryError(null);
+ setHistoryLoading(false);
+ return;
+ }
+
+ const activeSymbol = selectedSymbol;
+ let cancelled = false;
+
+ async function loadSelectedInstrumentDetails() {
+ setHistoryLoading(true);
+ setHistoryError(null);
+
+ try {
+ const [quote, history] = await Promise.all([
+ getMarketQuote(activeSymbol),
+ getMarketHistory(activeSymbol, selectedPeriod),
+ ]);
+
+ if (cancelled) {
+ return;
+ }
+
+ syncInstrumentQuote(activeSymbol, quote);
+ setSelectedHistory(history.points);
+ } catch (error) {
+ if (cancelled) {
+ return;
+ }
+
+ setSelectedHistory([]);
+ setHistoryError(
+ error instanceof Error
+ ? error.message
+ : "Unable to load market history.",
+ );
+ } finally {
+ if (!cancelled) {
+ setHistoryLoading(false);
+ }
+ }
+ }
+
+ void loadSelectedInstrumentDetails();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [selectedPeriod, selectedSymbol]);
+
+ useEffect(() => {
+ for (const instrument of pinnedInstruments) {
+ if (hydratedPinnedSymbols[instrument.symbol]) {
+ continue;
+ }
+
+ if (hydratingPinnedSymbols[instrument.symbol]) {
+ continue;
+ }
+
+ void hydratePinnedInstrument(instrument);
+ }
+ }, [hydratedPinnedSymbols, hydratingPinnedSymbols, pinnedInstruments]);
+
+ async function hydratePinnedInstrument(instrument: MarketInstrument) {
+ setHydratingPinnedSymbols((current) => ({
+ ...current,
+ [instrument.symbol]: true,
+ }));
+
+ try {
+ const quote = await getMarketQuote(instrument.symbol);
+ syncInstrumentQuote(instrument.symbol, quote);
+ } catch (error) {
+ console.error("Failed to hydrate pinned market instrument", error);
+ } finally {
+ setHydratingPinnedSymbols((current) => ({
+ ...current,
+ [instrument.symbol]: false,
+ }));
+ setHydratedPinnedSymbols((current) => ({
+ ...current,
+ [instrument.symbol]: true,
+ }));
+ }
+ }
+
+ function persistPinned(nextItems: MarketInstrument[]) {
+ savePinnedMarkets(nextItems.map(toPinnedInstrument));
+ }
+
+ function handleTogglePin(instrument: MarketInstrument) {
+ if (pinnedSymbols.has(instrument.symbol)) {
+ setPinnedInstruments((current) => {
+ const nextItems = current.filter(
+ (item) => item.symbol !== instrument.symbol,
+ );
+ persistPinned(nextItems);
+ return nextItems;
+ });
+ setHydratedPinnedSymbols((current) => {
+ const nextState = { ...current };
+ delete nextState[instrument.symbol];
+ return nextState;
+ });
+ setHydratingPinnedSymbols((current) => {
+ const nextState = { ...current };
+ delete nextState[instrument.symbol];
+ return nextState;
+ });
+ return;
+ }
+
+ setPinnedInstruments((current) => {
+ const nextPinned = [...current, instrument];
+ persistPinned(nextPinned);
+ return nextPinned;
+ });
+ setHydratedPinnedSymbols((current) => ({
+ ...current,
+ [instrument.symbol]: false,
+ }));
+ }
+
+ return (
+
+
+
+ Stock and Forex Market
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/client/src/components/NavBar.tsx b/app/client/src/components/NavBar.tsx
new file mode 100644
index 0000000..836e7bf
--- /dev/null
+++ b/app/client/src/components/NavBar.tsx
@@ -0,0 +1,163 @@
+import { Sidebar } from "react-pro-sidebar";
+import { Link, useLocation } from "react-router-dom";
+import { CgHome } from "react-icons/cg";
+import { AiOutlineFundProjectionScreen } from "react-icons/ai";
+import { MdCandlestickChart } from "react-icons/md";
+import { FaSignOutAlt, FaTimes } from "react-icons/fa";
+import { SIDEBAR_WIDTH } from "@/utils/constants";
+type NavigationBarProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ onLogout: () => void;
+};
+function NavBar({ isOpen, onClose, onLogout }: NavigationBarProps) {
+ const location = useLocation();
+ if (!isOpen) return null;
+
+ const navItems = [
+ { to: "/dashboard", label: "Dashboard", icon: },
+ { to: "/markets", label: "Markets", icon: },
+ {to:"/projection", label:"Projection", icon: }
+ ];
+
+ return (
+ <>
+ {/* Backdrop */}
+
+
+
+
+ {/* Header */}
+
+
+
+ U
+
+
+
+
+ Navigation
+
+
User Portal
+
+
+
+
+
+
+ {/* Nav links */}
+
+ {navItems.map((item) => {
+ const active = location.pathname === item.to;
+
+ return (
+
+
+ {item.icon}
+
+ {item.label}
+
+ );
+ })}
+
+
+ {/* Footer Sign Out */}
+
+
+
+
+
+ >
+ );
+}
+
+export default NavBar;
diff --git a/app/client/src/components/NoItemState.tsx b/app/client/src/components/NoItemState.tsx
new file mode 100644
index 0000000..37a1702
--- /dev/null
+++ b/app/client/src/components/NoItemState.tsx
@@ -0,0 +1,34 @@
+type NoItemStateProps = {
+ title: string;
+ description: string;
+ icon?: React.ReactNode;
+};
+export default function NoItemState({
+ title,
+ description,
+ icon,
+}: NoItemStateProps) {
+ return (
+
+ {/* Icon */}
+ {icon && (
+
+ {icon}
+
+ )}
+
+ {/* Title */}
+
{title}
+
+ {/* Description */}
+
{description}
+
+ );
+}
diff --git a/app/client/src/components/NoTransactionReport.tsx b/app/client/src/components/NoTransactionReport.tsx
new file mode 100644
index 0000000..01854db
--- /dev/null
+++ b/app/client/src/components/NoTransactionReport.tsx
@@ -0,0 +1,24 @@
+type NoTransactionReportProps = {
+ icon: React.ReactElement;
+ title: string;
+ description: string;
+};
+function NoTransactionReport(props: NoTransactionReportProps) {
+ const { icon, title, description } = props;
+ return (
+
+ {/* Icon */}
+
+ {icon}
+
+
+ {/* Title */}
+
{title}
+
+ {/* Description */}
+
{description}
+
+ );
+}
+
+export default NoTransactionReport;
diff --git a/app/client/src/components/ProjectionGraph.tsx b/app/client/src/components/ProjectionGraph.tsx
new file mode 100644
index 0000000..b6a3310
--- /dev/null
+++ b/app/client/src/components/ProjectionGraph.tsx
@@ -0,0 +1,90 @@
+import type { projectedDataResponse } from "@/types/responseTypes";
+import type { ChartData, ChartOptions } from "chart.js";
+import { Line } from "react-chartjs-2";
+import { Wallet } from "lucide-react";
+import NoItemState from "./NoItemState";
+
+interface graphProp {
+ data: projectedDataResponse;
+ name: string;
+}
+
+export default function ProjectionGraph({ data, name }: graphProp) {
+ const colors = [
+ "rgba(34, 250, 94, 1)",
+ "rgba(206, 232, 11, 1)",
+ "rgba(197, 34, 34, 1)",
+ ];
+
+ const chartOptions: ChartOptions<"line"> = {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: "top",
+ labels: {
+ font: {
+ size: 16,
+ weight: "bold",
+ },
+ padding: 50,
+ },
+ },
+ },
+ backgroundColor: "rgb(255, 255, 255, 0.8)",
+ scales: {
+ x: {
+ ticks: {
+ font: {
+ size: 14,
+ weight: "bold", // Set the font weight to bold
+ },
+ },
+ },
+ y: {
+ beginAtZero: true,
+ ticks: {
+ font: {
+ size: 14,
+ weight: "bold", // Set the font weight to bold
+ },
+ },
+ },
+ },
+ };
+
+ console.log(data);
+ const chartData: ChartData<"line"> = {
+ labels: data.dateLabel,
+ datasets: data.lineInfo.map((line, index) => ({
+ label: line.name,
+ data: line.data,
+ fill: true,
+ borderColor: colors[index % colors.length],
+ backgroundColor: colors[index % colors.length].replace("1)", "0.55)"),
+ borderWidth: 1,
+ })),
+ };
+
+ return (
+ <>
+ {chartData ? (
+
+
{name}
+
+
+ ) : (
+ <>
+ }
+ />
+ >
+ )}
+ >
+ );
+}
diff --git a/app/client/src/components/SankeyChart.tsx b/app/client/src/components/SankeyChart.tsx
new file mode 100644
index 0000000..3eefad8
--- /dev/null
+++ b/app/client/src/components/SankeyChart.tsx
@@ -0,0 +1,94 @@
+import {
+ ResponsiveContainer,
+ Sankey,
+ Tooltip,
+ useChartWidth,
+ Layer,
+ Rectangle,
+ type SankeyNodeProps,
+} from "recharts";
+
+const TEXT_OFFSET = 6;
+
+interface SankeyData {
+ nodes: Array<{ name: string }>; //names of nodes
+ links: Array<{
+ //links between nodes - source and target are indices of the nodes in the nodes array, value is the weight of the link
+ source: number;
+ target: number;
+ value: number;
+ }>;
+}
+
+interface SankeyChartProps {
+ data: SankeyData;
+}
+
+export function SankeyChart({ data }: SankeyChartProps) {
+ return (
+
+
+
+
+ );
+}
+
+function CustomNode({ x, y, width, height, index, payload }: SankeyNodeProps) {
+ const containerWidth = useChartWidth();
+ if (containerWidth == null) {
+ return null; // return null if used outside a chart context
+ }
+ //console.log("width of container:", containerWidth)
+ //console.log("x:", x, "width of node:", width);
+ const isOut = x + width + TEXT_OFFSET > containerWidth - TEXT_OFFSET * 3;
+ return (
+
+
+
+ {formatCategoryLabel(payload.name)}
+
+
+ {`$${payload.value}`}
+
+
+ );
+}
+
+function formatCategoryLabel(category: string): string {
+ return category
+ .split("_")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(" ");
+}
+
+export default SankeyChart;
diff --git a/app/client/src/components/SelectAccount.tsx b/app/client/src/components/SelectAccount.tsx
new file mode 100644
index 0000000..b542d4a
--- /dev/null
+++ b/app/client/src/components/SelectAccount.tsx
@@ -0,0 +1,42 @@
+import type { Account } from "@/types/AccountType";
+import React from "react";
+import { accountCategory } from "@/enum/AccountCategory.ts";
+
+interface selectProp {
+ accounts: Account[];
+ selectedAccount: number;
+ handleSelectAccount: (event: React.ChangeEvent) => void;
+}
+
+export default function SelectAccount({
+ accounts,
+ selectedAccount,
+ handleSelectAccount,
+}: selectProp) {
+ console.log(accounts);
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/app/client/src/components/SelectDebt.tsx b/app/client/src/components/SelectDebt.tsx
new file mode 100644
index 0000000..1e21e03
--- /dev/null
+++ b/app/client/src/components/SelectDebt.tsx
@@ -0,0 +1,34 @@
+import { type Debt } from "@/types/Debt";
+import React, { type SetStateAction } from "react";
+
+interface selectProp {
+ debts: Debt[];
+ selectedDebt: number;
+ setSelectedDebt: React.Dispatch>;
+}
+
+//Component that puts out a selection of all the user's debts
+export default function SelectDebt({
+ debts,
+ selectedDebt,
+ setSelectedDebt,
+}: selectProp) {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/app/client/src/components/SnapshotSection.tsx b/app/client/src/components/SnapshotSection.tsx
new file mode 100644
index 0000000..37cdbb6
--- /dev/null
+++ b/app/client/src/components/SnapshotSection.tsx
@@ -0,0 +1,99 @@
+// components/AccountCardsSection.tsx
+import { useSnapshotData } from "@/hooks/AggregatedSnapshot";
+import AccountCard from "./AccountCard";
+import { RefreshCw } from "lucide-react";
+
+function AccountCardsSection() {
+ const {
+ totalBalance,
+ currentIncome,
+ averageExpenses,
+ currentDebt,
+ totalSavings,
+ isLoading,
+ error,
+ refresh,
+ rawCurrentDebt,
+ rawTotalSavings,
+ } = useSnapshotData();
+
+ if (error) {
+ return (
+
+
{error}
+
+
+ );
+ }
+
+ return (
+
+ {isLoading ? (
+ // Loading skeletons
+ <>
+
+
+
+
+
+ >
+ ) : (
+ <>
+ = 0
+ ? "positive"
+ : "negative"
+ }
+ />
+
+
+ 0 ? "negative" : "neutral"}
+ />
+ 0 ? "positive" : "neutral"}
+ />
+ >
+ )}
+
+ {/* Optional refresh button */}
+
+
+ );
+}
+
+export default AccountCardsSection;
diff --git a/app/client/src/components/TransactionCard.tsx b/app/client/src/components/TransactionCard.tsx
new file mode 100644
index 0000000..6401b24
--- /dev/null
+++ b/app/client/src/components/TransactionCard.tsx
@@ -0,0 +1,68 @@
+import { useState } from "react";
+import { deleteTransaction } from "../api/Transaction";
+import { type Transaction } from "../types/Transaction";
+import TransactionPopup from "./TransationForm";
+import type { AuthSession } from "@/types/authTypes";
+
+interface cardProp {
+ transaction: Transaction;
+ session: AuthSession;
+ setTransaction: (editedTransaction: Transaction) => void;
+ removeTransaction: (removeTransaction: Transaction) => void;
+}
+
+export default function Card({
+ transaction,
+ session,
+ setTransaction,
+ removeTransaction,
+}: cardProp) {
+ const [seen, setSeen] = useState(false);
+
+ const toggle = () => {
+ setSeen(!seen);
+ };
+
+ //Deletes the user account from the server and then remove it from the list
+ const deleteTrans = () => {
+ try {
+ //Send a request to delete account's transaction
+ deleteTransaction(session, transaction).then((result) => {
+ //Sucessfully deleted transaction
+ if (result) {
+ removeTransaction(transaction);
+ }
+ });
+ } catch {
+ alert("Failed to delete transaction " + transaction.id);
+ }
+ };
+
+ return (
+ <>
+
+
{transaction.id}
+
+
{transaction.amount + " " + transaction.date}
+
+
{transaction.description}
+
+
+
+
+
+
+
+
+ {seen ? (
+
+ ) : null}
+ >
+ );
+}
diff --git a/app/client/src/components/TransactionList.tsx b/app/client/src/components/TransactionList.tsx
new file mode 100644
index 0000000..85deae9
--- /dev/null
+++ b/app/client/src/components/TransactionList.tsx
@@ -0,0 +1,131 @@
+import { useEffect, useState } from "react";
+import { getTransactions } from "../api/Transaction";
+import { type Transaction } from "../types/Transaction";
+import TransactionPopup from "./TransationForm";
+import TransactionCard from "./TransactionCard";
+import type { AuthSession } from "@/types/authTypes";
+import { type Account } from "@/types/AccountType";
+import { getUserAccounts } from "@/api/Account";
+
+interface listProp {
+ session: AuthSession;
+}
+
+export default function TransactionList({ session }: listProp) {
+ const [userAccounts, setUserAccounts] = useState([]);
+ const [selectedAccount, setSelectedAccount] = useState("");
+ const [accountTransactions, setAccountTransactions] = useState(
+ [],
+ );
+ const [seen, setSeen] = useState(false);
+
+ useEffect(() => {
+ getUserAccounts(session)
+ .then((accounts) => {
+ console.log(accounts);
+
+ //Detemrine accounts exist
+ if (accounts) {
+ setUserAccounts(accounts);
+ } else {
+ //alert("Failed to get accounts");
+ }
+ })
+ .catch(() => {
+ //alert("Failed to get accounts");
+ });
+ }, [session, selectedAccount]);
+
+ //Try to get the account's transaction from the server
+ useEffect(() => {
+ if (selectedAccount) {
+ getTransactions(session, selectedAccount)
+ .then((transactions) => {
+ //Determine if we acquired the accounts transaction
+ if (transactions) {
+ setAccountTransactions(transactions);
+ }
+ })
+ .catch(() => {
+ alert("Failed to get transaction");
+ });
+ }
+ }, [session, selectedAccount]);
+
+ //Adds a account to the list
+ const addTransaction = (newTransaction: Transaction) => {
+ setAccountTransactions((userTransactions) => [
+ ...userTransactions,
+ newTransaction,
+ ]);
+ };
+
+ //Removes any transaction from the list thats shares the same id
+ const removeTransaction = (remove: Transaction) => {
+ setAccountTransactions((userTransactions) =>
+ userTransactions.filter((transaction) => transaction.id !== remove.id),
+ );
+ };
+
+ const setTransaction = (editTransaction: Transaction) => {
+ setAccountTransactions(
+ accountTransactions.map((transaction) =>
+ transaction.id === editTransaction.id
+ ? { ...editTransaction }
+ : transaction,
+ ),
+ );
+ };
+
+ const toggle = () => {
+ setSeen(!seen);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {accountTransactions.map((transaction) => (
+
+ ))}
+
+
+
+
+
+
+
+ {seen ? (
+
+ ) : null}
+ >
+ );
+}
diff --git a/app/client/src/components/TransactionTable.tsx b/app/client/src/components/TransactionTable.tsx
new file mode 100644
index 0000000..c631f80
--- /dev/null
+++ b/app/client/src/components/TransactionTable.tsx
@@ -0,0 +1,742 @@
+import { useState, useEffect, useMemo } from "react";
+import { getAccountIdsForUser, getTransactions } from "../api/DashboardAPI";
+import {
+ deleteTransaction,
+ updateTransaction,
+ createTransaction,
+} from "../api/DashboardAPI.ts";
+import type { Transaction } from "../types/Transaction";
+import {
+ AiOutlineSearch,
+ AiOutlinePlus,
+ AiOutlineClose,
+ AiOutlineUpload,
+ AiOutlineTransaction,
+} from "react-icons/ai";
+import NoItemState from "./NoItemState.tsx";
+import { FiEdit2 } from "react-icons/fi";
+import type { MinimizedAccount } from "@/types/AccountType.ts";
+import CSVImportModal from "./CSVImportModal";
+import {
+ handleCurrencyBlur,
+ handleCurrencyChange,
+ handleListCurrencyBlur,
+ handleListCurrencyChange,
+} from "@/utils/handleInput.ts";
+
+interface TransactionTableProps {
+ initialLimit?: number;
+ loadMoreIncrement?: number;
+}
+
+// interface EditableTransaction extends Transaction {
+// }
+
+function TransactionTable({
+ initialLimit = 100,
+ loadMoreIncrement = 50,
+}: TransactionTableProps) {
+ const [allTransactions, setAllTransactions] = useState([]);
+ const [displayLimit, setDisplayLimit] = useState(initialLimit);
+ const [isLoading, setIsLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [showAddForm, setShowAddForm] = useState(false);
+ const [editingValues, setEditingValues] = useState<{
+ [key: number]: Partial;
+ }>({});
+ const [userAccounts, setUserAccounts] = useState([]);
+ const [selectedAccountId, setSelectedAccountId] = useState("");
+ const [showImportModal, setShowImportModal] = useState(false);
+ const [selectedAccountForImport, setSelectedAccountForImport] = useState<
+ number | null
+ >(null);
+ const [expandedRowId, setExpandedRowId] = useState(null);
+
+ // Fetch all transactions on component load
+ useEffect(() => {
+ const fetchTransactions = async () => {
+ try {
+ setIsLoading(true);
+ const txs = await getTransactions();
+ setAllTransactions(txs || []);
+ } catch (error) {
+ console.error("Error fetching transactions:", error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchTransactions();
+ }, []);
+
+ // Fetch accounts for dropdown
+ useEffect(() => {
+ const fetchAccounts = async () => {
+ try {
+ const accounts = await getAccountIdsForUser();
+ if (accounts && accounts.length > 0) {
+ setUserAccounts(accounts);
+ setSelectedAccountId(accounts[0].id);
+ }
+ } catch (error) {
+ console.error("Error fetching accounts:", error);
+ }
+ };
+ fetchAccounts();
+ }, []);
+
+ const handleImportSuccess = (newTransactions: Transaction[]) => {
+ setAllTransactions((prev) => [...newTransactions, ...prev]);
+ setShowImportModal(false);
+ setSelectedAccountForImport(null);
+ };
+
+ //helper function for date formatting
+ const formatDateForInput = (dateStr: string): string => {
+ if (!dateStr) return "";
+ // If it's already YYYY-MM-DD, return as is
+ if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return dateStr;
+ // Otherwise parse and format
+ return new Date(dateStr).toISOString().split("T")[0];
+ };
+
+ const handleRowClick = (transactionId: number) => {
+ const isExpanding = expandedRowId !== transactionId;
+ setExpandedRowId(isExpanding ? transactionId : null);
+
+ // Initialize editing values when expanding
+ if (isExpanding) {
+ const tx = allTransactions.find((t) => t.id === transactionId);
+ if (tx && !editingValues[transactionId]) {
+ setEditingValues((prev) => ({
+ ...prev,
+ [transactionId]: {
+ date: tx.date,
+ description: tx.description || "",
+ category: tx.category,
+ amount: tx.amount,
+ account_name: tx.account_name,
+ sender: tx.sender || "",
+ recipient: tx.recipient || "",
+ },
+ }));
+ }
+ }
+ };
+
+ // client-side search
+ const filteredTransactions = useMemo(() => {
+ if (!searchTerm.trim()) return allTransactions;
+
+ const term = searchTerm.toLowerCase();
+ return allTransactions.filter(
+ (tx) =>
+ tx.description?.toLowerCase().includes(term) ||
+ tx.category?.toLowerCase().includes(term) ||
+ tx.sender?.toLowerCase().includes(term) ||
+ tx.recipient?.toLowerCase().includes(term) ||
+ tx.account_name?.toLowerCase().includes(term) ||
+ new Date(tx.date).toLocaleDateString().includes(term) ||
+ tx.amount.toString().includes(term),
+ );
+ }, [allTransactions, searchTerm]);
+
+ const displayedTransactions = filteredTransactions.slice(0, displayLimit);
+ const hasMore = displayLimit < filteredTransactions.length;
+
+ // Format helpers
+ const formatAmount = (amount: number): string => {
+ return amount < 0
+ ? `-$${Math.abs(amount).toFixed(2)}`
+ : `$${amount.toFixed(2)}`;
+ };
+
+ const formatCategoryLabel = (category: string): string => {
+ return category
+ .split("_")
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
+ .join(" ");
+ };
+
+ // handle field changes in expanded edit form
+ const handleFieldChange = (
+ transactionId: number,
+ field: string,
+ value: string | number,
+ ) => {
+ setEditingValues((prev) => ({
+ ...prev,
+ [transactionId]: {
+ ...prev[transactionId],
+ [field]: value,
+ },
+ }));
+ };
+
+ // save edited transaction
+ const handleSaveEdit = async (transactionId: number) => {
+ const updates = editingValues[transactionId];
+ if (!updates) return;
+
+ try {
+ const success = await updateTransaction({
+ id: transactionId,
+ ...updates,
+ } as Transaction);
+
+ if (!success) throw new Error("Update failed");
+
+ const isExpanding = expandedRowId !== transactionId;
+ setExpandedRowId(isExpanding ? transactionId : null);
+
+ const freshTransactions = await getTransactions();
+ setAllTransactions(freshTransactions || []);
+
+ setEditingValues((prev) => {
+ const newState = { ...prev };
+ delete newState[transactionId];
+ return newState;
+ });
+
+ // close the expanded row
+ setExpandedRowId(null);
+ } catch (error) {
+ console.error("Failed to update transaction:", error);
+ alert("Failed to update transaction");
+ }
+ };
+
+ // delete transaction
+ const handleDelete = async (transactionId: number) => {
+ if (!confirm("Are you sure you want to delete this transaction?")) return;
+
+ try {
+ await deleteTransaction(transactionId);
+ setAllTransactions((prev) =>
+ prev.filter((tx) => tx.id !== transactionId),
+ );
+ } catch (error) {
+ console.error("Failed to delete transaction:", error);
+ alert("Failed to delete transaction");
+ }
+ };
+
+ //add new transaction state
+ const [newTransaction, setNewTransaction] = useState>({
+ date: new Date().toISOString().split("T")[0],
+ description: "",
+ category: "",
+ amount: 0,
+ sender: "",
+ recipient: "",
+ });
+
+ const [newAmount, setNewAmount] = useState("");
+
+ const handleAddTransaction = async () => {
+ if (
+ !newTransaction.date ||
+ !newTransaction.category ||
+ newAmount === undefined
+ ) {
+ alert("Please fill in required fields (date, category, amount)");
+ return;
+ }
+
+ if (!selectedAccountId) {
+ alert("Please select an account");
+ return;
+ }
+
+ try {
+ const created = await createTransaction({
+ ...newTransaction,
+ amount: Number(newAmount),
+ financialAccount_id: selectedAccountId as number,
+ } as Transaction);
+
+ const selectedAccount = userAccounts.find(
+ (acc) => acc.id === selectedAccountId,
+ );
+
+ const transactionWithAccountName = {
+ ...created,
+ account_name: selectedAccount?.name || "Unknown",
+ };
+
+ setAllTransactions((prev) => [transactionWithAccountName, ...prev]);
+ setNewTransaction({
+ date: new Date().toISOString().split("T")[0],
+ description: "",
+ category: "",
+ amount: 0,
+ sender: "",
+ recipient: "",
+ });
+ setSelectedAccountId(userAccounts[0]?.id || "");
+ setShowAddForm(false);
+
+ if (expandedRowId !== null) {
+ setExpandedRowId(null);
+ }
+ } catch (error) {
+ console.error("Failed to create transaction:", error);
+ alert("Failed to create transaction");
+ }
+ };
+
+ if (isLoading) {
+ return (
+
+
Loading transactions...
+
+ );
+ }
+
+ if (allTransactions.length === 0 && !showAddForm) {
+ return (
+
+
}
+ />
+
+
+
+
+
+ );
+}
+
+ return (
+
+ {/* Search Bar */}
+
+
+
+
setSearchTerm(e.target.value)}
+ className="w-full pl-10 pr-4 py-2 bg-black/50 border border-green-500/20 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-green-500"
+ />
+
+
+
+ {/* Import CSV Modal */}
+
+
+
+ {showImportModal && selectedAccountForImport && (
+
{
+ setShowImportModal(false);
+ setSelectedAccountForImport(null);
+ }}
+ onSuccess={handleImportSuccess}
+ accountId={selectedAccountForImport}
+ />
+ )}
+
+
+
+ {/* Add Transaction Form */}
+ {showAddForm && (
+
+
+
New Transaction
+
+
+
+
+ setNewTransaction({ ...newTransaction, date: e.target.value })
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white"
+ />
+
+ setNewTransaction({
+ ...newTransaction,
+ description: e.target.value,
+ })
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white"
+ />
+
+ setNewTransaction({
+ ...newTransaction,
+ category: e.target.value,
+ })
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white"
+ />
+ handleCurrencyChange(e, setNewAmount)}
+ onBlur={(e) =>
+ handleCurrencyBlur(e, e.target.value, setNewAmount)
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white"
+ />
+
+ {/* Account Selector */}
+
+
+
+ setNewTransaction({ ...newTransaction, sender: e.target.value })
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white"
+ />
+
+ setNewTransaction({
+ ...newTransaction,
+ recipient: e.target.value,
+ })
+ }
+ className="bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white"
+ />
+
+
+
+
+
+ )}
+
+ {/* Transactions Table */}
+
+ {/* Header */}
+
+
Date
+
Description
+
Category
+
Amount
+
Account
+
From
+
To
+
+
+
+ {/* Body */}
+
+
+ {displayedTransactions.map((tx, index) => {
+ const isExpanded = expandedRowId === tx.id;
+ const editValues = editingValues[tx.id] || {};
+ const currentAccountId =
+ editValues.financialAccount_id ||
+ userAccounts.find((acc) => acc.name === tx.account_name)?.id ||
+ "";
+
+ return (
+
+ {/* Main Row */}
+
handleRowClick(tx.id)}
+ >
+
+ {formatDateForInput(tx.date)}
+
+
+ {tx.description || "N/A"}
+
+
+ {formatCategoryLabel(tx.category)}
+
+
+ {formatAmount(tx.amount)}
+
+
+ {tx.account_name || "Unknown Account"}
+
+
+ {tx.sender || "-"}
+
+
+ {tx.recipient || "-"}
+
+
+
+
+
+
+ {/* Expanded Edit Row */}
+ {isExpanded && (
+
+
+
+
+
+ handleFieldChange(tx.id, "date", e.target.value)
+ }
+ className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+
+
+ handleFieldChange(
+ tx.id,
+ "description",
+ e.target.value,
+ )
+ }
+ className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+
+
+ handleFieldChange(
+ tx.id,
+ "category",
+ e.target.value,
+ )
+ }
+ className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+
+
+ handleListCurrencyChange(
+ e,
+ handleFieldChange,
+ tx.id,
+ "amount",
+ )
+ }
+ onBlur={(e) =>
+ handleListCurrencyBlur(
+ e,
+ handleFieldChange,
+ tx.id,
+ "amount",
+ )
+ }
+ className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+ {/* Account Selector for Edit Form */}
+
+
+
+
+
+
+
+
+ handleFieldChange(tx.id, "sender", e.target.value)
+ }
+ className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+
+
+ handleFieldChange(
+ tx.id,
+ "recipient",
+ e.target.value,
+ )
+ }
+ className="w-full bg-black/50 border border-green-500/30 rounded px-3 py-2 text-white text-sm"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+
+
+
+
+
+ )}
+
+ );
+ })}
+
+
+
+
+ {/* Load More Button */}
+ {hasMore && (
+
+
+
+ )}
+
+ );
+}
+
+export default TransactionTable;
diff --git a/app/client/src/components/TransationForm.tsx b/app/client/src/components/TransationForm.tsx
new file mode 100644
index 0000000..d26ca3a
--- /dev/null
+++ b/app/client/src/components/TransationForm.tsx
@@ -0,0 +1,322 @@
+import { useEffect, useState } from "react";
+import CsvUpload from "./csvread/CsvUpload";
+import "./userForm.css";
+import { getUserAccounts } from "../api/Account";
+import { validateTransactionForm } from "../utils/ValidateForms";
+import { postTranscations, putTranscations } from "../api/Transaction";
+import { handleCurrencyChange, handleCurrencyBlur } from "../utils/handleInput";
+import { type Transaction } from "../types/Transaction";
+import { type Account } from "@/types/AccountType";
+import { transactionCategory } from "@/enum/TransactionCategory";
+import type { AuthSession } from "@/types/authTypes";
+
+interface popupProp {
+ toggle: () => void;
+ session: AuthSession;
+ setTransaction?: (editedTransaction: Transaction) => void;
+ addTransaction?: (newTransaction: Transaction) => void;
+ edit: boolean;
+ selectedTransaction?: Transaction;
+}
+
+//Gets the keys of the enum
+type typeOfTransaction = keyof typeof transactionCategory;
+
+//Returns a form of for the user to enter their info
+export default function PopupForm({
+ toggle,
+ session,
+ setTransaction,
+ addTransaction,
+ edit,
+ selectedTransaction,
+}: popupProp) {
+ //Handles the submiting the form
+ const handleSubmit = () => {
+ const transferAmount = Number(amount);
+ let userTransaction: Transaction;
+ let recipient = "";
+ let sender = "";
+
+ if (
+ selectedType &&
+ validateTransactionForm(
+ selectedAccount,
+ selectedType,
+ transferAmount,
+ selectedDate,
+ undefined,
+ )
+ ) {
+ let target;
+
+ if (account) {
+ if (selectedType === transactionCategory.INCOME) {
+ target = account.find(
+ (account) => account.id === selectedAccount,
+ )?.name;
+ if (target) {
+ recipient = target;
+ }
+
+ sender = other;
+ } else {
+ recipient = other;
+
+ target = account.find(
+ (account) => account.id === selectedAccount,
+ )?.name;
+ if (target) {
+ sender = target;
+ }
+ }
+ }
+
+ userTransaction = {
+ id: 0,
+ financialAccount_id: Number(selectedAccount),
+ recipient: recipient,
+ sender: sender,
+ amount: transferAmount,
+ category: selectedType,
+ date: new Date(selectedDate),
+ };
+
+ if (edit && selectedTransaction) {
+ try {
+ //Editing selected transaction
+ userTransaction.id = selectedTransaction.id;
+
+ //Send a request to update the transaction
+ putTranscations(session, userTransaction).then((result) => {
+ //Determine if we're able to able to edit the transaction
+ if (result && setTransaction) {
+ setTransaction(userTransaction);
+ }
+ });
+ } catch {
+ alert("Failed to edit transaction");
+ }
+ } else {
+ try {
+ //Send a request to create the transaction
+ postTranscations(session, userTransaction).then((response) => {
+ //Successful put if response is returned
+ if (response && response.id) {
+ userTransaction.id = response.id;
+
+ if (addTransaction) {
+ addTransaction(userTransaction);
+ }
+ }
+ });
+ } catch {
+ alert("Failed to create transaction");
+ }
+ }
+
+ //Need to insert function that sets the state in parent component
+
+ toggle();
+ } else {
+ alert("Please fill out the transaction form");
+ }
+ };
+
+ //Handle when the user adds a file to the form
+ /*const handleFile = (event:React.ChangeEvent) =>{
+
+ if(event.target && event.target.files && event.target.files[0]){
+ setFile(event.target.files[0])
+ } else {
+ setFile(undefined)
+ }
+
+ //Insert cvs parsing function/validation function
+ }*/
+
+ //State of the user's account
+ const [account, setAccount] = useState();
+
+ useEffect(() => {
+ getUserAccounts(session)
+ .then((accounts) => {
+ console.log(accounts);
+ //Detemrine accounts exist
+ if (accounts) {
+ setAccount(accounts);
+ } else {
+ alert(
+ "Failed to retrieve user's accounts, cannot make a transaction",
+ );
+ //toggle();
+ }
+ })
+ .catch(() => {
+ alert("Failed to retrieve user's accounts, cannot make a transaction");
+ //toggle();
+ });
+ }, [session]);
+
+ //Holds state of user input
+ const [selectedAccount, setSelectedAccount] = useState(() => {
+ if (edit && selectedTransaction) {
+ return selectedTransaction.financialAccount_id;
+ } else {
+ return 0;
+ }
+ });
+
+ const [selectedType, setSelectedType] = useState(() => {
+ if (edit && selectedTransaction) {
+ return selectedTransaction.category;
+ } else {
+ return "";
+ }
+ });
+
+ const [amount, setAmount] = useState(() => {
+ if (edit && selectedTransaction) {
+ return selectedTransaction.amount.toFixed(2);
+ } else {
+ return "";
+ }
+ });
+ //const [file, setFile] = useState(undefined)
+
+ const [selectedDate, setSelectedDate] = useState(() => {
+ if (edit && selectedTransaction) {
+ return new Date(selectedTransaction.date).toISOString().slice(0, 10);
+ } else {
+ //Default
+ return "";
+ }
+ });
+
+ const [other, setOther] = useState(() => {
+ if (edit && selectedTransaction) {
+ if (selectedType === transactionCategory.INCOME) {
+ return selectedTransaction.sender;
+ } else {
+ return selectedTransaction.recipient;
+ }
+ } else {
+ //Default
+ return "";
+ }
+ });
+
+ //Holds the types of transfers
+ const transCat: typeOfTransaction[] = Object.keys(
+ transactionCategory,
+ ) as typeOfTransaction[];
+
+ return (
+ <>
+
+
+ {edit ?
Edit Transaction
:
Create Transaction
}
+
+
+
+
+
+ {selectedType == transactionCategory.INCOME ? (
+
+ ) : (
+
+ )}
+
setOther(event.target.value)}
+ >
+
+
+
+
+
+
+
+
handleCurrencyChange(event, setAmount)}
+ onBlur={(event) => handleCurrencyBlur(event, amount, setAmount)}
+ placeholder="0.00"
+ />
+
+
+
+
setSelectedDate(event.target.value)}
+ />
+
+
+ {edit ? null : (
+ <>
+
+ {!edit && selectedAccount && (
+
{
+ if (addTransaction) {
+ newTxs.forEach((tx) => addTransaction(tx));
+ }
+ }}
+ />
+ )}
+ >
+ )}
+
+
+
+
+ {edit ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/app/client/src/components/csvread/CsvConfirm.tsx b/app/client/src/components/csvread/CsvConfirm.tsx
new file mode 100644
index 0000000..2edcadb
--- /dev/null
+++ b/app/client/src/components/csvread/CsvConfirm.tsx
@@ -0,0 +1,43 @@
+//shows a summary of parsed CSV data before final import
+
+import type { TransactionDraft } from "../../utils/ConvertTransaction";
+
+interface Props {
+ rows: TransactionDraft[];
+ onConfirm: () => void;
+ onBack: () => void;
+}
+
+export default function CsvConfirmation({ rows, onConfirm, onBack }: Props) {
+ // calculates summary stats and displays them along with confirm/back buttons
+ const validRows = rows.filter((r) => r.errors.length === 0);
+ const invalidRows = rows.filter((r) => r.errors.length > 0);
+
+ const totalIncome = validRows
+ .filter((r) => (r.amount ?? 0) > 0)
+ .reduce((sum, r) => sum + (r.amount ?? 0), 0);
+
+ const totalExpenses = validRows
+ .filter((r) => (r.amount ?? 0) < 0)
+ .reduce((sum, r) => sum + (r.amount ?? 0), 0);
+
+ return (
+
+
Summary
+
+
+ Total rows: {rows.length}
+ Valid rows: {validRows.length}
+ Invalid rows: {invalidRows.length}
+ Total income: ${totalIncome.toFixed(2)}
+ Total expenses: ${totalExpenses.toFixed(2)}
+
+
+
+
+
+
+ );
+}
diff --git a/app/client/src/components/csvread/CsvDrop.tsx b/app/client/src/components/csvread/CsvDrop.tsx
new file mode 100644
index 0000000..a0c24e3
--- /dev/null
+++ b/app/client/src/components/csvread/CsvDrop.tsx
@@ -0,0 +1,48 @@
+//component to upload for CSV files
+// import { color } from "chart.js/helpers";
+import React, { useState } from "react";
+
+interface CsvDropProps {
+ onFileSelect: (file: File | null) => void;
+}
+// handles drag and drop for CSV file upload
+export default function CsvDrop({ onFileSelect }: CsvDropProps) {
+ const [isDragging, setIsDragging] = useState(false);
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ setIsDragging(false);
+
+ const file = e.dataTransfer.files?.[0];
+ onFileSelect(file || null);
+ };
+ //accepts only csv files
+ return (
+ setIsDragging(true)}
+ onDragOver={(e) => e.preventDefault()}
+ onDragLeave={() => setIsDragging(false)}
+ onDrop={handleDrop}
+ onClick={() => document.getElementById("csv-input")?.click()}
+ style={{
+ border: "2px dashed #888",
+ padding: "1.5rem",
+ borderRadius: 8,
+ textAlign: "center",
+ background: isDragging ? "#eef" : "#fafafa",
+ cursor: "pointer",
+ marginBottom: "1rem",
+ color: "#888",
+ }}
+ >
+
Drag & drop your CSV here, or click to select a file
+
onFileSelect(e.target.files?.[0] || null)}
+ />
+
+ );
+}
diff --git a/app/client/src/components/csvread/CsvError.tsx b/app/client/src/components/csvread/CsvError.tsx
new file mode 100644
index 0000000..f7be7ee
--- /dev/null
+++ b/app/client/src/components/csvread/CsvError.tsx
@@ -0,0 +1,22 @@
+//component to show parsing errors
+
+interface CsvErrorProps {
+ message: string;
+}
+
+export default function CsvError({ message }: CsvErrorProps) {
+ return (
+
+ {message}
+
+ );
+}
diff --git a/app/client/src/components/csvread/CsvFile.tsx b/app/client/src/components/csvread/CsvFile.tsx
new file mode 100644
index 0000000..447d040
--- /dev/null
+++ b/app/client/src/components/csvread/CsvFile.tsx
@@ -0,0 +1,38 @@
+//This displays file name, size, and a remove
+
+interface CsvFileProps {
+ file: File;
+ onRemove: () => void;
+}
+
+export default function CsvFile({ file, onRemove }: CsvFileProps) {
+ return (
+
+
{file.name}
+
{(file.size / 1024).toFixed(1)} KB
+
+
+
+ );
+}
diff --git a/app/client/src/components/csvread/CsvPreview.tsx b/app/client/src/components/csvread/CsvPreview.tsx
new file mode 100644
index 0000000..d5906fe
--- /dev/null
+++ b/app/client/src/components/csvread/CsvPreview.tsx
@@ -0,0 +1,71 @@
+// Component to display parsed CSV rows in a preview table
+
+import type { TransactionDraft } from "../../utils/ConvertTransaction";
+
+interface Props {
+ rows: TransactionDraft[];
+}
+
+export default function CsvPreviewTable({ rows }: Props) {
+ // count valid and invalid rows
+ const validCount = rows.filter((r) => r.errors.length === 0).length;
+ const invalidCount = rows.length - validCount;
+
+ return (
+
+
+ Total rows: {rows.length}
+ Valid rows: {validCount}
+ Invalid rows: {invalidCount}
+
+
+
+
+
+ | Date |
+ Description |
+ Amount |
+ Sender |
+ Recipient |
+ Category |
+ Errors |
+
+
+
+
+ {rows.map((row, index) => {
+ const isInvalid = row.errors.length > 0;
+
+ return (
+
+ | {row.date} |
+ {row.description} |
+ {row.amount} |
+ {row.sender} |
+ {row.recipient} |
+ {row.category} |
+ {isInvalid ? row.errors.join(", ") : "—"} |
+
+ );
+ })}
+
+
+
+ );
+}
+const cell: React.CSSProperties = {
+ border: "1px solid #ccc",
+ padding: "6px 8px",
+ fontSize: "0.9rem",
+};
diff --git a/app/client/src/components/csvread/CsvUpload.tsx b/app/client/src/components/csvread/CsvUpload.tsx
new file mode 100644
index 0000000..97de7c6
--- /dev/null
+++ b/app/client/src/components/csvread/CsvUpload.tsx
@@ -0,0 +1,187 @@
+// Component to handle file upload, validation, and error display for CSV files
+//handles one csv file at a time
+import { useState } from "react";
+import CsvDrop from "./CsvDrop";
+import CsvFile from "./CsvFile";
+import CsvError from "./CsvError";
+import CsvPreviewTable from "./CsvPreview";
+import CsvConfirmation from "./CsvConfirm";
+import { parseCsvFile } from "../../utils/ParseCsv";
+import type { TransactionDraft } from "../../utils/ConvertTransaction";
+import SuccessScreen from "./csvSuccess";
+import { uploadCsvTransactions } from "@/api/Transaction";
+// import type { AuthSession } from "@/types/authTypes";
+import type { Transaction } from "@/types/Transaction";
+
+export default function CsvUpload({
+ accountId,
+ // session,
+ onImported,
+}: {
+ accountId: number;
+ // session: AuthSession;
+ onImported: (txs: Transaction[]) => void;
+}) {
+ const [file, setFile] = useState(null); // selected CSV file
+ const [error, setError] = useState(null); // validation errors
+ const [parsedData, setParsedData] = useState(null); // state for parsed and validated data
+ const [isParsing, setIsParsing] = useState(false); // track if parsing is in progress
+ const [showConfirmation, setShowConfirmation] = useState(false);
+ const [importResult, setImportResult] = useState(null);
+
+ // File Validation
+ const handleFileSelect = (selected: File | null) => {
+ if (!selected) return;
+
+ // Validate type
+ if (!selected.name.endsWith(".csv")) {
+ setError("Please upload a CSV file.");
+ setFile(null);
+ return;
+ }
+
+ // Validate size
+ if (selected.size === 0) {
+ setError("File is empty.");
+ setFile(null);
+ return;
+ }
+
+ if (selected.size > 5 * 1024 * 1024) {
+ // limit file size to 5MB
+ setError("File is too large (max 5MB).");
+ setFile(null);
+ return;
+ }
+
+ setError(null);
+ setFile(selected);
+ setParsedData(null);
+ setShowConfirmation(false);
+ };
+
+ const clearFile = () => {
+ setFile(null);
+ setError(null);
+ setParsedData(null);
+ setShowConfirmation(false);
+ };
+
+ const handlePreview = async () => {
+ // parses the file and updates state with results
+ if (!file) return;
+
+ setIsParsing(true);
+ setError(null);
+
+ try {
+ const rows = await parseCsvFile(file); // returns array of transaction drafts with validation errors if any
+
+ if (rows.length === 0) {
+ setError("CSV contains no readable rows.");
+ setParsedData(null);
+ } else {
+ setParsedData(rows);
+ }
+ } catch (err: Error | unknown) {
+ // handle parsing errors
+ setError(
+ "Failed to parse CSV: " +
+ (err instanceof Error ? err.message : String(err)),
+ );
+ setParsedData(null);
+ }
+
+ setIsParsing(false);
+ };
+
+ //handle import sends valid transactions to backend for insertion into database
+ const handleImport = async () => {
+ if (!parsedData) return;
+
+ try {
+ const response = await uploadCsvTransactions(
+ // session,
+ accountId,
+ parsedData,
+ );
+
+ if ("error" in response) {
+ const err = response.error;
+ setError(typeof err === "string" ? err : "Unknown error");
+ return;
+ }
+
+ // Notify parent (PopupForm)
+ onImported(response.transactions);
+
+ setImportResult({
+ inserted: response.inserted,
+ skipped: response.skipped,
+ });
+ } catch (error) {
+ console.error("Import failed:", error);
+ setError("Error during import");
+ }
+ };
+
+ return (
+
+ {importResult && (
+ {
+ setImportResult(null);
+ setParsedData(null);
+ setFile(null);
+ setShowConfirmation(false);
+ }}
+ />
+ )}
+ {!importResult && (
+ <>
+
+
+ {error && }
+
+ {file && }
+
+ {!showConfirmation && (
+
+ )}
+
+ {parsedData && parsedData.length > 0 && !showConfirmation && (
+ <>
+
+
+
+ >
+ )}
+
+ {parsedData && parsedData.length > 0 && showConfirmation && (
+ setShowConfirmation(false)}
+ onConfirm={handleImport}
+ />
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/app/client/src/components/csvread/csvSuccess.tsx b/app/client/src/components/csvread/csvSuccess.tsx
new file mode 100644
index 0000000..227b2a6
--- /dev/null
+++ b/app/client/src/components/csvread/csvSuccess.tsx
@@ -0,0 +1,24 @@
+//util function to show success message after import
+export default function SuccessScreen({
+ inserted,
+ skipped,
+ onDone,
+}: {
+ inserted: number;
+ skipped: number;
+ onDone: () => void;
+}) {
+ return (
+
+
Import Complete
+
{inserted} transactions imported successfully.
+
{skipped} rows were skipped due to validation errors.
+
+
+
+ );
+}
diff --git a/app/client/src/components/market/MarketSearchBar.tsx b/app/client/src/components/market/MarketSearchBar.tsx
new file mode 100644
index 0000000..1baf1ed
--- /dev/null
+++ b/app/client/src/components/market/MarketSearchBar.tsx
@@ -0,0 +1,34 @@
+import { Search } from "lucide-react";
+
+import { Input } from "@/components/ui/input";
+
+type MarketSearchBarProps = {
+ searchTerm: string;
+ onChange: (value: string) => void;
+};
+
+export default function MarketSearchBar({
+ searchTerm,
+ onChange,
+}: MarketSearchBarProps) {
+ return (
+
+
+
+
+ onChange(event.target.value)}
+ placeholder="Try AAPL, NVDA, EUR/USD, or USDJPY"
+ className="h-12 border-white/10 bg-white/5 pl-10 text-white placeholder:text-emerald-100/35"
+ />
+
+
+ );
+}
diff --git a/app/client/src/components/market/MarketSearchResultsPanel.tsx b/app/client/src/components/market/MarketSearchResultsPanel.tsx
new file mode 100644
index 0000000..1884572
--- /dev/null
+++ b/app/client/src/components/market/MarketSearchResultsPanel.tsx
@@ -0,0 +1,147 @@
+import { Pin, TrendingDown, TrendingUp } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import type { MarketInstrument } from "@/types/Market";
+import { cn } from "@/utils/utils";
+import {
+ formatMarketPrice,
+ formatSignedPercent,
+ isPositiveChange,
+} from "@/utils/market";
+
+type MarketSearchResultsPanelProps = {
+ deferredSearchTerm: string;
+ searchLoading: boolean;
+ searchError: string | null;
+ searchResults: MarketInstrument[];
+ selectedSymbol: string | null;
+ pinnedSymbols: Set;
+ onSelectInstrument: (instrument: MarketInstrument) => void;
+ onTogglePin: (instrument: MarketInstrument) => void;
+};
+
+export default function MarketSearchResultsPanel({
+ deferredSearchTerm,
+ searchLoading,
+ searchError,
+ searchResults,
+ selectedSymbol,
+ pinnedSymbols,
+ onSelectInstrument,
+ onTogglePin,
+}: MarketSearchResultsPanelProps) {
+ return (
+
+
+
+
Search results
+
+ {deferredSearchTerm
+ ? `Showing matches for "${deferredSearchTerm}"`
+ : "Featured market instruments"}
+
+
+ {searchLoading && (
+
Loading...
+ )}
+
+
+
+ {searchError && (
+
+ {searchError}
+
+ )}
+
+ {!searchError && !searchLoading && searchResults.length === 0 && (
+
+ No instruments matched that search.
+
+ )}
+
+ {searchResults.map((instrument) => {
+ const positive = isPositiveChange(instrument.change);
+
+ return (
+
onSelectInstrument(instrument)}
+ onKeyDown={(event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ onSelectInstrument(instrument);
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ className={cn(
+ "flex w-full items-center justify-between gap-4 rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 text-left transition hover:border-emerald-300/30 hover:bg-white/[0.06]",
+ selectedSymbol === instrument.symbol &&
+ "border-emerald-300/45 bg-emerald-400/10",
+ )}
+ >
+
+
+
+ {instrument.displaySymbol}
+
+
+ {instrument.type}
+
+
+
+ {instrument.name}
+
+
+
+
+
+
+ {formatMarketPrice(instrument.price, instrument.currency)}
+
+
+ {positive ? (
+
+ ) : (
+
+ )}
+ {formatSignedPercent(instrument.changePercent)}
+
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/app/client/src/components/market/PinnedInstrumentsSection.tsx b/app/client/src/components/market/PinnedInstrumentsSection.tsx
new file mode 100644
index 0000000..b494432
--- /dev/null
+++ b/app/client/src/components/market/PinnedInstrumentsSection.tsx
@@ -0,0 +1,114 @@
+import { PinOff } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import type { MarketInstrument } from "@/types/Market";
+import { cn } from "@/utils/utils";
+import { formatMarketPrice, formatSignedPercent } from "@/utils/market";
+import { changeColorClass } from "@/components/market/marketSectionHelpers";
+
+type PinnedInstrumentsSectionProps = {
+ pinnedInstruments: MarketInstrument[];
+ selectedSymbol: string | null;
+ onSelectInstrument: (instrument: MarketInstrument) => void;
+ onTogglePin: (instrument: MarketInstrument) => void;
+};
+
+export default function PinnedInstrumentsSection({
+ pinnedInstruments,
+ selectedSymbol,
+ onSelectInstrument,
+ onTogglePin,
+}: PinnedInstrumentsSectionProps) {
+ return (
+
+
+
+
+ Pinned Forex and Stocks
+
+
+
+ {pinnedInstruments.length} pinned
+
+
+
+ {pinnedInstruments.length === 0 ? (
+
+ Pin a stock or forex pair from the search results to keep it on your
+ dashboard.
+
+ ) : (
+
+ {pinnedInstruments.map((instrument) => {
+ return (
+
onSelectInstrument(instrument)}
+ onKeyDown={(event) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ onSelectInstrument(instrument);
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ className={cn(
+ "rounded-3xl border border-white/10 bg-white/[0.04] p-4 text-left transition hover:border-emerald-300/30 hover:bg-white/[0.06]",
+ selectedSymbol === instrument.symbol &&
+ "border-emerald-300/45 bg-emerald-400/10 shadow-[0_12px_36px_rgba(16,185,129,0.14)]",
+ )}
+ >
+
+
+
+ {instrument.displaySymbol}
+
+
+ {instrument.name}
+
+
+
+
+
+
+
+
+ {formatMarketPrice(instrument.price, instrument.currency)}
+
+
+ {formatSignedPercent(instrument.changePercent)}
+
+
+
+ {instrument.type}
+
+
+
+ );
+ })}
+
+ )}
+
+ );
+}
diff --git a/app/client/src/components/market/SelectedInstrumentPanel.tsx b/app/client/src/components/market/SelectedInstrumentPanel.tsx
new file mode 100644
index 0000000..4a8953f
--- /dev/null
+++ b/app/client/src/components/market/SelectedInstrumentPanel.tsx
@@ -0,0 +1,301 @@
+import {
+ Area,
+ AreaChart,
+ CartesianGrid,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from "recharts";
+import { Activity, Pin, PinOff, TrendingDown, TrendingUp } from "lucide-react";
+
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import type { MarketHistoryPeriod, MarketInstrument } from "@/types/Market";
+import { cn } from "@/utils/utils";
+import {
+ MARKET_PERIOD_OPTIONS,
+ formatMarketPrice,
+ formatMarketTimestamp,
+ formatSignedPercent,
+ formatSignedValue,
+ isPositiveChange,
+} from "@/utils/market";
+import { changeColorClass } from "@/components/market/marketSectionHelpers";
+
+type ChartPoint = {
+ timestamp: number;
+ price: number;
+};
+
+type SelectedInstrumentPanelProps = {
+ selectedInstrument: MarketInstrument | null;
+ pinnedSymbols: Set;
+ selectedPeriod: MarketHistoryPeriod;
+ selectedChartData: ChartPoint[];
+ historyError: string | null;
+ historyLoading: boolean;
+ onTogglePin: (instrument: MarketInstrument) => void;
+ onSelectPeriod: (period: MarketHistoryPeriod) => void;
+};
+
+export default function SelectedInstrumentPanel({
+ selectedInstrument,
+ pinnedSymbols,
+ selectedPeriod,
+ selectedChartData,
+ historyError,
+ historyLoading,
+ onTogglePin,
+ onSelectPeriod,
+}: SelectedInstrumentPanelProps) {
+ const firstTimestamp = selectedChartData[0]?.timestamp ?? 0;
+ const lastTimestamp =
+ selectedChartData[selectedChartData.length - 1]?.timestamp ?? 0;
+ const totalSpanMs = Math.max(lastTimestamp - firstTimestamp, 0);
+
+ const axisLabelFormatter =
+ totalSpanMs <= 1000 * 60 * 60 * 36
+ ? new Intl.DateTimeFormat("en-US", {
+ hour: "numeric",
+ minute: "2-digit",
+ })
+ : totalSpanMs <= 1000 * 60 * 60 * 24 * 7
+ ? new Intl.DateTimeFormat("en-US", {
+ weekday: "short",
+ hour: "numeric",
+ })
+ : new Intl.DateTimeFormat("en-US", {
+ month: "short",
+ day: "numeric",
+ year: selectedChartData.length > 180 ? "2-digit" : undefined,
+ });
+
+ const tooltipLabelFormatter = new Intl.DateTimeFormat("en-US", {
+ dateStyle: "medium",
+ timeStyle: totalSpanMs <= 1000 * 60 * 60 * 24 * 14 ? "short" : undefined,
+ });
+
+ return (
+
+ {selectedInstrument ? (
+ <>
+
+
+
+
+ {selectedInstrument.displaySymbol}
+
+
+ {selectedInstrument.type === "stock" ? "Stock" : "Forex"}
+
+ {selectedInstrument.exchange && (
+
+ {selectedInstrument.exchange}
+
+ )}
+
+
+ {selectedInstrument.name}
+
+
+
+
+
+ {formatMarketPrice(
+ selectedInstrument.price,
+ selectedInstrument.currency,
+ )}
+
+
+ {isPositiveChange(selectedInstrument.change) ? (
+
+ ) : (
+
+ )}
+
+ {formatSignedValue(selectedInstrument.change, 2)}
+
+
+ {formatSignedPercent(selectedInstrument.changePercent)}
+
+
+
+
+
+ Updated {formatMarketTimestamp(selectedInstrument.timestamp)}
+
+
+
+
+
+
+
+
+ {MARKET_PERIOD_OPTIONS.map((option) => (
+
+ ))}
+
+
+
+ {historyError ? (
+
+ {historyError}
+
+ ) : historyLoading ? (
+
+ Loading historical data...
+
+ ) : selectedChartData.length > 1 ? (
+
+
+
+
+
+
+
+
+
+
+
+ axisLabelFormatter.format(new Date(Number(value)))
+ }
+ />
+
+ formatMarketPrice(
+ Number(value),
+ selectedInstrument.currency,
+ )
+ }
+ />
+
+ tooltipLabelFormatter.format(new Date(Number(value)))
+ }
+ formatter={(value) => [
+ formatMarketPrice(
+ Number(value ?? 0),
+ selectedInstrument.currency,
+ ),
+ "Price",
+ ]}
+ />
+
+
+
+
+ ) : (
+
+ Historical data is unavailable for this period.
+
+ )}
+
+ >
+ ) : (
+
+
+
+ Pick a stock or forex option to inspect
+
+
+ )}
+
+ );
+}
diff --git a/app/client/src/components/market/marketSectionHelpers.ts b/app/client/src/components/market/marketSectionHelpers.ts
new file mode 100644
index 0000000..452b4bc
--- /dev/null
+++ b/app/client/src/components/market/marketSectionHelpers.ts
@@ -0,0 +1,48 @@
+import type {
+ MarketHistoryPeriod,
+ MarketHistoryPoint,
+ MarketInstrument,
+ PinnedMarketInstrument,
+} from "@/types/Market";
+import { isPositiveChange } from "@/utils/market";
+
+const PINNED_HISTORY_PERIOD: MarketHistoryPeriod = "1mo";
+
+type PinnedHistoryMap = Record;
+
+function toPinnedInstrument(
+ instrument: MarketInstrument,
+): PinnedMarketInstrument {
+ return {
+ symbol: instrument.symbol,
+ displaySymbol: instrument.displaySymbol,
+ name: instrument.name,
+ type: instrument.type,
+ currency: instrument.currency,
+ exchange: instrument.exchange,
+ };
+}
+
+function fromPinnedInstrument(
+ instrument: PinnedMarketInstrument,
+): MarketInstrument {
+ return {
+ ...instrument,
+ price: null,
+ change: null,
+ changePercent: null,
+ timestamp: null,
+ };
+}
+
+function changeColorClass(change: number | null) {
+ return isPositiveChange(change) ? "text-emerald-300" : "text-rose-300";
+}
+
+export {
+ changeColorClass,
+ fromPinnedInstrument,
+ PINNED_HISTORY_PERIOD,
+ toPinnedInstrument,
+};
+export type { PinnedHistoryMap };
diff --git a/app/client/src/components/ui/badge.tsx b/app/client/src/components/ui/badge.tsx
new file mode 100644
index 0000000..4edaf50
--- /dev/null
+++ b/app/client/src/components/ui/badge.tsx
@@ -0,0 +1,33 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/utils/utils";
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors",
+ {
+ variants: {
+ variant: {
+ default: "border-transparent bg-primary text-primary-foreground",
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
+ outline: "border-border text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+export interface BadgeProps
+ extends
+ React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ );
+}
+
+export { Badge };
diff --git a/app/client/src/components/ui/button.tsx b/app/client/src/components/ui/button.tsx
new file mode 100644
index 0000000..5217a07
--- /dev/null
+++ b/app/client/src/components/ui/button.tsx
@@ -0,0 +1,51 @@
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
+
+import { cn } from "@/utils/utils";
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/80 focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-primary-foreground hover:bg-destructive/90",
+ outline:
+ "border border-border bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ },
+);
+
+export interface ButtonProps
+ extends
+ React.ButtonHTMLAttributes,
+ VariantProps {}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Button.displayName = "Button";
+
+export { Button };
diff --git a/app/client/src/components/ui/card.tsx b/app/client/src/components/ui/card.tsx
new file mode 100644
index 0000000..bb64ce6
--- /dev/null
+++ b/app/client/src/components/ui/card.tsx
@@ -0,0 +1,86 @@
+import * as React from "react";
+
+import { cn } from "@/utils/utils";
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Card.displayName = "Card";
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardHeader.displayName = "CardHeader";
+
+const CardTitle = React.forwardRef<
+ HTMLHeadingElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardTitle.displayName = "CardTitle";
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardDescription.displayName = "CardDescription";
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardContent.displayName = "CardContent";
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+CardFooter.displayName = "CardFooter";
+
+export {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+};
diff --git a/app/client/src/components/ui/input.tsx b/app/client/src/components/ui/input.tsx
new file mode 100644
index 0000000..45f5c04
--- /dev/null
+++ b/app/client/src/components/ui/input.tsx
@@ -0,0 +1,22 @@
+import * as React from "react";
+
+import { cn } from "@/utils/utils";
+
+const Input = React.forwardRef>(
+ ({ className, type, ...props }, ref) => {
+ return (
+
+ );
+ },
+);
+Input.displayName = "Input";
+
+export { Input };
diff --git a/app/client/src/components/ui/label.tsx b/app/client/src/components/ui/label.tsx
new file mode 100644
index 0000000..c8d48d3
--- /dev/null
+++ b/app/client/src/components/ui/label.tsx
@@ -0,0 +1,19 @@
+import * as React from "react";
+
+import { cn } from "@/utils/utils";
+
+const Label = React.forwardRef>(
+ ({ className, ...props }, ref) => (
+
+ ),
+);
+Label.displayName = "Label";
+
+export { Label };
diff --git a/app/client/src/components/userForm.css b/app/client/src/components/userForm.css
new file mode 100644
index 0000000..fb24786
--- /dev/null
+++ b/app/client/src/components/userForm.css
@@ -0,0 +1,82 @@
+.popupForm {
+ background-color: rgba(14, 15, 15, 0.776);
+ padding: 15px;
+ place-content: start;
+ width: max-content;
+ height: auto;
+ white-space: nowrap;
+}
+
+.formInput {
+ margin-top: 2.5%;
+ margin-bottom: 2.5%;
+ margin-left: 1%;
+ margin-right: 1%;
+ background-color: rgba(0, 0, 0, 0.566);
+ padding-left: 2%;
+ border-color: black;
+ border-width: 2px;
+}
+
+.formSelect {
+ margin-top: 2.5%;
+ margin-bottom: 2.5%;
+ margin-left: 1%;
+ background-color: rgba(0, 0, 0, 0.566);
+ padding-left: 2%;
+ border-color: black;
+ border-width: 2px;
+}
+
+.popup {
+ color: black;
+ background-color: rgb(34, 206, 66);
+ padding: 10px;
+ border-radius: 5px;
+ border-style: solid;
+ border-width: 2px;
+ border-color: rgba(34, 250, 94, 1);
+
+ position: fixed;
+ top: 10%;
+ bottom: 0;
+ right: 35%;
+ left: 35%;
+ z-index: 100;
+ width: max-content;
+ height: max-content;
+
+ font-size: 10pt;
+ color: antiquewhite;
+
+ justify-content: center;
+ align-items: center;
+}
+
+.formButton {
+ padding: 5px;
+ border-width: 1px;
+ border-color: black;
+ border-radius: 5px;
+}
+
+.formH2 {
+ font-weight: bold;
+ text-align: center;
+ padding-bottom: 2%;
+}
+
+.bottomButtons {
+ margin-top: 5px;
+ display: flex;
+ color: black;
+}
+
+.bottomButtons :first-child {
+ margin-right: auto;
+ background-color: lightcoral;
+}
+.bottomButtons button:last-child {
+ margin-left: auto;
+ background-color: aquamarine;
+}
diff --git a/app/client/src/enum/AccountCategory.ts b/app/client/src/enum/AccountCategory.ts
new file mode 100644
index 0000000..33c4cc2
--- /dev/null
+++ b/app/client/src/enum/AccountCategory.ts
@@ -0,0 +1,7 @@
+export const accountCategory = {
+ SAVINGS: "Savings",
+ CHEQUING: "Chequing",
+ INVESTMENT: "Investment",
+ CREDIT_CARD:"Credit Card",
+ UNCONFIRMED: "Unconfirmed"
+};
diff --git a/app/client/src/enum/TransactionCategory.ts b/app/client/src/enum/TransactionCategory.ts
new file mode 100644
index 0000000..6dcca84
--- /dev/null
+++ b/app/client/src/enum/TransactionCategory.ts
@@ -0,0 +1,8 @@
+export const transactionCategory = {
+ FOOD: "Food",
+ HOUSING: "Housing",
+ UTIL: "Utilzities",
+ TRANSPORTATION: "Transportation",
+ INCOME: "Income",
+ OTHER: "Other",
+};
diff --git a/app/client/src/hooks/AggregatedSnapshot.tsx b/app/client/src/hooks/AggregatedSnapshot.tsx
new file mode 100644
index 0000000..b02a7e0
--- /dev/null
+++ b/app/client/src/hooks/AggregatedSnapshot.tsx
@@ -0,0 +1,59 @@
+import { useState, useEffect } from "react";
+import { getSnapshotData } from "@/api/DashboardAPI";
+import type { SnapshotData } from "@/types/AggregatedSnapshot";
+
+export function useSnapshotData() {
+ const [data, setData] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchData = async () => {
+ try {
+ setIsLoading(true);
+ setError(null);
+ const snapshotData = await getSnapshotData();
+ setData(snapshotData);
+ } catch (err) {
+ setError("Failed to load financial data");
+ console.error("Error fetching snapshot data:", err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const refresh = () => {
+ fetchData();
+ };
+
+ // Format currency helper
+ const formatCurrency = (amount: number = 0): string => {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "CAD",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ }).format(amount);
+ };
+
+ return {
+ data,
+ isLoading,
+ error,
+ refresh,
+
+ totalBalance: data ? formatCurrency(data.totalBalance) : "$0",
+ currentIncome: data ? formatCurrency(data.currentIncome) : "$0",
+ averageExpenses: data ? formatCurrency(data.averageExpenses) : "$0",
+ currentDebt: data ? formatCurrency(data.currentDebt) : "$0",
+ totalSavings: data ? formatCurrency(data.totalSavings) : "$0",
+ rawTotalBalance: data?.totalBalance || 0,
+ rawCurrentIncome: data?.currentIncome || 0,
+ rawAverageExpenses: data?.averageExpenses || 0,
+ rawCurrentDebt: data?.currentDebt || 0,
+ rawTotalSavings: data?.totalSavings || 0,
+ };
+}
diff --git a/app/client/src/index.css b/app/client/src/index.css
new file mode 100644
index 0000000..c4522fa
--- /dev/null
+++ b/app/client/src/index.css
@@ -0,0 +1,155 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+/* @import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap"); */
+
+@custom-variant dark (&:is(.dark *));
+
+:root {
+ font-family: "Space Grotesk", "Segoe UI", Tahoma, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ color: #e3ffec;
+ background: #030805;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#root {
+ width: 100%;
+ min-height: 100%;
+ margin: 0;
+}
+
+body {
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+button:hover {
+ border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ a:hover {
+ color: #747bff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --radius-2xl: calc(var(--radius) + 8px);
+ --radius-3xl: calc(var(--radius) + 12px);
+ --radius-4xl: calc(var(--radius) + 16px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/app/client/src/main.tsx b/app/client/src/main.tsx
new file mode 100644
index 0000000..eff7ccc
--- /dev/null
+++ b/app/client/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import "./index.css";
+import App from "./App.tsx";
+
+createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/app/client/src/pages/DashboardPage.tsx b/app/client/src/pages/DashboardPage.tsx
new file mode 100644
index 0000000..e4d9458
--- /dev/null
+++ b/app/client/src/pages/DashboardPage.tsx
@@ -0,0 +1,96 @@
+import {
+ Chart,
+ PointElement,
+ LineElement,
+ ArcElement,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+} from "chart.js";
+import TransactionTable from "@/components/TransactionTable";
+import DashboardChartSection from "@/components/DashboardChartSection";
+import type { AuthSession } from "../types/authTypes";
+import BudgetExpenditureChart from "@/components/BudgetExpenditureChart";
+import SnapshotSection from "@/components/SnapshotSection";
+
+import AccountList from "@/components/AccountList";
+// import TransactionList from "@/components/TransactionList";
+
+Chart.register(
+ PointElement,
+ LineElement,
+ ArcElement,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+);
+
+type DashboardPageProps = {
+ session?: AuthSession;
+};
+
+function DashboardPage({ session }: DashboardPageProps) {
+ const glowLeft = (
+
+ );
+
+ const glowRight = (
+
+ );
+ return (
+
+ {glowLeft}
+ {glowRight}
+
+ Hello {session?.user.first_name ?? session?.user.name ?? "there"}
+
+
+ Here you can view your recent transactions and manage your finances.
+
+
+
+
+ Recent Transactions
+
+
+ Account List
+ {session && }
+
+ {/* Transaction List
+ {session && } */}
+
+ );
+}
+
+export default DashboardPage;
diff --git a/app/client/src/pages/LoginPage.tsx b/app/client/src/pages/LoginPage.tsx
new file mode 100644
index 0000000..ec8d618
--- /dev/null
+++ b/app/client/src/pages/LoginPage.tsx
@@ -0,0 +1,103 @@
+import { useState, type FormEvent } from "react";
+import { Link } from "react-router-dom";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import type { AuthUser, RequestAuth } from "../types/authTypes";
+
+type LoginPageProps = {
+ onLogin: (token: string, fallbackUser: Partial) => void;
+ requestAuth: RequestAuth;
+};
+
+function LoginPage({ onLogin, requestAuth }: LoginPageProps) {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [errorMessage, setErrorMessage] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ async function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ setErrorMessage("");
+ setIsSubmitting(true);
+
+ const normalizedEmail = email.trim().toLowerCase();
+ const result = await requestAuth("/api/login", {
+ email: normalizedEmail,
+ password,
+ });
+
+ setIsSubmitting(false);
+
+ if (!result.ok || !result.token) {
+ setErrorMessage(result.error ?? "Login failed.");
+ return;
+ }
+
+ onLogin(result.token, { email: normalizedEmail });
+ }
+
+ return (
+
+
+
+
+ Finus
+
+ Welcome back
+
+ Log in to continue managing your finances.
+
+
+
+
+
+
+
+ New here? Create an account
+
+
+
+
+ );
+}
+
+export default LoginPage;
diff --git a/app/client/src/pages/MarketsPage.tsx b/app/client/src/pages/MarketsPage.tsx
new file mode 100644
index 0000000..2d7ee09
--- /dev/null
+++ b/app/client/src/pages/MarketsPage.tsx
@@ -0,0 +1,57 @@
+import MarketDashboardSection from "@/components/MarketDashboardSection";
+import type { AuthSession } from "@/types/authTypes";
+
+type MarketsPageProps = {
+ session?: AuthSession;
+};
+
+function MarketsPage({ session }: MarketsPageProps) {
+ const glowLeft = (
+
+ );
+
+ const glowRight = (
+
+ );
+
+ return (
+
+ {glowLeft}
+ {glowRight}
+
+ Markets for {session?.user.first_name ?? session?.user.name ?? "you"}
+
+
+ Search for stocks and foreign exchange options
+
+
+
+ );
+}
+
+export default MarketsPage;
diff --git a/app/client/src/pages/ProjectionPage.tsx b/app/client/src/pages/ProjectionPage.tsx
new file mode 100644
index 0000000..2c54254
--- /dev/null
+++ b/app/client/src/pages/ProjectionPage.tsx
@@ -0,0 +1,525 @@
+import {
+ Chart,
+ PointElement,
+ LineElement,
+ ArcElement,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+} from "chart.js";
+
+import type { AuthSession } from "../types/authTypes";
+import SelectAccount from "@/components/SelectAccount";
+import React, { useEffect, useState } from "react";
+import { type Account } from "@/types/AccountType";
+import { getDebt, getDebtProjection } from "@/api/Debt";
+import { getSaving, getSavingProjection } from "@/api/Saving";
+import {
+ type LineInfo,
+ type projectedDataResponse,
+} from "@/types/responseTypes";
+import ProjectionGraph from "@/components/ProjectionGraph";
+import { TbGraph, TbCalculator, TbRefresh } from "react-icons/tb";
+import NoItemState from "@/components/NoItemState";
+import {
+ handleCurrencyChange,
+ handleCurrencyBlur,
+ handleNumberChange,
+} from "@/utils/handleInput";
+import type {
+ projectionDebtRequest,
+ projectionSavingRequest,
+} from "@/types/requestTypes";
+import {
+ validateDebtProjection,
+ validateSavingProjection,
+} from "@/utils/ValidateProjectionRequest";
+import { accountCategory } from "@/enum/AccountCategory";
+
+Chart.register(
+ PointElement,
+ LineElement,
+ ArcElement,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip,
+ Legend,
+);
+
+type ProjectionProp = {
+ session: AuthSession;
+};
+
+function ProjectionPage({ session }: ProjectionProp) {
+ const [accounts, setAccounts] = useState([]);
+ const [debts, setDebts] = useState([]);
+ const [selectedAccount, setSelectedAccount] = useState(0);
+ const [selectedDebt, setSelectedDebt] = useState(0);
+ const [nextDueDate, setNextDueDate] = useState("");
+ const [interest, setInterest] = useState("");
+ const [amount, setAmount] = useState("");
+ const [period, setPeriod] = useState("");
+ const [minPay, setMinPay] = useState("");
+ const [selectedType, setSelectedType] = useState<"Saving" | "Debt">("Saving");
+ const [savingData, setSavingData] = useState<
+ projectedDataResponse | undefined
+ >(undefined);
+ const [debtData, setDebtData] = useState(
+ undefined,
+ );
+ const [debtRequest, setDebtRequest] = useState<
+ projectionDebtRequest | undefined
+ >(undefined);
+ const [savingRequest, setSavingRequest] = useState<
+ projectionSavingRequest | undefined
+ >(undefined);
+ const [totalInterest, setTotalInterest] = useState(0);
+ const [totalPay, setTotalPay] = useState(0);
+ const [isLoading, setIsLoading] = useState(false);
+
+ const calculateProjection = async () => {
+ const inputAmount = Number(amount);
+ const inputMinPay = Number(minPay);
+ const inputInterest = Number(interest);
+ const inputPeriod = Number(period);
+
+ setIsLoading(true);
+
+ switch (selectedType) {
+ case "Debt":
+ if (
+ validateDebtProjection(
+ selectedDebt,
+ inputAmount,
+ inputMinPay,
+ inputInterest,
+ nextDueDate,
+ inputPeriod,
+ )
+ ) {
+ const newDebtRequest: projectionDebtRequest = {
+ id: selectedAccount.toString(),
+ category: accountCategory.CREDIT_CARD,
+ remainingAmount: inputAmount,
+ minimumPayment: inputMinPay,
+ interestRate: inputInterest,
+ nextDueDate: nextDueDate,
+ period: inputPeriod,
+ };
+
+ setDebtRequest(newDebtRequest);
+
+ try {
+ const data = await getDebtProjection(newDebtRequest);
+ if (data) {
+ let dataTotalInterest = 0;
+ let dataTotalPay = 0;
+ const debtLineData: LineInfo = {
+ name: "Remaining Debt",
+ data: [],
+ };
+ const graphData: projectedDataResponse = {
+ lineInfo: [],
+ dateLabel: [],
+ };
+
+ data.debtStages.map((stage) => {
+ debtLineData.data.push(stage.remainingDebt);
+ graphData.dateLabel.push(stage.installmentDate);
+ dataTotalInterest += stage.interestAmount;
+ dataTotalPay += stage.principalAmount;
+ });
+
+ graphData.lineInfo = [debtLineData];
+ setDebtData(graphData);
+ setTotalPay(dataTotalPay);
+ setTotalInterest(dataTotalInterest);
+ }
+ } catch {
+ alert("Failed to get debt projection");
+ }
+ } else {
+ alert("Please enter all fields");
+ }
+ break;
+
+ case "Saving":
+ if (
+ validateSavingProjection(
+ selectedAccount,
+ inputInterest,
+ inputAmount,
+ inputMinPay,
+ inputPeriod,
+ )
+ ) {
+ const newSavingRequest: projectionSavingRequest = {
+ financial_account_id: selectedAccount,
+ balance: inputAmount,
+ monthly_deposit: inputMinPay,
+ annual_interest_rate: inputInterest,
+ time_frame: inputPeriod,
+ };
+
+ setSavingRequest(newSavingRequest);
+
+ try {
+ const data = await getSavingProjection(newSavingRequest);
+ if (data) {
+ const graphData: projectedDataResponse = {
+ dateLabel: [],
+ lineInfo: [],
+ };
+ const bestCase: LineInfo = {
+ name: "Best Case (Optimistic)",
+ data: [],
+ };
+ const expectedCase: LineInfo = {
+ name: "Expected Case",
+ data: [],
+ };
+ const worstCase: LineInfo = {
+ name: "Worst Case (Conservative)",
+ data: [],
+ };
+
+ data.map((datapoint) => {
+ graphData.dateLabel.push(datapoint.date);
+ bestCase.data.push(datapoint.accumulative_best_balance);
+ expectedCase.data.push(datapoint.accumulative_expected_balance);
+ worstCase.data.push(datapoint.accumulative_worst_balance);
+ });
+
+ graphData.lineInfo = [bestCase, expectedCase, worstCase];
+ setSavingData(graphData);
+ }
+ } catch {
+ alert("Failed to get saving projection");
+ }
+ } else {
+ alert("Please enter all fields");
+ }
+ break;
+ }
+
+ setIsLoading(false);
+ };
+
+ const handleTypeChange = (typeChange: "Saving" | "Debt") => {
+ setSelectedType(typeChange);
+ setSavingData(undefined);
+ setDebtData(undefined);
+
+ if (typeChange === "Debt") {
+ if (debtRequest) {
+ setSelectedAccount(Number(debtRequest.id));
+ setAmount(debtRequest.remainingAmount.toFixed(2));
+ setInterest(debtRequest.interestRate.toString());
+ setMinPay(debtRequest.minimumPayment.toFixed(2));
+ setNextDueDate(debtRequest.nextDueDate);
+ setPeriod(debtRequest.period.toString());
+ } else {
+ setSelectedDebt(0);
+ setAmount("");
+ setInterest("");
+ setMinPay("");
+ setPeriod("");
+ setNextDueDate("");
+ }
+ } else if (typeChange === "Saving") {
+ if (savingRequest) {
+ setSelectedAccount(savingRequest.financial_account_id);
+ setAmount(savingRequest.balance.toFixed(2));
+ setInterest(savingRequest.annual_interest_rate.toString());
+ setMinPay(savingRequest.monthly_deposit.toFixed(2));
+ setPeriod(savingRequest.time_frame.toString());
+ } else {
+ setSelectedAccount(0);
+ setAmount("");
+ setInterest("");
+ setMinPay("");
+ setPeriod("");
+ }
+ }
+ };
+
+ const handleSelectAccount = (event: React.ChangeEvent) => {
+ const id = Number(event.target.value);
+
+ if (selectedType === "Debt") {
+ setSelectedDebt(id);
+ const target = debts.find((account) => account.id === id)?.balance;
+ setAmount(target ? target.toFixed(2) : "0");
+ } else if (selectedType === "Saving") {
+ setSelectedAccount(id);
+ const target = accounts.find((account) => account.id === id)?.balance;
+ setAmount(target ? target.toFixed(2) : "0");
+ }
+ };
+
+ useEffect(() => {
+ if (selectedType === "Saving") {
+ getSaving().then((userAccounts) => {
+ if (userAccounts) setAccounts(userAccounts);
+ });
+ }
+ }, [session, selectedType]);
+
+ useEffect(() => {
+ if (selectedType === "Debt") {
+ getDebt().then((userDebts) => {
+ if (userDebts) setDebts(userDebts);
+ });
+ }
+ }, [session, selectedType]);
+
+ const typeButtons = [
+ { key: "Saving", label: "Savings Projection" },
+ { key: "Debt", label: "Debt Payoff Projection" },
+ ];
+
+ const glowLeft = (
+
+ );
+
+ const glowRight = (
+
+ );
+
+ return (
+
+ {glowLeft}
+ {glowRight}
+
+
+
Financial Projections
+
+ Plan your financial future with savings growth and debt payoff
+ simulations
+
+
+ {/* Type Selector */}
+
+ {typeButtons.map(({ key, label }) => (
+
+ ))}
+
+
+ {/* Main Card */}
+
+
+ {selectedType === "Saving"
+ ? "Savings Account Details"
+ : "Debt Details"}
+
+
+ {/* Account Selector */}
+
+
+
+ {selectedType === "Saving" ? (
+
+ ) : (
+
+ )}
+
+
+
+ {/* Input Grid */}
+
+
+
+
+
+ $
+
+ handleCurrencyChange(e, setAmount)}
+ onBlur={(e) => handleCurrencyBlur(e, amount, setAmount)}
+ className="w-full pl-8 pr-4 py-2 bg-black/50 border border-green-500/30 rounded-lg text-white focus:outline-none focus:border-green-500"
+ placeholder="0.00"
+ />
+
+
+
+
+
+
+
+ $
+
+ handleCurrencyChange(e, setMinPay)}
+ onBlur={(e) => handleCurrencyBlur(e, minPay, setMinPay)}
+ className="w-full pl-8 pr-4 py-2 bg-black/50 border border-green-500/30 rounded-lg text-white focus:outline-none focus:border-green-500"
+ placeholder="0.00"
+ />
+
+
+
+
+
+ handleNumberChange(e, setInterest, 0, 100)}
+ className="w-full px-4 py-2 bg-black/50 border border-green-500/30 rounded-lg text-white focus:outline-none focus:border-green-500"
+ placeholder="0.00"
+ />
+
+
+
+
+ handleNumberChange(e, setPeriod, 0, 100)}
+ className="w-full px-4 py-2 bg-black/50 border border-green-500/30 rounded-lg text-white focus:outline-none focus:border-green-500"
+ placeholder={selectedType === "Debt" ? "30" : "5"}
+ />
+
+
+ {selectedType === "Debt" && (
+
+
+ setNextDueDate(e.target.value)}
+ className="w-full px-4 py-2 bg-black/50 border border-green-500/30 rounded-lg text-white focus:outline-none focus:border-green-500"
+ />
+
+ )}
+
+
+ {/* Calculate Button */}
+
+
+
+
+
+ {/* Results Section */}
+
+
+ {selectedType === "Saving"
+ ? "Savings Growth Projection"
+ : "Debt Payoff Schedule"}
+
+
+ {selectedType === "Saving" ? (
+ savingData ? (
+ <>
+
+ >
+ ) : (
+
}
+ />
+ )
+ ) : null}
+
+ {selectedType === "Debt" ? (
+ debtData ? (
+ <>
+
+
+
+
+
Total Amount Paid
+
+ ${totalPay.toFixed(2)}
+
+
+
+
+ Total Interest Paid
+
+
+ ${totalInterest.toFixed(2)}
+
+
+
+
+ >
+ ) : (
+
}
+ />
+ )
+ ) : null}
+
+
+
+ );
+}
+
+export default ProjectionPage;
diff --git a/app/client/src/pages/SignUpPage.tsx b/app/client/src/pages/SignUpPage.tsx
new file mode 100644
index 0000000..5c86fc3
--- /dev/null
+++ b/app/client/src/pages/SignUpPage.tsx
@@ -0,0 +1,234 @@
+import { useState, type FormEvent } from "react";
+import { Link } from "react-router-dom";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ MAX_AGE,
+ MIN_AGE,
+ type AuthUser,
+ type RequestAuth,
+} from "../types/authTypes";
+
+type SignUpPageProps = {
+ onSignup: (token: string, fallbackUser: Partial) => void;
+ requestAuth: RequestAuth;
+};
+
+function SignUpPage({ onSignup, requestAuth }: SignUpPageProps) {
+ const [username, setUsername] = useState("");
+ const [firstName, setFirstName] = useState("");
+ const [lastName, setLastName] = useState("");
+ const [age, setAge] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [errorMessage, setErrorMessage] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ async function handleSubmit(event: FormEvent) {
+ event.preventDefault();
+ setErrorMessage("");
+
+ const cleanedUsername = username.trim();
+ const cleanedFirstName = firstName.trim();
+ const cleanedLastName = lastName.trim();
+ const parsedAge = Number(age);
+ const normalizedEmail = email.trim().toLowerCase();
+
+ if (password !== confirmPassword) {
+ setErrorMessage("Passwords do not match.");
+ return;
+ }
+
+ if (
+ !Number.isInteger(parsedAge) ||
+ parsedAge < MIN_AGE ||
+ parsedAge > MAX_AGE
+ ) {
+ setErrorMessage(`Age must be between ${MIN_AGE} and ${MAX_AGE}.`);
+ return;
+ }
+
+ setIsSubmitting(true);
+
+ const signupResult = await requestAuth("/api/signup", {
+ username: cleanedUsername,
+ first_name: cleanedFirstName,
+ last_name: cleanedLastName,
+ age: parsedAge,
+ email: normalizedEmail,
+ password,
+ });
+
+ if (!signupResult.ok) {
+ setIsSubmitting(false);
+ setErrorMessage(signupResult.error ?? "Signup failed.");
+ return;
+ }
+
+ const loginResult = await requestAuth("/api/login", {
+ email: normalizedEmail,
+ password,
+ });
+
+ setIsSubmitting(false);
+
+ if (!loginResult.ok || !loginResult.token) {
+ setErrorMessage(
+ loginResult.error ?? "Account created, but auto-login failed.",
+ );
+ return;
+ }
+
+ onSignup(loginResult.token, {
+ email: normalizedEmail,
+ first_name: cleanedFirstName,
+ last_name: cleanedLastName,
+ name: `${cleanedFirstName} ${cleanedLastName}`.trim(),
+ age: parsedAge,
+ });
+ }
+
+ return (
+
+
+
+
+ Finus
+
+ Create account
+
+ Set up your account in less than a minute.
+
+
+
+
+
+
+
+ Already have an account? Log in
+
+
+
+
+ );
+}
+
+export default SignUpPage;
diff --git a/app/client/src/types/AccountType.ts b/app/client/src/types/AccountType.ts
new file mode 100644
index 0000000..341d9b1
--- /dev/null
+++ b/app/client/src/types/AccountType.ts
@@ -0,0 +1,15 @@
+//Coppied from Finance-Account type from server
+export interface Account {
+ id: number;
+ name: string;
+ type: string;
+ balance: number;
+ value: number;
+ subtype?: string;
+ last_updated: string;
+}
+
+export interface MinimizedAccount {
+ id: number;
+ name: string;
+}
diff --git a/app/client/src/types/AggregatedSnapshot.ts b/app/client/src/types/AggregatedSnapshot.ts
new file mode 100644
index 0000000..65d1676
--- /dev/null
+++ b/app/client/src/types/AggregatedSnapshot.ts
@@ -0,0 +1,7 @@
+export type SnapshotData = {
+ totalBalance: number;
+ currentIncome: number;
+ averageExpenses: number;
+ currentDebt: number;
+ totalSavings: number;
+};
diff --git a/app/client/src/types/BudgetWithExpenditure.ts b/app/client/src/types/BudgetWithExpenditure.ts
new file mode 100644
index 0000000..d77d6b6
--- /dev/null
+++ b/app/client/src/types/BudgetWithExpenditure.ts
@@ -0,0 +1,6 @@
+export type BudgetWithExpenditure = {
+ id: string;
+ category: string;
+ budgetAmount: number;
+ actualAmount: number;
+};
diff --git a/app/client/src/types/Debt.ts b/app/client/src/types/Debt.ts
new file mode 100644
index 0000000..6f1a3c5
--- /dev/null
+++ b/app/client/src/types/Debt.ts
@@ -0,0 +1,16 @@
+export type Debt = {
+ id: string;
+ name: string;
+ category: string;
+ remainingAmount: number;
+ minimumPayment: number;
+ interestRate?: number;
+ nextDueDate: string; // format: YYYY-MM-DD
+ period: number; // number of days between each payment installment
+};
+
+export type DebtStage = {
+ paidAmount: number;
+ remainingDebt: number;
+ installmentDate: string; // YYYY-MM-DD
+};
diff --git a/app/client/src/types/IncomeType.ts b/app/client/src/types/IncomeType.ts
new file mode 100644
index 0000000..d96fa96
--- /dev/null
+++ b/app/client/src/types/IncomeType.ts
@@ -0,0 +1,6 @@
+export interface Income {
+ id: number;
+ name: string;
+ income: number;
+ description: string;
+}
diff --git a/app/client/src/types/Market.ts b/app/client/src/types/Market.ts
new file mode 100644
index 0000000..ace9cf1
--- /dev/null
+++ b/app/client/src/types/Market.ts
@@ -0,0 +1,33 @@
+export type MarketInstrumentType = "stock" | "forex";
+
+export type MarketInstrument = {
+ symbol: string;
+ displaySymbol: string;
+ name: string;
+ type: MarketInstrumentType;
+ currency: string;
+ exchange?: string;
+ price: number | null;
+ change: number | null;
+ changePercent: number | null;
+ timestamp: number | null;
+};
+
+export type MarketHistoryPoint = {
+ timestamp: number;
+ price: number;
+};
+
+export type MarketHistoryPeriod =
+ | "1d"
+ | "5d"
+ | "1mo"
+ | "3mo"
+ | "6mo"
+ | "1y"
+ | "5y";
+
+export type PinnedMarketInstrument = Pick<
+ MarketInstrument,
+ "symbol" | "displaySymbol" | "name" | "type" | "currency" | "exchange"
+>;
diff --git a/app/client/src/types/Transaction.ts b/app/client/src/types/Transaction.ts
new file mode 100644
index 0000000..528e9fd
--- /dev/null
+++ b/app/client/src/types/Transaction.ts
@@ -0,0 +1,11 @@
+export type Transaction = {
+ id: number;
+ financialAccount_id: number;
+ amount: number;
+ category: string;
+ date: string; // ISO format date string - used to be Date
+ sender: string;
+ recipient: string;
+ description?: string;
+ account_name?: string;
+};
diff --git a/app/client/src/types/authTypes.ts b/app/client/src/types/authTypes.ts
new file mode 100644
index 0000000..76d1c72
--- /dev/null
+++ b/app/client/src/types/authTypes.ts
@@ -0,0 +1,28 @@
+export type AuthUser = {
+ id?: number;
+ email: string;
+ name?: string;
+ first_name?: string;
+ last_name?: string;
+ age?: number;
+};
+
+export type AuthSession = {
+ token: string;
+ user: AuthUser;
+};
+
+export type AuthApiResponse = {
+ ok: boolean;
+ token?: string;
+ error?: string;
+ message?: string;
+};
+
+export type RequestAuth = (
+ path: string,
+ payload: Record,
+) => Promise;
+
+export const MIN_AGE = 1;
+export const MAX_AGE = 120;
diff --git a/app/client/src/types/goals.ts b/app/client/src/types/goals.ts
new file mode 100644
index 0000000..5e25225
--- /dev/null
+++ b/app/client/src/types/goals.ts
@@ -0,0 +1,31 @@
+export type GoalType = "reduce_spending" | "save"; // just two for simplicity, but can expand
+
+export interface BaseGoal {
+ id: string;
+ name: string;
+ type: GoalType;
+ category: string;
+ target: number;
+ period: "m" | "w"; //"na" for savings
+ progress_percentage: number;
+ current_amount: number; //this is the amount that has been saved or spent depending on goal type. This is aggregated on the server, and is not actually stored in DB
+}
+
+export interface SpendingLimitGoal extends BaseGoal {
+ type: "reduce_spending";
+ period: "m" | "w";
+}
+
+export interface SavingsTargetGoal extends BaseGoal {
+ type: "save";
+}
+
+export type Goal = SpendingLimitGoal | SavingsTargetGoal;
+
+export interface GoalsPanelProps {
+ goals: Goal[];
+ onAddGoal: () => void;
+ onEditGoal: (goalId: string, updates: Partial) => void;
+ onDeleteGoal: (goalId: string) => void;
+ maxGoals?: number;
+}
diff --git a/app/client/src/types/requestTypes.ts b/app/client/src/types/requestTypes.ts
new file mode 100644
index 0000000..01e3792
--- /dev/null
+++ b/app/client/src/types/requestTypes.ts
@@ -0,0 +1,17 @@
+export interface projectionDebtRequest {
+ id: string;
+ category: string;
+ remainingAmount: number;
+ minimumPayment: number;
+ interestRate: number;
+ nextDueDate: string; // format: YYYY-MM-DD
+ period: number; // number of days between each payment installment
+}
+
+export interface projectionSavingRequest {
+ financial_account_id: number;
+ balance: number;
+ monthly_deposit: number;
+ annual_interest_rate: number;
+ time_frame: number;
+}
diff --git a/app/client/src/types/responseTypes.ts b/app/client/src/types/responseTypes.ts
new file mode 100644
index 0000000..6b4308f
--- /dev/null
+++ b/app/client/src/types/responseTypes.ts
@@ -0,0 +1,38 @@
+//
+export interface updateResponse {
+ lastUpdated?: Date;
+ id?: number;
+}
+
+export interface projectedDataResponse {
+ lineInfo: LineInfo[];
+ dateLabel: string[];
+}
+
+export interface LineInfo {
+ data: number[];
+ name: string;
+}
+
+export type DebtPayoffResponse = {
+ id: string;
+ category: string;
+ minimumPayment: number;
+ interestRate: number;
+ debtStages: AdvancedDebtStage[];
+};
+
+export type AdvancedDebtStage = {
+ id: number;
+ principalAmount: number;
+ interestAmount: number;
+ remainingDebt: number;
+ installmentDate: string; // YYYY-MM-DD
+};
+
+export interface savingProjectionResponseData {
+ accumulative_best_balance: number;
+ accumulative_expected_balance: number;
+ accumulative_worst_balance: number;
+ date: string;
+}
diff --git a/app/client/src/utils/ConvertTransaction.ts b/app/client/src/utils/ConvertTransaction.ts
new file mode 100644
index 0000000..f2f2375
--- /dev/null
+++ b/app/client/src/utils/ConvertTransaction.ts
@@ -0,0 +1,24 @@
+//function to convert a normalized row into a transaction draft
+
+import type { NormalizedRow } from "./NormalizeRow";
+import { validateRow } from "./ValidateRow";
+
+export interface TransactionDraft {
+ date: string | null;
+ description: string | null;
+ amount: number | null;
+ sender?: string | null;
+ recipient?: string | null;
+ category?: string | null;
+ errors: string[];
+}
+
+export function convertDraft(row: NormalizedRow): TransactionDraft {
+ // convert normalized row to transaction draft and validate it
+ const errors = validateRow(row);
+
+ return {
+ ...row,
+ errors,
+ };
+}
diff --git a/app/client/src/utils/NormalizeRow.ts b/app/client/src/utils/NormalizeRow.ts
new file mode 100644
index 0000000..f320a8f
--- /dev/null
+++ b/app/client/src/utils/NormalizeRow.ts
@@ -0,0 +1,73 @@
+// Utility function to normalize row data in a CSV file
+
+export interface NormalizedRow {
+ date: string | null;
+ description: string | null;
+ amount: number | null;
+ sender?: string | null;
+ recipient?: string | null;
+ category?: string | null;
+}
+
+export function normalizeRow(raw: unknown): NormalizedRow {
+ //row is treated as an object with unknown values
+ const record = raw as Record;
+
+ const clean = (value: unknown): string | null => {
+ // helper to trim and clean string-like values
+ if (value === undefined || value === null) return null;
+ return String(value).trim() || null;
+ };
+ const rawDate = clean(record.date); // normalize fields using helpers
+ const date = normalizeDate(rawDate);
+ const rawAmount = clean(record.amount);
+ const amount = normalizeAmount(rawAmount);
+
+ return {
+ date,
+ description: clean(record.description),
+ amount,
+ sender: clean(record.sender),
+ recipient: clean(record.recipient),
+ category: clean(record.category),
+ };
+}
+
+// helper to normalize date formats into YYYY-MM-DD
+function normalizeDate(input: string | null): string | null {
+ if (!input) return null;
+
+ // Replace separators with hyphens
+ const data = input.replace(/[.\s]/g, "-").replace(/\//g, "-");
+
+ // Already normalized YYYY-MM-DD
+ if (/^\d{4}-\d{2}-\d{2}$/.test(data)) return data;
+
+ // Match M-D-YYYY or MM-DD-YYYY
+ const mdY = /^(\d{1,2})-(\d{1,2})-(\d{4})$/;
+ const match = data.match(mdY);
+
+ if (match) {
+ const [, mRaw, dRaw, y] = match;
+
+ const m = mRaw.padStart(2, "0");
+ const d = dRaw.padStart(2, "0");
+
+ return `${y}-${m}-${d}`;
+ }
+
+ return null;
+}
+// helper to normalize amount fields
+
+// Stryker disable all
+function normalizeAmount(input: string | null): number | null {
+ if (!input) return null;
+ let amount = input.replace(/,/g, "").trim(); //trim
+ if (/^\(.+\)$/.test(amount)) {
+ //if amount is in parentheses, treat as negative
+ amount = "-" + amount.slice(1, -1);
+ }
+ const num = Number(amount);
+ return isNaN(num) ? null : num;
+}
diff --git a/app/client/src/utils/ParseCsv.ts b/app/client/src/utils/ParseCsv.ts
new file mode 100644
index 0000000..2e5a6b9
--- /dev/null
+++ b/app/client/src/utils/ParseCsv.ts
@@ -0,0 +1,38 @@
+//this utility parses csv files using papaparse
+// Parses CSV, normalizes rows, validates them, and returns TransactionDraft[]
+
+import Papa, { type ParseResult } from "papaparse";
+import { normalizeRow } from "./NormalizeRow";
+import { convertDraft, type TransactionDraft } from "./ConvertTransaction";
+
+export function parseCsvFile(file: File): Promise {
+ //uses promises to handle async parsing
+ return new Promise((resolve, reject) => {
+ Papa.parse(file, {
+ header: true,
+ skipEmptyLines: true,
+ dynamicTyping: true,
+ beforeFirstChunk: (chunk) => chunk.replace(/^\uFEFF/, ""),
+ transformHeader: (header) => header.trim().toLowerCase(),
+
+ complete: (result: ParseResult) => {
+ // handle parsing errors
+ const { data, errors } = result;
+ if (errors.length > 0) {
+ reject(errors[0].message);
+ return;
+ }
+
+ const drafts: TransactionDraft[] = data.map((raw: unknown) => {
+ // normalize and convert each row to a transaction draft
+ const normalized = normalizeRow(raw);
+ return convertDraft(normalized);
+ });
+
+ resolve(drafts); // resolve with array of transaction drafts, each containing validation errors if any
+ },
+
+ error: (err: Error) => reject(err.message), // handle file read errors
+ });
+ });
+}
diff --git a/app/client/src/utils/ValidateFile.ts b/app/client/src/utils/ValidateFile.ts
new file mode 100644
index 0000000..20bd1dd
--- /dev/null
+++ b/app/client/src/utils/ValidateFile.ts
@@ -0,0 +1,25 @@
+// Utility function to validate the entire CSV file
+
+import type { TransactionDraft } from "./ConvertTransaction";
+
+export interface FileValidationResult {
+ // summary of validation results for the whole file
+ validCount: number;
+ invalidCount: number;
+ tooManyInvalid: boolean;
+}
+
+export function validateFile(rows: TransactionDraft[]): FileValidationResult {
+ //returns a summary of validation results for the whole file
+ const validCount = rows.filter((r) => r.errors.length === 0).length;
+ const invalidCount = rows.length - validCount;
+ // Stryker disable all
+ const tooManyInvalid =
+ rows.length === 0 ? false : invalidCount / rows.length > 0.2; //if more than 20% of the rows are invalid we send a warning
+
+ return {
+ validCount,
+ invalidCount,
+ tooManyInvalid,
+ };
+}
diff --git a/app/client/src/utils/ValidateForms.ts b/app/client/src/utils/ValidateForms.ts
new file mode 100644
index 0000000..83d41a2
--- /dev/null
+++ b/app/client/src/utils/ValidateForms.ts
@@ -0,0 +1,64 @@
+import { accountCategory } from "@/enum/AccountCategory";
+import { transactionCategory } from "@/enum/TransactionCategory";
+
+export function validateAccountForm(
+ name: string,
+ accountType: string,
+ balance: number,
+ csvFile?: File,
+ subtype?: string,
+ interest?: number,
+) {
+ let result = false;
+
+ if (name) {
+ //Checks if there a cvs file uploaded first
+ //Add cvs checker here=
+ if (csvFile) {
+ result = true;
+
+ //Checks if all the manditory fields are valid and optional ones too if they exist
+ } else if (
+ Object.values(accountCategory).includes(accountType) &&
+ balance >= 0 &&
+ (subtype == undefined || subtype) &&
+ (interest == undefined || (interest >= 0 && interest < 100))
+ ) {
+ result = true;
+ }
+ console.log(accountType);
+ console.log(accountCategory);
+ }
+ console.log(result);
+ return result;
+}
+
+export function validateTransactionForm(
+ account_id: number,
+ transactionType: string,
+ amount: number,
+ date: string,
+ csvFile?: File,
+) {
+ let result = false;
+
+ //Determine if account was selected
+ if (account_id) {
+ //Add csv checker here to validate
+ if (csvFile) {
+ result = true;
+ } else if (
+ Object.values(transactionCategory).includes(transactionType) &&
+ amount > 0 &&
+ date
+ ) {
+ result = true;
+ }
+ }
+
+ return result;
+}
+
+export function validateIncomeForm(name: string, income: number) {
+ return name.trim().length > 0 && income > 0;
+}
diff --git a/app/client/src/utils/ValidateProjectionRequest.ts b/app/client/src/utils/ValidateProjectionRequest.ts
new file mode 100644
index 0000000..0d6349c
--- /dev/null
+++ b/app/client/src/utils/ValidateProjectionRequest.ts
@@ -0,0 +1,35 @@
+export function validateDebtProjection(
+ id: number,
+ remainingAmount: number,
+ minimumPayment: number,
+ interestRate: number,
+ nextDueDate: string,
+ period: number,
+) {
+ return (
+ id &&
+ remainingAmount >= 0 &&
+ minimumPayment > 0 &&
+ interestRate >= 0 &&
+ interestRate <= 100 &&
+ nextDueDate &&
+ period
+ );
+}
+
+export function validateSavingProjection(
+ id: number,
+ interestRate: number,
+ balence: number,
+ monthly_deposit: number,
+ time_frame: number,
+) {
+ return (
+ id &&
+ interestRate >= 0 &&
+ interestRate <= 100 &&
+ balence >= 0 &&
+ monthly_deposit >= 0 &&
+ time_frame > 0
+ );
+}
diff --git a/app/client/src/utils/ValidateRow.ts b/app/client/src/utils/ValidateRow.ts
new file mode 100644
index 0000000..19620bd
--- /dev/null
+++ b/app/client/src/utils/ValidateRow.ts
@@ -0,0 +1,24 @@
+// Utility function to validate a normalized CSV row
+
+import type { NormalizedRow } from "./NormalizeRow";
+
+export function validateRow(row: NormalizedRow): string[] {
+ const errors: string[] = [];
+
+ // validate date
+ if (!row.date) {
+ errors.push("Invalid or missing date");
+ }
+
+ // validate description
+ if (!row.description) {
+ errors.push("Missing description");
+ }
+
+ // validate amount
+ if (row.amount === null) {
+ errors.push("Invalid or missing amount");
+ }
+
+ return errors;
+}
diff --git a/app/client/src/utils/constants.ts b/app/client/src/utils/constants.ts
new file mode 100644
index 0000000..34b4687
--- /dev/null
+++ b/app/client/src/utils/constants.ts
@@ -0,0 +1,3 @@
+export const BASE_URL = "http://52.15.117.1:3000";
+export const SESSION_STORAGE_KEY = "finus-session";
+export const SIDEBAR_WIDTH = 260;
diff --git a/app/client/src/utils/fakeData.ts b/app/client/src/utils/fakeData.ts
new file mode 100644
index 0000000..3709ecd
--- /dev/null
+++ b/app/client/src/utils/fakeData.ts
@@ -0,0 +1,64 @@
+import type { BudgetWithExpenditure } from "@/types/BudgetWithExpenditure";
+
+export const budgetsWithExpenditure: BudgetWithExpenditure[] = [
+ {
+ id: "1",
+ category: "Rent",
+ budgetAmount: 1200,
+ actualAmount: 1200,
+ },
+ {
+ id: "2",
+ category: "Groceries",
+ budgetAmount: 400,
+ actualAmount: 465,
+ },
+ {
+ id: "3",
+ category: "Transportation",
+ budgetAmount: 150,
+ actualAmount: 132,
+ },
+ {
+ id: "4",
+ category: "Utilities",
+ budgetAmount: 200,
+ actualAmount: 187,
+ },
+ {
+ id: "5",
+ category: "Internet & Phone",
+ budgetAmount: 120,
+ actualAmount: 118,
+ },
+ {
+ id: "6",
+ category: "Dining Out",
+ budgetAmount: 180,
+ actualAmount: 245,
+ },
+ {
+ id: "7",
+ category: "Entertainment",
+ budgetAmount: 100,
+ actualAmount: 92,
+ },
+ {
+ id: "8",
+ category: "Health & Fitness",
+ budgetAmount: 90,
+ actualAmount: 110,
+ },
+ {
+ id: "9",
+ category: "Shopping",
+ budgetAmount: 150,
+ actualAmount: 210,
+ },
+ {
+ id: "10",
+ category: "Savings",
+ budgetAmount: 300,
+ actualAmount: 300,
+ },
+];
diff --git a/app/client/src/utils/handleInput.ts b/app/client/src/utils/handleInput.ts
new file mode 100644
index 0000000..4b79e95
--- /dev/null
+++ b/app/client/src/utils/handleInput.ts
@@ -0,0 +1,93 @@
+import type React from "react";
+
+//Handles state when currency is changed
+export const handleCurrencyChange = (
+ event: React.ChangeEvent,
+ setCurrency: React.Dispatch>,
+): void => {
+ let input = event.target.value;
+ const pattern = /^-?\d*\.?\d{0,2}$/;
+
+ //Determine if the input follows the format/pattern
+ if (pattern.test(input)) {
+ input = input.replace(/^0+(?=\d)/, "");
+ setCurrency(input);
+ }
+};
+
+export const handleCurrencyBlur = (
+ event: React.ChangeEvent,
+ currency: string,
+ setCurrency: React.Dispatch>,
+): void => {
+ if (event.target.value !== "") {
+ setCurrency(parseFloat(currency).toFixed(2));
+ }
+};
+
+//Handles on change of whole numbers
+export const handleNumberChange = (
+ event: React.ChangeEvent,
+ setNumber: React.Dispatch>,
+ min: number,
+ max: number,
+) => {
+ let input = event.target.value;
+ let changeNumber;
+ const pattern = /^\d*\.?\d{0,2}$/;
+
+ console.log(input);
+ console.log(pattern.test(input));
+
+ //Determine if the input follows the format/pattern
+ if (pattern.test(input) || input === "") {
+ input = input.replace(/^0+(?=\d)/, "");
+ changeNumber = Number(input);
+
+ if (changeNumber > max) {
+ changeNumber = max;
+ }
+
+ if (changeNumber < min) {
+ changeNumber = min;
+ }
+ console.log(changeNumber);
+ setNumber(changeNumber.toString());
+ }
+};
+
+export const handleListCurrencyChange = (
+ event: React.ChangeEvent,
+ handleChange: (
+ accountId: number,
+ field: string,
+ value: string | number,
+ ) => void,
+ id: number,
+ field: string,
+) => {
+ let input = event.target.value;
+ const pattern = /^-?\d*\.?\d{0,2}$/;
+
+ console.log(pattern.test(input));
+
+ //Determine if the input follows the format/pattern
+ if (pattern.test(input)) {
+ input = input.replace(/^0+(?=\d)/, "");
+ handleChange(id, field, input);
+ }
+};
+export const handleListCurrencyBlur = (
+ event: React.ChangeEvent,
+ handleChange: (
+ accountId: number,
+ field: string,
+ value: string | number,
+ ) => void,
+ id: number,
+ field: string,
+) => {
+ if (event.target.value !== "") {
+ handleChange(id, field, parseFloat(event.target.value).toFixed(2));
+ }
+};
diff --git a/app/client/src/utils/market.ts b/app/client/src/utils/market.ts
new file mode 100644
index 0000000..5393cf6
--- /dev/null
+++ b/app/client/src/utils/market.ts
@@ -0,0 +1,129 @@
+import type {
+ MarketHistoryPeriod,
+ MarketHistoryPoint,
+ MarketInstrument,
+} from "@/types/Market";
+
+const MARKET_PERIOD_OPTIONS: Array<{
+ value: MarketHistoryPeriod;
+ label: string;
+}> = [
+ { value: "1d", label: "1D" },
+ { value: "5d", label: "5D" },
+ { value: "1mo", label: "1M" },
+ { value: "3mo", label: "3M" },
+ { value: "6mo", label: "6M" },
+ { value: "1y", label: "1Y" },
+ { value: "5y", label: "5Y" },
+];
+
+function getIntervalForPeriod(
+ period: MarketHistoryPeriod,
+): "5m" | "15m" | "1d" | "1wk" | "1mo" {
+ if (period === "1d") {
+ return "5m";
+ }
+
+ if (period === "5d") {
+ return "15m";
+ }
+
+ if (period === "1y") {
+ return "1wk";
+ }
+
+ if (period === "5y") {
+ return "1mo";
+ }
+
+ return "1d";
+}
+
+function formatMarketPrice(value: number | null, currency = "USD"): string {
+ if (value === null) {
+ return "N/A";
+ }
+
+ if (currency === "JPY") {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency,
+ maximumFractionDigits: 2,
+ }).format(value);
+ }
+
+ if (currency.length === 3) {
+ return new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency,
+ maximumFractionDigits: value >= 100 ? 2 : 4,
+ }).format(value);
+ }
+
+ return value.toFixed(4);
+}
+
+function formatSignedValue(value: number | null, digits = 2): string {
+ if (value === null) {
+ return "N/A";
+ }
+
+ const sign = value >= 0 ? "+" : "";
+ return `${sign}${value.toFixed(digits)}`;
+}
+
+function formatSignedPercent(value: number | null): string {
+ if (value === null) {
+ return "N/A";
+ }
+
+ const sign = value >= 0 ? "+" : "";
+ return `${sign}${value.toFixed(2)}%`;
+}
+
+function formatMarketTimestamp(timestamp: number | null): string {
+ if (!timestamp) {
+ return "Unavailable";
+ }
+
+ return new Intl.DateTimeFormat("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ }).format(new Date(timestamp * 1000));
+}
+
+function isPositiveChange(value: number | null): boolean {
+ return (value ?? 0) >= 0;
+}
+
+function toChartSeries(points: MarketHistoryPoint[]) {
+ return points.map((point) => ({
+ timestamp: point.timestamp * 1000,
+ price: point.price,
+ }));
+}
+
+function mergeInstrumentQuote(
+ instrument: MarketInstrument,
+ quote: Pick<
+ MarketInstrument,
+ "price" | "change" | "changePercent" | "timestamp"
+ >,
+): MarketInstrument {
+ return {
+ ...instrument,
+ ...quote,
+ };
+}
+
+export {
+ MARKET_PERIOD_OPTIONS,
+ formatMarketPrice,
+ formatMarketTimestamp,
+ formatSignedPercent,
+ formatSignedValue,
+ getIntervalForPeriod,
+ isPositiveChange,
+ mergeInstrumentQuote,
+ toChartSeries,
+};
diff --git a/app/client/src/utils/marketStorage.ts b/app/client/src/utils/marketStorage.ts
new file mode 100644
index 0000000..ecd94f5
--- /dev/null
+++ b/app/client/src/utils/marketStorage.ts
@@ -0,0 +1,76 @@
+import type { PinnedMarketInstrument } from "@/types/Market";
+import { loadSession } from "@/utils/storage";
+
+const PINNED_MARKETS_STORAGE_KEY_PREFIX = "finus-pinned-markets";
+const LEGACY_PINNED_MARKETS_STORAGE_KEY = PINNED_MARKETS_STORAGE_KEY_PREFIX;
+const PINNED_MARKETS_CLEARED_EVENT = "finus:pinned-markets-cleared";
+
+function resolvePinnedMarketsStorageKey(): string | null {
+ const session = loadSession();
+ const userId = session?.user.id;
+ const userEmail = session?.user.email?.trim().toLowerCase();
+
+ if (typeof userId === "number" && Number.isFinite(userId)) {
+ return `${PINNED_MARKETS_STORAGE_KEY_PREFIX}:id:${userId}`;
+ }
+
+ if (userEmail) {
+ return `${PINNED_MARKETS_STORAGE_KEY_PREFIX}:email:${userEmail}`;
+ }
+
+ return null;
+}
+
+function clearLegacyPinnedMarkets() {
+ localStorage.removeItem(LEGACY_PINNED_MARKETS_STORAGE_KEY);
+}
+
+function loadPinnedMarkets(): PinnedMarketInstrument[] {
+ clearLegacyPinnedMarkets();
+
+ const storageKey = resolvePinnedMarketsStorageKey();
+ if (!storageKey) {
+ return [];
+ }
+
+ const raw = localStorage.getItem(storageKey);
+ if (!raw) {
+ return [];
+ }
+
+ try {
+ const parsed = JSON.parse(raw) as PinnedMarketInstrument[];
+ if (!Array.isArray(parsed)) {
+ return [];
+ }
+
+ return parsed.filter(
+ (item) =>
+ typeof item?.symbol === "string" &&
+ typeof item?.displaySymbol === "string" &&
+ typeof item?.name === "string" &&
+ (item?.type === "stock" || item?.type === "forex") &&
+ typeof item?.currency === "string",
+ );
+ } catch {
+ return [];
+ }
+}
+
+function savePinnedMarkets(items: PinnedMarketInstrument[]) {
+ clearLegacyPinnedMarkets();
+
+ const storageKey = resolvePinnedMarketsStorageKey();
+ if (!storageKey) {
+ return;
+ }
+
+ localStorage.setItem(storageKey, JSON.stringify(items));
+}
+
+export {
+ loadPinnedMarkets,
+ PINNED_MARKETS_CLEARED_EVENT,
+ savePinnedMarkets,
+ PINNED_MARKETS_STORAGE_KEY_PREFIX,
+};
diff --git a/app/client/src/utils/storage.ts b/app/client/src/utils/storage.ts
new file mode 100644
index 0000000..9194a10
--- /dev/null
+++ b/app/client/src/utils/storage.ts
@@ -0,0 +1,30 @@
+import type { AuthSession } from "@/types/authTypes";
+import { SESSION_STORAGE_KEY } from "./constants";
+import { isValidAuthUser } from "./token";
+function loadSession(): AuthSession | null {
+ const raw = localStorage.getItem(SESSION_STORAGE_KEY);
+ if (!raw) {
+ return null;
+ }
+
+ try {
+ const parsed = JSON.parse(raw) as AuthSession;
+ if (!parsed.token || !isValidAuthUser(parsed.user)) {
+ return null;
+ }
+
+ return parsed;
+ } catch {
+ return null;
+ }
+}
+
+function saveSession(session: AuthSession) {
+ localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session));
+}
+
+function clearSession() {
+ localStorage.removeItem(SESSION_STORAGE_KEY);
+}
+
+export { loadSession, saveSession, clearSession };
diff --git a/app/client/src/utils/token.ts b/app/client/src/utils/token.ts
new file mode 100644
index 0000000..4ad4a4b
--- /dev/null
+++ b/app/client/src/utils/token.ts
@@ -0,0 +1,58 @@
+import type { AuthUser } from "@/types/authTypes";
+
+function isValidAuthUser(value: unknown): value is AuthUser {
+ if (!value || typeof value !== "object") {
+ return false;
+ }
+
+ return typeof (value as { email?: unknown }).email === "string";
+}
+
+function decodeTokenClaims(token: string): Record | null {
+ const parts = token.split(".");
+ if (parts.length < 2) {
+ return null;
+ }
+
+ try {
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
+ const json = atob(base64);
+ const parsed = JSON.parse(json) as Record;
+ return parsed;
+ } catch {
+ return null;
+ }
+}
+
+function resolveUserFromToken(
+ token: string,
+ fallbackUser: Partial,
+): AuthUser {
+ const claims = decodeTokenClaims(token);
+
+ const emailFromToken =
+ claims && typeof claims.email === "string" ? claims.email : undefined;
+ const nameFromToken =
+ claims && typeof claims.name === "string" ? claims.name : undefined;
+ const firstNameFromToken =
+ claims && typeof claims.first_name === "string"
+ ? claims.first_name
+ : undefined;
+ const lastNameFromToken =
+ claims && typeof claims.last_name === "string"
+ ? claims.last_name
+ : undefined;
+ const subFromToken =
+ claims && typeof claims.sub === "string" ? Number(claims.sub) : undefined;
+
+ return {
+ id: Number.isFinite(subFromToken) ? subFromToken : fallbackUser.id,
+ email: emailFromToken ?? fallbackUser.email ?? "",
+ name: nameFromToken ?? fallbackUser.name,
+ first_name: firstNameFromToken ?? fallbackUser.first_name,
+ last_name: lastNameFromToken ?? fallbackUser.last_name,
+ age: fallbackUser.age,
+ };
+}
+
+export { isValidAuthUser, resolveUserFromToken };
diff --git a/app/client/src/utils/utils.ts b/app/client/src/utils/utils.ts
new file mode 100644
index 0000000..365058c
--- /dev/null
+++ b/app/client/src/utils/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/app/client/stryker.conf.json b/app/client/stryker.conf.json
new file mode 100644
index 0000000..6d6ef8c
--- /dev/null
+++ b/app/client/stryker.conf.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
+ "testRunner": "vitest",
+ "vitest": {
+ "configFile": "vitest.config.ts"
+ },
+ "mutate": ["src/**/*.ts", "!src/**/*.d.ts"],
+ "reporters": ["html", "clear-text", "progress"],
+ "coverageAnalysis": "off",
+ "timeoutMS": 60000,
+ "ignorePatterns": [
+ "src/api/**/*.ts",
+ "src/utils/constants.ts",
+ "src/utils/fakeData.ts",
+ "src/utils/market.ts",
+ "src/utils/marketStorage.ts",
+ "src/utils/storage.ts",
+ "src/utils/token.ts",
+ "src/enum/**/*.ts",
+ "src/utils/utils.ts",
+ "src/components",
+ "src/utils/ValidateForms.ts"
+ ]
+}
diff --git a/app/client/tailwind.config.ts b/app/client/tailwind.config.ts
new file mode 100644
index 0000000..c8ac769
--- /dev/null
+++ b/app/client/tailwind.config.ts
@@ -0,0 +1,12 @@
+// tailwind.config.ts
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
+
+export default config;
diff --git a/app/client/test/convertTransaction.test.ts b/app/client/test/convertTransaction.test.ts
new file mode 100644
index 0000000..3861af1
--- /dev/null
+++ b/app/client/test/convertTransaction.test.ts
@@ -0,0 +1,67 @@
+import { describe, it, expect } from "vitest";
+import { convertDraft } from "../src/utils/ConvertTransaction";
+import type { NormalizedRow } from "../src/utils/NormalizeRow";
+//tests for convertDraft
+describe("convertDraft", () => {
+ it("returns a valid TransactionDraft when the row is valid", () => {
+ const row: NormalizedRow = {
+ date: "2024-01-01",
+ description: "Test",
+ amount: 100,
+ sender: "Joe",
+ recipient: "Bob",
+ category: "Food",
+ };
+
+ const draft = convertDraft(row);
+
+ expect(draft.errors).toEqual([]);
+ expect(draft.date).toBe("2024-01-01");
+ expect(draft.description).toBe("Test");
+ expect(draft.amount).toBe(100);
+ expect(draft.sender).toBe("Joe");
+ expect(draft.recipient).toBe("Bob");
+ expect(draft.category).toBe("Food");
+ });
+
+ it("includes validation errors for missing fields", () => {
+ const row: NormalizedRow = {
+ date: null,
+ description: null,
+ amount: null,
+ sender: null,
+ recipient: null,
+ category: null,
+ };
+
+ const draft = convertDraft(row);
+
+ expect(draft.errors).toEqual([
+ "Invalid or missing date",
+ "Missing description",
+ "Invalid or missing amount",
+ ]);
+ });
+
+ it("passes through all fields unchanged", () => {
+ const row: NormalizedRow = {
+ date: "2024-02-10",
+ description: "Lunch",
+ amount: 12.5,
+ sender: "Me",
+ recipient: "Cafe",
+ category: "Food",
+ };
+
+ const draft = convertDraft(row);
+
+ expect(draft).toMatchObject({
+ date: "2024-02-10",
+ description: "Lunch",
+ amount: 12.5,
+ sender: "Me",
+ recipient: "Cafe",
+ category: "Food",
+ });
+ });
+});
diff --git a/app/client/test/handleInput.test.ts b/app/client/test/handleInput.test.ts
new file mode 100644
index 0000000..9fe8da4
--- /dev/null
+++ b/app/client/test/handleInput.test.ts
@@ -0,0 +1,203 @@
+import { describe, it, expect, vi } from "vitest";
+import {
+ handleCurrencyChange,
+ handleCurrencyBlur,
+ handleNumberChange,
+} from "../src/utils/handleInput";
+//tests for handleCurrencyChange and handleCurrencyBlur functions
+function mockEvent(value: string) {
+ return {
+ target: { value },
+ } as React.ChangeEvent;
+}
+
+describe("handleCurrencyChange", () => {
+ it("accepts valid currency input", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("12.34");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).toHaveBeenCalledWith("12.34");
+ });
+
+ it("rejects invalid currency input", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("12.345");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).not.toHaveBeenCalled();
+ });
+
+ it("empty string", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).toHaveBeenCalledWith("");
+ });
+
+ it("strips leading zeros", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("00045.20");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).toHaveBeenCalledWith("45.20");
+ });
+
+ it("accepts partial decimal input like '12.' or '12.3'", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("12.");
+
+ handleCurrencyChange(event, setCurrency);
+ expect(setCurrency).toHaveBeenCalledWith("12.");
+
+ const setCurrency2 = vi.fn();
+ const event2 = mockEvent("12.3");
+
+ handleCurrencyChange(event2, setCurrency2);
+ expect(setCurrency2).toHaveBeenCalledWith("12.3");
+ });
+});
+
+describe("handleCurrencyBlur", () => {
+ it("formats to two decimals on blur", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("12.3");
+
+ handleCurrencyBlur(event, "12.3", setCurrency);
+
+ expect(setCurrency).toHaveBeenCalledWith("12.30");
+ });
+
+ it("does nothing when input is empty", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("");
+
+ handleCurrencyBlur(event, "", setCurrency);
+
+ expect(setCurrency).not.toHaveBeenCalled();
+ });
+
+ it("formats integer values to two decimals", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("45");
+
+ handleCurrencyBlur(event, "45", setCurrency);
+
+ expect(setCurrency).toHaveBeenCalledWith("45.00");
+ });
+});
+
+it("accepts whole numbers without decimals", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("45");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).toHaveBeenCalledWith("45");
+});
+
+it("does not accept whitespace-only input", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent(" ");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).not.toHaveBeenCalled();
+});
+
+it("rejects non-empty strings that are not valid currency", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("Stryker was here!");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).not.toHaveBeenCalled();
+});
+
+it("does not strip non-leading zeros", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("10.20");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).toHaveBeenCalledWith("10.20");
+});
+
+it("does not strip zeros inside the number", () => {
+ const setCurrency = vi.fn();
+ const event = mockEvent("101");
+
+ handleCurrencyChange(event, setCurrency);
+
+ expect(setCurrency).toHaveBeenCalledWith("101");
+});
+
+describe("handleNumberChange", () => {
+ it("accepts valid number input", () => {
+ const setNumber = vi.fn();
+ const event = mockEvent("12");
+
+ handleNumberChange(event, setNumber, 0, 100);
+
+ expect(setNumber).toHaveBeenCalledWith("12");
+ });
+
+ it("Doesn't accept decimals", () => {
+ const setNumber = vi.fn();
+ const event = mockEvent("12.");
+
+ handleNumberChange(event, setNumber, 0, 100);
+
+ expect(setNumber).not.toHaveBeenCalledWith();
+ });
+
+ it("Empty string", () => {
+ const setNumber = vi.fn();
+ const event = mockEvent("");
+
+ handleNumberChange(event, setNumber, 0, 100);
+
+ expect(setNumber).toHaveBeenCalledWith("");
+ });
+
+ it("strip leading 0s", () => {
+ const setNumber = vi.fn();
+ const event = mockEvent("0000123");
+
+ handleNumberChange(event, setNumber, 0, 100);
+
+ expect(setNumber).toHaveBeenCalledWith("123");
+ });
+
+ it("rejects letters in input", () => {
+ const setNumber = vi.fn();
+ const event = mockEvent("12A");
+
+ handleNumberChange(event, setNumber, 0, 100);
+
+ expect(setNumber).not.toHaveBeenCalledWith();
+ });
+
+ it("Sets the value to the maximum", () => {
+ const setNumber = vi.fn();
+ const event = mockEvent("120");
+
+ handleNumberChange(event, setNumber, 0, 100);
+
+ expect(setNumber).not.toHaveBeenCalledWith("100");
+ });
+
+ it("Sets value to the minimum", () => {
+ const setNumber = vi.fn();
+ const event = mockEvent("1");
+
+ handleNumberChange(event, setNumber, 10, 100);
+
+ expect(setNumber).not.toHaveBeenCalledWith("10");
+ });
+});
diff --git a/app/client/test/normalizeRow.test.ts b/app/client/test/normalizeRow.test.ts
new file mode 100644
index 0000000..3807eb0
--- /dev/null
+++ b/app/client/test/normalizeRow.test.ts
@@ -0,0 +1,186 @@
+import { describe, it, expect } from "vitest";
+import { normalizeRow } from "../src/utils/NormalizeRow";
+//tests for normalizeRow function we can run with vitest
+describe("normalizeRow", () => {
+ it("normalizes a fully valid row", () => {
+ const raw = {
+ date: "2024-01-15",
+ description: " Test transaction ",
+ amount: "100.50",
+ sender: " Joe ",
+ recipient: " Bob ",
+ category: " Food ",
+ };
+
+ const result = normalizeRow(raw);
+
+ expect(result).toEqual({
+ date: "2024-01-15",
+ description: "Test transaction",
+ amount: 100.5,
+ sender: "Joe",
+ recipient: "Bob",
+ category: "Food",
+ });
+ });
+
+ it("handles null and undefined fields", () => {
+ const raw = {
+ date: null,
+ description: undefined,
+ amount: null,
+ sender: undefined,
+ recipient: null,
+ category: undefined,
+ };
+
+ const result = normalizeRow(raw);
+
+ expect(result).toEqual({
+ date: null,
+ description: null,
+ amount: null,
+ sender: null,
+ recipient: null,
+ category: null,
+ });
+ });
+ //tests for different date formats
+ it("normalizes MM-DD-YYYY date format", () => {
+ const raw = { date: "01-31-2024" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBe("2024-01-31");
+ });
+ //tests for invalid date formats
+ it("returns null for invalid dates", () => {
+ const raw = { date: "not-a-date" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBeNull();
+ });
+ //tests for amount normalization
+ it("normalizes positive amounts", () => {
+ const raw = { amount: "1,234.56" };
+ const result = normalizeRow(raw);
+ expect(result.amount).toBe(1234.56);
+ });
+ //tests for negative amounts with minus sign
+ it("normalizes negative amounts in parentheses", () => {
+ const raw = { amount: "(500.00)" };
+ const result = normalizeRow(raw);
+ expect(result.amount).toBe(-500);
+ });
+ //tests for null amount
+ it("returns null for invalid amounts", () => {
+ const raw = { amount: "abc" };
+ const result = normalizeRow(raw);
+ expect(result.amount).toBeNull();
+ });
+ //tests for trimming string fields
+ it("trims all string fields", () => {
+ const raw = {
+ description: " rent payment ",
+ sender: " joe ",
+ recipient: " bob ",
+ category: " misc ",
+ };
+
+ const result = normalizeRow(raw);
+
+ expect(result.description).toBe("rent payment");
+ expect(result.sender).toBe("joe");
+ expect(result.recipient).toBe("bob");
+ expect(result.category).toBe("misc");
+ });
+});
+it("normalizes dates with dots", () => {
+ const raw = { date: "2024.01.15" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBe("2024-01-15");
+});
+
+it("normalizes dates with slashes", () => {
+ const raw = { date: "2024/01/15" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBe("2024-01-15");
+});
+
+it("normalizes dates with spaces", () => {
+ const raw = { date: "2024 01 15" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBe("2024-01-15");
+});
+
+it("rejects parentheses amounts with extra characters before", () => {
+ const raw = {
+ date: "2024-01-01",
+ description: "",
+ amount: "X(500.00)",
+ sender: "",
+ recipient: "",
+ category: "",
+ };
+ const result = normalizeRow(raw);
+ expect(result.amount).toBeNull();
+});
+
+it("rejects parentheses amounts with extra characters after", () => {
+ const raw = {
+ date: "2024-01-01",
+ description: "",
+ amount: "(500.00)X",
+ sender: "",
+ recipient: "",
+ category: "",
+ };
+ const result = normalizeRow(raw);
+ expect(result.amount).toBeNull();
+});
+
+it("rejects MM-DD-YYYY with extra characters before", () => {
+ const raw = { date: "X01-31-2024" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBeNull();
+});
+
+it("rejects MM-DD-YYYY with extra characters after", () => {
+ const raw = { date: "01-31-2024X" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBeNull();
+});
+it("pads single-digit month and day", () => {
+ const raw = { date: "1-2-2024" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBe("2024-01-02");
+});
+
+it("trims whitespace around amount", () => {
+ const raw = { amount: " 123.45 " };
+ const result = normalizeRow(raw);
+ expect(result.amount).toBe(123.45);
+});
+
+it("parses negative parentheses amounts with leading whitespace", () => {
+ const raw = {
+ date: "2024-01-01",
+ description: "",
+ amount: " (500.00)",
+ sender: "",
+ recipient: "",
+ category: "",
+ };
+
+ const result = normalizeRow(raw);
+ expect(result.amount).toBe(-500);
+});
+
+it("rejects YYYY-MM-DD with leading garbage", () => {
+ const raw = { date: "X2024-01-15" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBeNull();
+});
+
+it("rejects YYYY-MM-DD with trailing garbage", () => {
+ const raw = { date: "2024-01-15X" };
+ const result = normalizeRow(raw);
+ expect(result.date).toBeNull();
+});
diff --git a/app/client/test/parseCsvFile.test.ts b/app/client/test/parseCsvFile.test.ts
new file mode 100644
index 0000000..c4efc92
--- /dev/null
+++ b/app/client/test/parseCsvFile.test.ts
@@ -0,0 +1,202 @@
+import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
+import { parseCsvFile } from "../src/utils/ParseCsv";
+import Papa, {
+ ParseResult,
+ ParseError,
+ ParseConfig,
+ ParseMeta,
+} from "papaparse";
+import { normalizeRow } from "../src/utils/NormalizeRow";
+import { convertDraft } from "../src/utils/ConvertTransaction";
+
+// Extend PapaParse types to match real runtime behavior, this ensures our mocks are correctly typed and we can call the complete and error callbacks in our tests without type issues.
+type FixedParseConfig = ParseConfig & {
+ error?: (err: Error, file?: File) => void;
+ complete?: (results: ParseResult, file?: File) => void;
+};
+
+vi.mock("papaparse", () => ({
+ default: {
+ parse: vi.fn(),
+ },
+}));
+
+// Mock helpers
+vi.mock("../src/utils/NormalizeRow", () => ({
+ normalizeRow: vi.fn((row) => ({ normalized: row })),
+}));
+
+vi.mock("../src/utils/ConvertTransaction", () => ({
+ convertDraft: vi.fn((row) => ({ converted: row, errors: [] })),
+}));
+
+function mockFile(name: string): File {
+ return new File(["dummy"], name, { type: "text/csv" });
+}
+
+const mockMeta: ParseMeta = {
+ delimiter: ",",
+ linebreak: "\n",
+ aborted: false,
+ truncated: false,
+ cursor: 0,
+};
+
+describe("parseCsvFile", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("resolves with converted drafts when parsing succeeds", async () => {
+ const file = mockFile("good.csv");
+ const mockParse = Papa.parse as unknown as Mock;
+
+ mockParse.mockImplementation(
+ (_file: File, config: FixedParseConfig) => {
+ const result: ParseResult = {
+ data: [{ a: 1 }, { a: 2 }],
+ errors: [],
+ meta: mockMeta,
+ };
+
+ config.complete?.(result, _file);
+ },
+ );
+
+ const result = await parseCsvFile(file);
+
+ expect(normalizeRow).toHaveBeenCalledTimes(2);
+ expect(convertDraft).toHaveBeenCalledTimes(2);
+
+ expect(result).toEqual([
+ { converted: { normalized: { a: 1 } }, errors: [] },
+ { converted: { normalized: { a: 2 } }, errors: [] },
+ ]);
+ });
+
+ it("rejects when Papa.parse returns errors", async () => {
+ const file = mockFile("bad.csv");
+ const mockParse = Papa.parse as unknown as Mock;
+
+ mockParse.mockImplementation(
+ (_file: File, config: FixedParseConfig) => {
+ const result: ParseResult = {
+ data: [],
+ errors: [{ message: "Parse error" } as ParseError],
+ meta: mockMeta,
+ };
+
+ config.complete?.(result, _file);
+ },
+ );
+
+ await expect(parseCsvFile(file)).rejects.toBe("Parse error");
+ });
+
+ it("rejects when Papa.parse triggers its error callback", async () => {
+ const file = mockFile("error.csv");
+ const mockParse = Papa.parse as unknown as Mock;
+
+ mockParse.mockImplementation(
+ (_file: File, config: FixedParseConfig) => {
+ config.error?.(new Error("File read error"), _file);
+ },
+ );
+
+ await expect(parseCsvFile(file)).rejects.toBe("File read error");
+ });
+
+ it("calls Papa.parse with correct config", async () => {
+ const file = mockFile("config.csv");
+ const mockParse = Papa.parse as unknown as Mock;
+
+ mockParse.mockImplementation(
+ (_file: File, config: FixedParseConfig) => {
+ const result: ParseResult = {
+ data: [],
+ errors: [],
+ meta: mockMeta,
+ };
+
+ config.complete?.(result, _file);
+ },
+ );
+
+ await parseCsvFile(file);
+
+ expect(mockParse).toHaveBeenCalledWith(
+ file,
+ expect.objectContaining({
+ header: true,
+ skipEmptyLines: true,
+ dynamicTyping: true,
+ complete: expect.any(Function),
+ error: expect.any(Function),
+ }),
+ );
+ });
+});
+
+it("removes BOM using beforeFirstChunk", async () => {
+ const file = mockFile("bom.csv");
+ const mockParse = Papa.parse as unknown as Mock;
+
+ mockParse.mockImplementation((_file, config: FixedParseConfig) => {
+ // Simulate Papa calling beforeFirstChunk
+ const cleaned = config.beforeFirstChunk?.("\uFEFFa,b,c");
+
+ expect(cleaned).toBe("a,b,c");
+
+ const result: ParseResult = {
+ data: [],
+ errors: [],
+ meta: mockMeta,
+ };
+
+ config.complete?.(result, _file);
+ });
+
+ await parseCsvFile(file);
+});
+
+it("normalizes headers using transformHeader", async () => {
+ const file = mockFile("headers.csv");
+ const mockParse = Papa.parse as unknown as Mock;
+
+ mockParse.mockImplementation((_file, config: FixedParseConfig) => {
+ const transformed = config.transformHeader?.(" Amount ", 0);
+
+ expect(transformed).toBe("amount");
+
+ const result: ParseResult = {
+ data: [],
+ errors: [],
+ meta: mockMeta,
+ };
+
+ config.complete?.(result, _file);
+ });
+
+ await parseCsvFile(file);
+});
+
+it("does not remove BOM when it is not at the start", async () => {
+ const file = mockFile("bom-middle.csv");
+ const mockParse = Papa.parse as unknown as Mock;
+
+ mockParse.mockImplementation((_file, config: FixedParseConfig) => {
+ const cleaned = config.beforeFirstChunk?.("a\uFEFF,b,c");
+
+ expect(cleaned).toBe("a\uFEFF,b,c");
+
+ const result: ParseResult = {
+ data: [],
+ errors: [],
+ meta: mockMeta,
+ };
+
+ config.complete?.(result, _file);
+ });
+
+ await parseCsvFile(file);
+});
diff --git a/app/client/test/sampleCSV1.csv b/app/client/test/sampleCSV1.csv
new file mode 100644
index 0000000..df5dc67
--- /dev/null
+++ b/app/client/test/sampleCSV1.csv
@@ -0,0 +1,11 @@
+date,description,amount,sender,recipient,category
+3/1/2024,Coffee Shop,25.5,Me,Starbucks,Food
+03/01/2024,Rent Payment,1000,Me,Landlord,Housing
+2024-03-03,Grocery Run,45.2,Me,Superstore,Food
+3/4/2024,Internet Bill,95,Me,Bell MTS,Utilities
+3/5/2024,Gas,60,Me,Shell,Transport
+3/6/2024,Restaurant,75,Me,Earls,Food
+3/7/2024,Phone Bill,70,Me,Rogers,Utilities
+3/8/2024,Gift Received,150,Aunt,,Gifts
+3/9/2024,Freelance Payment,600,Client,,Income
+3/10/2024,Gym Membership,45,Me,Gym,Fitness
diff --git a/app/client/test/validateFile.test.ts b/app/client/test/validateFile.test.ts
new file mode 100644
index 0000000..f6284c2
--- /dev/null
+++ b/app/client/test/validateFile.test.ts
@@ -0,0 +1,87 @@
+import { describe, it, expect } from "vitest";
+import { validateFile } from "../src/utils/ValidateFile";
+import type { TransactionDraft } from "../src/utils/ConvertTransaction";
+// validate file tests
+describe("validateFile", () => {
+ it("returns correct counts when all rows are valid", () => {
+ const rows: TransactionDraft[] = [
+ { date: "2024-01-01", description: "A", amount: 10, errors: [] },
+ { date: "2024-01-02", description: "B", amount: 20, errors: [] },
+ ];
+
+ const result = validateFile(rows);
+
+ expect(result).toEqual({
+ validCount: 2,
+ invalidCount: 0,
+ tooManyInvalid: false,
+ });
+ });
+ // test the case where all rows are invalid
+ it("returns correct counts when all rows are invalid", () => {
+ const rows: TransactionDraft[] = [
+ { date: null, description: null, amount: null, errors: ["x"] },
+ { date: null, description: null, amount: null, errors: ["y"] },
+ ];
+
+ const result = validateFile(rows);
+
+ expect(result).toEqual({
+ validCount: 0,
+ invalidCount: 2,
+ tooManyInvalid: true, // 100% invalid
+ });
+ });
+ //here we check the case where 20% or more of rows are invalid
+ it("detects when more than 20% of rows are invalid", () => {
+ const rows: TransactionDraft[] = [
+ { date: "2024-01-01", description: "A", amount: 10, errors: [] },
+ { date: "2024-01-02", description: "B", amount: 20, errors: [] },
+ { date: null, description: null, amount: null, errors: ["bad"] },
+ ];
+
+ const result = validateFile(rows);
+
+ expect(result.validCount).toBe(2);
+ expect(result.invalidCount).toBe(1);
+ expect(result.tooManyInvalid).toBe(true);
+ });
+
+ //here we check the case where exactly 20% of rows are invalid
+ it("does not flag tooManyInvalid when invalid rows are 20% or less", () => {
+ const rows: TransactionDraft[] = [
+ { date: "2024-01-01", description: "A", amount: 10, errors: [] },
+ { date: "2024-01-02", description: "B", amount: 20, errors: [] },
+ { date: "2024-01-03", description: "C", amount: 30, errors: [] },
+ { date: null, description: null, amount: null, errors: ["bad"] },
+ { date: "2024-01-04", description: "D", amount: 40, errors: [] },
+ ];
+
+ const result = validateFile(rows);
+
+ expect(result.validCount).toBe(4);
+ expect(result.invalidCount).toBe(1);
+ expect(result.tooManyInvalid).toBe(false);
+ });
+
+ it("handles an empty file", () => {
+ const rows: TransactionDraft[] = [];
+
+ const result = validateFile(rows);
+
+ expect(result).toEqual({
+ validCount: 0,
+ invalidCount: 0,
+ tooManyInvalid: false,
+ });
+ });
+});
+
+it("does not flag tooManyInvalid when rows are empty even if invalid rows exist conceptually", () => {
+ // simulate invalid rows but pass empty array
+ const result = validateFile([]);
+
+ expect(result.validCount).toBe(0);
+ expect(result.invalidCount).toBe(0);
+ expect(result.tooManyInvalid).toBe(false);
+});
diff --git a/app/client/test/validateForms.test.ts b/app/client/test/validateForms.test.ts
new file mode 100644
index 0000000..512a0ae
--- /dev/null
+++ b/app/client/test/validateForms.test.ts
@@ -0,0 +1,140 @@
+import { describe, it, expect } from "vitest";
+import {
+ validateAccountForm,
+ validateTransactionForm,
+ validateIncomeForm,
+} from "../src/utils/ValidateForms";
+import { accountCategory } from "../src/enum/AccountCategory";
+import { transactionCategory } from "../src/enum/TransactionCategory";
+//tests for validateForms functions
+function mockFile(): File {
+ return new File(["dummy"], "test.csv", { type: "text/csv" });
+}
+
+describe("validateAccountForm", () => {
+ it("returns true when CSV file is provided (CSV overrides all other checks)", () => {
+ const file = mockFile();
+ const result = validateAccountForm("My Account", "Checking", 100, file);
+ expect(result).toBe(true);
+ });
+
+ it("returns true for valid manual account input", () => {
+ const validType = Object.values(accountCategory)[0];
+
+ const result = validateAccountForm("My Account", validType, 500);
+ expect(result).toBe(true);
+ });
+
+ it("returns false when name is missing", () => {
+ const validType = Object.values(accountCategory)[0];
+
+ const result = validateAccountForm("", validType, 500);
+ expect(result).toBe(false);
+ });
+
+ it("returns false for invalid account type", () => {
+ const result = validateAccountForm("My Account", "INVALID_TYPE", 100);
+ expect(result).toBe(false);
+ });
+
+ it("returns false for negative balance", () => {
+ const validType = Object.values(accountCategory)[0];
+
+ const result = validateAccountForm("My Account", validType, -10);
+ expect(result).toBe(false);
+ });
+
+ it("returns false for invalid interest rate", () => {
+ const validType = Object.values(accountCategory)[0];
+
+ const result = validateAccountForm(
+ "My Account",
+ validType,
+ 100,
+ undefined,
+ undefined,
+ 200,
+ );
+ expect(result).toBe(false);
+ });
+
+ it("returns true when optional subtype and interest are valid", () => {
+ const validType = Object.values(accountCategory)[0];
+
+ const result = validateAccountForm(
+ "My Account",
+ validType,
+ 100,
+ undefined,
+ "sub",
+ 5,
+ );
+ expect(result).toBe(true);
+ });
+});
+
+describe("validateTransactionForm", () => {
+ it("returns true when CSV file is provided", () => {
+ const file = mockFile();
+ const result = validateTransactionForm(
+ 123,
+ "Deposit",
+ 100,
+ "2024-01-01",
+ file,
+ );
+ expect(result).toBe(true);
+ });
+
+ it("returns true for valid manual transaction input", () => {
+ const validType = Object.values(transactionCategory)[0];
+
+ const result = validateTransactionForm(123, validType, 50, "2024-01-01");
+ expect(result).toBe(true);
+ });
+
+ it("returns false when account_id is missing", () => {
+ const validType = Object.values(transactionCategory)[0];
+
+ const result = validateTransactionForm(0, validType, 50, "2024-01-01");
+ expect(result).toBe(false);
+ });
+
+ it("returns false for invalid transaction type", () => {
+ const result = validateTransactionForm(
+ 123,
+ "INVALID_TYPE",
+ 50,
+ "2024-01-01",
+ );
+ expect(result).toBe(false);
+ });
+
+ it("returns false for non-positive amount", () => {
+ const validType = Object.values(transactionCategory)[0];
+
+ const result = validateTransactionForm(123, validType, 0, "2024-01-01");
+ expect(result).toBe(false);
+ });
+
+ it("returns false when date is missing", () => {
+ const validType = Object.values(transactionCategory)[0];
+
+ const result = validateTransactionForm(123, validType, 50, "");
+ expect(result).toBe(false);
+ });
+});
+
+describe("validateIncomeForm", () => {
+ it("returns true for valid name and positive income", () => {
+ expect(validateIncomeForm("Job", 1000)).toBe(true);
+ });
+
+ it("returns false for missing name", () => {
+ expect(validateIncomeForm("", 1000)).toBe(false);
+ });
+
+ it("returns false for non-positive income", () => {
+ expect(validateIncomeForm("Job", 0)).toBe(false);
+ });
+});
diff --git a/app/client/test/validateRow.test.ts b/app/client/test/validateRow.test.ts
new file mode 100644
index 0000000..b6e5a34
--- /dev/null
+++ b/app/client/test/validateRow.test.ts
@@ -0,0 +1,84 @@
+import { describe, it, expect } from "vitest";
+import { validateRow } from "../src/utils/ValidateRow";
+import type { NormalizedRow } from "../src/utils/NormalizeRow";
+//tests for validateRow function
+describe("validateRow", () => {
+ it("returns no errors for a fully valid row", () => {
+ const row: NormalizedRow = {
+ date: "2024-01-01",
+ description: "Test transaction",
+ amount: 100,
+ sender: "Joe",
+ recipient: "Bob",
+ category: "Food",
+ };
+
+ const errors = validateRow(row);
+
+ expect(errors).toEqual([]);
+ });
+
+ it("reports missing or invalid date", () => {
+ const row: NormalizedRow = {
+ date: null,
+ description: "Test",
+ amount: 50,
+ sender: null,
+ recipient: null,
+ category: null,
+ };
+
+ const errors = validateRow(row);
+
+ expect(errors).toContain("Invalid or missing date");
+ });
+
+ it("reports missing description", () => {
+ const row: NormalizedRow = {
+ date: "2024-01-01",
+ description: null,
+ amount: 50,
+ sender: null,
+ recipient: null,
+ category: null,
+ };
+
+ const errors = validateRow(row);
+
+ expect(errors).toContain("Missing description");
+ });
+
+ it("reports missing or invalid amount", () => {
+ const row: NormalizedRow = {
+ date: "2024-01-01",
+ description: "Test",
+ amount: null,
+ sender: null,
+ recipient: null,
+ category: null,
+ };
+
+ const errors = validateRow(row);
+
+ expect(errors).toContain("Invalid or missing amount");
+ });
+
+ it("reports all errors when all fields are invalid", () => {
+ const row: NormalizedRow = {
+ date: null,
+ description: null,
+ amount: null,
+ sender: null,
+ recipient: null,
+ category: null,
+ };
+
+ const errors = validateRow(row);
+
+ expect(errors).toEqual([
+ "Invalid or missing date",
+ "Missing description",
+ "Invalid or missing amount",
+ ]);
+ });
+});
diff --git a/app/client/tsconfig.app.json b/app/client/tsconfig.app.json
new file mode 100644
index 0000000..a132313
--- /dev/null
+++ b/app/client/tsconfig.app.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/app/client/tsconfig.json b/app/client/tsconfig.json
new file mode 100644
index 0000000..fec8c8e
--- /dev/null
+++ b/app/client/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
diff --git a/app/client/tsconfig.node.json b/app/client/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/app/client/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/app/client/vite.config.ts b/app/client/vite.config.ts
new file mode 100644
index 0000000..9cd2cb7
--- /dev/null
+++ b/app/client/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+import path from "path";
+import tailwindcss from "@tailwindcss/vite";
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react(), tailwindcss()],
+
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+ server: {
+ cors: { origin: "http://localhost:3000/.*" },
+ port: 8080,
+ },
+});
diff --git a/app/client/vitest.config.ts b/app/client/vitest.config.ts
new file mode 100644
index 0000000..c6a4ad2
--- /dev/null
+++ b/app/client/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "vitest/config";
+import path from "path";
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});
diff --git a/app/server/.env b/app/server/.env
new file mode 100644
index 0000000..a885f00
--- /dev/null
+++ b/app/server/.env
@@ -0,0 +1,15 @@
+#THIS IS NOT .gitignored'D!
+#DON'T PUT REAL SECRETS IN HERE!!!
+
+API_GATEWAY_PORT=3000
+
+#CI
+DOCKER_HUB_USERNAME=measureonecodetwice
+RELEASE_VERSION=local
+
+#DEPLOYMENT
+MYSQL_ROOT_PASSWORD=dummypw
+MYSQL_PASSWORD=dummypw
+MYSQL_USER=finus_app
+
+JWT_SECRET=dummysecret
diff --git a/app/server/default_container.env b/app/server/default_container.env
new file mode 100644
index 0000000..76451cf
--- /dev/null
+++ b/app/server/default_container.env
@@ -0,0 +1,13 @@
+API_GATEWAY_ADDR=http://api-gateway:8000
+AUTH_SERVICE_ADDR=http://auth:8000
+USER_SERVICE_ADDR=http://user:8000
+ANALYTICS_SERVICE_ADDR=http://analytics:8000
+MARKET_SERVICE_ADDR=http://market:8000
+
+MYSQL_HOST=database
+MYSQL_PORT=3306
+DB_NAME=finus
+
+#Use the CORS_ALLOWED_ORIGIN environment variable to
+#specify an additional (to localhost) non-localhost allowed origin.
+CORS_ALLOWED_ORIGIN=http://localhost
diff --git a/app/server/docker-compose.test.yml b/app/server/docker-compose.test.yml
new file mode 100644
index 0000000..fa92e8c
--- /dev/null
+++ b/app/server/docker-compose.test.yml
@@ -0,0 +1,94 @@
+#Run with ./run-tests.sh
+services:
+ # ------ UNIT TESTS ------
+ auth-test:
+ build:
+ context: services/ts
+ dockerfile: auth/Dockerfile.test
+
+ user-test:
+ build:
+ context: services/ts
+ dockerfile: user/Dockerfile.test
+
+ analytics-test:
+ build:
+ context: services/python/analytics
+ dockerfile: Dockerfile.test
+
+ market-test:
+ build:
+ context: services/python/market
+ dockerfile: Dockerfile.test
+
+ # # ------ MUTATION TESTS ------
+ # THIS CAN'T RUN WITH STRYKER UNLESS WE MOVE TO NODE
+ # user-mutation-test:
+ # build:
+ # context: ./services/ts
+ # dockerfile: user/Dockerfile.mutation.test
+
+ # auth-mutation-test:
+ # build:
+ # context: ./services/ts
+ # dockerfile: auth/Dockerfile.mutation.test
+
+ market-mutation-test:
+ build:
+ context: services/python/market
+ dockerfile: Dockerfile.mutation.test
+
+ # analytics-mutation-test:
+ # build:
+ # context: services/python/analytics
+ # dockerfile: Dockerfile.mutation.test
+
+ # MISSING
+ # AUTH
+
+ # ------ INTEGRATION TESTS ------
+ market-integration-test:
+ build:
+ context: services/python/market
+ dockerfile: Dockerfile.integration.test
+ env_file:
+ - ./default_container.env
+
+ user-integration-test:
+ build:
+ context: ./services/ts
+ dockerfile: user/Dockerfile.integration.test
+ env_file:
+ - ./default_container.env
+ environment:
+ - JWT_SECRET
+
+ load-test:
+ image: grafana/k6:0.49.0
+ # Mount the k6 scripts read-only so the same load test can be run
+ # either through Docker or locally with a host-installed k6 binary.
+ volumes:
+ - ./load-tests:/scripts:ro
+ environment:
+ - LOAD_TEST_BASE_URL=http://api-gateway:8000
+ - LOAD_TEST_USERS=20
+ - REQUESTS_PER_USER_PER_MINUTE=10
+ - LOAD_TEST_DURATION=2m
+ command: run /scripts/api-gateway-capacity.js
+
+
+ auth-integration-test:
+ build:
+ context: ./services/ts
+ dockerfile: auth/Dockerfile.integration.test
+ env_file:
+ - ./default_container.env
+
+ analytics-integration-test:
+ build:
+ context: ./services/python/analytics
+ dockerfile: Dockerfile.integration.test
+ env_file:
+ - ./default_container.env
+ environment:
+ - JWT_SECRET
diff --git a/app/server/docker-compose.yml b/app/server/docker-compose.yml
new file mode 100644
index 0000000..c5d1445
--- /dev/null
+++ b/app/server/docker-compose.yml
@@ -0,0 +1,98 @@
+services:
+ api-gateway:
+ build:
+ context: services/ts
+ dockerfile: api-gateway/Dockerfile
+ image: $DOCKER_HUB_USERNAME/finus-api-gateway:$RELEASE_VERSION
+ env_file:
+ - path: ./default_container.env
+ environment:
+ - NODE_ENV=development
+ ports:
+ - "$API_GATEWAY_PORT:8000"
+ volumes:
+ - client_state_data:/var/lib/finus-client-state
+ depends_on:
+ - auth
+ - user
+ - analytics
+ - market
+
+ auth:
+ build:
+ context: services/ts
+ dockerfile: auth/Dockerfile
+ image: $DOCKER_HUB_USERNAME/finus-auth:$RELEASE_VERSION
+ env_file:
+ - path: ./default_container.env
+ environment:
+ - MYSQL_USER
+ - MYSQL_PASSWORD
+ - JWT_SECRET
+ depends_on:
+ - database
+
+
+ user:
+ build:
+ context: services/ts
+ dockerfile: user/Dockerfile
+ image: $DOCKER_HUB_USERNAME/finus-user:$RELEASE_VERSION
+ env_file:
+ - path: ./default_container.env
+ environment:
+ - MYSQL_USER
+ - MYSQL_PASSWORD
+ - JWT_SECRET
+ depends_on:
+ - database
+
+ analytics:
+ build:
+ context: services/python/analytics
+ dockerfile: Dockerfile
+ image: $DOCKER_HUB_USERNAME/finus-analytics:$RELEASE_VERSION
+ env_file:
+ - path: ./default_container.env
+ environment:
+ - MYSQL_USER
+ - MYSQL_PASSWORD
+ - JWT_SECRET
+ depends_on:
+ - database
+
+ market:
+ build:
+ context: services/python/market
+ dockerfile: Dockerfile
+ image: $DOCKER_HUB_USERNAME/finus-market:$RELEASE_VERSION
+ env_file:
+ - path: ./default_container.env
+
+ #connect manually with `mysql -h 127.0.0.1 -P 3306 -u root -p`
+ database:
+ image: mysql:8.0-debian
+ cap_add:
+ #this is needed to avoid mbind operation not permitted errors when users keep requesting a ton of charts over and over
+ - SYS_NICE
+ volumes:
+ - db_data:/var/lib/mysql
+ #startup scripts are executed in alphabetical order
+ - ./services/database/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql
+ - ./services/database/grantPrivileges.sh:/docker-entrypoint-initdb.d/02-grantPrivileges.sh
+ environment:
+ - MYSQL_ROOT_PASSWORD
+ - MYSQL_USER
+ - MYSQL_PASSWORD
+ ports:
+ - "3306:3306"
+ healthcheck:
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
+ interval: 0.5s
+ timeout: 10s
+ retries: 100
+ start_period: 0s
+
+volumes:
+ db_data:
+ client_state_data:
diff --git a/app/server/load-tests/README.md b/app/server/load-tests/README.md
new file mode 100644
index 0000000..7cb5ef7
--- /dev/null
+++ b/app/server/load-tests/README.md
@@ -0,0 +1,25 @@
+# Load Testing
+
+This folder contains the `k6` capacity test for the backend.
+
+The main script is `api-gateway-capacity.js`.
+
+It validates the course capacity requirement by simulating:
+
+- `20` concurrent users
+- `200` total requests per minute
+
+How it works:
+
+- Each virtual user signs up and logs in with a unique test account.
+- Each virtual user creates one account and one starter transaction.
+- After setup, each virtual user sends `10` authenticated requests per minute.
+- `20` users x `10` requests per minute = `200` requests per minute total.
+
+How to run it:
+
+```bash
+cd app/server
+docker compose up -d
+docker compose -f docker-compose.test.yml up load-test --abort-on-container-failure
+```
diff --git a/app/server/load-tests/api-gateway-capacity.js b/app/server/load-tests/api-gateway-capacity.js
new file mode 100644
index 0000000..c0aedc2
--- /dev/null
+++ b/app/server/load-tests/api-gateway-capacity.js
@@ -0,0 +1,221 @@
+/*
+ Load testing script
+
+ It is designed to be run either:
+ 1. through Docker using the command from app/server:
+ docker compose up -d
+ docker compose -f docker-compose.yml -f docker-compose.test.yml up load-test --abort-on-container-failure.
+ 2. Using k6 which needs to be installed first then run with from app/server/load-tests:
+ k6.exe run api-gateway-capacity.js
+
+ Performance targets:
+ 20 concurrent users, each sending 10 requests per minute.
+ 20 users * 10 requests/minute = 200 requests/minute total.
+
+ */
+
+import http from "k6/http";
+import { check, sleep } from "k6";
+import { Counter } from "k6/metrics";
+
+const CONCURRENT_USERS = Number(__ENV.LOAD_TEST_USERS || 20);
+const REQUESTS_PER_USER_PER_MINUTE = Number(
+ __ENV.REQUESTS_PER_USER_PER_MINUTE || 10,
+);
+const TEST_DURATION = __ENV.LOAD_TEST_DURATION || "2m";
+const API_BASE_URL = __ENV.LOAD_TEST_BASE_URL || "http://localhost:3000";
+const EMAIL_RUN_ID = __ENV.LOAD_TEST_RUN_ID || Date.now().toString();
+
+// Each iteration sends exactly one authenticated request, so sleeping for
+// 60 / 10 = 6 seconds keeps one user at 10 requests per minute.
+const REQUEST_INTERVAL_SECONDS = 60 / REQUESTS_PER_USER_PER_MINUTE;
+
+// Count successful business requests so the summary clearly shows whether
+// the server handled the intended request volume successfully.
+const successfulRequests = new Counter("successful_requests");
+
+export const options = {
+ scenarios: {
+ capacity_requirement: {
+ executor: "constant-vus",
+ vus: CONCURRENT_USERS,
+ duration: TEST_DURATION,
+ },
+ },
+ thresholds: {
+ // The capacity test should be stable; failures indicate the system
+ // cannot reliably sustain the configured user/request volume.
+ http_req_failed: ["rate<0.01"],
+ checks: ["rate>0.99"],
+ },
+ summaryTrendStats: ["avg", "min", "med", "p(90)", "p(95)", "max"],
+};
+
+let authToken;
+let accountId;
+let transactionSequence = 0;
+
+function buildUserIdentity(userNumber) {
+ const suffix = `${EMAIL_RUN_ID}_${userNumber}`;
+ return {
+ username: `load_user_${suffix}`,
+ email: `load_user_${suffix}@finus.test`,
+ password: "LoadTestPass123!",
+ first_name: "Load",
+ last_name: `User${userNumber}`,
+ age: 30,
+ };
+}
+
+function signupAndLoginForUser(userNumber) {
+ const user = buildUserIdentity(userNumber);
+
+ // Signup provisions a real user/profile pair in the database so each
+ // user exercises the same auth and persistence flow as production.
+ const signupResponse = http.post(
+ `${API_BASE_URL}/api/signup`,
+ JSON.stringify(user),
+ {
+ headers: { "Content-Type": "application/json" },
+ tags: { endpoint: "signup" },
+ },
+ );
+
+ // Re-runs should still work if the same run id is reused and the account
+ // already exists, so we accept the duplicate-account response as setup-safe.
+ check(signupResponse, {
+ "signup succeeded or already existed": (response) =>
+ response.status === 201 || response.status === 409,
+ });
+
+ const loginResponse = http.post(
+ `${API_BASE_URL}/api/login`,
+ JSON.stringify({
+ email: user.email,
+ password: user.password,
+ }),
+ {
+ headers: { "Content-Type": "application/json" },
+ tags: { endpoint: "login" },
+ },
+ );
+
+ check(loginResponse, {
+ "login returned token": (response) =>
+ response.status === 200 && Boolean(response.json("token")),
+ });
+
+ authToken = loginResponse.json("token");
+}
+
+function createAccountForUser(userNumber) {
+ // Each user creates one account once so read endpoints have
+ // meaningful user-owned data to query during the steady-state load.
+ const accountResponse = http.post(
+ `${API_BASE_URL}/api/accounts`,
+ JSON.stringify({
+ id: 0,
+ name: `Load Checking ${EMAIL_RUN_ID}_${userNumber}`,
+ type: "CHEQUING",
+ balance: 1000,
+ value: 1000,
+ last_updated: new Date().toISOString(),
+ }),
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${authToken}`,
+ },
+ tags: { endpoint: "create_account" },
+ },
+ );
+
+ check(accountResponse, {
+ "account created": (response) =>
+ response.status === 200 || response.status === 201,
+ });
+
+ accountId = accountResponse.json("id");
+}
+
+function createTransactionForUser(userNumber) {
+ transactionSequence += 1;
+
+ const transactionResponse = http.post(
+ `${API_BASE_URL}/api/transactions`,
+ JSON.stringify({
+ id: 0,
+ financialAccount_id: accountId,
+ amount: 25 + transactionSequence,
+ description: `Load test transaction ${transactionSequence}`,
+ sender: `Employer_${userNumber}`,
+ recipient: `Load_User_${userNumber}`,
+ date: new Date().toISOString(),
+ category: "INCOME",
+ }),
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${authToken}`,
+ },
+ tags: { endpoint: "create_transaction" },
+ },
+ );
+
+ const requestPassed = check(transactionResponse, {
+ "transaction created": (response) =>
+ response.status === 200 || response.status === 201,
+ });
+
+ if (requestPassed) {
+ successfulRequests.add(1);
+ }
+}
+
+function ensureUserIsInitialized() {
+ if (authToken && accountId) {
+ return;
+ }
+
+ const userNumber = __VU;
+ signupAndLoginForUser(userNumber);
+ createAccountForUser(userNumber);
+ createTransactionForUser(userNumber);
+}
+
+function authenticatedGet(path, endpointTag) {
+ const response = http.get(`${API_BASE_URL}${path}`, {
+ headers: { Authorization: `Bearer ${authToken}` },
+ tags: { endpoint: endpointTag },
+ });
+
+ const requestPassed = check(response, {
+ [`${endpointTag} returned 200`]: (res) => res.status === 200,
+ });
+
+ if (requestPassed) {
+ successfulRequests.add(1);
+ }
+
+ return response;
+}
+
+export default function () {
+ ensureUserIsInitialized();
+
+ // Rotate endpoints so the load is spread across the gateway and the user
+ // service instead of repeatedly hitting a single route.
+ const routeIndex = __ITER % 3;
+ if (routeIndex === 0) {
+ authenticatedGet("/api/accounts", "accounts_list");
+ } else if (routeIndex === 1) {
+ authenticatedGet(
+ `/api/transactions?financialAccount_id=${accountId}`,
+ "transaction_list",
+ );
+ } else {
+ createTransactionForUser(__VU);
+ }
+
+ sleep(REQUEST_INTERVAL_SECONDS);
+}
diff --git a/app/server/run_tests.sh b/app/server/run_tests.sh
new file mode 100755
index 0000000..ea4ddcc
--- /dev/null
+++ b/app/server/run_tests.sh
@@ -0,0 +1,73 @@
+#!/bin/bash
+
+quitting=false
+quit() {
+ if ! $quitting; then
+ quitting=true
+ cleanup
+ exit 0
+ fi
+}
+cleanup() {
+ echo "Cleaning up..."
+ docker compose -f docker-compose.test.yml -f docker-compose.yml down
+}
+trap quit SIGINT
+
+#--- Build images ---
+echo "Building app images..."
+docker compose build -q
+status=$?
+if [ $status -ne 0 ]; then
+ echo "Failed to build services"
+ exit $status
+fi
+
+echo "Building test images..."
+docker compose -f docker-compose.test.yml build -q
+status=$?
+if [ $status -ne 0 ]; then
+ echo "Failed to build test images"
+ exit $status
+fi
+
+#--- Launch services ---
+docker compose up -d
+
+#--- Wait for services to be healthy ---
+source .env
+
+is_app_healthy() {
+ echo $(curl localhost:$API_GATEWAY_PORT/health 2> /dev/null | grep -Pc '^{(\"[^\"]*\":\"ok\",?)*}$')
+}
+
+curr_checks=0
+max_checks=10
+while [[ healthy=$(is_app_healthy) -ne 1 ]] && [ $curr_checks -lt $max_checks ]; do
+ curr_checks=$(($curr_checks + 1))
+ sleep 2.5
+ echo "App unhealthy. Retrying ($curr_checks of $max_checks)"
+done
+
+if [ $healthy -ne 1 ]; then
+ echo "App unhealthy after $max_checks retries. Exiting"
+ exit 1
+fi
+
+#--- Run tests ---
+docker compose -f docker-compose.test.yml up --abort-on-container-failure
+test_status=$?
+
+if [ $test_status -eq 0 ]; then
+ echo '+--------------------+'
+ echo '| ✅ All tests pass! |'
+ echo '+--------------------+'
+else
+ echo '+--------------------+'
+ echo '| ❌ Tests failed |'
+ echo '+--------------------+'
+
+fi
+cleanup
+
+exit $test_status
diff --git a/app/server/services/database/grantPrivileges.sh b/app/server/services/database/grantPrivileges.sh
new file mode 100755
index 0000000..85630dc
--- /dev/null
+++ b/app/server/services/database/grantPrivileges.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+#set -e
+#mysql -u root -p"$MYSQL_ROOT_PASSWORD" -e "
+# GRANT ALL PRIVILEGES ON finus.* TO '$MYSQL_USER'@'%';
+# "
+
+set -e
+
+mysql -u root -p"$MYSQL_ROOT_PASSWORD" < 0.5:
+ savings_subtypes = ['RRSP', 'TFSA', 'FHSA', 'RESP', 'RDSP']
+ subtype = random.choice(savings_subtypes)
+
+ elif acc_type == 'credit_card':
+ credit_subtypes = ['na', 'loan']
+ subtype = random.choice(credit_subtypes)
+
+ try:
+ cursor.execute("""
+ INSERT INTO finus.financialAccount (id, name, type, balance, value, last_updated, subtype)
+ VALUES (%s, %s, %s, %s, %s, %s, %s)
+ """, (financialAccount_id, name, acc_type, balance, value, last_updated, subtype))
+
+ financialAccount_ids.append(financialAccount_id)
+
+ cursor.execute("""
+ INSERT INTO finus.profile_financialAccount (profile_id, financialAccount_id)
+ VALUES (%s, %s)
+ """, (profile_id, financialAccount_id))
+
+ except Exception as e:
+ print(e)
+
+ # financialAccount_ids.append(financialAccount_id)
+
+
+ return financialAccount_ids
+
+
+#this will add just the test user that you can log in as, as well as their profiles and etc if NUM_USERS is set to 1
+def create_users_and_profiles(cursor):
+ user_ids = []
+ profile_ids = []
+
+ if NUM_USERS == 1:
+ print('Creating the TEST user John Finus...')
+ uid = 2
+ first_name = TEST_USER_F_NAME
+ last_name = TEST_USER_L_NAME
+ username = TEST_USER_NAME
+ email = TEST_USER_EMAIL
+ age = TEST_USER_AGE
+
+ cursor.execute("""
+ INSERT INTO finus.finusAccount
+ (id, username, email, first_name, last_name, age)
+ VALUES (%s, %s, %s, %s, %s, %s)
+ """, (uid, username, email, first_name, last_name, age))
+
+ user_id = cursor.lastrowid
+ user_ids.append(user_id)
+
+ # profile for each user
+ profile_name = f"{first_name}'s Profile"
+ profile_desc = f"Main profile for {first_name} {last_name}"
+ cursor.execute("""
+ INSERT INTO finus.profile (name, description)
+ VALUES (%s, %s)
+ """, (profile_name, profile_desc))
+
+ profile_id = cursor.lastrowid
+ profile_ids.append(profile_id)
+
+ pw_hash = hash_password(TEST_USER_PASSWORD)
+ cursor.execute("""
+ INSERT INTO finus.credentials (finus_account_id, pw_hash)
+ VALUES (%s, %s)
+ """, (user_id, pw_hash))
+
+ cursor.execute("""
+ INSERT INTO finus.finusAccount_profile (profile_id, account_id)
+ VALUES (%s, %s)
+ """, (profile_id, user_id))
+
+ else:
+ for i in range(NUM_USERS):
+ first_name = random.choice(FIRST_NAMES)
+ last_name = random.choice(LAST_NAMES)
+ username = f"{first_name.lower()}.{last_name.lower()}{random.randint(1, 99)}"
+ email = f"{username}@example.com"
+ age = random.randint(18, 75)
+
+ cursor.execute("""
+ INSERT INTO finus.finusAccount
+ (username, email, first_name, last_name, age)
+ VALUES (%s, %s, %s, %s, %s)
+ """, (username, email, first_name, last_name, age))
+
+ user_id = cursor.lastrowid
+ user_ids.append(user_id)
+
+ # profile for each user
+ profile_name = f"{first_name}'s Profile"
+ profile_desc = f"Main profile for {first_name} {last_name}"
+ cursor.execute("""
+ INSERT INTO finus.profile (name, description)
+ VALUES (%s, %s)
+ """, (profile_name, profile_desc))
+
+ profile_id = cursor.lastrowid
+ profile_ids.append(profile_id)
+
+ pw_hash = hash_password("password123")
+ cursor.execute("""
+ INSERT INTO finus.credentials (finus_account_id, pw_hash)
+ VALUES (%s, %s)
+ """, (user_id, pw_hash))
+
+
+
+ cursor.execute("""
+ INSERT INTO finus.finusAccount_profile (profile_id, account_id)
+ VALUES (%s, %s)
+ """, (profile_id, user_id))
+
+ return user_ids, profile_ids
+
+
+
+def create_transactions(cursor, account_ids):
+ print(f"Creating transactions (about {len(account_ids) * TRANSACTIONS_PER_ACCOUNT} total)...")
+
+ for account_id in account_ids:
+ for _ in range(TRANSACTIONS_PER_ACCOUNT): # variable number of transactions
+ amount = 0
+
+ category = random.choice(TRANSACTION_CATEGORIES)
+ if category in ['salary', 'e-transfer', 'cash']:
+ amount = random.randint(10, 1000)
+ else:
+ amount = random.randint(-50, -5)
+
+ description = random.choice(TRANSACTION_DESCRIPTIONS)
+ sender = random.choice(SENDERS_RECIPIENTS) if amount > 0 else None
+ recipient = random.choice(SENDERS_RECIPIENTS) if amount < 0 else None
+
+ # random date within the last year
+ days_offset = random.randint(0, 365)
+ transaction_date = START_DATE + timedelta(days=days_offset)
+
+ cursor.execute("""
+ INSERT INTO finus.transaction
+ (financialAccount_id, amount, category, description, sender, recipient, date)
+ VALUES (%s, %s, %s, %s, %s, %s, %s)
+ """, (account_id, amount, category, description, sender, recipient, transaction_date))
+
+ #print(f'Added a transaction with category: {category}, amount: {amount}')
+
+
+
+def create_investments(cursor, account_ids):
+
+ for account_id in random.sample(account_ids, min(len(account_ids) // 2, 5)): # Up to 5 accounts
+ # create 1-3 investments per account
+ for _ in range(random.randint(1, 3)):
+ inv_type = random.choice(INVESTMENT_TYPES)
+ name = f"Investment {random.randint(1000, 9999)}"
+ description = f"A {inv_type} investment"
+
+ cursor.execute("""
+ INSERT INTO finus.investment (account_id, type, name, description)
+ VALUES (%s, %s, %s, %s)
+ """, (account_id, inv_type, name, description))
+
+ investment_id = cursor.lastrowid
+
+ # create investment state
+ quantity = random.uniform(1, 100)
+ total_cost = quantity * random.uniform(10, 100)
+
+ cursor.execute("""
+ INSERT INTO finus.investmentState (investment_id, quantity, total_cost, at)
+ VALUES (%s, %s, %s, %s)
+ """, (investment_id, quantity, total_cost, datetime.now()))
+
+ # add subtype-specific data
+ if inv_type == 'stock':
+ symbols = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'TSLA']
+ symbol = random.choice(symbols)
+ cursor.execute("""
+ INSERT INTO finus.stockInvestment (investment_id, symbol)
+ VALUES (%s, %s)
+ """, (investment_id, symbol))
+
+ elif inv_type == 'fixedInterest':
+ rate = random.uniform(0.01, 0.08) # 1-8% interest
+ frequency_years = random.choice([0, 1, 2, 3, 5])
+ frequency_months = random.choice([0, 1, 3, 6]) if frequency_years == 0 else 0
+
+ cursor.execute("""
+ INSERT INTO finus.fixedInterestInvestment
+ (investment_id, rate, frequency_years, frequency_months)
+ VALUES (%s, %s, %s, %s)
+ """, (investment_id, rate, frequency_years, frequency_months))
+
+
+
+def clear_database(cursor):
+ print("Clearing existing data...")
+
+ tables_to_clear = [
+ 'transaction',
+ 'stockInvestment',
+ 'fixedInterestInvestment',
+ 'investmentState',
+ 'investment',
+ 'finusAccount_profile',
+ 'profile_goal',
+ 'goal',
+ 'financialAccount',
+ 'credentials',
+ 'finusAccount',
+ 'profile'
+ ]
+
+ for table in tables_to_clear:
+ print('Clearing table:', table)
+ cursor.execute(f"DELETE FROM finus.{table}")
+
+ cursor.execute("ALTER TABLE finus.finusAccount AUTO_INCREMENT = 1")
+ cursor.execute("ALTER TABLE finus.profile AUTO_INCREMENT = 1")
+ cursor.execute("ALTER TABLE finus.goal AUTO_INCREMENT = 1")
+ cursor.execute("ALTER TABLE finus.transaction AUTO_INCREMENT = 1")
+ cursor.execute("ALTER TABLE finus.investment AUTO_INCREMENT = 1")
+
+
+
+def populate_lookup_tables(cursor):
+
+ for acc_type in FINANCIAL_ACCOUNT_TYPES:
+ cursor.execute(
+ "INSERT IGNORE INTO finus.financialAccountType (type) VALUES (%s)",
+ (acc_type,)
+ )
+
+ for subtype in FINANCIAL_ACCOUNT_SUBTYPES:
+ cursor.execute(
+ "INSERT IGNORE INTO finus.financialAccountSubtype (subtype) VALUES (%s)",
+ (subtype,)
+ )
+
+ for inv_type in INVESTMENT_TYPES:
+ cursor.execute(
+ "INSERT IGNORE INTO finus.investmentType (type) VALUES (%s)",
+ (inv_type,)
+ )
+
+ # for goal_type in GOAL_TYPES:
+ # cursor.execute(
+ # "INSERT IGNORE INTO finus.goalType (type) VALUES (%s)",
+ # (goal_type,)
+ # )
+
+
+
+if __name__ == "__main__":
+
+ # get database connection
+ connection = get_db_connection()
+ if not connection:
+ print("Failed to connect to database")
+ exit(1)
+
+ parser = argparse.ArgumentParser(description='Populate Finus database with mock data')
+ parser.add_argument('--clear', action='store_true', help='Clear existing data before populating')
+ parser.add_argument('--users', type=int, default=NUM_USERS, help=f'Number of users to create (default: {NUM_USERS})')
+ args = parser.parse_args()
+
+
+
+ try:
+ cursor = connection.cursor()
+
+ # clear existing data if requested
+ if args.clear:
+ print("Clearing existing data")
+ clear_database(cursor)
+
+ print("Starting db population")
+ # populate lookup tables
+ populate_lookup_tables(cursor)
+ connection.commit()
+
+ # Create main data
+ print(f"Creating {args.users} users with profiles")
+ user_ids, profile_ids = create_users_and_profiles(cursor)
+ connection.commit()
+
+ print(f"Creating goals for profiles")
+ create_goals(cursor, profile_ids)
+ connection.commit()
+
+ print(f"Creating financial accounts for profiles")
+ account_ids = create_financial_accounts(cursor, profile_ids)
+ connection.commit()
+
+ print(f"Creating investments for financial accounts")
+ create_transactions(cursor, account_ids)
+ connection.commit()
+
+ print(f"Creating investments for financial accounts")
+ create_investments(cursor, account_ids)
+ connection.commit()
+
+ print(f" Created: {len(user_ids)} users")
+ print(f" Created: {len(profile_ids)} profiles")
+ print(f" Created: {len(account_ids)} financial accounts")
+ print(f" Created: ~{len(account_ids) * TRANSACTIONS_PER_ACCOUNT} transactions")
+
+ except Error as e:
+ print(f"Error during database population: {e}")
+ connection.rollback()
+ finally:
+ if connection.is_connected():
+ cursor.close()
+ connection.close()
\ No newline at end of file
diff --git a/app/server/services/database/schema.sql b/app/server/services/database/schema.sql
new file mode 100644
index 0000000..340e43e
--- /dev/null
+++ b/app/server/services/database/schema.sql
@@ -0,0 +1,161 @@
+CREATE DATABASE IF NOT EXISTS finus;
+
+CREATE TABLE finus.finusAccount (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ username VARCHAR(100) NOT NULL,
+ email VARCHAR(100) NOT NULL,
+ first_name VARCHAR(100) NOT NULL,
+ last_name VARCHAR(100) NOT NULL,
+ age INTEGER NOT NULL,
+ created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE finus.credentials (
+ #We use a hash algorithm which includes the salt in the hash.
+ finus_account_id INTEGER NOT NULL AUTO_INCREMENT,
+ pw_hash BLOB(256) NOT NULL,
+ PRIMARY KEY (finus_account_id),
+ FOREIGN KEY (finus_account_id) REFERENCES finus.finusAccount(id) ON DELETE CASCADE
+);
+
+CREATE TABLE finus.profile (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ name VARCHAR(100) NOT NULL,
+ description VARCHAR(500),
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE finus.finusAccount_profile (
+ profile_id INTEGER NOT NULL,
+ account_id INTEGER NOT NULL,
+ PRIMARY KEY (profile_id, account_id),
+ FOREIGN KEY (profile_id) REFERENCES finus.profile(id),
+ FOREIGN KEY (account_id) REFERENCES finus.finusAccount(id) ON DELETE CASCADE -- this used to mention financialAccount, but should be mentioning finusAccount
+);
+
+
+CREATE TABLE finus.goal (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ name VARCHAR(50) NOT NULL,
+ type VARCHAR(50) NOT NULL,
+ category VARCHAR(50),
+ period VARCHAR(1) NOT NULL,
+ target DECIMAL(12,2) NOT NULL, -- this is the monetary amount that we are trying to reach, there is no current amount to track, as that is just recalculated from the transactions
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE finus.profile_goal (
+ profile_id INTEGER NOT NULL,
+ goal_id INTEGER NOT NULL,
+ PRIMARY KEY (profile_id, goal_id),
+ FOREIGN KEY (profile_id) REFERENCES finus.profile(id),
+ FOREIGN KEY (goal_id) REFERENCES finus.goal(id)
+);
+
+CREATE TABLE finus.financialAccountType (
+ type VARCHAR(50) NOT NULL,
+ -- Note that there are other types of accounts such as joint, business, transmission, etc. This simplified set is good enough for now
+ -- type ENUM('savings', 'chequing', 'credit') DEFAULT 'unconfirmed',
+ PRIMARY KEY (type)
+);
+
+CREATE TABLE finus.financialAccountSubtype(
+ subtype VARCHAR(50) NOT NULL,
+ -- These subtypes are really only needed for savings accounts as they can be taxed differently and might have weird rules about them
+ -- type ENUM('RRSP', 'TFSA', 'FHSA', 'RESP', 'RDSP') DEFAULT 'NA',
+ PRIMARY KEY (subtype)
+);
+
+CREATE TABLE finus.financialAccount (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ name VARCHAR(50) NOT NULL,
+ type VARCHAR(50) NOT NULL,
+ balance DECIMAL(12,2) NOT NULL,
+ value INTEGER NOT NULL,
+ last_updated DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ subtype VARCHAR(50),
+ PRIMARY KEY (id),
+ FOREIGN KEY (type) REFERENCES finus.financialAccountType(type) ON DELETE CASCADE,
+ FOREIGN KEY (subtype) REFERENCES finus.financialAccountSubtype(subtype) ON DELETE CASCADE
+
+);
+
+CREATE TABLE finus.profile_financialAccount (
+ profile_id INTEGER NOT NULL,
+ financialAccount_id INTEGER NOT NULL,
+ PRIMARY KEY (profile_id, financialAccount_id),
+ FOREIGN KEY (profile_id) REFERENCES finus.profile(id),
+ FOREIGN KEY (financialAccount_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE
+);
+
+CREATE TABLE finus.transaction (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ financialAccount_id INTEGER NOT NULL,
+ amount INTEGER NOT NULL,
+ category VARCHAR(50) NOT NULL, -- Had to add this for analytics
+ description VARCHAR(500),
+ sender VARCHAR(50),
+ recipient VARCHAR(50),
+ date DATETIME,
+ PRIMARY KEY (id),
+ FOREIGN KEY (financialAccount_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE
+);
+
+CREATE TABLE finus.asset (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ name VARCHAR(50) NOT NULL,
+ fixed_compound_rate DOUBLE,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE finus.investmentType(
+ type VARCHAR(50) NOT NULL,
+ PRIMARY KEY (type)
+);
+
+CREATE TABLE finus.investment (
+ id INTEGER NOT NULL AUTO_INCREMENT,
+ account_id INTEGER NOT NULL,
+ type VARCHAR(50) NOT NULL,
+ name VARCHAR(50) NOT NULL,
+ description VARCHAR(500) NOT NULL,
+ PRIMARY KEY (id),
+ FOREIGN KEY (account_id) REFERENCES finus.financialAccount(id) ON DELETE CASCADE,
+ FOREIGN KEY (type) REFERENCES finus.investmentType(type)
+);
+
+
+CREATE TABLE finus.investmentState (
+ investment_id INTEGER NOT NULL,
+ quantity DOUBLE NOT NULL DEFAULT 1,
+ total_cost DOUBLE NOT NULL,
+ at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (investment_id),
+ FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE
+);
+
+
+## Example of adding extra data
+CREATE TABLE finus.stockInvestment(
+ investment_id INTEGER NOT NULL,
+ symbol VARCHAR(50) NOT NULL,
+ PRIMARY KEY (investment_id),
+ FOREIGN KEY (investment_id) REFERENCES finus.investment(id)
+);
+
+CREATE TABLE finus.fixedInterestInvestment(
+ investment_id INTEGER NOT NULL,
+ rate DOUBLE NOT NULL,
+ frequency_years INTEGER NOT NULL,
+ frequency_months INTEGER NOT NULL,
+ PRIMARY KEY (investment_id),
+ FOREIGN KEY (investment_id) REFERENCES finus.investment(id) ON DELETE CASCADE
+);
+
+#Populate lookup tables
+INSERT INTO finus.financialAccountType (type) VALUES ('chequing'), ('savings'), ('credit_card'), ('investment');
+INSERT INTO finus.financialAccountSubtype (subtype) VALUES ('RRSP'), ('TFSA'), ('FHSA'), ('RESP'), ('RDSP'), ('Loan'), ('na');
+-- loan is used for credit_card accounts that are for loans like mortgage and etc, this is used to track debt
+INSERT INTO finus.investmentType (type) VALUES ('fixedInterest'), ('stock'); #These have to match table names
+
diff --git a/app/server/services/database/updateSchema.sh b/app/server/services/database/updateSchema.sh
new file mode 100755
index 0000000..947919f
--- /dev/null
+++ b/app/server/services/database/updateSchema.sh
@@ -0,0 +1,4 @@
+echo "Wipe database:"
+mysql -c -h 127.0.0.1 -P 3306 -u root -p -e "DROP DATABASE finus";
+echo "Load schema:"
+mysql -c -h 127.0.0.1 -P 3306 -u root -p -e "$(cat schema.sql)"
diff --git a/app/server/services/python/.gitignore b/app/server/services/python/.gitignore
new file mode 100644
index 0000000..9b1ee42
--- /dev/null
+++ b/app/server/services/python/.gitignore
@@ -0,0 +1,175 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/app/server/services/python/analytics/Dockerfile b/app/server/services/python/analytics/Dockerfile
new file mode 100644
index 0000000..63065b0
--- /dev/null
+++ b/app/server/services/python/analytics/Dockerfile
@@ -0,0 +1,11 @@
+FROM python:3.11-slim
+
+WORKDIR /app/server/services/python/analytics
+
+COPY requirements.txt .
+
+RUN --mount=type=cache,target=/root/.cache/pip \
+pip install -r requirements.txt
+
+COPY . .
+CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"]
diff --git a/app/server/services/python/analytics/Dockerfile.integration.test b/app/server/services/python/analytics/Dockerfile.integration.test
new file mode 100644
index 0000000..8b1f0e8
--- /dev/null
+++ b/app/server/services/python/analytics/Dockerfile.integration.test
@@ -0,0 +1,13 @@
+FROM measureonecodetwice/finus-analytics:local
+
+WORKDIR /app
+
+COPY integration ./integration
+COPY requirements.txt .
+COPY requirements-test.txt .
+
+RUN pip install -r requirements-test.txt
+
+ENV CI=true
+
+CMD ["python", "-m", "pytest", "integration/", "-v"]
diff --git a/app/server/services/python/analytics/Dockerfile.mutation.test b/app/server/services/python/analytics/Dockerfile.mutation.test
new file mode 100644
index 0000000..9d7a14d
--- /dev/null
+++ b/app/server/services/python/analytics/Dockerfile.mutation.test
@@ -0,0 +1,21 @@
+FROM measureonecodetwice/finus-analytics:local
+
+WORKDIR /app
+
+COPY src ./src
+COPY test ./test
+COPY requirements.txt .
+COPY requirements-test.txt .
+COPY pyproject.toml .
+
+RUN pip install -r requirements-test.txt
+
+RUN cp -r src analytics_src
+RUN cp -r test mutation_test
+RUN find mutation_test -type f -name "*.py" -exec perl -0pi -e "s/\\bfrom src import\\b/from analytics_src import/g; s/\\bimport src\\./import analytics_src./g; s/\\bsrc\\./analytics_src./g" {} +
+RUN rm -rf src test
+
+ENV PYTHONPATH=/app
+ENV CI=true
+
+CMD ["mutmut", "run"]
diff --git a/app/server/services/python/analytics/Dockerfile.test b/app/server/services/python/analytics/Dockerfile.test
new file mode 100644
index 0000000..0daf46b
--- /dev/null
+++ b/app/server/services/python/analytics/Dockerfile.test
@@ -0,0 +1,17 @@
+FROM measureonecodetwice/finus-analytics:local
+
+WORKDIR /app/server/services/python/analytics
+
+COPY test ./test
+COPY requirements-test.txt .
+
+RUN pip install -r requirements-test.txt
+
+ENV PYTHONPATH=/app/server/services/python/analytics
+ENV JWT_SECRET=test_secret_key_12345
+ENV CI=true
+
+CMD ["python", "-m", "pytest", "test/", "-v", "--cov=src"]
+
+
+
diff --git a/app/server/services/python/analytics/integration/conftest.py b/app/server/services/python/analytics/integration/conftest.py
new file mode 100644
index 0000000..6a83ef3
--- /dev/null
+++ b/app/server/services/python/analytics/integration/conftest.py
@@ -0,0 +1,192 @@
+from typing import Dict
+import os
+import time
+import random
+import string
+import re
+import httpx
+import pytest
+from datetime import datetime, timedelta
+
+ANALYTICS_SERVICE_ADDR = os.getenv("ANALYTICS_SERVICE_ADDR", "http://analytics:8000")
+USER_SERVICE_ADDR = os.getenv("USER_SERVICE_ADDR", "http://user:8000")
+AUTH_SERVICE_ADDR = os.getenv("AUTH_SERVICE_ADDR", "http://auth:8000")
+READY_TIMEOUT_SECONDS = float(os.getenv("ANALYTICS_READY_TIMEOUT_SECONDS", "60"))
+
+def wait_for_service(base_url: str, timeout_seconds: float) -> None:
+ deadline = time.monotonic() + timeout_seconds
+ last_error = "service never became reachable"
+
+ while time.monotonic() < deadline:
+ try:
+ response = httpx.get(f"{base_url}/health", timeout=5.0)
+ if response.status_code == 200 and response.text == "ok":
+ return
+ last_error = f"unexpected health response: {response.status_code} {response.text}"
+ except httpx.HTTPError as exc:
+ last_error = str(exc)
+
+ time.sleep(1)
+
+ pytest.fail(f"Market service was not ready within {timeout_seconds}s: {last_error}")
+
+@pytest.fixture(scope="session")
+def auth_client() -> httpx.Client:
+ wait_for_service(AUTH_SERVICE_ADDR, READY_TIMEOUT_SECONDS)
+ with httpx.Client(base_url=AUTH_SERVICE_ADDR, timeout=15.0) as client:
+ yield client
+
+@pytest.fixture(scope="session")
+def user_client() -> httpx.Client:
+ wait_for_service(USER_SERVICE_ADDR, READY_TIMEOUT_SECONDS)
+ with httpx.Client(base_url=USER_SERVICE_ADDR, timeout=15.0) as client:
+ yield client
+
+@pytest.fixture(scope="session")
+def analytics_client() -> httpx.Client:
+ wait_for_service(ANALYTICS_SERVICE_ADDR, READY_TIMEOUT_SECONDS)
+
+ with httpx.Client(base_url=ANALYTICS_SERVICE_ADDR, timeout=15.0) as client:
+ yield client
+
+def sleep(ms: int):
+ time.sleep(ms / 1000)
+
+
+def create_signup_body(prefix: str) -> dict:
+ safe_prefix = re.sub(r"[^\w]", "", prefix)
+ unique = f"{safe_prefix}{int(time.time() * 1000)}{''.join(random.choices(string.ascii_lowercase + string.digits, k=6))}"
+
+ return {
+ "username": unique,
+ "email": f"{unique}@sample.com",
+ "first_name": "Test",
+ "last_name": "User",
+ "age": 30,
+ "password": "123ABC!7",
+ }
+
+
+def create_authenticated_account(
+ auth_client: httpx.Client,
+ user_client: httpx.Client,
+ prefix: str,
+ account_overrides: dict = None,
+):
+ deadline = time.time() + 20
+ last_failure = "setup did not start"
+
+ account_overrides = account_overrides or {}
+
+ while time.time() < deadline:
+ signup_body = create_signup_body(prefix)
+
+ try:
+ # Signup
+ signup = auth_client.post("/signup", json=signup_body)
+ if signup.status_code != 201:
+ last_failure = f"signup={signup.status_code} {signup.text}"
+ sleep(500)
+ continue
+
+ # Login
+ login = auth_client.post(
+ "/login",
+ json={
+ "email": signup_body["email"],
+ "password": signup_body["password"],
+ },
+ )
+
+ login_json = login.json() if login.content else {}
+
+ if login.status_code != 200 or "token" not in login_json:
+ last_failure = f"login={login.status_code} {login.text}"
+ sleep(500)
+ continue
+
+ token = login_json["token"]
+
+ # Create account
+ account_payload = {
+ "name": account_overrides.get("name", "Sample Account"),
+ "type": account_overrides.get("type", "savings"),
+ "balance": account_overrides.get("balance", 1000),
+ "value": account_overrides.get("value", 1000),
+ "subtype": account_overrides.get("subtype", "RRSP"),
+ }
+
+ account = user_client.post(
+ "/accounts",
+ json=account_payload,
+ headers={"Authorization": f"Bearer {token}"},
+ )
+
+ account_json = account.json() if account.content else {}
+
+ print("Account creation response:", account.status_code, account_json)
+
+ if account.status_code != 200 or "id" not in account_json:
+ last_failure = f"account={account.status_code} {account.text}"
+ sleep(500)
+ continue
+
+ account_id = account_json["id"]
+
+ return {
+ "token": token,
+ "account_id": account_id,
+ }
+
+ except httpx.HTTPError as error:
+ last_failure = str(error)
+ print("Setup error:", last_failure)
+ sleep(500)
+
+ raise RuntimeError(f"Timed out creating authenticated account: {last_failure}")
+
+def create_transaction(user_client, account_id, token):
+ transactions = []
+
+ base_date = datetime(2024, 1, 1)
+
+ for _ in range(12):
+ # Random amount between 1 and 10000
+ amount = round(random.uniform(1, 10000), 2)
+
+ # Random date within 720 days
+ random_days = random.randint(0, 720)
+ random_seconds = random.randint(0, 86400)
+
+ transaction_date = base_date + timedelta(days=random_days, seconds=random_seconds)
+
+ transaction_payload = {
+ "financialAccount_id": account_id,
+ "amount": amount,
+ "description": "Sample transaction",
+ "sender": "Unknown",
+ "recipient": "Tester",
+ "date": transaction_date.isoformat(),
+ "category": "None",
+ }
+
+ response = user_client.post(
+ "/transactions",
+ json=transaction_payload,
+ headers={"Authorization": f"Bearer {token}"}
+ )
+
+ print("Transaction:", response.status_code, response.text)
+ transactions.append(response.json() if response.content else {})
+
+@pytest.fixture(scope="session")
+def auth_account(auth_client, user_client) -> Dict:
+ account = create_authenticated_account(
+ auth_client=auth_client,
+ user_client=user_client,
+ prefix="testuser",
+ )
+ print("Authenticated account created:", account)
+ create_transaction(user_client, account["account_id"], account["token"])
+ return account
+
diff --git a/app/server/services/python/analytics/integration/test_debts_integration.py b/app/server/services/python/analytics/integration/test_debts_integration.py
new file mode 100644
index 0000000..c253fbb
--- /dev/null
+++ b/app/server/services/python/analytics/integration/test_debts_integration.py
@@ -0,0 +1,137 @@
+from typing import Dict
+
+import httpx
+import pytest
+
+def test_predict_debt_payoff(auth_account: httpx.Client, analytics_client: httpx.Client):
+ token = auth_account["token"]
+ account_id = auth_account["account_id"]
+
+ payload = {
+ "id": str(account_id),
+ "category": "Credit Card",
+ "remainingAmount": 2000,
+ "interestRate": 20,
+ "minimumPayment": 100,
+ "period": 30, # days between payments
+ "nextDueDate": "2024-07-01"
+ }
+
+ response = analytics_client.post(
+ "/predict-debt-payoff",
+ json=payload,
+ headers={"Authorization": f"Bearer {token}"},
+ )
+
+ assert response.status_code == 200, response.text
+
+ data = response.json()
+
+ assert isinstance(data, dict)
+
+ assert "id" in data
+ assert "category" in data
+ assert "minimumPayment" in data
+ assert "interestRate" in data
+ assert "debtStages" in data
+
+ assert isinstance(data["debtStages"], list)
+ assert len(data["debtStages"]) > 0
+
+ debts = data["debtStages"]
+
+ for debt in debts:
+ assert "id" in debt
+ assert "principalAmount" in debt
+ assert "interestAmount" in debt
+ assert "remainingDebt" in debt
+ assert "installmentDate" in debt
+
+def test_predict_debt_payoff_with_unreasonable_payment(auth_account: httpx.Client, analytics_client: httpx.Client):
+ token = auth_account["token"]
+ account_id = auth_account["account_id"]
+
+ # Test with a minimum payment that is too low to ever pay off the debt
+ payload = {
+ "id": str(account_id),
+ "category": "Credit Card",
+ "remainingAmount": 400000,
+ "minimumPayment": 100,
+ "interestRate": 20.0,
+ "period": 14, # days between payments
+ "nextDueDate": "2024-07-01"
+ }
+
+ response = analytics_client.post(
+ "/predict-debt-payoff",
+ json=payload,
+ headers={"Authorization": f"Bearer {token}"},
+ )
+
+ assert response.status_code == 400
+
+ data = response.json()
+ print(f"Data: {data}")
+
+ assert isinstance(data, dict)
+ assert "detail" in data
+ assert "Minimum payment is too low. Debt will never be paid off." in data["detail"]
+
+def test_predict_debt_payoff_with_negative_minimum_payment(analytics_client: httpx.Client, auth_account: Dict):
+ token = auth_account["token"]
+ account_id = auth_account["account_id"]
+
+ # Test with negative minimum payment
+ payload = {
+ "id": str(account_id),
+ "category": "Credit Card",
+ "remainingAmount": 400000,
+ "interestRate": 20.0,
+ "minimumPayment": -100,
+ "period": 14, # days between payments
+ "nextDueDate": "2024-07-01"
+ }
+
+ response = analytics_client.post(
+ "/predict-debt-payoff",
+ json=payload,
+ headers={"Authorization": f"Bearer {token}"},
+ )
+
+ assert response.status_code == 422, response.text
+
+ data = response.json()
+
+ assert "detail" in data
+ assert isinstance(data["detail"], list)
+ assert any("minimumPayment" in str(error) for error in data["detail"]), "Error should mention minimumPayment"
+
+def test_predict_debt_payoff_with_negative_period(analytics_client: httpx.Client, auth_account: Dict):
+ token = auth_account["token"]
+ account_id = auth_account["account_id"]
+
+ # Test with negative period
+ payload = {
+ "id": str(account_id),
+ "category": "Credit Card",
+ "remainingAmount": 400000,
+ "interestRate": 20.0,
+ "minimumPayment": -100,
+ "period": -14, # days between payments
+ "nextDueDate": "2024-07-01"
+ }
+
+ response = analytics_client.post(
+ "/predict-debt-payoff",
+ json=payload,
+ headers={"Authorization": f"Bearer {token}"},
+ )
+
+ assert response.status_code == 422, response.text
+
+ data = response.json()
+
+ assert "detail" in data
+ assert isinstance(data["detail"], list)
+ assert any("period" in str(error) for error in data["detail"]), "Error should mention period"
+
diff --git a/app/server/services/python/analytics/integration/test_savings_integration.py b/app/server/services/python/analytics/integration/test_savings_integration.py
new file mode 100644
index 0000000..0f40b5e
--- /dev/null
+++ b/app/server/services/python/analytics/integration/test_savings_integration.py
@@ -0,0 +1,112 @@
+import os
+import time
+from typing import Dict
+
+import httpx
+import pytest
+
+
+def test_analytics_health_endpoint(analytics_client: httpx.Client) -> None:
+ response = analytics_client.get("/health")
+
+ assert response.status_code == 200
+ assert response.text == "ok"
+
+ print("Analytics service is healthy.")
+
+def test_compound_interest(analytics_client: httpx.Client, auth_account: Dict) -> None:
+ token = auth_account["token"]
+ account_id = auth_account["account_id"]
+
+ payload = {
+ "financial_account_id": account_id,
+ "balance": 1000,
+ "monthly_deposit": 100,
+ "annual_interest_rate": 5.0,
+ "time_frame": 2,
+ }
+
+ response = analytics_client.post(
+ "/compound-interest",
+ json=payload,
+ headers={"Authorization": f"Bearer {token}"},
+ )
+
+ assert response.status_code == 200, response.text
+
+ data = response.json()
+
+ assert isinstance(data, list)
+ assert len(data) > 0
+
+ for entry in data:
+ assert "accumulative_best_balance" in entry
+ assert "accumulative_expected_balance" in entry
+ assert "accumulative_worst_balance" in entry
+ assert "date" in entry
+
+ for entry in data:
+ best = entry["accumulative_best_balance"]
+ expected = entry["accumulative_expected_balance"]
+ worst = entry["accumulative_worst_balance"]
+
+ assert best >= expected >= worst >= 0, "Balances should be non-negative and ordered correctly"
+
+
+ first = data[0]["accumulative_expected_balance"]
+ last = data[-1]["accumulative_expected_balance"]
+
+ assert last >= first, "Balance should grow over time"
+
+def test_compound_interest_with_negative_monthly_deposit(analytics_client: httpx.Client, auth_account: Dict) -> None:
+ token = auth_account["token"]
+ account_id = auth_account["account_id"]
+
+ # Test with negative monthly deposit
+ payload = {
+ "financial_account_id": account_id,
+ "balance": 1000,
+ "monthly_deposit": -100,
+ "annual_interest_rate": 5.0,
+ "time_frame": 2,
+ }
+
+ response = analytics_client.post(
+ "/compound-interest",
+ json=payload,
+ headers={"Authorization": f"Bearer {token}"},
+ )
+
+ assert response.status_code == 422, response.text
+
+ data = response.json()
+ assert "detail" in data
+ assert isinstance(data["detail"], list)
+ assert any("monthly_deposit" in str(error) for error in data["detail"]), "Error should mention monthly_deposit"
+
+def test_compound_interest_with_negative_time_frame(analytics_client: httpx.Client, auth_account: Dict) -> None:
+ token = auth_account["token"]
+ account_id = auth_account["account_id"]
+
+ # Test with negative time frame
+ payload = {
+ "financial_account_id": account_id,
+ "balance": 1000,
+ "monthly_deposit": 100,
+ "annual_interest_rate": 5.0,
+ "time_frame": -2,
+ }
+
+ response = analytics_client.post(
+ "/compound-interest",
+ json=payload,
+ headers={"Authorization": f"Bearer {token}"},
+ )
+
+ assert response.status_code == 422, response.text
+
+ data = response.json()
+ assert "detail" in data
+ assert isinstance(data["detail"], list)
+ assert any("time_frame" in str(error) for error in data["detail"]), "Error should mention time_frame"
+
diff --git a/app/server/services/python/analytics/pyproject.toml b/app/server/services/python/analytics/pyproject.toml
new file mode 100644
index 0000000..688e46f
--- /dev/null
+++ b/app/server/services/python/analytics/pyproject.toml
@@ -0,0 +1,8 @@
+[tool.mutmut]
+paths_to_mutate = ["analytics_src/"]
+pytest_add_cli_args_test_selection = ["mutation_test/"]
+pytest_add_cli_args = ["-q"]
+mutate_only_covered_lines = true
+do_not_mutate = ["analytics_src/__init__.py"]
+also_copy = ["mutation_test/"]
+debug = true
diff --git a/app/server/services/python/analytics/requirements-test.txt b/app/server/services/python/analytics/requirements-test.txt
new file mode 100644
index 0000000..04b8062
--- /dev/null
+++ b/app/server/services/python/analytics/requirements-test.txt
@@ -0,0 +1,13 @@
+-r requirements.txt
+pydantic==2.5.0
+pandas==2.1.4
+numpy==1.26.3
+mysql-connector-python==8.0.33
+dotenv==0.9.9
+pyjwt==2.11.0
+pytest>=7.0.0
+pytest-asyncio>=0.21.0
+pytest-cov>=4.1.0
+pytest-mock>=3.11.0
+httpx>=0.27.0,<0.28.0
+mutmut>=3.2.3
\ No newline at end of file
diff --git a/app/server/services/python/analytics/requirements.txt b/app/server/services/python/analytics/requirements.txt
new file mode 100644
index 0000000..1a15f0f
--- /dev/null
+++ b/app/server/services/python/analytics/requirements.txt
@@ -0,0 +1,12 @@
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+pydantic==2.5.0
+pandas==2.1.4
+numpy==1.26.3
+mysql-connector-python==8.0.33
+dotenv==0.9.9
+pyjwt==2.11.0
+# pytest>=7.0.0
+# pytest-asyncio>=0.21.0
+# pytest-cov>=4.1.0
+# pytest-mock>=3.11.0
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/__init__.py b/app/server/services/python/analytics/src/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/server/services/python/analytics/src/dependencies.py b/app/server/services/python/analytics/src/dependencies.py
new file mode 100644
index 0000000..e64f222
--- /dev/null
+++ b/app/server/services/python/analytics/src/dependencies.py
@@ -0,0 +1,39 @@
+from typing import Optional
+from fastapi import HTTPException, Header
+import mysql.connector
+import os
+import jwt
+from dotenv import load_dotenv
+
+load_dotenv()
+SECRET_KEY = os.getenv('JWT_SECRET')
+ALGORITHM = 'HS256'
+
+def get_db_connection(timeout: int = 10):
+ #check env vars
+ if not all([os.getenv("MYSQL_HOST"), os.getenv("MYSQL_USER"), os.getenv("MYSQL_PASSWORD"), os.getenv("DB_NAME")]):
+ raise HTTPException(status_code=500, detail="Missing environment variables")
+ return mysql.connector.connect(
+ host=os.getenv("MYSQL_HOST"),
+ user=os.getenv("MYSQL_USER"),
+ password=os.getenv("MYSQL_PASSWORD"),
+ database=os.getenv("DB_NAME"),
+ connection_timeout=timeout
+ )
+
+async def get_current_user(authorization: Optional[str] = Header(None)):
+ if not authorization:
+ raise HTTPException(status_code=401, detail="Authorization header missing")
+
+ try:
+ scheme, token = authorization.split()
+ if scheme.lower() != 'bearer':
+ raise HTTPException(status_code=401, detail="Invalid authentication scheme")
+
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
+ user_id = payload.get('sub')
+
+ return int(user_id)
+
+ except Exception as e:
+ raise HTTPException(status_code=401, detail=str(e))#general exception for simpler unit
diff --git a/app/server/services/python/analytics/src/logic/__init__.py b/app/server/services/python/analytics/src/logic/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/server/services/python/analytics/src/logic/budget.py b/app/server/services/python/analytics/src/logic/budget.py
new file mode 100644
index 0000000..cea8f34
--- /dev/null
+++ b/app/server/services/python/analytics/src/logic/budget.py
@@ -0,0 +1,51 @@
+from src.logic.budgetCalc import BudgetCalculator
+from src.logic.budgetPerfCalc import BudgetPerformanceCalculator
+from src.models.schemas import BudgetResponse
+from src.queries.budget import get_user_transactions_with_connection, get_user_goals
+from src.utils.trans_cat_classifier import CategoryClassifier
+import pandas as pd
+
+_default_classifier = CategoryClassifier()
+_default_calculator = BudgetCalculator(_default_classifier)
+_default_performance = BudgetPerformanceCalculator()
+
+
+#Take all transactions over the last 12 months and sum up the amount for each category found
+# Find the top 10 most impactful categories and calculate a budget for each
+#At the moment, this uses a 50/30/20 rule, where categories are classified as needs, wants, savings and then a proportional budget is calculated
+# Monthly budget is generated based on the average monthly income, but then it can be divided or multiplied to match the specified period (week is divided by 4.33, year multiplied by 12)
+
+#More technical detail:
+#Pools of money are created for needs, wants and savings based on income per month - the budget is created on a monthly basis and can be rescaled for weeks or years later.
+# Each savings goal adds a flat 5% amount increase to the savings pool, money is taken away from wants pool and added to savings
+# Each expenditure reduction goal adds a progressive % to savings pool based on how close the expenditure is to the limit, and takes away this money from wants pool - adding it to savings
+# Needs pool is untouched always
+# The most impactful categories are then used to calculate a budget for each category by drawing out of the pools
+# Categories are categorized into needs, wants, savings and then a proportional budget is calculated
+
+def generate_budget(period: str, user_id: int) -> BudgetResponse:
+ end_date = pd.Timestamp.today()
+ start_date = end_date - pd.Timedelta(days=365)
+ transactions = get_user_transactions_with_connection(
+ user_id,
+ start_date.strftime('%Y-%m-%d'),
+ end_date.strftime('%Y-%m-%d')
+ )
+
+ goals = get_user_goals(user_id)
+
+ income = [t for t in transactions if t['amount'] > 0]
+ avg_monthly_income = sum(t['amount'] for t in income) / 12
+
+ return _default_calculator.generate_budget(transactions, period, avg_monthly_income, goals)
+
+
+#Calculate the performance against a proposed user budget
+#returns a dictionary with the performance for each category (budget vs actual)
+async def generate_budget_performance(
+ user_id: int,
+ budget_response: BudgetResponse,
+ period: str,
+ reference_date=None
+) -> dict:
+ return await _default_performance.generate_performance(user_id, budget_response, period, reference_date)
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/logic/budgetCalc.py b/app/server/services/python/analytics/src/logic/budgetCalc.py
new file mode 100644
index 0000000..70a49aa
--- /dev/null
+++ b/app/server/services/python/analytics/src/logic/budgetCalc.py
@@ -0,0 +1,221 @@
+from typing import List, Dict, Optional, Tuple
+import pandas as pd
+from src.models.schemas import BudgetCategory, BudgetResponse
+from src.utils.trans_cat_classifier import CategoryClassifier
+
+
+class BudgetCalculator:
+ def __init__(self, classifier: CategoryClassifier):
+ self.classifier = classifier
+ self.WEEKS_PER_MONTH = 4.33
+
+
+
+ def separate_income_expenses(self, transactions: List[Dict]) -> Tuple[List[Dict], List[Dict]]:
+ income = [t for t in transactions if t['amount'] > 0]
+ expenses = [t for t in transactions if t['amount'] < 0]
+ return income, expenses
+
+
+
+ def calculate_monthly_averages(self, expenses: List[Dict]) -> Dict[str, float]:
+ expense_by_category = {}
+ for t in expenses:
+ cat = t['category']
+ amount = abs(t['amount'])
+ expense_by_category[cat] = expense_by_category.get(cat, 0) + amount
+
+ return {cat: total / 12 for cat, total in expense_by_category.items()}
+
+
+
+ def get_top_categories(self, monthly_avg: Dict[str, float], n: int = 10) -> List[Tuple[str, float]]:
+ return sorted(monthly_avg.items(), key=lambda x: x[1], reverse=True)[:n]
+
+
+
+ def calculate_budget_pools(self, avg_monthly_income: float) -> Dict[str, float]:
+ return {
+ 'needs': avg_monthly_income * 0.5,
+ 'wants': avg_monthly_income * 0.3,
+ 'savings': avg_monthly_income * 0.2
+ }
+
+
+ #This calculates the impact of goals on the budget - the goal type is important
+ # Pure savings goals incur a flat boost to savings
+ # Reduce spending goals reduce wants by 15% if the spending is over the limit, this penalty becomes smaller the further away the spending is from the limit
+ def calculate_goal_impact(
+ self,
+ goals: List[Dict],
+ avg_monthly_income: float,
+ monthly_avg_by_category: Dict[str, float]
+ ) -> Tuple[float, float, List[str]]:
+ savings_boost = 0.0
+ wants_reduction = 0.0
+ categories_to_include = set()
+
+ for goal in goals:
+ category = goal.get('category')
+ goal_type = goal.get('type')
+
+ if category:
+ categories_to_include.add(category)
+
+ if goal_type == 'save':
+ #save goals add 5% boost to savings
+ boost_amount = avg_monthly_income * 0.05
+ savings_boost += boost_amount
+
+ elif goal_type == 'reduce_spending':
+ #get current spending for this category
+ current_spending = monthly_avg_by_category.get(category, 0)
+ target_amount = float(goal.get('target', 0))
+
+ if target_amount > 0:
+ percent_of_limit = (current_spending / target_amount) * 100
+
+ if percent_of_limit >= 100:
+ #already over limit: reduce wants by 7%
+ reduction = avg_monthly_income * 0.07
+ wants_reduction = max(wants_reduction, reduction)
+ elif percent_of_limit >= 80:
+ #approaching limit: reduce wants by 5%
+ reduction = avg_monthly_income * 0.05
+ wants_reduction = max(wants_reduction, reduction)
+ elif percent_of_limit >= 60:
+ #getting close: reduce wants by 3%
+ reduction = avg_monthly_income * 0.03
+ wants_reduction = max(wants_reduction, reduction)
+
+ return savings_boost, wants_reduction, list(categories_to_include)
+
+
+
+ def calculate_category_budget(
+ self,
+ category: str,
+ avg_spent: float,
+ category_type: str,
+ monthly_avg_by_category: Dict[str, float],
+ category_types: Dict[str, str],
+ pools: Dict[str, float],
+ goals: List[Dict] = None
+ ) -> float:
+ goals = goals or []
+ goal = next((g for g in goals if g.get('category') == category and g.get('type') == 'reduce_spending'), None)
+
+ if category_type == 'need':
+ pool_total = pools['needs']
+ pool_actual = sum(
+ monthly_avg_by_category.get(c, 0)
+ for c, t in category_types.items() if t == 'need'
+ )
+ else: # want
+ pool_total = pools['wants']
+ pool_actual = sum(
+ monthly_avg_by_category.get(c, 0)
+ for c, t in category_types.items() if t == 'want'
+ )
+
+ #apply spending limit goal if exists
+ if pool_actual > 0:
+ recommended = (avg_spent / pool_actual) * pool_total
+ else:
+ recommended = avg_spent * 0.9
+
+ #apply spending limit goal if exists
+ if goal and goal.get('target', 0) > 0:
+ goal_target_monthly = float(goal['target'])
+ recommended = min(recommended, goal_target_monthly)
+
+ return recommended# try to reduce spending by 10% if there is absolutely no income
+
+
+
+ def scale_budget(self, amount: float, period: str) -> float:
+ if period == 'w':
+ return amount / self.WEEKS_PER_MONTH
+ elif period == 'm':
+ return amount
+ else: # 'y'
+ return amount * 12
+
+
+
+ def generate_budget(
+ self,
+ transactions: List[Dict],
+ period: str,
+ avg_monthly_income: float,
+ goals: Optional[List[Dict]] = None
+ ) -> BudgetResponse:
+ goals = goals or []
+ income, expenses = self.separate_income_expenses(transactions)
+ monthly_avg = self.calculate_monthly_averages(expenses)
+ monthly_avg_savings = self.calculate_monthly_averages(income)
+ pools = self.calculate_budget_pools(avg_monthly_income)
+
+ savings_boost, wants_reduction, goal_categories = self.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg
+ )
+
+ if savings_boost > 0:
+ #transfer from wants to savings
+ pools['wants'] = max(0, pools['wants'] - savings_boost)
+ pools['savings'] = pools['savings'] + savings_boost
+
+ if wants_reduction > 0:
+ #reduce wants and increase savings
+ pools['wants'] = max(0, pools['wants'] - wants_reduction)
+ pools['savings'] = pools['savings'] + wants_reduction
+
+ category_types = self.classifier.classify_many(set(monthly_avg.keys()))
+
+ #ensure goal categories are included in category_types
+ for goal_cat in goal_categories:
+ if goal_cat not in category_types:
+ #default to 'want' if not classified
+ category_types[goal_cat] = 'want'
+
+ top_categories = self.get_top_categories(monthly_avg)
+ top_category_names = {cat for cat, _ in top_categories}
+
+ #add any missing goal categories to top_categories
+ for goal_cat in goal_categories:
+ if goal_cat not in top_category_names and goal_cat in monthly_avg:#some goals may have expenses in their categories
+ top_categories.append((goal_cat, monthly_avg[goal_cat]))
+ if goal_cat not in top_category_names and goal_cat in monthly_avg_savings:#some goals are just savings goals and their categories have no expenses, so they won't be in monthly expenses
+ top_categories.append((goal_cat, monthly_avg_savings[goal_cat]))
+ if goal_cat not in top_category_names:#lastly some goals may not have expenses or savings, so they're just 0
+ top_categories.append((goal_cat, 0))
+
+ #re-sort
+ top_categories.sort(key=lambda x: x[1], reverse=True)
+
+
+
+ budget_categories = []
+ for category, avg_spent in top_categories:
+ cat_type = category_types.get(category, 'want')
+ monthly_recommended = self.calculate_category_budget(
+ category, avg_spent, cat_type,
+ monthly_avg, category_types, pools, goals
+ )
+
+
+ scaled_recommended = self.scale_budget(monthly_recommended, period)
+
+ budget_categories.append(BudgetCategory(
+ category=category,
+ type=cat_type,
+ avg_monthly_spent=round(avg_spent, 2),
+ monthly_budget=round(monthly_recommended, 2),
+ recommended_budget=round(scaled_recommended, 2),
+ is_essential=cat_type == 'need'
+ ))
+
+ return BudgetResponse(
+ budget=budget_categories,
+ generated_date=pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')
+ )
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/logic/budgetPerfCalc.py b/app/server/services/python/analytics/src/logic/budgetPerfCalc.py
new file mode 100644
index 0000000..215984f
--- /dev/null
+++ b/app/server/services/python/analytics/src/logic/budgetPerfCalc.py
@@ -0,0 +1,94 @@
+from typing import List, Dict, Optional
+import pandas as pd
+from src.models.schemas import BudgetCategory, BudgetResponse
+from src.queries.budget import get_user_transactions_with_connection
+
+class BudgetPerformanceCalculator:
+ def __init__(self):
+ self.SECONDS_PER_DAY = 24 * 60 * 60 - 1
+
+ def get_period_boundaries(self, period: str, reference_date: Optional[pd.Timestamp] = None):
+ if reference_date is None:
+ reference_date = pd.Timestamp.now()
+
+ if period == 'w':
+ days_to_sunday = (reference_date.dayofweek + 1) % 7
+ period_start = (reference_date - pd.Timedelta(days=days_to_sunday)).normalize()
+ period_end = period_start + pd.Timedelta(days=6, hours=23, minutes=59, seconds=59)
+ elif period == 'm':
+ period_start = pd.Timestamp(reference_date.year, reference_date.month, 1)
+ next_month = period_start + pd.offsets.MonthBegin(1)
+ period_end = next_month - pd.Timedelta(seconds=1)
+ else: # 'y'
+ period_start = pd.Timestamp(reference_date.year, 1, 1)
+ period_end = pd.Timestamp(reference_date.year, 12, 31, 23, 59, 59)
+
+ return period_start, period_end
+
+ def calculate_actual_spending(self, transactions: List[Dict]) -> Dict[str, float]:
+ spending = {}
+ for t in transactions:
+ if t['amount'] < 0:
+ cat = t['category']
+ amount = abs(t['amount'])
+ spending[cat] = spending.get(cat, 0) + amount
+ return spending
+
+ def calculate_performance(
+ self,
+ budget_categories: List[BudgetCategory],
+ actual_spending: Dict[str, float],
+ period: str
+ ) -> dict:
+ performance_items = []
+
+ for budget_cat in budget_categories:
+ category = budget_cat.category
+ budget_amount = budget_cat.recommended_budget
+ actual = actual_spending.get(category, 0)
+
+ variance = actual - budget_amount
+ percent_used = (actual / budget_amount * 100) if budget_amount > 0 else 0
+
+ if variance > 100:
+ status = 'overspent'
+ elif variance < -100:
+ status = 'under_budget'
+ else:
+ status = 'on_track'
+
+ performance_items.append({
+ 'category': category,
+ 'budget': round(budget_amount, 2),
+ 'actual': round(actual, 2),
+ 'variance': round(variance, 2),
+ 'percent_used': round(percent_used, 1),
+ 'status': status
+ })
+
+ # sort by highest actual spending
+ performance_items.sort(key=lambda x: x['actual'], reverse=True)
+
+ return {
+ 'categories': [p['category'] for p in performance_items],
+ 'budgetAmounts': [p['budget'] for p in performance_items],
+ 'actualAmounts': [p['actual'] for p in performance_items]
+ }
+
+ async def generate_performance(
+ self,
+ user_id: int,
+ budget_response: BudgetResponse,
+ period: str,
+ reference_date: Optional[pd.Timestamp] = None
+ ) -> dict:
+ period_start, period_end = self.get_period_boundaries(period, reference_date)
+
+ start_str = period_start.strftime('%Y-%m-%d')
+ end_str = period_end.strftime('%Y-%m-%d')
+
+ transactions = get_user_transactions_with_connection(user_id, start_str, end_str)
+ actual_spending = self.calculate_actual_spending(transactions)
+
+ performance = self.calculate_performance(budget_response.budget, actual_spending, period)
+ return performance
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/logic/debt.py b/app/server/services/python/analytics/src/logic/debt.py
new file mode 100644
index 0000000..4dafcdf
--- /dev/null
+++ b/app/server/services/python/analytics/src/logic/debt.py
@@ -0,0 +1,48 @@
+import pandas as pd
+from typing import List, Optional
+from datetime import datetime, timedelta
+from src.models.schemas import DebtPayoffRequest, DebtPayoffResponse, DebtPayoffStage, BadRequestError
+
+
+def generate_debt_payoff_stages(dbr: DebtPayoffRequest) -> DebtPayoffResponse:
+ remaining_debt = dbr.remainingAmount
+ minimum_payment = dbr.minimumPayment
+ interest_rate = dbr.interestRate or 0
+
+ monthly_interest_rate = round(interest_rate / 12 / 100, 4) if interest_rate else 0
+
+ expected_date = datetime.strptime(dbr.nextDueDate, "%Y-%m-%d")
+ stages: List[DebtPayoffStage] = []
+
+ i = 1
+ while remaining_debt > 0:
+ interest = remaining_debt * monthly_interest_rate
+ principal = minimum_payment - interest
+
+ if principal > remaining_debt:
+ principal = remaining_debt
+
+ if principal <= 0:
+ raise BadRequestError("Minimum payment is too low. Debt will never be paid off.")
+
+ new_remaining = remaining_debt - principal
+
+ stages.append(DebtPayoffStage(
+ id=i,
+ principalAmount=round(principal, 2),
+ interestAmount=round(interest, 2),
+ remainingDebt=round(new_remaining, 2),
+ installmentDate=expected_date.strftime("%Y-%m-%d")
+ ))
+
+ remaining_debt = new_remaining
+ expected_date += timedelta(days=dbr.period)
+ i += 1
+
+ return DebtPayoffResponse(
+ id=dbr.id,
+ category=dbr.category,
+ minimumPayment=dbr.minimumPayment,
+ interestRate=interest_rate,
+ debtStages=stages,
+ )
diff --git a/app/server/services/python/analytics/src/logic/incomeflow.py b/app/server/services/python/analytics/src/logic/incomeflow.py
new file mode 100644
index 0000000..4a08f86
--- /dev/null
+++ b/app/server/services/python/analytics/src/logic/incomeflow.py
@@ -0,0 +1,61 @@
+import pandas as pd
+from typing import List, Dict
+
+def build_sankey_data(transactions: List[Dict]) -> Dict:
+ if not transactions:
+ return {
+ 'nodes': [{'name': 'total income'}, {'name': 'no data'}],
+ 'links': []
+ }
+
+ df = pd.DataFrame(transactions)
+ df_agg = df.groupby('category').agg({'amount': 'sum'}).reset_index()
+
+ total_income = df_agg[df_agg['amount'] > 0]['amount'].sum()
+ total_expenses = abs(df_agg[df_agg['amount'] < 0]['amount'].sum())
+
+ node_set = {'total income'}
+ for _, row in df_agg.iterrows():
+ node_set.add(row['category'])
+
+ if total_income > total_expenses:
+ node_set.add('unspent')
+ elif total_expenses > total_income:
+ node_set.add('overspent')
+ node_set.add('savings')
+
+ nodes_list = list(node_set)
+ node_to_index = {node: idx for idx, node in enumerate(nodes_list)}
+ links = []
+
+ for _, row in df_agg[df_agg['amount'] > 0].iterrows():
+ links.append({
+ 'source': node_to_index[row['category']],
+ 'target': node_to_index['total income'],
+ 'value': int(row['amount'])
+ })
+
+ for _, row in df_agg[df_agg['amount'] < 0].iterrows():
+ links.append({
+ 'source': node_to_index['total income'],
+ 'target': node_to_index[row['category']],
+ 'value': int(abs(row['amount']))
+ })
+
+ if total_income > total_expenses:
+ links.append({
+ 'source': node_to_index['total income'],
+ 'target': node_to_index['unspent'],
+ 'value': int(total_income - total_expenses)
+ })
+ elif total_expenses > total_income:
+ links.append({
+ 'source': node_to_index['savings'],
+ 'target': node_to_index['overspent'],
+ 'value': int(total_expenses - total_income)
+ })
+
+ return {
+ 'nodes': [{'name': node} for node in nodes_list],
+ 'links': links
+ }
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/logic/savings.py b/app/server/services/python/analytics/src/logic/savings.py
new file mode 100644
index 0000000..a822b14
--- /dev/null
+++ b/app/server/services/python/analytics/src/logic/savings.py
@@ -0,0 +1,105 @@
+import pandas as pd
+from typing import List, Dict
+from datetime import date
+from dateutil.relativedelta import relativedelta
+from src.models.schemas import ProjectedSavingsRequest, CompoundInterestResponse
+
+def calculate_savings_over_time(
+ accounts: List[Dict],
+ transactions: List[Dict],
+ start_date,
+ end_date,
+ period: str,
+ period_name: str
+) -> Dict:
+ df = pd.DataFrame(transactions)
+ if df.empty:
+ return {'labels': [], 'datasets': [{'label': f'{period_name} Savings', 'data': []}]}
+
+ df['date'] = pd.to_datetime(df['date'])
+ df = df[df['date'] >= start_date]
+ df = df.sort_values('date')
+
+ account_balances = {acc['id']: acc['balance'] for acc in accounts}
+ current_balances = account_balances.copy()
+ savings_over_time = []
+
+ if period in ['w', 'm']:
+ date_range = pd.date_range(start=start_date, end=end_date, freq='D')
+ for date in date_range:
+ day_transactions = df[df['date'].dt.date == date.date()]
+ for _, tx in day_transactions.iterrows():
+ current_balances[tx['financialAccount_id']] += tx['amount']
+ savings_over_time.append({
+ 'date': date.strftime('%Y-%m-%d'),
+ 'savings': sum(current_balances.values())
+ })
+ else:
+ df['year_month'] = df['date'].dt.to_period('M')
+ monthly_groups = df.groupby('year_month')
+ for month in pd.date_range(start=start_date, end=end_date, freq='M'):#change this for production - GitHub actions might fail if this is ME, and work with M instead
+ month_str = month.strftime('%Y-%m')
+ month_period = pd.Period(month_str, freq='M')
+ if month_period in monthly_groups.groups:
+ for _, tx in monthly_groups.get_group(month_period).iterrows():
+ current_balances[tx['financialAccount_id']] += tx['amount']
+ savings_over_time.append({'date': month_str, 'savings': sum(current_balances.values())})
+
+ return {
+ 'labels': [item['date'] for item in savings_over_time],
+ 'datasets': [{
+ 'label': f'{period_name} Savings',
+ 'data': [item['savings'] for item in savings_over_time]
+ }]
+ }
+
+
+def calculate_compound_interest(request: ProjectedSavingsRequest) -> List[CompoundInterestResponse]:
+ annual_rate = request.annual_interest_rate / 100
+
+ monthly_rate = annual_rate / 12
+
+ worst_balance = request.balance
+ expected_balance = request.balance
+ best_balance = request.balance
+
+ monthly_deposit = request.monthly_deposit
+ time_frame = request.time_frame
+
+ results: List[CompoundInterestResponse] = []
+
+ months = time_frame * 12
+ current_date = date.today()
+
+ # Monte Carlo simulation approach is more robust
+ best_monthly_rate = monthly_rate * 2
+ worst_monthly_rate = monthly_rate * 0.5
+
+ for month in range(months):
+ # add monthly deposit at beginning of month
+ worst_balance += monthly_deposit
+ expected_balance += monthly_deposit
+ best_balance += monthly_deposit
+
+ # apply compound interest (standard formula)
+ worst_balance = worst_balance * (1 + worst_monthly_rate)
+ expected_balance = expected_balance * (1 + monthly_rate)
+ best_balance = best_balance * (1 + best_monthly_rate)
+
+ worst_balance = round(worst_balance, 2)
+ expected_balance = round(expected_balance, 2)
+ best_balance = round(best_balance, 2)
+
+ # calculate next month's date
+ next_date = current_date + relativedelta(months=month + 1)
+
+ results.append(
+ CompoundInterestResponse(
+ accumulative_worst_balance=worst_balance,
+ accumulative_expected_balance=expected_balance,
+ accumulative_best_balance=best_balance,
+ date=next_date.strftime("%B %Y")
+ )
+ )
+
+ return results
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/main.py b/app/server/services/python/analytics/src/main.py
new file mode 100644
index 0000000..9393c48
--- /dev/null
+++ b/app/server/services/python/analytics/src/main.py
@@ -0,0 +1,178 @@
+from fastapi import FastAPI, Depends, Query, HTTPException
+from fastapi.responses import PlainTextResponse
+from fastapi.middleware.cors import CORSMiddleware
+import pandas as pd
+from typing import List
+import uvicorn
+import os
+
+
+from src.dependencies import get_db_connection, get_current_user
+from src.utils.dates import period_calc
+from src.logic.budget import generate_budget, generate_budget_performance
+from src.queries import savings as savings_queries, incomeflow as incomeflow_queries
+from src.logic import savings as savings_service, incomeflow as incomeflow_service, debt as debt_service
+from src.models.schemas import (
+ BadRequestError,
+ ProjectedSavingsRequest,
+ CompoundInterestResponse,
+ DebtPayoffRequest
+)
+
+
+app = FastAPI()
+
+# CORS setup
+origins = [
+ "http://localhost:8080",
+ "http://localhost:3000",
+ "http://127.0.0.1:8080",
+ "http://127.0.0.1:3000",
+ "http://18.190.215.135", #prod webserver
+ "http://3.142.125.202", #dev webserver
+]
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.get('/charts/savings')
+async def get_savings(period: str, user_id: int = Depends(get_current_user)):
+ try:
+ connection = get_db_connection()
+ if not connection:
+ raise HTTPException(status_code=500, detail="Database connection failed")
+
+ cursor = connection.cursor(dictionary=True)
+
+ # Get savings accounts
+ accounts = savings_queries.get_savings_accounts(cursor, user_id)
+ if not accounts:
+ cursor.close()
+ connection.close()
+ return None
+
+ # Get transactions
+ account_ids = [acc['id'] for acc in accounts]
+ transactions = savings_queries.get_savings_transactions(cursor, account_ids)
+
+ cursor.close()
+ connection.close()
+
+ if not transactions:
+ return None
+
+ # Calculate date range
+ end_date = pd.Timestamp.today()
+ start_date, _, _, period_name = period_calc(period, end_date)
+
+ # Process data
+ result = savings_service.calculate_savings_over_time(
+ accounts, transactions, start_date, end_date, period, period_name
+ )
+
+ return result
+
+ except Exception as e:
+ print(f"Error: {e}")
+ raise HTTPException(status_code=500, detail=f"Error: {e}")
+
+
+@app.get('/charts/incomeflow')
+async def get_income_flow(
+ period: str = Query(default='w', enum=['w', 'm', 'y']),
+ user_id: int = Depends(get_current_user)
+):
+ try:
+ connection = get_db_connection()
+ if not connection:
+ raise HTTPException(status_code=500, detail="Database connection failed")
+
+ cursor = connection.cursor(dictionary=True)
+
+ # Calculate date range
+ end_date = pd.Timestamp.today()
+ start_date, _, _, _ = period_calc(period, end_date)
+
+ # Get transactions
+ transactions = incomeflow_queries.get_user_transactions(cursor, user_id, start_date, end_date)
+
+ cursor.close()
+ connection.close()
+
+ if not transactions:
+ return None
+
+ # Build sankey data
+ return incomeflow_service.build_sankey_data(transactions)
+
+ except Exception as e:
+ print(f"Error generating income flow: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Error generating income flow: {str(e)}")
+
+
+@app.get('/charts/budget-expenditure')
+async def get_budget(
+ period: str = Query(default='w', enum=['w', 'm', 'y']),
+ user_id: int = Depends(get_current_user)
+):
+ try:
+ print(f"Generating budget for user {user_id} with period {period}")
+ budget = generate_budget(period, user_id)
+ performance = await generate_budget_performance(user_id, budget, period)
+ print(f'generated performance: {performance}')
+ return performance
+ except Exception as e:
+ print(e)
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.post('/predict-debt-payoff')
+async def predict_debt_payoff(
+ requestBody: DebtPayoffRequest,
+ user_id: int = Depends(get_current_user),
+):
+ try:
+ print(f"Generating predicted debt payoff for user {user_id}")
+ print(requestBody)
+ return debt_service.generate_debt_payoff_stages(requestBody)
+ except BadRequestError as e:
+ raise HTTPException(status_code=400, detail=str(e))
+ except Exception as e:
+ print(e)
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.post('/compound-interest')
+async def compound_interest(
+ requestBody: ProjectedSavingsRequest,
+ user_id: int = Depends(get_current_user),
+) -> List[CompoundInterestResponse]:
+ try:
+ print(f"Generating compound interests for user {user_id}")
+ print(requestBody)
+ return savings_service.calculate_compound_interest(requestBody)
+ except Exception as e:
+ print(e)
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get('/health', response_class=PlainTextResponse)
+async def health():
+ status = 'ok'
+ try:
+ conn = get_db_connection(1)
+ conn.close()
+ except Exception:
+ status = 'unhealthy'
+
+ return status
+
+if __name__ == "__main__":
+ port = int(os.getenv("PORT", 8000))
+ uvicorn.run(app, port=port)
diff --git a/app/server/services/python/analytics/src/models/__init__.py b/app/server/services/python/analytics/src/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/server/services/python/analytics/src/models/schemas.py b/app/server/services/python/analytics/src/models/schemas.py
new file mode 100644
index 0000000..26323d6
--- /dev/null
+++ b/app/server/services/python/analytics/src/models/schemas.py
@@ -0,0 +1,103 @@
+from numpy import number
+from pydantic import BaseModel, Field
+from typing import List, Literal, Optional
+from datetime import datetime
+
+class SavingsDataPoint(BaseModel):
+ date: str
+ savings: float
+
+class SavingsChartResponse(BaseModel):
+ labels: List[str]
+ datasets: List[dict]
+
+class SankeyNode(BaseModel):
+ name: str
+
+class SankeyLink(BaseModel):
+ source: int
+ target: int
+ value: int
+
+class SankeyResponse(BaseModel):
+ nodes: List[SankeyNode]
+ links: List[SankeyLink]
+
+class Transaction(BaseModel):
+ amount: float
+ category: str
+ date: datetime
+ account_name: Optional[str] = None
+
+class BudgetCategory(BaseModel):
+ category: str
+ type: Literal['need', 'want', 'savings']
+ avg_monthly_spent: float
+ monthly_budget: float
+ recommended_budget: float
+ is_essential: bool
+
+class BudgetResponse(BaseModel):
+ budget: List[BudgetCategory]
+ generated_date: str
+
+class BudgetPerformanceItem(BaseModel):
+ category: str
+ budget: float
+ actual: float
+ variance: float
+ percent_used: float
+ status: Literal['overspent', 'under_budget', 'on_track']
+
+class BudgetPerformanceResponse(BaseModel):
+ categories: List[str]
+ budgetAmounts: List[float]
+ actualAmounts: List[float]
+
+class DebtPayoffRequest(BaseModel):
+ id: str
+ category: str
+ remainingAmount: float
+ minimumPayment: float = Field(..., gt=0, description="Minimum payment must be greater than 0")
+ interestRate: Optional[float] = Field(default=0, description="Interest rate in percentage")
+ nextDueDate: str # YYYY-MM-DD
+ period: int = Field(..., gt=0, description="Period must be greater than 0") # in days
+
+class DebtPayoffStage(BaseModel):
+ id: int
+ principalAmount: float
+ interestAmount: float
+ remainingDebt: float
+ installmentDate: str
+
+class DebtPayoffResponse(BaseModel):
+ id: str
+ category: str
+ minimumPayment: float
+ interestRate: float
+ debtStages: List[DebtPayoffStage]
+
+class ProjectedSavingsRequest(BaseModel):
+ financial_account_id: int
+ balance: float
+ monthly_deposit: float = Field(..., gt=0, description="Monthly deposit must be greater than 0")
+ annual_interest_rate: Optional[float] = None # annual interest rate in percentage
+ time_frame: int = Field(..., gt=0, description="Time frame must be greater than 0") # in years
+class MonthlySavingGrowthRate(BaseModel):
+ best_case: float
+ expected_case: float
+ worst_case: float
+class ProjectedSavingsResponse(BaseModel):
+ balance: float
+ monthly_contribution: float
+ interest_rate: float | None # annual interest rate in percentage
+ time_frame: int # in years
+ stat: List[MonthlySavingGrowthRate]
+class CompoundInterestResponse(BaseModel):
+ accumulative_best_balance: float
+ accumulative_expected_balance: float
+ accumulative_worst_balance: float
+ date: str
+
+class BadRequestError(Exception):
+ pass
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/queries/__init__.py b/app/server/services/python/analytics/src/queries/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/server/services/python/analytics/src/queries/budget.py b/app/server/services/python/analytics/src/queries/budget.py
new file mode 100644
index 0000000..6306a78
--- /dev/null
+++ b/app/server/services/python/analytics/src/queries/budget.py
@@ -0,0 +1,62 @@
+from typing import List, Dict, Any
+from src.dependencies import get_db_connection
+
+def get_user_transactions(cursor, user_id: int, start_date: str, end_date: str) -> List[Dict[str, Any]]:
+ """Fetch user transactions within date range."""
+ query = """
+ SELECT t.amount, t.category, t.date, fa.name as account_name
+ FROM finus.transaction t
+ JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id
+ JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id
+ JOIN finus.profile p ON pfa.profile_id = p.id
+ JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id
+ JOIN finus.finusAccount u ON uap.account_id = u.id
+ WHERE u.id = %s AND t.date BETWEEN %s AND %s
+ ORDER BY t.date
+ """
+ cursor.execute(query, (user_id, start_date, end_date))
+ return cursor.fetchall()
+
+def get_user_transactions_with_connection(user_id: int, start_date: str, end_date: str):
+ connection = get_db_connection()
+ cursor = connection.cursor(dictionary=True)
+ try:
+ return get_user_transactions(cursor, user_id, start_date, end_date)
+ finally:
+ cursor.close()
+ connection.close()
+
+#this was added to make goals influence the budget - analytics service will fetch goals on its own
+def get_user_goals(user_id: int) -> List[Dict]:
+ """Fetch active goals for a user."""
+ connection = get_db_connection()
+ cursor = connection.cursor(dictionary=True)
+
+ #first get the user's profile
+ cursor.execute("""
+ SELECT profile_id
+ FROM finus.finusAccount_profile
+ WHERE account_id = %s
+ """, (user_id,))
+ profile = cursor.fetchone()
+
+ if not profile:
+ cursor.close()
+ connection.close()
+ return []
+
+ profile_id = profile['profile_id']
+
+ #fetch goals for this profile
+ cursor.execute("""
+ SELECT g.*
+ FROM finus.goal g
+ JOIN finus.profile_goal pg ON g.id = pg.goal_id
+ WHERE pg.profile_id = %s
+ """, (profile_id,))
+
+ goals = cursor.fetchall()
+ cursor.close()
+ connection.close()
+
+ return goals
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/queries/incomeflow.py b/app/server/services/python/analytics/src/queries/incomeflow.py
new file mode 100644
index 0000000..c1391e5
--- /dev/null
+++ b/app/server/services/python/analytics/src/queries/incomeflow.py
@@ -0,0 +1,15 @@
+from typing import List, Dict
+
+def get_user_transactions(cursor, user_id: int, start_date, end_date) -> List[Dict]:
+ cursor.execute("""
+ SELECT t.amount, t.category, t.date
+ FROM finus.transaction t
+ JOIN finus.financialAccount fa ON t.financialAccount_id = fa.id
+ JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id
+ JOIN finus.profile p ON pfa.profile_id = p.id
+ JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id
+ JOIN finus.finusAccount u ON uap.account_id = u.id
+ WHERE u.id = %s AND t.date BETWEEN %s AND %s
+ ORDER BY t.date
+ """, (user_id, start_date, end_date))
+ return cursor.fetchall()
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/queries/savings.py b/app/server/services/python/analytics/src/queries/savings.py
new file mode 100644
index 0000000..b7909f0
--- /dev/null
+++ b/app/server/services/python/analytics/src/queries/savings.py
@@ -0,0 +1,23 @@
+from typing import List, Dict
+
+def get_savings_accounts(cursor, user_id: int) -> List[Dict]:
+ cursor.execute("""
+ SELECT fa.id, fa.balance
+ FROM finus.financialAccount fa
+ JOIN finus.profile_financialAccount pfa ON fa.id = pfa.financialAccount_id
+ JOIN finus.profile p ON pfa.profile_id = p.id
+ JOIN finus.finusAccount_profile uap ON p.id = uap.profile_id
+ JOIN finus.finusAccount u ON uap.account_id = u.id
+ WHERE u.id = %s AND fa.type = 'savings'
+ """, (user_id,))
+ return cursor.fetchall()
+
+def get_savings_transactions(cursor, account_ids: List[int]) -> List[Dict]:
+ placeholders = ','.join(['%s'] * len(account_ids))
+ cursor.execute(f"""
+ SELECT financialAccount_id, amount, date
+ FROM finus.transaction
+ WHERE financialAccount_id IN ({placeholders})
+ ORDER BY date ASC
+ """, account_ids)
+ return cursor.fetchall()
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/utils/__init__.py b/app/server/services/python/analytics/src/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/server/services/python/analytics/src/utils/dates.py b/app/server/services/python/analytics/src/utils/dates.py
new file mode 100644
index 0000000..724be06
--- /dev/null
+++ b/app/server/services/python/analytics/src/utils/dates.py
@@ -0,0 +1,22 @@
+import pandas as pd
+from typing import Tuple
+
+def period_calc(period: str, end_date: pd.Timestamp) -> Tuple[pd.Timestamp, str, str, str]:
+ """Calculate date range based on period"""
+ if period == 'w':
+ start_date = end_date - pd.Timedelta(days=7)
+ freq = 'D'
+ date_format = '%Y-%m-%d'
+ period_name = 'Weekly'
+ elif period == 'm':
+ start_date = end_date - pd.Timedelta(days=30)
+ freq = 'D'
+ date_format = '%Y-%m-%d'
+ period_name = 'Monthly'
+ else: # 'y'
+ start_date = end_date - pd.Timedelta(days=365)
+ freq = 'M'
+ date_format = '%Y-%m'
+ period_name = 'Yearly'
+
+ return start_date, freq, date_format, period_name
\ No newline at end of file
diff --git a/app/server/services/python/analytics/src/utils/trans_cat_classifier.py b/app/server/services/python/analytics/src/utils/trans_cat_classifier.py
new file mode 100644
index 0000000..0a9cde0
--- /dev/null
+++ b/app/server/services/python/analytics/src/utils/trans_cat_classifier.py
@@ -0,0 +1,40 @@
+from typing import Set, Dict
+
+# this handles classification of transaction categories
+# expand on these lists in the future - can also make them dynamic with ML
+class CategoryClassifier:
+ def __init__(self):
+ self.needs_categories: Set[str] = {
+ 'rent', 'mortgage', 'utilities', 'groceries', 'healthcare',
+ 'insurance', 'transportation', 'gas', 'electricity', 'water',
+ 'internet', 'phone', 'minimum_debt_payment'
+ }
+
+ self.wants_categories: Set[str] = {
+ 'dining', 'restaurant', 'entertainment', 'shopping', 'clothing',
+ 'travel', 'vacation', 'hobbies', 'streaming', 'subscriptions',
+ 'coffee', 'bars', 'alcohol'
+ }
+
+ self.savings_categories: Set[str] = {
+ 'savings', 'investment', 'retirement', 'emergency_fund'
+ }
+
+ def classify(self, category: str) -> str:
+ """Classify a single category."""
+ cat_lower = category.lower()
+ if cat_lower in self.needs_categories:
+ return 'need'
+ elif cat_lower in self.wants_categories:
+ return 'want'
+ elif cat_lower in self.savings_categories:
+ return 'savings'
+ return 'want' # default
+
+ def classify_many(self, categories: Set[str]) -> Dict[str, str]:
+ """Classify multiple categories."""
+ return {cat: self.classify(cat) for cat in categories}
+
+ def get_categories_by_type(self, category_types: Dict[str, str], target_type: str) -> Set[str]:
+ """Get all categories of a specific type."""
+ return {cat for cat, typ in category_types.items() if typ == target_type}
\ No newline at end of file
diff --git a/app/server/services/python/analytics/test/__init__.py b/app/server/services/python/analytics/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/server/services/python/analytics/test/conftest.py b/app/server/services/python/analytics/test/conftest.py
new file mode 100644
index 0000000..1dfc278
--- /dev/null
+++ b/app/server/services/python/analytics/test/conftest.py
@@ -0,0 +1,167 @@
+import pytest
+import pandas as pd
+from unittest.mock import patch, MagicMock
+from datetime import datetime, timedelta
+from src.models.schemas import BudgetCategory, BudgetResponse, DebtPayoffRequest
+from src.utils.trans_cat_classifier import CategoryClassifier
+import jwt
+import os
+
+@pytest.fixture
+def sample_transactions():
+ base_date = datetime.now() - timedelta(days=30)
+ return [
+ {'amount': 5000, 'category': 'salary', 'date': base_date},
+ {'amount': 200, 'category': 'groceries', 'date': base_date + timedelta(days=2)},
+ {'amount': -50, 'category': 'dining', 'date': base_date + timedelta(days=3)},
+ {'amount': -100, 'category': 'groceries', 'date': base_date + timedelta(days=5)},
+ {'amount': -30, 'category': 'coffee', 'date': base_date + timedelta(days=7)},
+ {'amount': -200, 'category': 'entertainment', 'date': base_date + timedelta(days=10)},
+ {'amount': 5000, 'category': 'salary', 'date': base_date + timedelta(days=30)},
+ ]
+
+@pytest.fixture
+def sample_budget_response():
+ categories = [
+ BudgetCategory(
+ category='groceries',
+ type='need',
+ avg_monthly_spent=300,
+ monthly_budget=280,
+ recommended_budget=65, # weekly
+ is_essential=True
+ ),
+ BudgetCategory(
+ category='dining',
+ type='want',
+ avg_monthly_spent=200,
+ monthly_budget=150,
+ recommended_budget=35, # weekly
+ is_essential=False
+ ),
+ BudgetCategory(
+ category='entertainment',
+ type='want',
+ avg_monthly_spent=150,
+ monthly_budget=120,
+ recommended_budget=28, # weekly
+ is_essential=False
+ ),
+ ]
+ return BudgetResponse(
+ budget=categories,
+ generated_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ )
+
+@pytest.fixture
+def classifier():
+ return CategoryClassifier()
+
+@pytest.fixture(autouse=True)
+def mock_db_queries():
+ with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock:
+ yield mock
+
+
+@pytest.fixture
+def mock_savings_accounts():
+ return [
+ {'id': 1, 'balance': 5000},
+ {'id': 2, 'balance': 3000},
+ ]
+
+@pytest.fixture
+def mock_savings_transactions():
+ base_date = datetime.now() - timedelta(days=30)
+ return [
+ {'financialAccount_id': 1, 'amount': 100, 'date': base_date + timedelta(days=5)},
+ {'financialAccount_id': 1, 'amount': 200, 'date': base_date + timedelta(days=10)},
+ {'financialAccount_id': 2, 'amount': -50, 'date': base_date + timedelta(days=7)},
+ {'financialAccount_id': 2, 'amount': 150, 'date': base_date + timedelta(days=15)},
+ {'financialAccount_id': 1, 'amount': -75, 'date': base_date + timedelta(days=20)},
+ ]
+@pytest.fixture
+def mock_savings_transactions_by_financial_account():
+ return pd.DataFrame([
+ {"financialAccountId": "1", "date": "2026-01-10", "amount": 100},
+ {"financialAccountId": "1", "date": "2025-01-20", "amount": 50},
+ {"financialAccountId": "1", "date": "2026-02-01", "amount": 200},
+ ])
+
+@pytest.fixture
+def mock_incomeflow_transactions():
+ base_date = datetime.now() - timedelta(days=30)
+ return [
+ # Income transactions
+ {'amount': 5000, 'category': 'salary', 'date': base_date},
+ {'amount': 200, 'category': 'freelance', 'date': base_date + timedelta(days=7)},
+ {'amount': 100, 'category': 'interest', 'date': base_date + timedelta(days=14)},
+ # Expense transactions
+ {'amount': -1500, 'category': 'rent', 'date': base_date + timedelta(days=1)},
+ {'amount': -300, 'category': 'groceries', 'date': base_date + timedelta(days=3)},
+ {'amount': -200, 'category': 'dining', 'date': base_date + timedelta(days=5)},
+ {'amount': -100, 'category': 'entertainment', 'date': base_date + timedelta(days=8)},
+ {'amount': -400, 'category': 'utilities', 'date': base_date + timedelta(days=12)},
+ ]
+@pytest.fixture
+def mock_transactions_with_same_amount():
+ return [
+ {"date": "2026-01-01", "amount": 100},
+ {"date": "2026-02-01", "amount": 100},
+ {"date": "2026-03-01", "amount": 100},
+ {"date": "2026-04-01", "amount": 100},
+ {"date": "2026-05-01", "amount": 100},
+ ]
+
+@pytest.fixture
+def mock_monthly_diff():
+ return pd.DataFrame({
+ "month": [1, 2, 3],
+ "amount": [0, 0, 100],
+ "balance": [0, 0, 100]
+ })
+
+@pytest.fixture
+def mock_debt_payoff_request():
+ return DebtPayoffRequest(
+ id="1",
+ category="credit_card",
+ remainingAmount=1000.0,
+ minimumPayment=200.0,
+ interestRate=12.0,
+ nextDueDate="2024-01-01",
+ period=30
+ )
+
+@pytest.fixture
+def mock_env_vars():
+ with patch.dict(os.environ, {
+ 'JWT_SECRET': 'test_secret_key_12345',
+ 'MYSQL_HOST': 'localhost',
+ 'MYSQL_USER': 'test_user',
+ 'MYSQL_PASSWORD': 'test_password',
+ 'DB_NAME': 'test_db'
+ }):
+ yield
+
+@pytest.fixture
+def valid_token():
+ payload = {'sub': '123', 'user_id': '123', 'id': '123'}
+ return jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256')
+
+@pytest.fixture
+def expired_token():
+ import time
+ payload = {'sub': '123', 'exp': int(time.time()) - 3600}
+ return jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256')
+
+@pytest.fixture
+def invalid_token():
+ return "invalid.token.string"
+
+@pytest.fixture
+def mock_db_connection():
+ mock_conn = MagicMock()
+ mock_cursor = MagicMock()
+ mock_conn.cursor.return_value = mock_cursor
+ return mock_conn
\ No newline at end of file
diff --git a/app/server/services/python/analytics/test/test_budgetCalc.py b/app/server/services/python/analytics/test/test_budgetCalc.py
new file mode 100644
index 0000000..7811609
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_budgetCalc.py
@@ -0,0 +1,622 @@
+import pytest
+from unittest.mock import Mock, patch
+from src.logic.budgetCalc import BudgetCalculator
+from src.utils.trans_cat_classifier import CategoryClassifier
+from src.queries import budget
+
+class TestBudgetCalculator:
+
+ @pytest.fixture
+ def calculator(self):
+ classifier = CategoryClassifier()
+ return BudgetCalculator(classifier)
+
+ def test_separate_income_expenses(self, calculator, sample_transactions):
+ income, expenses = calculator.separate_income_expenses(sample_transactions)
+
+ assert len(income) == 3 # Three positive transactions
+ assert len(expenses) == 4 # Four expense transactions
+ assert all(t['amount'] > 0 for t in income)
+ assert all(t['amount'] < 0 for t in expenses)
+
+ def test_calculate_monthly_averages(self, calculator):
+ """Test calculating monthly averages per category."""
+ expenses = [
+ {'category': 'groceries', 'amount': -300},
+ {'category': 'groceries', 'amount': -150},
+ {'category': 'dining', 'amount': -200},
+ {'category': 'entertainment', 'amount': -100},
+ ]
+
+ # With 12 months of data, the function divides by 12
+ result = calculator.calculate_monthly_averages(expenses)
+
+ assert result['groceries'] == 450 / 12 # 37.5
+ assert result['dining'] == 200 / 12 # 16.67
+ assert result['entertainment'] == 100 / 12 # 8.33
+
+ def test_get_top_categories(self, calculator):
+ """Test getting top N categories by spending."""
+ monthly_avg = {
+ 'groceries': 500,
+ 'rent': 1500,
+ 'dining': 300,
+ 'entertainment': 200,
+ 'utilities': 400
+ }
+
+ top_3 = calculator.get_top_categories(monthly_avg, 3)
+ assert len(top_3) == 3
+ assert top_3[0][0] == 'rent' # Highest
+ assert top_3[1][0] == 'groceries'
+ assert top_3[2][0] == 'utilities'
+
+ def test_calculate_budget_pools(self, calculator):
+ """Test 50/30/20 budget pool calculation."""
+ avg_income = 5000
+ pools = calculator.calculate_budget_pools(avg_income)
+
+ assert pools['needs'] == 2500 # 50%
+ assert pools['wants'] == 1500 # 30%
+ assert pools['savings'] == 1000 # 20%
+
+ def test_calculate_category_budget_need(self, calculator):
+ monthly_avg = {'rent': 1200, 'groceries': 400}
+ category_types = {'rent': 'need', 'groceries': 'need'}
+ pools = {'needs': 2000, 'wants': 1000, 'savings': 500}
+
+ result = calculator.calculate_category_budget(
+ 'rent', 1200, 'need', monthly_avg, category_types, pools
+ )
+
+ # rent is 1200/1600 of needs pool = 1500
+ assert result == 1500
+
+ def test_calculate_category_budget_want(self, calculator):
+ monthly_avg = {'dining': 500, 'entertainment': 300}
+ category_types = {'dining': 'want', 'entertainment': 'want'}
+ pools = {'needs': 2000, 'wants': 1000, 'savings': 500}
+
+ result = calculator.calculate_category_budget(
+ 'dining', 500, 'want', monthly_avg, category_types, pools
+ )
+
+ # dining is 500/800 of wants pool = 625
+ assert result == 625
+
+ def test_scale_budget(self, calculator):
+ monthly_amount = 1000
+
+ weekly = calculator.scale_budget(monthly_amount, 'w')
+ monthly = calculator.scale_budget(monthly_amount, 'm')
+ yearly = calculator.scale_budget(monthly_amount, 'y')
+
+ assert weekly == 1000 / 4.33
+ assert monthly == 1000
+ assert yearly == 12000
+
+ @pytest.mark.asyncio
+ async def test_generate_budget(self, calculator, sample_transactions):
+ avg_income = 5000
+
+ result = calculator.generate_budget(sample_transactions, 'w', avg_income)
+
+ assert hasattr(result, 'budget')
+ assert hasattr(result, 'generated_date')
+ assert len(result.budget) <= 10 # At most 10 categories
+
+
+ #check if multiple saving goals adds 5% boost each
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_save_goals(self, calculator):
+ goals = [
+ {'type': 'save', 'category': 'vacation'},
+ {'type': 'save', 'category': 'emergency_fund'},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {}
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ # 2 goals * 5% of $5000 = $500
+ assert savings_boost == 500
+ assert wants_reduction == 0
+ assert set(categories) == {'vacation', 'emergency_fund'}
+
+
+ #check if a single savings goal adds a 5% boost
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_single_save_goal(self, calculator):
+ goals = [
+ {'type': 'save', 'category': 'vacation'},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {}
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ assert savings_boost == 250 # 5% of $5000
+ assert wants_reduction == 0
+ assert categories == ['vacation']
+
+
+
+ #test reduce spending goal when spending exceeds limit (>=100%)
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_reduce_spending_over_limit(self, calculator):
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 400},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {'groceries': 450} # Over limit
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ assert savings_boost == 0
+ assert wants_reduction == 5000 * 0.07 # 7% of $5000 = $350
+ assert categories == ['groceries']
+
+
+ #test reduce spending goal when spending is 80-99% of limit
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_reduce_spending_approaching_limit(self, calculator):
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 100},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {'groceries': 87.5} # 87.5% of limit
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ assert savings_boost == 0
+ assert wants_reduction == 5000 * 0.05 # 5% of $5000 = $250
+ assert categories == ['groceries']
+
+
+ #test reduce spending goal when spending is 60-79% of limit
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_reduce_spending_getting_close(self, calculator):
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 100},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {'groceries': 70} # 70% of limit
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ assert savings_boost == 0
+ assert wants_reduction == 5000 * 0.03 # 3% of $5000 = $150
+ assert categories == ['groceries']
+
+
+ #test reduce spending goal when spending is below 60% of limit
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_reduce_spending_below_threshold(self, calculator):
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 100},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {'groceries': 50} # 50% of limit
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ assert savings_boost == 0
+ assert wants_reduction == 0
+ assert categories == ['groceries']
+
+
+ #test multiple reduce spending goals - should take the max reduction
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_multiple_reduce_spending_goals(self, calculator):
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 100},
+ {'type': 'reduce_spending', 'category': 'dining', 'target': 100},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {
+ 'groceries': 101, #over limit - 7% reduction
+ 'dining': 83, # 83% of limit - 5% reduction
+ }
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ assert savings_boost == 0
+ #should take the max reduction (7% from groceries)
+ assert wants_reduction == 5000 * 0.07 # $350
+ assert set(categories) == {'groceries', 'dining'}
+
+
+ #test mixed save and reduce spending goals
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_mixed_goals(self, calculator):
+ goals = [
+ {'type': 'save', 'category': 'vacation'},
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 100},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {'groceries': 101} # Over limit
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ #save goal: 5% boost = $250
+ assert savings_boost == 250
+ #reduce spending: 7% reduction = $350
+ assert round(wants_reduction) == 350
+ assert set(categories) == {'vacation', 'groceries'}
+
+
+ #test category budget calculation with a reduce_spending goal cap
+ @pytest.mark.asyncio
+ async def test_calculate_category_budget_with_reduce_spending_goal(self, calculator):
+ monthly_avg = {'groceries': 300}
+ category_types = {'groceries': 'want'}
+ pools = {'needs': 2000, 'wants': 1000, 'savings': 500}
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 250}
+ ]
+
+ #without goal, recommended would be (300 / 300) * 1000 = 1000
+ #with goal, should be capped at 250
+ result = calculator.calculate_category_budget(
+ 'groceries', 300, 'want', monthly_avg, category_types, pools, goals
+ )
+
+ assert result == 250
+
+
+ #test category budget where goal cap is higher than calculated budget
+ @pytest.mark.asyncio
+ async def test_calculate_category_budget_with_goal_above_cap(self, calculator):
+ monthly_avg = {'groceries': 300}
+ category_types = {'groceries': 'want'}
+ pools = {'needs': 2000, 'wants': 500, 'savings': 500}
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 400}
+ ]
+
+ #without goal: (300 / 300) * 500 = 500
+ #goal cap is 400, but calculated is 500, so cap to 400
+ result = calculator.calculate_category_budget(
+ 'groceries', 300, 'want', monthly_avg, category_types, pools, goals
+ )
+
+ assert result == 400
+
+
+ #test category budget where goal doesn't affect the calculation
+ @pytest.mark.asyncio
+ async def test_calculate_category_budget_with_no_goal_impact(self, calculator):
+ monthly_avg = {'groceries': 300}
+ category_types = {'groceries': 'want'}
+ pools = {'needs': 2000, 'wants': 1000, 'savings': 500}
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 500}
+ ]
+
+ # Calculated budget is 1000, goal cap is 500, so cap to 500
+ result = calculator.calculate_category_budget(
+ 'groceries', 300, 'want', monthly_avg, category_types, pools, goals
+ )
+
+ # Since calculated budget is 1000, it gets capped to 500
+ assert result == 500
+
+
+
+ #test full budget generation with goals that influence the pools
+ @pytest.mark.asyncio
+ async def test_generate_budget_with_goals(self, calculator, sample_transactions):
+ avg_income = 5000
+ goals = [
+ {'type': 'save', 'category': 'vacation'},
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 300},
+ ]
+
+ #create transactions with high grocery spending to trigger reduction
+ sample_transactions.append({'amount': -350, 'category': 'groceries', 'date': '2024-01-15'})
+
+ result = calculator.generate_budget(sample_transactions, 'w', avg_income, goals)
+
+ assert hasattr(result, 'budget')
+ assert len(result.budget) > 0
+
+ #verify that the grocery category appears and has a capped budget
+ grocery_goal = next((g for g in result.budget if g.category == 'groceries'), None)
+ if grocery_goal:
+ #should be capped at 300 (or less)
+ assert grocery_goal.recommended_budget <= 300
+
+
+ #test full budget generation with only save goals
+ @pytest.mark.asyncio
+ async def test_generate_budget_with_save_goal_only(self, calculator, sample_transactions):
+ avg_income = 5000
+
+ goals = [
+ {'type': 'save', 'category': 'vacation'},
+ {'type': 'save', 'category': 'emergency_fund'},
+ ]
+
+ result = calculator.generate_budget(sample_transactions, 'w', avg_income, goals)
+
+ assert hasattr(result, 'budget')
+ #verify goal categories are included
+ goal_categories = {g.category for g in result.budget}
+ assert 'vacation' in goal_categories
+ assert 'emergency_fund' in goal_categories
+
+
+ #test full budget generation with reduce spending goals
+ @pytest.mark.asyncio
+ async def test_generate_budget_with_reduce_spending_goal(self, calculator, sample_transactions):
+ avg_income = 5000
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 300},
+ ]
+
+ #add high grocery spending
+ sample_transactions.append({'amount': -350, 'category': 'groceries', 'date': '2024-01-15'})
+
+ result = calculator.generate_budget(sample_transactions, 'w', avg_income, goals)
+
+ assert hasattr(result, 'budget')
+
+ #verify grocery budget is capped
+ grocery = next((g for g in result.budget if g.category == 'groceries'), None)
+ if grocery:
+ assert grocery.recommended_budget <= 300
+
+
+ #test that goal categories appear even if not in top 10
+ @pytest.mark.asyncio
+ async def test_generate_budget_with_goal_category_not_in_top_10(self, calculator, sample_transactions):
+ avg_income = 5000
+
+ #add a goal for a category with very low spending
+ goals = [
+ {'type': 'save', 'category': 'rare_category'},
+ ]
+
+ #add a small transaction for this category
+ sample_transactions.append({'amount': -10, 'category': 'rare_category', 'date': '2024-01-15'})
+
+ result = calculator.generate_budget(sample_transactions, 'w', avg_income, goals)
+
+ #verify the goal category appears in the budget
+ goal_categories = {g.category for g in result.budget}
+ assert 'rare_category' in goal_categories
+
+
+ #test full budget generation with no goals (should work same as before)
+ @pytest.mark.asyncio
+ async def test_generate_budget_with_no_goals(self, calculator, sample_transactions):
+ avg_income = 5000
+
+ result = calculator.generate_budget(sample_transactions, 'w', avg_income, None)
+
+ assert hasattr(result, 'budget')
+ assert len(result.budget) > 0
+
+
+ #test full budget generation with empty goals list
+ @pytest.mark.asyncio
+ async def test_generate_budget_with_empty_goals_list(self, calculator, sample_transactions):
+ avg_income = 5000
+
+ result = calculator.generate_budget(sample_transactions, 'w', avg_income, [])
+
+ assert hasattr(result, 'budget')
+ assert len(result.budget) > 0
+
+
+ #test reduce spending goal with zero target (should be ignored).
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_zero_target(self, calculator):
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 0},
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {'groceries': 450}
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ assert savings_boost == 0
+ assert wants_reduction == 0
+ assert categories == ['groceries']
+
+
+ #test goal with missing category (should still work)
+ @pytest.mark.asyncio
+ async def test_calculate_goal_impact_missing_category(self, calculator):
+ goals = [
+ {'type': 'save'}, #no category
+ {'type': 'reduce_spending', 'target': 400}, #no category
+ ]
+ avg_monthly_income = 5000
+ monthly_avg_by_category = {}
+
+ savings_boost, wants_reduction, categories = calculator.calculate_goal_impact(
+ goals, avg_monthly_income, monthly_avg_by_category
+ )
+
+ assert savings_boost == 250 #save goal still applies
+ assert wants_reduction == 0
+ assert categories == [] #no categories added since they're missing
+
+
+ #test category budget when pool_actual is zero (no expenses in any want category)
+ @pytest.mark.asyncio
+ async def test_calculate_category_budget_with_zero_pool_actual(self, calculator):
+ monthly_avg = {'groceries': 300}
+ category_types = {'groceries': 'want'}
+
+ monthly_avg = {} #no want categories have any spending
+ category_types = {'new_category': 'want'}
+ pools = {'needs': 2000, 'wants': 1000, 'savings': 500}
+ goals = []
+
+ result = calculator.calculate_category_budget(
+ 'new_category', 0, 'want', monthly_avg, category_types, pools, goals
+ )
+
+ #when pool_actual is 0 and avg_spent is 0, recommended = 0 * 0.9 = 0
+ assert result == 0
+
+ #test that wants_reduction properly reduces wants and increases savings
+ @pytest.mark.asyncio
+ async def test_generate_budget_with_wants_reduction(self, calculator):
+ avg_income = 5000
+
+ groceries_per_month = 450
+ grocery_transactions = []
+ for month in range(1, 13):
+ grocery_transactions.append({
+ 'amount': -groceries_per_month,
+ 'category': 'groceries',
+ 'date': f'2024-{month:02d}-15'
+ })
+
+ base_transactions = [
+ {'amount': 5000, 'category': 'salary', 'date': '2024-01-01'},
+ {'amount': -200, 'category': 'dining', 'date': '2024-01-02'},
+ {'amount': -150, 'category': 'dining', 'date': '2024-01-03'},
+ {'amount': -100, 'category': 'entertainment', 'date': '2024-01-04'},
+ ]
+
+ # Add monthly salary transactions
+ salary_transactions = []
+ for month in range(1, 13):
+ salary_transactions.append({
+ 'amount': 5000,
+ 'category': 'salary',
+ 'date': f'2024-{month:02d}-01'
+ })
+
+ all_transactions = salary_transactions + grocery_transactions + base_transactions
+
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 400},
+ ]
+
+ avg_income = 5000
+
+ result = calculator.generate_budget(all_transactions, 'w', avg_income, goals)
+
+ assert hasattr(result, 'budget')
+ assert len(result.budget) > 0
+ wants_budget_total = 0
+ for cat in result.budget:
+ if cat.type == 'want':
+ wants_budget_total += cat.recommended_budget
+ assert wants_budget_total <= 1200, f"Wants budget {wants_budget_total} should be reduced from baseline"
+ grocery = next((cat for cat in result.budget if cat.category == 'groceries'), None)
+ if grocery:
+ assert grocery.recommended_budget <= 400 # Should be capped at target
+
+
+
+ #test with multiple reduce spending goals to ensure max reduction is applied
+ @pytest.mark.asyncio
+ async def test_generate_budget_with_multiple_reduce_spending_goals(self, calculator):
+ transactions = []
+ for month in range(1, 13):
+ transactions.append({
+ 'amount': 5000, 'category': 'salary', 'date': f'2024-{month:02d}-01'
+ })
+ for month in range(1, 13):
+ transactions.append({
+ 'amount': -450, 'category': 'groceries', 'date': f'2024-{month:02d}-10'
+ })
+
+ for month in range(1, 13):
+ transactions.append({
+ 'amount': -350, 'category': 'dining', 'date': f'2024-{month:02d}-15'
+ })
+ for month in range(1, 13):
+ transactions.append({
+ 'amount': -250, 'category': 'entertainment', 'date': f'2024-{month:02d}-20'
+ })
+
+ avg_income = 5000
+
+ goals = [
+ {'type': 'reduce_spending', 'category': 'groceries', 'target': 400}, #over limit: 450 -> 7% reduction
+ {'type': 'reduce_spending', 'category': 'dining', 'target': 400}, #350/400=87.5% -> 5% reduction
+ {'type': 'reduce_spending', 'category': 'entertainment', 'target': 300}, #250/300=83.3% -> 5% reduction
+ ]
+
+ result = calculator.generate_budget(transactions, 'w', avg_income, goals)
+
+ wants_budget_total = sum(
+ cat.recommended_budget for cat in result.budget if cat.type == 'want'
+ )
+
+ assert wants_budget_total <= 1200
+ grocery = next((cat for cat in result.budget if cat.category == 'groceries'), None)
+ dining = next((cat for cat in result.budget if cat.category == 'dining'), None)
+
+ if grocery:
+ assert grocery.recommended_budget <= 400
+ if dining:
+ assert dining.recommended_budget <= 400
+
+
+ #test multiple goal categories covering all three branches
+ @pytest.mark.asyncio
+ async def test_generate_budget_multiple_goal_categories_all_branches(self, calculator):
+ transactions = []
+ for month in range(1, 13):
+ transactions.append({'amount': 5000, 'category': 'salary', 'date': f'2024-{month:02d}-01'})
+
+ for month in range(1, 13):
+ transactions.append({'amount': -5000, 'category': 'rent', 'date': f'2024-{month:02d}-05'})
+ transactions.append({'amount': -1000, 'category': 'groceries', 'date': f'2024-{month:02d}-10'})
+ transactions.append({'amount': -800, 'category': 'dining', 'date': f'2024-{month:02d}-12'})
+ transactions.append({'amount': -600, 'category': 'entertainment', 'date': f'2024-{month:02d}-15'})
+
+ for month in range(1, 13):
+ transactions.append({'amount': -50, 'category': 'groceries_small', 'date': f'2024-{month:02d}-20'})
+
+ for month in range(1, 13):
+ transactions.append({'amount': 150, 'category': 'investment', 'date': f'2024-{month:02d}-25'})
+
+ avg_income = 5000 + 150
+
+ goals = [
+ {'type': 'save', 'category': 'groceries_small'}, #in monthly_avg
+ {'type': 'save', 'category': 'investment'}, #in monthly_avg_savings
+ {'type': 'save', 'category': 'new_goal'}, #in neither
+ ]
+
+ result = calculator.generate_budget(transactions, 'w', avg_income, goals)
+
+ goal_categories = {cat.category for cat in result.budget}
+ assert 'groceries_small' in goal_categories
+ assert 'investment' in goal_categories
+ assert 'new_goal' in goal_categories
+
+ groceries = next(cat for cat in result.budget if cat.category == 'groceries_small')
+ investment = next(cat for cat in result.budget if cat.category == 'investment')
+ new_goal = next(cat for cat in result.budget if cat.category == 'new_goal')
+
+ assert groceries.avg_monthly_spent > 0
+ assert investment.avg_monthly_spent > 0
+ assert new_goal.avg_monthly_spent == 0
\ No newline at end of file
diff --git a/app/server/services/python/analytics/test/test_budgetPerfCalc.py b/app/server/services/python/analytics/test/test_budgetPerfCalc.py
new file mode 100644
index 0000000..12cd384
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_budgetPerfCalc.py
@@ -0,0 +1,221 @@
+import pytest
+import pandas as pd
+from datetime import datetime
+from unittest.mock import patch, ANY
+from src.logic.budgetPerfCalc import BudgetPerformanceCalculator
+from src.models.schemas import BudgetResponse, BudgetCategory
+
+pytestmark = pytest.mark.asyncio
+
+class TestBudgetPerformanceCalculator:
+
+ @pytest.fixture
+ def performance_calc(self):
+ return BudgetPerformanceCalculator()
+
+ @pytest.fixture
+ def sample_budget_response_with_high_targets(self):
+ categories = [
+ BudgetCategory(
+ category='groceries',
+ type='need',
+ avg_monthly_spent=300,
+ monthly_budget=280,
+ recommended_budget=500,
+ is_essential=True
+ ),
+ BudgetCategory(
+ category='dining',
+ type='want',
+ avg_monthly_spent=200,
+ monthly_budget=150,
+ recommended_budget=400,
+ is_essential=False
+ ),
+ BudgetCategory(
+ category='entertainment',
+ type='want',
+ avg_monthly_spent=150,
+ monthly_budget=120,
+ recommended_budget=300,
+ is_essential=False
+ ),
+ ]
+ return BudgetResponse(
+ budget=categories,
+ generated_date='2024-03-15 12:00:00'
+ )
+
+ @pytest.fixture
+ def mock_transactions_low_spending(self):
+ """Create transactions with low actual spending."""
+ return [
+ {'category': 'groceries', 'amount': -50, 'date': '2024-03-10'},
+ {'category': 'groceries', 'amount': -30, 'date': '2024-03-12'},
+ {'category': 'dining', 'amount': -40, 'date': '2024-03-14'},
+ {'category': 'entertainment', 'amount': -20, 'date': '2024-03-15'},
+ ]
+
+ @pytest.fixture
+ def mock_transactions(self):
+ return [
+ {'category': 'groceries', 'amount': -300, 'date': '2024-03-10'},
+ {'category': 'groceries', 'amount': -150, 'date': '2024-03-12'},
+ {'category': 'dining', 'amount': -200, 'date': '2024-03-14'},
+ {'category': 'entertainment', 'amount': -100, 'date': '2024-03-15'},
+ ]
+
+ @pytest.fixture
+ def sample_budget_response(self):
+ categories = [
+ BudgetCategory(
+ category='groceries',
+ type='need',
+ avg_monthly_spent=300,
+ monthly_budget=280,
+ recommended_budget=65,
+ is_essential=True
+ ),
+ BudgetCategory(
+ category='dining',
+ type='want',
+ avg_monthly_spent=200,
+ monthly_budget=150,
+ recommended_budget=35,
+ is_essential=False
+ ),
+ BudgetCategory(
+ category='entertainment',
+ type='want',
+ avg_monthly_spent=150,
+ monthly_budget=120,
+ recommended_budget=28,
+ is_essential=False
+ ),
+ ]
+ return BudgetResponse(
+ budget=categories,
+ generated_date=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+ )
+
+ # Synchronous tests
+ def test_get_period_boundaries_weekly(self, performance_calc):
+ ref_date = pd.Timestamp('2024-03-15')
+ start, end = performance_calc.get_period_boundaries('w', ref_date)
+ assert start.strftime('%Y-%m-%d') == '2024-03-10'
+ assert end.strftime('%Y-%m-%d') == '2024-03-16'
+
+ def test_get_period_boundaries_monthly(self, performance_calc):
+ ref_date = pd.Timestamp('2024-03-15')
+ start, end = performance_calc.get_period_boundaries('m', ref_date)
+ assert start.strftime('%Y-%m-%d') == '2024-03-01'
+ assert end.strftime('%Y-%m-%d') == '2024-03-31'
+
+ def test_get_period_boundaries_yearly(self, performance_calc):
+ ref_date = pd.Timestamp('2024-03-15')
+ start, end = performance_calc.get_period_boundaries('y', ref_date)
+ assert start.strftime('%Y-%m-%d') == '2024-01-01'
+ assert end.strftime('%Y-%m-%d') == '2024-12-31'
+
+ def test_calculate_actual_spending(self, performance_calc, mock_transactions):
+ result = performance_calc.calculate_actual_spending(mock_transactions)
+ assert result['groceries'] == 450
+ assert result['dining'] == 200
+ assert result['entertainment'] == 100
+
+ # Async tests with proper mocking
+ async def test_generate_performance(
+ self,
+ performance_calc,
+ sample_budget_response,
+ mock_transactions
+ ):
+ user_id = 123
+ period = 'w'
+
+ # Mock the database function at the EXACT import path used in budgetPerfCalc
+ with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get:
+ # Configure the mock to return our test data
+ mock_get.return_value = mock_transactions
+
+ # Call the async function
+ result = await performance_calc.generate_performance(
+ user_id,
+ sample_budget_response,
+ period
+ )
+
+ # Assertions
+ assert 'categories' in result
+ assert 'budgetAmounts' in result
+ assert 'actualAmounts' in result
+ assert len(result['categories']) == len(sample_budget_response.budget)
+
+ # Verify the mock was called with correct arguments
+ mock_get.assert_called_once_with(user_id, ANY, ANY)
+
+ async def test_generate_performance_empty_transactions(
+ self,
+ performance_calc,
+ sample_budget_response
+ ):
+ user_id = 123
+ period = 'w'
+
+ with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get:
+ mock_get.return_value = []
+
+ result = await performance_calc.generate_performance(
+ user_id,
+ sample_budget_response,
+ period
+ )
+
+ # All actual amounts should be 0
+ assert all(amount == 0 for amount in result['actualAmounts'])
+ mock_get.assert_called_once()
+
+
+ async def test_calculate_performance_underflow(self, performance_calc, sample_budget_response_with_high_targets):
+ #create actual spending that is much lower than budget - this will cause variance < -100 - underflow
+ actual_spending = {
+ 'groceries': 50,
+ 'dining': 30,
+ 'entertainment': 20
+ }
+
+ result = performance_calc.calculate_performance(
+ sample_budget_response_with_high_targets.budget,
+ actual_spending,
+ 'w'
+ )
+
+ assert len(result) > 0
+
+ #verify all variances are less than -100
+ for i, category in enumerate(result['categories']):
+ budget = sample_budget_response_with_high_targets.budget[i].recommended_budget
+ actual = actual_spending[category]
+ variance = actual - budget
+
+ assert variance < -100
+
+
+ async def test_generate_performance_db_error(
+ self,
+ performance_calc,
+ sample_budget_response
+ ):
+ user_id = 123
+ period = 'w'
+
+ with patch('src.logic.budgetPerfCalc.get_user_transactions_with_connection') as mock_get:
+ mock_get.side_effect = Exception("Database connection failed")
+
+ # The error should propagate
+ with pytest.raises(Exception, match="Database connection failed"):
+ await performance_calc.generate_performance(
+ user_id,
+ sample_budget_response,
+ period
+ )
\ No newline at end of file
diff --git a/app/server/services/python/analytics/test/test_budget_integration.py b/app/server/services/python/analytics/test/test_budget_integration.py
new file mode 100644
index 0000000..df131ad
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_budget_integration.py
@@ -0,0 +1,42 @@
+import pytest
+from unittest.mock import patch, AsyncMock
+from src.logic.budget import generate_budget, generate_budget_performance
+from src.models.schemas import BudgetResponse
+
+class TestBudgetIntegration:
+
+ @pytest.mark.asyncio
+ async def test_full_budget_flow(self):
+ user_id = 123
+ period = 'w'
+
+ # Mock transactions for generate_budget
+ mock_transactions = [
+ {'amount': 5000, 'category': 'salary', 'date': '2024-01-01'},
+ {'amount': -300, 'category': 'groceries', 'date': '2024-01-02'},
+ {'amount': -200, 'category': 'dining', 'date': '2024-01-03'},
+ {'amount': 5000, 'category': 'salary', 'date': '2024-02-01'},
+ {'amount': -250, 'category': 'groceries', 'date': '2024-02-02'},
+ ]
+
+ mock_goals = []
+
+ with patch('src.logic.budget.get_user_transactions_with_connection') as mock_transactions_query, \
+ patch('src.logic.budget.get_user_goals') as mock_goals_query:
+
+ mock_transactions_query.return_value = mock_transactions
+ mock_goals_query.return_value = mock_goals
+
+ # Generate budget
+ budget = generate_budget(period, user_id)
+
+ assert isinstance(budget, BudgetResponse)
+ assert len(budget.budget) > 0
+
+ # Generate performance
+ performance = await generate_budget_performance(user_id, budget, period)
+
+ assert 'categories' in performance
+ assert 'budgetAmounts' in performance
+ assert 'actualAmounts' in performance
+ assert len(performance['categories']) == len(budget.budget)
\ No newline at end of file
diff --git a/app/server/services/python/analytics/test/test_cat_classifier.py b/app/server/services/python/analytics/test/test_cat_classifier.py
new file mode 100644
index 0000000..ef7cb27
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_cat_classifier.py
@@ -0,0 +1,55 @@
+import pytest
+from src.utils.trans_cat_classifier import CategoryClassifier
+
+class TestCategoryClassifier:
+
+ def test_classify_needs(self, classifier):
+ assert classifier.classify('rent') == 'need'
+ assert classifier.classify('groceries') == 'need'
+ assert classifier.classify('healthcare') == 'need'
+ assert classifier.classify('utilities') == 'need'
+
+ def test_classify_wants(self, classifier):
+ assert classifier.classify('dining') == 'want'
+ assert classifier.classify('entertainment') == 'want'
+ assert classifier.classify('shopping') == 'want'
+ assert classifier.classify('coffee') == 'want'
+
+ def test_classify_savings(self, classifier):
+ assert classifier.classify('savings') == 'savings'
+ assert classifier.classify('investment') == 'savings'
+ assert classifier.classify('retirement') == 'savings'
+
+ def test_classify_unknown_defaults_to_want(self, classifier):
+ assert classifier.classify('unknown_category') == 'want'
+ assert classifier.classify('random_stuff') == 'want'
+
+ def test_classify_case_insensitive(self, classifier):
+ assert classifier.classify('RENT') == 'need'
+ assert classifier.classify('Groceries') == 'need'
+ assert classifier.classify('DINING') == 'want'
+
+ def test_classify_many(self, classifier):
+ categories = {'rent', 'dining', 'savings', 'unknown'}
+ result = classifier.classify_many(categories)
+
+ assert result['rent'] == 'need'
+ assert result['dining'] == 'want'
+ assert result['savings'] == 'savings'
+ assert result['unknown'] == 'want'
+
+ def test_get_categories_by_type(self, classifier):
+ category_types = {
+ 'rent': 'need',
+ 'groceries': 'need',
+ 'dining': 'want',
+ 'savings': 'savings'
+ }
+
+ needs = classifier.get_categories_by_type(category_types, 'need')
+ wants = classifier.get_categories_by_type(category_types, 'want')
+ savings = classifier.get_categories_by_type(category_types, 'savings')
+
+ assert needs == {'rent', 'groceries'}
+ assert wants == {'dining'}
+ assert savings == {'savings'}
\ No newline at end of file
diff --git a/app/server/services/python/analytics/test/test_debts.py b/app/server/services/python/analytics/test/test_debts.py
new file mode 100644
index 0000000..54b9d25
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_debts.py
@@ -0,0 +1,52 @@
+import pytest
+from src.logic.debt import generate_debt_payoff_stages
+from src.models.schemas import BadRequestError, DebtPayoffRequest, DebtPayoffResponse
+
+class TestDebts:
+ def test_generate_debt_payoff(self, mock_debt_payoff_request):
+ result = generate_debt_payoff_stages(mock_debt_payoff_request)
+
+ assert result.id == mock_debt_payoff_request.id
+ assert result.category == mock_debt_payoff_request.category
+ assert len(result.debtStages) > 0
+
+ # final debt should be zero
+ assert result.debtStages[-1].remainingDebt == 0
+
+
+ def test_generate_debt_payoff_no_interest(self, mock_debt_payoff_request):
+ mock_debt_payoff_request.interestRate = 0
+
+ result = generate_debt_payoff_stages(mock_debt_payoff_request)
+
+ # No interest should be charged
+ for stage in result.debtStages:
+ assert stage.interestAmount == 0
+
+ assert len(result.debtStages) == 5
+
+
+ def test_last_payment_adjustment(self, mock_debt_payoff_request):
+ mock_debt_payoff_request.remainingAmount = 450
+ mock_debt_payoff_request.minimumPayment = 200
+ mock_debt_payoff_request.interestRate = 0
+
+ result = generate_debt_payoff_stages(mock_debt_payoff_request)
+
+ last_stage = result.debtStages[-1]
+
+ # Last payment should exactly clear the debt
+ assert last_stage.remainingDebt == 0
+ assert last_stage.principalAmount <= mock_debt_payoff_request.minimumPayment
+
+
+ def test_min_payment_too_low(self, mock_debt_payoff_request):
+ mock_debt_payoff_request.minimumPayment = 1
+ mock_debt_payoff_request.interestRate = 100 # Very high interest
+
+ with pytest.raises(BadRequestError) as exc:
+ generate_debt_payoff_stages(mock_debt_payoff_request)
+
+ assert "too low" in str(exc.value)
+
+
diff --git a/app/server/services/python/analytics/test/test_dependencies.py b/app/server/services/python/analytics/test/test_dependencies.py
new file mode 100644
index 0000000..7a32238
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_dependencies.py
@@ -0,0 +1,127 @@
+import importlib
+
+import pytest
+from unittest.mock import patch, MagicMock
+import mysql.connector
+from src.dependencies import get_db_connection, get_current_user
+from fastapi import HTTPException
+import os
+import jwt
+
+class TestDatabaseConnection:
+
+ @patch('mysql.connector.connect')
+ def test_get_db_connection_success(self, mock_connect, mock_env_vars):
+ mock_connection = MagicMock()
+ mock_connect.return_value = mock_connection
+
+ result = get_db_connection()
+
+ mock_connect.assert_called_once_with(
+ host='localhost',
+ user='test_user',
+ password='test_password',
+ database='test_db',
+ connection_timeout=10
+ )
+
+ assert result == mock_connection
+
+ @patch('mysql.connector.connect')
+ def test_get_db_connection_missing_env_vars(self, mock_connect):
+ with patch.dict(os.environ, {}, clear=True):
+ with pytest.raises(HTTPException):
+ get_db_connection()
+
+ @patch('mysql.connector.connect')
+ def test_get_db_connection_failure(self, mock_connect):
+ mock_connect.side_effect = mysql.connector.Error('Connection failed')
+ with patch.dict(os.environ, {'MYSQL_HOST': 'localhost', 'MYSQL_USER': 'test_user', 'MYSQL_PASSWORD': 'test_password', 'DB_NAME': 'test_db'}, clear=True):
+ with pytest.raises(mysql.connector.Error):
+ get_db_connection()
+
+
+
+class TestJWTAuthentication:
+
+ @pytest.mark.asyncio
+ async def test_get_current_user_perfect_case(self):
+ payload = {'sub': '123'}
+ token = jwt.encode(payload, 'test', algorithm='HS256')
+
+ if isinstance(token, bytes):
+ token = token.decode('utf-8')
+
+ with patch.dict(os.environ, {'JWT_SECRET': 'test'}):
+ import src.dependencies
+ importlib.reload(src.dependencies)
+ from src.dependencies import get_current_user, SECRET_KEY
+
+ assert SECRET_KEY == 'test'
+
+ result = await get_current_user(authorization=f'Bearer {token}')
+ assert result == 123
+
+ @pytest.mark.asyncio
+ async def test_get_current_user_missing_user_id(self):
+
+ payload = {'sub': None}
+ token = jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256')
+
+ # Ensure token is string
+ if isinstance(token, bytes):
+ token = token.decode('utf-8')
+
+ with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}):
+ with pytest.raises(HTTPException) as exc_info:
+ await get_current_user(authorization=f'Bearer {token}')
+
+ assert exc_info.value.status_code == 401
+
+
+ @pytest.mark.asyncio
+ async def test_get_current_user_invalid_signature(self):
+
+ payload = {'sub': '123'}
+ token = jwt.encode(payload, 'wrong_secret', algorithm='HS256')
+
+ if isinstance(token, bytes):
+ token = token.decode('utf-8')
+
+ with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}):
+ with pytest.raises(HTTPException) as exc_info:
+ await get_current_user(authorization=f'Bearer {token}')
+
+ assert exc_info.value.status_code == 401
+
+ @pytest.mark.asyncio
+ async def test_get_current_user_no_auth_header(self):
+
+ payload = {'sub': '123'}
+ token = jwt.encode(payload, 'wrong_secret', algorithm='HS256')
+
+ if isinstance(token, bytes):
+ token = token.decode('utf-8')
+
+ with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}):
+ with pytest.raises(HTTPException) as exc_info:
+ await get_current_user(authorization=None)
+
+ assert exc_info.value.status_code == 401
+ assert exc_info.value.detail == 'Authorization header missing'
+
+ @pytest.mark.asyncio
+ async def test_get_current_user_no_bearer(self):
+
+ payload = {'sub': '123'}
+ token = jwt.encode(payload, 'test_secret_key_12345', algorithm='HS256')
+
+ if isinstance(token, bytes):
+ token = token.decode('utf-8')
+
+ with patch.dict(os.environ, {'JWT_SECRET': 'test_secret_key_12345'}):
+ with pytest.raises(HTTPException) as exc_info:
+ await get_current_user(authorization=f'something {token}')
+
+ assert exc_info.value.status_code == 401
+ # assert exc_info.value.detail == '401: Invalid authentication scheme'
diff --git a/app/server/services/python/analytics/test/test_incomeflow.py b/app/server/services/python/analytics/test/test_incomeflow.py
new file mode 100644
index 0000000..18945bf
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_incomeflow.py
@@ -0,0 +1,131 @@
+# tests/test_service_incomeflow.py
+import pytest
+import pandas as pd
+from src.logic.incomeflow import build_sankey_data
+
+class TestIncomeflowService:
+
+ def test_build_sankey_data_with_income_and_expenses(self, mock_incomeflow_transactions):
+ result = build_sankey_data(mock_incomeflow_transactions)
+
+ assert 'nodes' in result
+ assert 'links' in result
+
+ # Should have total income node
+ node_names = [node['name'] for node in result['nodes']]
+ assert 'total income' in node_names
+
+ # Should have all unique categories as nodes
+ expected_categories = {'salary', 'freelance', 'interest', 'rent',
+ 'groceries', 'dining', 'entertainment', 'utilities'}
+ for category in expected_categories:
+ assert category in node_names
+
+ # Should have links
+ assert len(result['links']) > 0
+
+ # Verify link structure
+ for link in result['links']:
+ assert 'source' in link
+ assert 'target' in link
+ assert 'value' in link
+ assert isinstance(link['value'], int)
+
+ def test_build_sankey_data_with_overflow(self):
+ transactions = [
+ {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'},
+ {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'},
+ {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'},
+ {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'},
+ ]
+
+ result = build_sankey_data(transactions)
+
+ # Should have 'unspent' node
+ node_names = [node['name'] for node in result['nodes']]
+ assert 'unspent' in node_names
+
+ # Should have link to unspent
+ unspent_index = node_names.index('unspent')
+ total_income_index = node_names.index('total income')
+
+ # Find link from total income to unspent
+ unspent_links = [link for link in result['links']
+ if link['source'] == total_income_index
+ and link['target'] == unspent_index]
+ assert len(unspent_links) == 1
+ assert unspent_links[0]['value'] == 4000 # 6000 - 2000
+
+ def test_build_sankey_data_with_overspending(self):
+ transactions = [
+ {'amount': 3000, 'category': 'salary', 'date': '2024-03-01'},
+ {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'},
+ {'amount': -1000, 'category': 'groceries', 'date': '2024-03-04'},
+ {'amount': -800, 'category': 'dining', 'date': '2024-03-05'},
+ ]
+
+ result = build_sankey_data(transactions)
+
+ # Should have 'overspent' and 'savings' nodes
+ node_names = [node['name'] for node in result['nodes']]
+ assert 'overspent' in node_names
+ assert 'savings' in node_names
+
+ # Should have link from savings to overspent
+ savings_index = node_names.index('savings')
+ overspent_index = node_names.index('overspent')
+
+ overspent_links = [link for link in result['links']
+ if link['source'] == savings_index
+ and link['target'] == overspent_index]
+ assert len(overspent_links) == 1
+ assert overspent_links[0]['value'] == 300 # 3300 - 3000
+
+ def test_build_sankey_data_empty_transactions(self):
+ """Test sankey data with no transactions."""
+ result = build_sankey_data([])
+
+ assert result['nodes'] == [{'name': 'total income'}, {'name': 'no data'}]
+ assert result['links'] == []
+
+ def test_build_sankey_data_only_income(self):
+ transactions = [
+ {'amount': 5000, 'category': 'salary', 'date': '2024-03-01'},
+ {'amount': 1000, 'category': 'freelance', 'date': '2024-03-02'},
+ ]
+
+ result = build_sankey_data(transactions)
+
+ node_names = [node['name'] for node in result['nodes']]
+ assert 'unspent' in node_names
+
+ # All income should flow to unspent
+ total_income_index = node_names.index('total income')
+ unspent_index = node_names.index('unspent')
+
+ unspent_links = [link for link in result['links']
+ if link['source'] == total_income_index
+ and link['target'] == unspent_index]
+ assert unspent_links[0]['value'] == 6000
+
+ def test_build_sankey_data_only_expenses(self):
+ transactions = [
+ {'amount': -1500, 'category': 'rent', 'date': '2024-03-03'},
+ {'amount': -500, 'category': 'groceries', 'date': '2024-03-04'},
+ ]
+
+ result = build_sankey_data(transactions)
+
+ node_names = [node['name'] for node in result['nodes']]
+ assert 'overspent' in node_names
+ assert 'savings' in node_names
+
+ # Expenses should come from savings
+ savings_index = node_names.index('savings')
+ overspent_index = node_names.index('overspent')
+ rent_index = node_names.index('rent')
+ groceries_index = node_names.index('groceries')
+
+ # Links from savings to overspent and to categories
+ savings_links = [link for link in result['links'] if link['source'] == savings_index]
+ assert len(savings_links) == 1 # savings into overspent
\ No newline at end of file
diff --git a/app/server/services/python/analytics/test/test_periodCalc.py b/app/server/services/python/analytics/test/test_periodCalc.py
new file mode 100644
index 0000000..8e272e9
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_periodCalc.py
@@ -0,0 +1,146 @@
+import pytest
+import pandas as pd
+from src.utils.dates import period_calc
+
+class TestPeriodCalc:
+
+ def test_period_calc_weekly(self):
+ end_date = pd.Timestamp('2024-03-15')
+
+ start_date, freq, date_format, period_name = period_calc('w', end_date)
+
+ # Check start date is 7 days before end date
+ expected_start = end_date - pd.Timedelta(days=7)
+ assert start_date == expected_start
+ assert start_date.strftime('%Y-%m-%d') == '2024-03-08'
+
+ # Check other return values
+ assert freq == 'D'
+ assert date_format == '%Y-%m-%d'
+ assert period_name == 'Weekly'
+
+ def test_period_calc_monthly(self):
+ end_date = pd.Timestamp('2024-03-15')
+
+ start_date, freq, date_format, period_name = period_calc('m', end_date)
+
+ # Check start date is 30 days before end date
+ expected_start = end_date - pd.Timedelta(days=30)
+ assert start_date == expected_start
+ assert start_date.strftime('%Y-%m-%d') == '2024-02-14' # March 15 - 30 days = Feb 14
+
+ # Check other return values
+ assert freq == 'D'
+ assert date_format == '%Y-%m-%d'
+ assert period_name == 'Monthly'
+
+ def test_period_calc_yearly(self):
+ end_date = pd.Timestamp('2024-03-15')
+
+ start_date, freq, date_format, period_name = period_calc('y', end_date)
+
+ # Check start date is 365 days before end date
+ expected_start = end_date - pd.Timedelta(days=365)
+ assert start_date == expected_start
+ assert start_date.strftime('%Y-%m-%d') == '2023-03-16' # 2024 is leap year
+
+ # Check other return values
+ assert freq == 'M'
+ assert date_format == '%Y-%m'
+ assert period_name == 'Yearly'
+
+ def test_period_calc_with_different_end_dates(self):
+ test_cases = [
+ # (end_date, expected_weekly_start, expected_monthly_start, expected_yearly_start)
+ ('2024-01-01', '2023-12-25', '2023-12-02', '2023-01-01'),
+ ('2024-06-15', '2024-06-08', '2024-05-16', '2023-06-16'),
+ ('2024-12-31', '2024-12-24', '2024-12-01', '2024-01-01'),
+ ]
+
+ for end_str, expected_weekly, expected_monthly, expected_yearly in test_cases:
+ end_date = pd.Timestamp(end_str)
+
+ # Test weekly
+ start, freq, fmt, name = period_calc('w', end_date)
+ assert start.strftime('%Y-%m-%d') == expected_weekly
+
+ # Test monthly
+ start, freq, fmt, name = period_calc('m', end_date)
+ assert start.strftime('%Y-%m-%d') == expected_monthly
+
+ # Test yearly
+ start, freq, fmt, name = period_calc('y', end_date)
+ assert start.strftime('%Y-%m-%d') == expected_yearly
+
+ def test_period_calc_edge_cases(self):
+
+ end_date = pd.Timestamp('2024-02-29') # Leap year
+ start, _, _, _ = period_calc('m', end_date)
+ assert start.strftime('%Y-%m-%d') == '2024-01-30' # Feb 29 - 30 days = Jan 30
+
+ end_date = pd.Timestamp('2024-12-31')
+ start, _, _, _ = period_calc('y', end_date)
+ assert start.strftime('%Y-%m-%d') == '2024-01-01'
+
+ end_date = pd.Timestamp('2024-01-01')
+ start, _, _, _ = period_calc('m', end_date)
+ assert start.strftime('%Y-%m-%d') == '2023-12-02' # Jan 1 - 30 days = Dec 2
+
+ def test_period_calc_invalid_period(self):
+ end_date = pd.Timestamp('2024-03-15')
+
+ # Invalid period should go to else branch (yearly)
+ start, freq, date_format, period_name = period_calc('invalid', end_date)
+
+ expected_start = end_date - pd.Timedelta(days=365)
+ assert start == expected_start
+ assert freq == 'M'
+ assert date_format == '%Y-%m'
+ assert period_name == 'Yearly'
+
+ def test_period_calc_with_time_component(self):
+ end_date = pd.Timestamp('2024-03-15 14:30:45')
+
+ start, _, _, _ = period_calc('w', end_date)
+
+ # Time component should be preserved in the calculation
+ expected_start = end_date - pd.Timedelta(days=7)
+ assert start == expected_start
+ assert start.hour == 14
+ assert start.minute == 30
+ assert start.second == 45
+
+ def test_period_calc_returns_tuple(self):
+ end_date = pd.Timestamp('2024-03-15')
+ result = period_calc('w', end_date)
+
+ assert isinstance(result, tuple)
+ assert len(result) == 4
+
+ start, freq, date_format, period_name = result
+ assert isinstance(start, pd.Timestamp)
+ assert isinstance(freq, str)
+ assert isinstance(date_format, str)
+ assert isinstance(period_name, str)
+
+ @pytest.mark.parametrize("period,expected_freq,expected_format,expected_name", [
+ ('w', 'D', '%Y-%m-%d', 'Weekly'),
+ ('m', 'D', '%Y-%m-%d', 'Monthly'),
+ ('y', 'M', '%Y-%m', 'Yearly'),
+ ])
+ def test_period_calc_parametrized(self, period, expected_freq, expected_format, expected_name):
+ end_date = pd.Timestamp('2024-03-15')
+
+ start, freq, date_format, period_name = period_calc(period, end_date)
+
+ assert freq == expected_freq
+ assert date_format == expected_format
+ assert period_name == expected_name
+
+ # Verify start date calculation
+ if period == 'w':
+ assert start == end_date - pd.Timedelta(days=7)
+ elif period == 'm':
+ assert start == end_date - pd.Timedelta(days=30)
+ else:
+ assert start == end_date - pd.Timedelta(days=365)
\ No newline at end of file
diff --git a/app/server/services/python/analytics/test/test_savings.py b/app/server/services/python/analytics/test/test_savings.py
new file mode 100644
index 0000000..ccf647f
--- /dev/null
+++ b/app/server/services/python/analytics/test/test_savings.py
@@ -0,0 +1,311 @@
+# tests/test_savings.py
+import pandas as pd
+import pytest
+from datetime import date, timedelta
+from dateutil.relativedelta import relativedelta
+from src.models.schemas import ProjectedSavingsRequest, CompoundInterestResponse
+from src.logic.savings import calculate_compound_interest, calculate_savings_over_time
+
+class TestSavingsProjection:
+
+ @pytest.fixture
+ def mock_savings_transactions(self):
+ base_date = pd.Timestamp('2024-03-15')
+ return [
+ {'financialAccount_id': 1, 'amount': 100, 'date': base_date - timedelta(days=5)},
+ {'financialAccount_id': 1, 'amount': 200, 'date': base_date - timedelta(days=10)},
+ {'financialAccount_id': 2, 'amount': -50, 'date': base_date - timedelta(days=7)},
+ {'financialAccount_id': 2, 'amount': 150, 'date': base_date - timedelta(days=15)},
+ {'financialAccount_id': 1, 'amount': -75, 'date': base_date - timedelta(days=20)},
+ {'financialAccount_id': 1, 'amount': -25, 'date': base_date - timedelta(days=5)},
+ {'financialAccount_id': 1, 'amount': -25, 'date': base_date - timedelta(days=5)},
+ {'financialAccount_id': 1, 'amount': 25, 'date': base_date},
+ {'financialAccount_id': 1, 'amount': 25, 'date': base_date},
+ {'financialAccount_id': 1, 'amount': 25, 'date': base_date - timedelta(days=62)},
+ ]
+
+ @pytest.fixture
+ def mock_savings_accounts(self):
+ return [
+ {'id': 1, 'balance': 5000},
+ {'id': 2, 'balance': 3000},
+ ]
+
+ @pytest.fixture
+ def mock_savings_request(self):
+ return ProjectedSavingsRequest(
+ financial_account_id=1,
+ balance=1000.00,
+ monthly_deposit=100.00,
+ annual_interest_rate=5.0, # 5% annual interest
+ time_frame=1 # 1 year
+ )
+
+ @pytest.fixture
+ def expected_compound_calculation(self):
+ """Helper to calculate expected values for verification"""
+ def calculate(balance, monthly_deposit, monthly_rate, months):
+ result = balance
+ for _ in range(months):
+ result += monthly_deposit
+ result *= (1 + monthly_rate)
+ return round(result, 2)
+ return calculate
+
+ def test_calculate_savings_over_time_weekly(self, mock_savings_accounts, mock_savings_transactions):
+ end_date = pd.Timestamp('2024-03-15')
+ start_date = end_date - pd.Timedelta(days=7)
+
+ result = calculate_savings_over_time(
+ mock_savings_accounts,
+ mock_savings_transactions,
+ start_date,
+ end_date,
+ 'w',
+ 'Weekly'
+ )
+
+ assert 'labels' in result
+ assert 'datasets' in result
+ assert len(result['labels']) == 8
+ assert result['datasets'][0]['label'] == 'Weekly Savings'
+
+ for value in result['datasets'][0]['data']:
+ assert isinstance(value, (int, float))
+
+ def test_calculate_savings_over_time_monthly(self, mock_savings_accounts, mock_savings_transactions):
+ end_date = pd.Timestamp('2024-03-15')
+ start_date = end_date - pd.Timedelta(days=30)
+
+ result = calculate_savings_over_time(
+ mock_savings_accounts,
+ mock_savings_transactions,
+ start_date,
+ end_date,
+ 'm',
+ 'Monthly'
+ )
+
+ assert result['datasets'][0]['label'] == 'Monthly Savings'
+ assert len(result['labels']) > 0
+
+ def test_calculate_savings_over_time_yearly(self, mock_savings_accounts, mock_savings_transactions):
+ end_date = pd.Timestamp('2024-03-15')
+ start_date = end_date - pd.Timedelta(days=365)
+
+ result = calculate_savings_over_time(
+ mock_savings_accounts,
+ mock_savings_transactions,
+ start_date,
+ end_date,
+ 'y',
+ 'Yearly'
+ )
+
+ assert result['datasets'][0]['label'] == 'Yearly Savings'
+ # Yearly should have monthly labels
+ assert all(len(label) == 7 for label in result['labels']) # YYYY-MM format
+
+ def test_calculate_savings_over_time_empty_transactions(self, mock_savings_accounts):
+ end_date = pd.Timestamp('2024-03-15')
+ start_date = end_date - pd.Timedelta(days=7)
+
+ result = calculate_savings_over_time(
+ mock_savings_accounts,
+ [], # Empty transactions
+ start_date,
+ end_date,
+ 'w',
+ 'Weekly'
+ )
+
+ # Should still return structure with zero data
+ assert 'labels' in result
+ assert 'datasets' in result
+ # All savings values should be the initial balances
+ expected_initial = sum(acc['balance'] for acc in mock_savings_accounts)
+ assert all(value == expected_initial for value in result['datasets'][0]['data'])
+
+ def test_calculate_savings_over_time_no_accounts(self):
+ end_date = pd.Timestamp('2024-03-15')
+ start_date = end_date - pd.Timedelta(days=7)
+
+ result = calculate_savings_over_time(
+ [], # Empty accounts
+ [],
+ start_date,
+ end_date,
+ 'w',
+ 'Weekly'
+ )
+
+ # Should return empty structure
+ assert result['labels'] == []
+ assert result['datasets'][0]['data'] == []
+
+ def test_calculate_compound_interest_basic(self, mock_savings_request, expected_compound_calculation):
+ result = calculate_compound_interest(mock_savings_request)
+
+ # Verify result structure
+ assert isinstance(result, list)
+ assert len(result) == 12 # 1 year = 12 months
+
+ # Verify first month
+ first_month = result[0]
+ assert isinstance(first_month, CompoundInterestResponse)
+ assert first_month.accumulative_expected_balance > mock_savings_request.balance
+
+ # Verify monthly progression
+ for i in range(1, len(result)):
+ assert result[i].accumulative_expected_balance > result[i-1].accumulative_expected_balance
+
+ # Verify date formatting
+ current_date = date.today()
+ first_expected_date = (current_date + relativedelta(months=1)).strftime("%B %Y")
+ assert result[0].date == first_expected_date
+
+ def test_calculate_compound_interest_zero_balance(self, mock_savings_request):
+ mock_savings_request.balance = 0.0
+ result = calculate_compound_interest(mock_savings_request)
+
+ assert len(result) == 12
+ # After 12 months of $100 deposits with interest, should be > $1200
+ assert result[-1].accumulative_expected_balance > 1200.0
+
+ def test_calculate_compound_interest_zero_deposit(self, mock_savings_request):
+ mock_savings_request.monthly_deposit = 0.0
+ result = calculate_compound_interest(mock_savings_request)
+
+ # Should still grow from interest alone
+ assert result[-1].accumulative_expected_balance > mock_savings_request.balance
+
+ def test_calculate_compound_interest_high_interest_rate(self, mock_savings_request):
+ mock_savings_request.annual_interest_rate = 20.0 # 20% annual
+ result = calculate_compound_interest(mock_savings_request)
+
+ # Growth should be significantly higher
+ assert result[-1].accumulative_expected_balance > 2000.0
+
+ def test_calculate_compound_interest_zero_interest_rate(self, mock_savings_request):
+ mock_savings_request.annual_interest_rate = 0.0
+ result = calculate_compound_interest(mock_savings_request)
+
+ # With no interest, final balance should be: balance + (monthly_deposit * 12)
+ expected = mock_savings_request.balance + (mock_savings_request.monthly_deposit * 12)
+ assert result[-1].accumulative_expected_balance == expected
+
+ def test_calculate_compound_interest_negative_interest_rate(self, mock_savings_request):
+ mock_savings_request.annual_interest_rate = -5.0
+ result = calculate_compound_interest(mock_savings_request)
+
+ # With negative interest, balance should decrease
+ expected_no_interest = mock_savings_request.balance + (mock_savings_request.monthly_deposit * 12)
+ assert result[-1].accumulative_expected_balance < expected_no_interest
+
+ def test_calculate_compound_interest_long_term(self, mock_savings_request):
+ mock_savings_request.time_frame = 5
+ result = calculate_compound_interest(mock_savings_request)
+
+ assert len(result) == 60 # 5 years * 12 months
+
+ # Verify best/west/expected ordering
+ for month in result:
+ assert month.accumulative_best_balance >= month.accumulative_expected_balance >= month.accumulative_worst_balance
+
+ def test_calculate_compound_interest_short_term(self, mock_savings_request):
+ mock_savings_request.time_frame = 1 # 1 year
+ result = calculate_compound_interest(mock_savings_request)
+
+ assert len(result) == 12
+ assert result[0].accumulative_expected_balance > mock_savings_request.balance
+
+ def test_calculate_compound_interest_rounding(self, mock_savings_request):
+ result = calculate_compound_interest(mock_savings_request)
+
+ for month in result:
+ # Check that values have at most 2 decimal places
+ best_str = f"{month.accumulative_best_balance:.10f}"
+ expected_str = f"{month.accumulative_expected_balance:.10f}"
+ worst_str = f"{month.accumulative_worst_balance:.10f}"
+
+ # After the decimal point, there should be at most 2 non-zero digits
+ # or they should be exactly 0
+ best_decimal = best_str.split('.')[1].rstrip('0')
+ expected_decimal = expected_str.split('.')[1].rstrip('0')
+ worst_decimal = worst_str.split('.')[1].rstrip('0')
+
+ assert len(best_decimal) <= 2 or best_decimal == ''
+ assert len(expected_decimal) <= 2 or expected_decimal == ''
+ assert len(worst_decimal) <= 2 or worst_decimal == ''
+
+ def test_calculate_compound_interest_best_worst_cases(self, mock_savings_request):
+ result = calculate_compound_interest(mock_savings_request)
+
+ for month in result:
+ assert month.accumulative_best_balance >= month.accumulative_expected_balance
+ assert month.accumulative_expected_balance >= month.accumulative_worst_balance
+
+ def test_calculate_compound_interest_monthly_progression(self, mock_savings_request):
+ result = calculate_compound_interest(mock_savings_request)
+
+ for i in range(1, len(result)):
+ # Expected case should always increase
+ assert result[i].accumulative_expected_balance > result[i-1].accumulative_expected_balance
+
+ # Best case should always increase
+ assert result[i].accumulative_best_balance > result[i-1].accumulative_best_balance
+
+ # Worst case should always increase (unless negative interest)
+ assert result[i].accumulative_worst_balance > result[i-1].accumulative_worst_balance
+
+ def test_calculate_compound_interest_large_values(self, mock_savings_request):
+ mock_savings_request.balance = 1000000.00
+ mock_savings_request.monthly_deposit = 50000.00
+ mock_savings_request.time_frame = 10
+
+ result = calculate_compound_interest(mock_savings_request)
+
+ # Should still produce valid numbers (not inf or NaN)
+ assert len(result) == 120
+ assert all(isinstance(m.accumulative_expected_balance, float) for m in result)
+ assert all(m.accumulative_expected_balance < float('inf') for m in result)
+ assert all(not pd.isna(m.accumulative_expected_balance) for m in result)
+
+ def test_calculate_compound_interest_different_accounts(self):
+ test_cases = [
+ (5000, 200, 3.5, 2), # High balance, moderate deposits
+ (100, 500, 7.0, 3), # Low balance, high deposits
+ (10000, 1, 4.0, 1), # High balance, no deposits
+ (0, 1000, 6.0, 5), # Zero balance, high deposits
+ ]
+
+ for balance, deposit, rate, years in test_cases:
+ request = ProjectedSavingsRequest(
+ financial_account_id=1,
+ balance=balance,
+ monthly_deposit=deposit,
+ annual_interest_rate=rate,
+ time_frame=years
+ )
+ result = calculate_compound_interest(request)
+
+ assert len(result) == years * 12
+ # Final balance should be greater than starting balance + total deposits
+ total_deposits = deposit * (years * 12)
+ if rate > 0:
+ assert result[-1].accumulative_expected_balance > balance + total_deposits
+ else:
+ assert result[-1].accumulative_expected_balance == balance + total_deposits
+
+ def test_calculate_compound_interest_date_handling(self, mock_savings_request):
+ result = calculate_compound_interest(mock_savings_request)
+
+ # Parse dates and verify they are sequential
+ from datetime import datetime
+ dates = [datetime.strptime(r.date, "%B %Y") for r in result]
+
+ for i in range(1, len(dates)):
+ # Each month should be exactly 1 month apart
+ expected_next = dates[i-1] + relativedelta(months=1)
+ assert dates[i].year == expected_next.year
+ assert dates[i].month == expected_next.month
\ No newline at end of file
diff --git a/app/server/services/python/market/Dockerfile b/app/server/services/python/market/Dockerfile
new file mode 100644
index 0000000..62b4ba5
--- /dev/null
+++ b/app/server/services/python/market/Dockerfile
@@ -0,0 +1,10 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+COPY requirements.txt .
+
+RUN --mount=type=cache,target=/root/.cache/pip \
+pip install -r requirements.txt
+
+COPY . .
+CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0"]
diff --git a/app/server/services/python/market/Dockerfile.integration.test b/app/server/services/python/market/Dockerfile.integration.test
new file mode 100644
index 0000000..d0d3e0e
--- /dev/null
+++ b/app/server/services/python/market/Dockerfile.integration.test
@@ -0,0 +1,13 @@
+FROM measureonecodetwice/finus-market:local
+
+WORKDIR /app
+
+COPY integration ./integration
+COPY requirements.txt .
+COPY requirements-test.txt .
+
+RUN pip install -r requirements-test.txt
+
+ENV CI=true
+
+CMD ["python", "-m", "pytest", "integration/", "-v"]
diff --git a/app/server/services/python/market/Dockerfile.mutation.test b/app/server/services/python/market/Dockerfile.mutation.test
new file mode 100644
index 0000000..6f01ab3
--- /dev/null
+++ b/app/server/services/python/market/Dockerfile.mutation.test
@@ -0,0 +1,21 @@
+FROM measureonecodetwice/finus-market:local
+
+WORKDIR /app
+
+COPY src ./src
+COPY test ./test
+COPY requirements.txt .
+COPY requirements-test.txt .
+COPY pyproject.toml .
+
+RUN pip install -r requirements-test.txt
+
+RUN cp -r src market_src
+RUN cp -r test mutation_test
+RUN find mutation_test -type f -name "*.py" -exec perl -0pi -e "s/\\bfrom src import\\b/from market_src import/g; s/\\bimport src\\./import market_src./g; s/\\bsrc\\./market_src./g" {} +
+RUN rm -rf src test
+
+ENV PYTHONPATH=/app
+ENV CI=true
+
+CMD ["mutmut", "run"]
diff --git a/app/server/services/python/market/Dockerfile.test b/app/server/services/python/market/Dockerfile.test
new file mode 100644
index 0000000..81be651
--- /dev/null
+++ b/app/server/services/python/market/Dockerfile.test
@@ -0,0 +1,15 @@
+FROM measureonecodetwice/finus-market:local
+
+WORKDIR /app
+
+COPY test ./test
+COPY requirements.txt .
+COPY requirements-test.txt .
+
+RUN pip install -r requirements-test.txt
+
+ENV PYTHONPATH=/app
+ENV JWT_SECRET=test_secret_key_12345
+ENV CI=true
+
+CMD ["python", "-m", "pytest", "test/", "-v", "--cov=src"]
diff --git a/app/server/services/python/market/integration/test_market_integration.py b/app/server/services/python/market/integration/test_market_integration.py
new file mode 100644
index 0000000..de030b8
--- /dev/null
+++ b/app/server/services/python/market/integration/test_market_integration.py
@@ -0,0 +1,181 @@
+import os
+import time
+
+import httpx
+import pytest
+
+
+MARKET_SERVICE_ADDR = os.getenv("MARKET_SERVICE_ADDR", "http://market:8000")
+READY_TIMEOUT_SECONDS = float(os.getenv("MARKET_READY_TIMEOUT_SECONDS", "60"))
+
+
+def wait_for_market_service(base_url: str, timeout_seconds: float) -> None:
+ deadline = time.monotonic() + timeout_seconds
+ last_error = "market service never became reachable"
+
+ while time.monotonic() < deadline:
+ try:
+ response = httpx.get(f"{base_url}/health", timeout=5.0)
+ if response.status_code == 200 and response.text == "ok":
+ return
+ last_error = f"unexpected health response: {response.status_code} {response.text}"
+ except httpx.HTTPError as exc:
+ last_error = str(exc)
+
+ time.sleep(1)
+
+ pytest.fail(f"Market service was not ready within {timeout_seconds}s: {last_error}")
+
+
+@pytest.fixture(scope="session")
+def market_client() -> httpx.Client:
+ wait_for_market_service(MARKET_SERVICE_ADDR, READY_TIMEOUT_SECONDS)
+
+ with httpx.Client(base_url=MARKET_SERVICE_ADDR, timeout=15.0) as client:
+ yield client
+
+
+def test_market_health_endpoint(market_client: httpx.Client) -> None:
+ response = market_client.get("/health")
+
+ assert response.status_code == 200
+ assert response.text == "ok"
+
+
+def test_search_endpoint_returns_enriched_market_results(
+ market_client: httpx.Client,
+) -> None:
+ response = market_client.get("/markets/search", params={"q": "", "limit": 2})
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert isinstance(payload, list)
+ assert 0 < len(payload) <= 2
+
+ result = payload[0]
+ assert result["symbol"]
+ assert result["displaySymbol"]
+ assert result["name"]
+ assert result["type"] in {"stock", "forex"}
+ assert isinstance(result["price"], (int, float))
+ assert isinstance(result["timestamp"], int)
+ assert result["source"] == "yahoo"
+
+
+def test_search_endpoint_respects_limit_param(market_client: httpx.Client) -> None:
+ response = market_client.get("/markets/search", params={"q": "", "limit": 1})
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert isinstance(payload, list)
+ assert len(payload) == 1
+
+
+def test_search_endpoint_can_return_forex_results_for_forex_query(
+ market_client: httpx.Client,
+) -> None:
+ response = market_client.get("/markets/search", params={"q": "EUR", "limit": 8})
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert isinstance(payload, list)
+ assert any(item["type"] == "forex" for item in payload)
+
+
+def test_search_endpoint_rejects_limit_above_maximum(
+ market_client: httpx.Client,
+) -> None:
+ response = market_client.get("/markets/search", params={"q": "", "limit": 21})
+
+ assert response.status_code == 422
+
+
+def test_search_endpoint_rejects_limit_below_minimum(
+ market_client: httpx.Client,
+) -> None:
+ response = market_client.get("/markets/search", params={"q": "", "limit": 0})
+
+ assert response.status_code == 422
+
+
+def test_quote_endpoint_returns_live_snapshot(market_client: httpx.Client) -> None:
+ response = market_client.get("/markets/quote", params={"symbol": "AAPL"})
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["symbol"] == "AAPL"
+ assert isinstance(payload["price"], (int, float))
+ assert isinstance(payload["timestamp"], int)
+ assert payload["source"] == "yahoo"
+
+
+def test_quote_endpoint_requires_symbol(market_client: httpx.Client) -> None:
+ response = market_client.get("/markets/quote")
+
+ assert response.status_code == 422
+
+
+def test_history_endpoint_returns_points_for_supported_period(
+ market_client: httpx.Client,
+) -> None:
+ response = market_client.get(
+ "/markets/history",
+ params={"symbol": "AAPL", "period": "1mo", "interval": "1d"},
+ )
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["symbol"] == "AAPL"
+ assert payload["period"] == "1mo"
+ assert payload["interval"] == "1d"
+ assert payload["source"] == "yahoo"
+ assert isinstance(payload["points"], list)
+ assert payload["points"]
+ assert isinstance(payload["points"][0]["timestamp"], int)
+ assert isinstance(payload["points"][0]["price"], (int, float))
+
+
+def test_history_endpoint_points_are_in_chronological_order(
+ market_client: httpx.Client,
+) -> None:
+ response = market_client.get(
+ "/markets/history",
+ params={"symbol": "AAPL", "period": "1mo", "interval": "1d"},
+ )
+
+ assert response.status_code == 200
+ timestamps = [point["timestamp"] for point in response.json()["points"]]
+ assert timestamps == sorted(timestamps)
+
+
+def test_history_endpoint_requires_symbol(market_client: httpx.Client) -> None:
+ response = market_client.get(
+ "/markets/history",
+ params={"period": "1mo", "interval": "1d"},
+ )
+
+ assert response.status_code == 422
+
+
+def test_history_endpoint_rejects_unsupported_period(
+ market_client: httpx.Client,
+) -> None:
+ response = market_client.get(
+ "/markets/history",
+ params={"symbol": "AAPL", "period": "bad", "interval": "1d"},
+ )
+
+ assert response.status_code == 400
+ assert response.json()["detail"] == "Unsupported period."
+
+
+def test_history_endpoint_rejects_unsupported_interval(
+ market_client: httpx.Client,
+) -> None:
+ response = market_client.get(
+ "/markets/history",
+ params={"symbol": "AAPL", "period": "1mo", "interval": "bad"},
+ )
+
+ assert response.status_code == 400
+ assert response.json()["detail"] == "Unsupported interval."
diff --git a/app/server/services/python/market/pyproject.toml b/app/server/services/python/market/pyproject.toml
new file mode 100644
index 0000000..34fe554
--- /dev/null
+++ b/app/server/services/python/market/pyproject.toml
@@ -0,0 +1,8 @@
+[tool.mutmut]
+paths_to_mutate = ["market_src/"]
+pytest_add_cli_args_test_selection = ["mutation_test/"]
+pytest_add_cli_args = ["-q"]
+mutate_only_covered_lines = true
+do_not_mutate = ["market_src/__init__.py"]
+also_copy = ["mutation_test/"]
+debug = true
diff --git a/app/server/services/python/market/requirements-test.txt b/app/server/services/python/market/requirements-test.txt
new file mode 100644
index 0000000..9924071
--- /dev/null
+++ b/app/server/services/python/market/requirements-test.txt
@@ -0,0 +1,5 @@
+-r requirements.txt
+pytest>=7.0.0
+pytest-cov>=4.1.0
+httpx>=0.27.0,<0.28.0
+mutmut>=3.2.3
diff --git a/app/server/services/python/market/requirements.txt b/app/server/services/python/market/requirements.txt
new file mode 100644
index 0000000..0ac6586
--- /dev/null
+++ b/app/server/services/python/market/requirements.txt
@@ -0,0 +1,5 @@
+fastapi==0.104.1
+uvicorn[standard]==0.24.0
+yfinance==0.2.33
+pandas==2.1.4
+requests==2.31.0
\ No newline at end of file
diff --git a/app/server/services/python/market/src/__init__.py b/app/server/services/python/market/src/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/app/server/services/python/market/src/__init__.py
@@ -0,0 +1 @@
+
diff --git a/app/server/services/python/market/src/config.py b/app/server/services/python/market/src/config.py
new file mode 100644
index 0000000..d57f798
--- /dev/null
+++ b/app/server/services/python/market/src/config.py
@@ -0,0 +1,124 @@
+YAHOO_SEARCH_URL = "https://query1.finance.yahoo.com/v1/finance/search"
+YAHOO_CHART_URL = "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
+YAHOO_SYMBOL_SAFE_CHARS = "=^.-"
+SUPPORTED_PERIODS = {"1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y"}
+SUPPORTED_INTERVALS = {"5m", "15m", "1d", "1wk", "1mo"}
+DEFAULT_SEARCH_LIMIT = 8
+REQUEST_HEADERS = {
+ "User-Agent": (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/137.0.0.0 Safari/537.36"
+ )
+}
+
+FEATURED_STOCKS = [
+ {
+ "symbol": "AAPL",
+ "displaySymbol": "AAPL",
+ "name": "Apple Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "MSFT",
+ "displaySymbol": "MSFT",
+ "name": "Microsoft Corporation",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "NVDA",
+ "displaySymbol": "NVDA",
+ "name": "NVIDIA Corporation",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "AMZN",
+ "displaySymbol": "AMZN",
+ "name": "Amazon.com, Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "GOOGL",
+ "displaySymbol": "GOOGL",
+ "name": "Alphabet Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "TSLA",
+ "displaySymbol": "TSLA",
+ "name": "Tesla, Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+]
+
+FOREX_CATALOG = [
+ {
+ "symbol": "EURUSD=X",
+ "displaySymbol": "EUR/USD",
+ "name": "Euro to US Dollar",
+ "type": "forex",
+ "currency": "USD",
+ },
+ {
+ "symbol": "GBPUSD=X",
+ "displaySymbol": "GBP/USD",
+ "name": "British Pound to US Dollar",
+ "type": "forex",
+ "currency": "USD",
+ },
+ {
+ "symbol": "USDJPY=X",
+ "displaySymbol": "USD/JPY",
+ "name": "US Dollar to Japanese Yen",
+ "type": "forex",
+ "currency": "JPY",
+ },
+ {
+ "symbol": "USDCAD=X",
+ "displaySymbol": "USD/CAD",
+ "name": "US Dollar to Canadian Dollar",
+ "type": "forex",
+ "currency": "CAD",
+ },
+ {
+ "symbol": "AUDUSD=X",
+ "displaySymbol": "AUD/USD",
+ "name": "Australian Dollar to US Dollar",
+ "type": "forex",
+ "currency": "USD",
+ },
+ {
+ "symbol": "NZDUSD=X",
+ "displaySymbol": "NZD/USD",
+ "name": "New Zealand Dollar to US Dollar",
+ "type": "forex",
+ "currency": "USD",
+ },
+ {
+ "symbol": "USDCHF=X",
+ "displaySymbol": "USD/CHF",
+ "name": "US Dollar to Swiss Franc",
+ "type": "forex",
+ "currency": "CHF",
+ },
+ {
+ "symbol": "EURGBP=X",
+ "displaySymbol": "EUR/GBP",
+ "name": "Euro to British Pound",
+ "type": "forex",
+ "currency": "GBP",
+ },
+]
+
diff --git a/app/server/services/python/market/src/main.py b/app/server/services/python/market/src/main.py
new file mode 100644
index 0000000..c19d8dc
--- /dev/null
+++ b/app/server/services/python/market/src/main.py
@@ -0,0 +1,73 @@
+import os
+
+import uvicorn
+from fastapi import FastAPI, HTTPException, Query
+from fastapi.responses import PlainTextResponse
+
+from .config import DEFAULT_SEARCH_LIMIT, SUPPORTED_INTERVALS, SUPPORTED_PERIODS
+from .service import build_history, enrich_search_results, fetch_quote_snapshot
+from .yahoo_client import (
+ search_forex_catalog,
+ search_stock_catalog,
+ yahoo_search,
+)
+
+app = FastAPI()
+
+
+@app.get('/health', response_class=PlainTextResponse)
+def test_endpoint():
+ return "ok"
+
+
+@app.get("/markets/search")
+def search_markets(
+ q: str = Query(default="", min_length=0),
+ limit: int = Query(default=DEFAULT_SEARCH_LIMIT, ge=1, le=20),
+):
+ forex_matches = search_forex_catalog(q, limit)
+ stock_matches = search_stock_catalog(q, limit)
+ yahoo_matches = yahoo_search(q, limit)
+
+ combined = []
+ seen_symbols = set()
+ for item in [*stock_matches, *forex_matches, *yahoo_matches]:
+ if item["symbol"] in seen_symbols:
+ continue
+ seen_symbols.add(item["symbol"])
+ combined.append(item)
+ if len(combined) >= limit:
+ break
+
+ return enrich_search_results(combined)
+
+
+@app.get("/markets/quote")
+def get_market_quote(symbol: str = Query(min_length=1)):
+ return fetch_quote_snapshot(symbol)
+
+
+@app.get("/markets/history")
+def get_market_history(
+ symbol: str = Query(min_length=1),
+ period: str = Query(default="6mo"),
+ interval: str = Query(default="1d"),
+):
+ if period not in SUPPORTED_PERIODS:
+ raise HTTPException(status_code=400, detail="Unsupported period.")
+ if interval not in SUPPORTED_INTERVALS:
+ raise HTTPException(status_code=400, detail="Unsupported interval.")
+
+ points, source = build_history(symbol, period, interval)
+ return {
+ "symbol": symbol,
+ "period": period,
+ "interval": interval,
+ "source": source,
+ "points": points,
+ }
+
+
+if __name__ == "__main__":
+ port = int(os.getenv("PORT", 8000))
+ uvicorn.run(app, port=port)
diff --git a/app/server/services/python/market/src/service.py b/app/server/services/python/market/src/service.py
new file mode 100644
index 0000000..ab964f7
--- /dev/null
+++ b/app/server/services/python/market/src/service.py
@@ -0,0 +1,58 @@
+from fastapi import HTTPException
+
+from .utils import format_change, to_float
+from .yahoo_client import extract_chart_points, fetch_yahoo_chart
+
+
+def fetch_quote_snapshot(symbol: str) -> dict:
+ chart_result = fetch_yahoo_chart(symbol, "1mo", "1d")
+ points = extract_chart_points(chart_result)
+ meta = chart_result.get("meta") or {}
+
+ latest_point = points[-1]
+ latest_price = to_float(meta.get("regularMarketPrice"))
+ if latest_price is None:
+ latest_price = latest_point["price"]
+
+ previous_close = to_float(meta.get("chartPreviousClose"))
+ if previous_close is None:
+ previous_close = to_float(meta.get("previousClose"))
+ if previous_close is None and len(points) > 1:
+ previous_close = points[-2]["price"]
+
+ change, change_percent = format_change(latest_price, previous_close)
+
+ return {
+ "symbol": symbol,
+ "price": latest_price,
+ "change": change,
+ "changePercent": change_percent,
+ "timestamp": latest_point["timestamp"],
+ "source": "yahoo",
+ }
+
+
+def build_history(symbol: str, period: str, interval: str) -> tuple[list[dict], str]:
+ chart_result = fetch_yahoo_chart(symbol, period, interval)
+ return extract_chart_points(chart_result), "yahoo"
+
+
+def enrich_search_results(items: list[dict]) -> list[dict]:
+ enriched = []
+ for item in items:
+ try:
+ snapshot = fetch_quote_snapshot(item["symbol"])
+ enriched.append(
+ {
+ **item,
+ "price": snapshot["price"],
+ "change": snapshot["change"],
+ "changePercent": snapshot["changePercent"],
+ "timestamp": snapshot["timestamp"],
+ "source": snapshot["source"],
+ }
+ )
+ except HTTPException:
+ continue
+
+ return enriched
diff --git a/app/server/services/python/market/src/utils.py b/app/server/services/python/market/src/utils.py
new file mode 100644
index 0000000..9b060f5
--- /dev/null
+++ b/app/server/services/python/market/src/utils.py
@@ -0,0 +1,46 @@
+import math
+from datetime import datetime, timezone
+
+
+def is_number(value) -> bool:
+ return isinstance(value, (int, float)) and math.isfinite(value)
+
+
+def to_float(value) -> float | None:
+ if is_number(value):
+ return round(float(value), 6)
+ return None
+
+
+def to_timestamp(value) -> int | None:
+ if value is None:
+ return None
+
+ if hasattr(value, "to_pydatetime"):
+ value = value.to_pydatetime()
+
+ if isinstance(value, datetime):
+ if value.tzinfo is None:
+ value = value.replace(tzinfo=timezone.utc)
+ return int(value.timestamp())
+
+ return None
+
+
+def format_change(
+ current: float | None, previous: float | None
+) -> tuple[float | None, float | None]:
+ if current is None or previous is None:
+ return None, None
+
+ change = round(current - previous, 6)
+ if previous == 0:
+ return change, None
+
+ change_percent = round((change / previous) * 100, 4)
+ return change, change_percent
+
+
+def normalize_query(value: str) -> str:
+ return "".join(char.lower() for char in value if char.isalnum())
+
diff --git a/app/server/services/python/market/src/yahoo_client.py b/app/server/services/python/market/src/yahoo_client.py
new file mode 100644
index 0000000..a0e612a
--- /dev/null
+++ b/app/server/services/python/market/src/yahoo_client.py
@@ -0,0 +1,171 @@
+from typing import Literal
+from urllib.parse import quote
+
+import requests
+from fastapi import HTTPException
+
+from .config import (
+ FEATURED_STOCKS,
+ FOREX_CATALOG,
+ REQUEST_HEADERS,
+ YAHOO_CHART_URL,
+ YAHOO_SEARCH_URL,
+ YAHOO_SYMBOL_SAFE_CHARS,
+)
+from .utils import normalize_query, to_float
+
+
+def fetch_yahoo_chart(symbol: str, period: str, interval: str) -> dict:
+ try:
+ response = requests.get(
+ YAHOO_CHART_URL.format(symbol=quote(symbol, safe=YAHOO_SYMBOL_SAFE_CHARS)),
+ params={
+ "range": period,
+ "interval": interval,
+ "includePrePost": "false",
+ "events": "div,splits",
+ },
+ headers=REQUEST_HEADERS,
+ timeout=8,
+ )
+ response.raise_for_status()
+ payload = response.json()
+ except (requests.RequestException, ValueError) as exc:
+ raise HTTPException(
+ status_code=502,
+ detail="Unable to reach Yahoo market data.",
+ ) from exc
+
+ chart = payload.get("chart") or {}
+ if chart.get("error"):
+ description = chart["error"].get("description")
+ raise HTTPException(
+ status_code=404,
+ detail=description or "Yahoo market data not found for symbol.",
+ )
+
+ results = chart.get("result") or []
+ if not results:
+ raise HTTPException(
+ status_code=404,
+ detail="Yahoo market data not found for symbol.",
+ )
+
+ return results[0]
+
+
+def extract_chart_points(chart_result: dict) -> list[dict]:
+ timestamps = chart_result.get("timestamp") or []
+ indicators = chart_result.get("indicators") or {}
+ quotes = indicators.get("quote") or []
+ if not quotes:
+ raise HTTPException(
+ status_code=404,
+ detail="Historical data not found for symbol.",
+ )
+
+ first_quote = quotes[0] or {}
+ closes = first_quote.get("close") or []
+ points = []
+ for timestamp, close in zip(timestamps, closes):
+ close_price = to_float(close)
+ if close_price is None:
+ continue
+
+ points.append({"timestamp": int(timestamp), "price": close_price})
+
+ if not points:
+ raise HTTPException(
+ status_code=404,
+ detail="Historical data not found for symbol.",
+ )
+
+ return points
+
+
+def search_forex_catalog(query: str, limit: int) -> list[dict]:
+ normalized_query = normalize_query(query)
+ if not normalized_query:
+ return FOREX_CATALOG[:limit]
+
+ matches = []
+ for item in FOREX_CATALOG:
+ haystack = normalize_query(
+ f"{item['symbol']} {item['displaySymbol']} {item['name']}"
+ )
+ if normalized_query in haystack:
+ matches.append(item)
+
+ return matches[:limit]
+
+
+def search_stock_catalog(query: str, limit: int) -> list[dict]:
+ normalized_query = normalize_query(query)
+ if not normalized_query:
+ return FEATURED_STOCKS[:limit]
+
+ matches = []
+ for item in FEATURED_STOCKS:
+ haystack = normalize_query(
+ f"{item['symbol']} {item['displaySymbol']} {item['name']}"
+ )
+ if normalized_query in haystack:
+ matches.append(item)
+
+ return matches[:limit]
+
+
+def yahoo_search(query: str, limit: int) -> list[dict]:
+ try:
+ response = requests.get(
+ YAHOO_SEARCH_URL,
+ params={"q": query, "quotesCount": limit * 2, "newsCount": 0},
+ headers=REQUEST_HEADERS,
+ timeout=6,
+ )
+ response.raise_for_status()
+ payload = response.json()
+ except (requests.RequestException, ValueError):
+ return []
+
+ results = []
+ for item in payload.get("quotes") or []:
+ quote_type = item.get("quoteType")
+ symbol = item.get("symbol")
+ if not symbol:
+ continue
+
+ if quote_type == "EQUITY":
+ instrument_type: Literal["stock", "forex"] = "stock"
+ elif quote_type == "CURRENCY":
+ instrument_type = "forex"
+ else:
+ continue
+
+ display_symbol = symbol if instrument_type == "stock" else symbol.replace("=X", "")
+ name = item.get("longname") or item.get("shortname") or symbol
+ currency = item.get("currency") or "USD"
+
+ results.append(
+ {
+ "symbol": symbol,
+ "displaySymbol": display_symbol,
+ "name": name,
+ "type": instrument_type,
+ "currency": currency,
+ "exchange": item.get("exchange"),
+ }
+ )
+
+ unique_results = []
+ seen_symbols = set()
+ for result in results:
+ if result["symbol"] in seen_symbols:
+ continue
+ seen_symbols.add(result["symbol"])
+ unique_results.append(result)
+ if len(unique_results) >= limit:
+ break
+
+ return unique_results
+
diff --git a/app/server/services/python/market/test/__init__.py b/app/server/services/python/market/test/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/app/server/services/python/market/test/__init__.py
@@ -0,0 +1 @@
+
diff --git a/app/server/services/python/market/test/test_config.py b/app/server/services/python/market/test/test_config.py
new file mode 100644
index 0000000..bde78e1
--- /dev/null
+++ b/app/server/services/python/market/test/test_config.py
@@ -0,0 +1,149 @@
+from src import config
+
+
+def test_market_config_core_constants():
+ assert (
+ config.YAHOO_SEARCH_URL
+ == "https://query1.finance.yahoo.com/v1/finance/search"
+ )
+ assert (
+ config.YAHOO_CHART_URL
+ == "https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
+ )
+ assert config.YAHOO_SYMBOL_SAFE_CHARS == "=^.-"
+ assert config.DEFAULT_SEARCH_LIMIT == 8
+ assert config.SUPPORTED_PERIODS == {
+ "1d",
+ "5d",
+ "1mo",
+ "3mo",
+ "6mo",
+ "1y",
+ "2y",
+ "5y",
+ }
+ assert config.SUPPORTED_INTERVALS == {"5m", "15m", "1d", "1wk", "1mo"}
+
+
+def test_market_config_request_headers():
+ assert config.REQUEST_HEADERS == {
+ "User-Agent": (
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
+ "Chrome/137.0.0.0 Safari/537.36"
+ )
+ }
+
+
+def test_market_config_featured_stocks_catalog_contents():
+ assert config.FEATURED_STOCKS == [
+ {
+ "symbol": "AAPL",
+ "displaySymbol": "AAPL",
+ "name": "Apple Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "MSFT",
+ "displaySymbol": "MSFT",
+ "name": "Microsoft Corporation",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "NVDA",
+ "displaySymbol": "NVDA",
+ "name": "NVIDIA Corporation",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "AMZN",
+ "displaySymbol": "AMZN",
+ "name": "Amazon.com, Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "GOOGL",
+ "displaySymbol": "GOOGL",
+ "name": "Alphabet Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "TSLA",
+ "displaySymbol": "TSLA",
+ "name": "Tesla, Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ ]
+
+
+def test_market_config_forex_catalog_contents():
+ assert config.FOREX_CATALOG == [
+ {
+ "symbol": "EURUSD=X",
+ "displaySymbol": "EUR/USD",
+ "name": "Euro to US Dollar",
+ "type": "forex",
+ "currency": "USD",
+ },
+ {
+ "symbol": "GBPUSD=X",
+ "displaySymbol": "GBP/USD",
+ "name": "British Pound to US Dollar",
+ "type": "forex",
+ "currency": "USD",
+ },
+ {
+ "symbol": "USDJPY=X",
+ "displaySymbol": "USD/JPY",
+ "name": "US Dollar to Japanese Yen",
+ "type": "forex",
+ "currency": "JPY",
+ },
+ {
+ "symbol": "USDCAD=X",
+ "displaySymbol": "USD/CAD",
+ "name": "US Dollar to Canadian Dollar",
+ "type": "forex",
+ "currency": "CAD",
+ },
+ {
+ "symbol": "AUDUSD=X",
+ "displaySymbol": "AUD/USD",
+ "name": "Australian Dollar to US Dollar",
+ "type": "forex",
+ "currency": "USD",
+ },
+ {
+ "symbol": "NZDUSD=X",
+ "displaySymbol": "NZD/USD",
+ "name": "New Zealand Dollar to US Dollar",
+ "type": "forex",
+ "currency": "USD",
+ },
+ {
+ "symbol": "USDCHF=X",
+ "displaySymbol": "USD/CHF",
+ "name": "US Dollar to Swiss Franc",
+ "type": "forex",
+ "currency": "CHF",
+ },
+ {
+ "symbol": "EURGBP=X",
+ "displaySymbol": "EUR/GBP",
+ "name": "Euro to British Pound",
+ "type": "forex",
+ "currency": "GBP",
+ },
+ ]
diff --git a/app/server/services/python/market/test/test_main.py b/app/server/services/python/market/test/test_main.py
new file mode 100644
index 0000000..51832a0
--- /dev/null
+++ b/app/server/services/python/market/test/test_main.py
@@ -0,0 +1,136 @@
+import os
+import runpy
+from unittest.mock import patch
+
+import pytest
+from fastapi.testclient import TestClient
+
+import src.main
+
+
+client = TestClient(src.main.app)
+
+
+def test_health_endpoint():
+ response = client.get("/health")
+ assert response.status_code == 200
+ assert response.text == "ok"
+
+
+def test_search_route_combines_sources_and_enriches_results():
+ with patch("src.main.search_forex_catalog", return_value=[{"symbol": "EURUSD=X"}]), patch(
+ "src.main.search_stock_catalog", return_value=[{"symbol": "AAPL"}]
+ ), patch("src.main.yahoo_search", return_value=[{"symbol": "AAPL"}, {"symbol": "MSFT"}]), patch(
+ "src.main.enrich_search_results",
+ return_value=[{"symbol": "AAPL"}, {"symbol": "EURUSD=X"}, {"symbol": "MSFT"}],
+ ) as enrich_mock:
+ response = client.get("/markets/search", params={"q": "apple", "limit": 5})
+
+ assert response.status_code == 200
+ assert response.json() == [{"symbol": "AAPL"}, {"symbol": "EURUSD=X"}, {"symbol": "MSFT"}]
+ enrich_mock.assert_called_once_with(
+ [{"symbol": "AAPL"}, {"symbol": "EURUSD=X"}, {"symbol": "MSFT"}]
+ )
+
+
+def test_search_route_stops_combining_once_limit_is_reached():
+ with patch(
+ "src.main.search_forex_catalog",
+ return_value=[{"symbol": "EURUSD=X"}, {"symbol": "GBPUSD=X"}],
+ ), patch(
+ "src.main.search_stock_catalog",
+ return_value=[{"symbol": "AAPL"}, {"symbol": "MSFT"}],
+ ), patch(
+ "src.main.yahoo_search",
+ return_value=[{"symbol": "TSLA"}],
+ ), patch(
+ "src.main.enrich_search_results",
+ side_effect=lambda items: items,
+ ) as enrich_mock:
+ response = client.get("/markets/search", params={"q": "a", "limit": 2})
+
+ assert response.status_code == 200
+ assert response.json() == [{"symbol": "AAPL"}, {"symbol": "MSFT"}]
+ enrich_mock.assert_called_once_with([{"symbol": "AAPL"}, {"symbol": "MSFT"}])
+
+
+def test_search_route_uses_default_query_and_limit():
+ with patch("src.main.search_forex_catalog", return_value=[]) as forex_mock, patch(
+ "src.main.search_stock_catalog", return_value=[]
+ ) as stock_mock, patch("src.main.yahoo_search", return_value=[]) as yahoo_mock, patch(
+ "src.main.enrich_search_results",
+ return_value=[],
+ ) as enrich_mock:
+ response = client.get("/markets/search")
+
+ assert response.status_code == 200
+ assert response.json() == []
+ forex_mock.assert_called_once_with("", src.main.DEFAULT_SEARCH_LIMIT)
+ stock_mock.assert_called_once_with("", src.main.DEFAULT_SEARCH_LIMIT)
+ yahoo_mock.assert_called_once_with("", src.main.DEFAULT_SEARCH_LIMIT)
+ enrich_mock.assert_called_once_with([])
+
+
+def test_quote_route_delegates_to_service():
+ with patch("src.main.fetch_quote_snapshot", return_value={"symbol": "AAPL"}) as quote_mock:
+ response = client.get("/markets/quote", params={"symbol": "AAPL"})
+
+ assert response.status_code == 200
+ assert response.json() == {"symbol": "AAPL"}
+ quote_mock.assert_called_once_with("AAPL")
+
+
+def test_history_route_returns_payload_and_validation_errors():
+ with patch(
+ "src.main.build_history",
+ return_value=([{"timestamp": 1, "price": 10.0}], "yahoo"),
+ ) as history_mock:
+ response = client.get(
+ "/markets/history",
+ params={"symbol": "AAPL", "period": "1mo", "interval": "1d"},
+ )
+
+ assert response.status_code == 200
+ assert response.json() == {
+ "symbol": "AAPL",
+ "period": "1mo",
+ "interval": "1d",
+ "source": "yahoo",
+ "points": [{"timestamp": 1, "price": 10.0}],
+ }
+ history_mock.assert_called_once_with("AAPL", "1mo", "1d")
+
+ invalid_period = client.get(
+ "/markets/history",
+ params={"symbol": "AAPL", "period": "bad", "interval": "1d"},
+ )
+ assert invalid_period.status_code == 400
+ assert invalid_period.json()["detail"] == "Unsupported period."
+
+ invalid_interval = client.get(
+ "/markets/history",
+ params={"symbol": "AAPL", "period": "1mo", "interval": "bad"},
+ )
+ assert invalid_interval.status_code == 400
+ assert invalid_interval.json()["detail"] == "Unsupported interval."
+
+
+def test_history_route_uses_default_period_and_interval():
+ with patch(
+ "src.main.build_history",
+ return_value=([{"timestamp": 1, "price": 10.0}], "yahoo"),
+ ) as history_mock:
+ response = client.get("/markets/history", params={"symbol": "AAPL"})
+
+ assert response.status_code == 200
+ assert response.json()["period"] == "6mo"
+ assert response.json()["interval"] == "1d"
+ history_mock.assert_called_once_with("AAPL", "6mo", "1d")
+
+
+def test_main_module_runs_uvicorn_when_executed_as_script():
+ with patch.dict(os.environ, {"PORT": "9999"}), patch("uvicorn.run") as run_mock:
+ runpy.run_module("src.main", run_name="__main__")
+
+ run_mock.assert_called_once()
+ assert run_mock.call_args.kwargs["port"] == 9999
diff --git a/app/server/services/python/market/test/test_service.py b/app/server/services/python/market/test/test_service.py
new file mode 100644
index 0000000..f009734
--- /dev/null
+++ b/app/server/services/python/market/test/test_service.py
@@ -0,0 +1,447 @@
+from unittest.mock import patch
+
+from fastapi import HTTPException
+
+from src import service
+
+
+def test_fetch_quote_snapshot_uses_market_price_and_previous_close():
+ chart_result = {
+ "meta": {
+ "regularMarketPrice": 151.23,
+ "chartPreviousClose": 149.0,
+ }
+ }
+ points = [
+ {"timestamp": 1700000000, "price": 149.0},
+ {"timestamp": 1700003600, "price": 150.5},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 151.23,
+ "change": 2.23,
+ "changePercent": 1.4966,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_handles_missing_meta_key():
+ chart_result = {}
+ points = [
+ {"timestamp": 1700000000, "price": 149.0},
+ {"timestamp": 1700003600, "price": 150.5},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 150.5,
+ "change": 1.5,
+ "changePercent": 1.0067,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_requests_the_expected_chart_defaults():
+ chart_result = {"meta": {"regularMarketPrice": 151.23, "chartPreviousClose": 149.0}}
+ points = [
+ {"timestamp": 1700000000, "price": 149.0},
+ {"timestamp": 1700003600, "price": 150.5},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result) as chart_mock, patch(
+ "src.service.extract_chart_points", return_value=points
+ ) as extract_mock:
+ service.fetch_quote_snapshot("AAPL")
+
+ chart_mock.assert_called_once_with("AAPL", "1mo", "1d")
+ extract_mock.assert_called_once_with(chart_result)
+
+
+def test_fetch_quote_snapshot_handles_missing_meta_key():
+ chart_result = {}
+ points = [
+ {"timestamp": 1700000000, "price": 149.0},
+ {"timestamp": 1700003600, "price": 150.5},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 150.5,
+ "change": 1.5,
+ "changePercent": 1.0067,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_falls_back_to_chart_points_when_meta_missing():
+ chart_result = {"meta": {}}
+ points = [
+ {"timestamp": 1700000000, "price": 149.0},
+ {"timestamp": 1700003600, "price": 150.5},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 150.5,
+ "change": 1.5,
+ "changePercent": 1.0067,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_falls_back_to_previous_close_meta():
+ chart_result = {"meta": {"regularMarketPrice": 151.23, "previousClose": 150.0}}
+ points = [
+ {"timestamp": 1700000000, "price": 149.0},
+ {"timestamp": 1700003600, "price": 150.5},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 151.23,
+ "change": 1.23,
+ "changePercent": 0.82,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_falls_back_to_previous_point_without_meta_closes():
+ chart_result = {"meta": {}}
+ points = [
+ {"timestamp": 1700000000, "price": 100.0},
+ {"timestamp": 1700003600, "price": 110.0},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 110.0,
+ "change": 10.0,
+ "changePercent": 10.0,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_returns_none_change_with_single_point():
+ chart_result = {"meta": {}}
+ points = [{"timestamp": 1700003600, "price": 150.5}]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 150.5,
+ "change": None,
+ "changePercent": None,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_prefers_chart_previous_close_over_previous_close():
+ chart_result = {
+ "meta": {
+ "regularMarketPrice": 151.23,
+ "chartPreviousClose": 149.0,
+ "previousClose": 140.0,
+ }
+ }
+ points = [
+ {"timestamp": 1700000000, "price": 140.0},
+ {"timestamp": 1700003600, "price": 150.5},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot["change"] == 2.23
+ assert snapshot["changePercent"] == 1.4966
+
+
+def test_fetch_quote_snapshot_falls_back_when_meta_values_are_not_numeric():
+ chart_result = {
+ "meta": {
+ "regularMarketPrice": "bad",
+ "chartPreviousClose": "bad",
+ "previousClose": "bad",
+ }
+ }
+ points = [
+ {"timestamp": 1700000000, "price": 149.0},
+ {"timestamp": 1700003600, "price": 150.5},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 150.5,
+ "change": 1.5,
+ "changePercent": 1.0067,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_preserves_zero_market_price_and_previous_close():
+ chart_result = {
+ "meta": {
+ "regularMarketPrice": 0.0,
+ "chartPreviousClose": 0.0,
+ }
+ }
+ points = [
+ {"timestamp": 1700000000, "price": 10.0},
+ {"timestamp": 1700003600, "price": 12.0},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot == {
+ "symbol": "AAPL",
+ "price": 0.0,
+ "change": 0.0,
+ "changePercent": None,
+ "timestamp": 1700003600,
+ "source": "yahoo",
+ }
+
+
+def test_fetch_quote_snapshot_uses_last_point_timestamp_and_price_fallback():
+ chart_result = {"meta": {}}
+ points = [
+ {"timestamp": 1700000000, "price": 100.0},
+ {"timestamp": 1700003600, "price": 110.0},
+ {"timestamp": 1700007200, "price": 120.0},
+ ]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result), patch(
+ "src.service.extract_chart_points", return_value=points
+ ):
+ snapshot = service.fetch_quote_snapshot("AAPL")
+
+ assert snapshot["price"] == 120.0
+ assert snapshot["timestamp"] == 1700007200
+ assert snapshot["change"] == 10.0
+ assert snapshot["changePercent"] == 9.0909
+
+
+def test_build_history_returns_points_and_source():
+ chart_result = {"meta": {"symbol": "AAPL"}}
+ points = [{"timestamp": 1700000000, "price": 123.45}]
+
+ with patch("src.service.fetch_yahoo_chart", return_value=chart_result) as chart_mock, patch(
+ "src.service.extract_chart_points", return_value=points
+ ) as extract_mock:
+ assert service.build_history("AAPL", "1mo", "1d") == (points, "yahoo")
+ chart_mock.assert_called_once_with("AAPL", "1mo", "1d")
+ extract_mock.assert_called_once_with(chart_result)
+
+
+def test_enrich_search_results_skips_items_that_raise_http_exceptions():
+ with patch(
+ "src.service.fetch_quote_snapshot",
+ side_effect=[
+ {
+ "symbol": "AAPL",
+ "price": 100.0,
+ "change": 1.0,
+ "changePercent": 1.0,
+ "timestamp": 1700000000,
+ "source": "yahoo",
+ },
+ HTTPException(status_code=404, detail="missing"),
+ ],
+ ):
+ enriched = service.enrich_search_results(
+ [
+ {"symbol": "AAPL", "name": "Apple"},
+ {"symbol": "MISS", "name": "Missing"},
+ ]
+ )
+
+ assert enriched == [
+ {
+ "symbol": "AAPL",
+ "name": "Apple",
+ "price": 100.0,
+ "change": 1.0,
+ "changePercent": 1.0,
+ "timestamp": 1700000000,
+ "source": "yahoo",
+ }
+ ]
+
+
+def test_enrich_search_results_preserves_existing_fields():
+ with patch(
+ "src.service.fetch_quote_snapshot",
+ return_value={
+ "symbol": "AAPL",
+ "price": 100.0,
+ "change": 1.0,
+ "changePercent": 1.0,
+ "timestamp": 1700000000,
+ "source": "yahoo",
+ },
+ ):
+ enriched = service.enrich_search_results(
+ [
+ {
+ "symbol": "AAPL",
+ "name": "Apple",
+ "type": "stock",
+ "currency": "USD",
+ }
+ ]
+ )
+
+ assert enriched == [
+ {
+ "symbol": "AAPL",
+ "name": "Apple",
+ "type": "stock",
+ "currency": "USD",
+ "price": 100.0,
+ "change": 1.0,
+ "changePercent": 1.0,
+ "timestamp": 1700000000,
+ "source": "yahoo",
+ }
+ ]
+
+
+def test_enrich_search_results_returns_empty_for_empty_input():
+ with patch("src.service.fetch_quote_snapshot") as snapshot_mock:
+ enriched = service.enrich_search_results([])
+
+ assert enriched == []
+ snapshot_mock.assert_not_called()
+
+
+def test_enrich_search_results_propagates_non_http_exceptions():
+ with patch(
+ "src.service.fetch_quote_snapshot",
+ side_effect=RuntimeError("boom"),
+ ):
+ try:
+ service.enrich_search_results([{"symbol": "AAPL"}])
+ except RuntimeError as exc:
+ assert str(exc) == "boom"
+ else:
+ raise AssertionError("Expected RuntimeError to propagate")
+
+
+def test_enrich_search_results_calls_snapshot_for_each_symbol_in_order():
+ with patch(
+ "src.service.fetch_quote_snapshot",
+ side_effect=[
+ {
+ "symbol": "AAPL",
+ "price": 1.0,
+ "change": 0.1,
+ "changePercent": 10.0,
+ "timestamp": 1,
+ "source": "yahoo",
+ },
+ {
+ "symbol": "MSFT",
+ "price": 2.0,
+ "change": 0.2,
+ "changePercent": 11.0,
+ "timestamp": 2,
+ "source": "yahoo",
+ },
+ ],
+ ) as snapshot_mock:
+ enriched = service.enrich_search_results(
+ [
+ {"symbol": "AAPL", "name": "Apple"},
+ {"symbol": "MSFT", "name": "Microsoft"},
+ ]
+ )
+
+ assert [item["symbol"] for item in enriched] == ["AAPL", "MSFT"]
+ assert [call.args[0] for call in snapshot_mock.call_args_list] == ["AAPL", "MSFT"]
+
+
+def test_enrich_search_results_skips_failed_item_and_continues_afterward():
+ with patch(
+ "src.service.fetch_quote_snapshot",
+ side_effect=[
+ HTTPException(status_code=404, detail="missing"),
+ {
+ "symbol": "MSFT",
+ "price": 2.0,
+ "change": 0.2,
+ "changePercent": 11.0,
+ "timestamp": 2,
+ "source": "yahoo",
+ },
+ ],
+ ):
+ enriched = service.enrich_search_results(
+ [
+ {"symbol": "MISS", "name": "Missing"},
+ {"symbol": "MSFT", "name": "Microsoft"},
+ ]
+ )
+
+ assert enriched == [
+ {
+ "symbol": "MSFT",
+ "name": "Microsoft",
+ "price": 2.0,
+ "change": 0.2,
+ "changePercent": 11.0,
+ "timestamp": 2,
+ "source": "yahoo",
+ }
+ ]
diff --git a/app/server/services/python/market/test/test_utils.py b/app/server/services/python/market/test/test_utils.py
new file mode 100644
index 0000000..31b4928
--- /dev/null
+++ b/app/server/services/python/market/test/test_utils.py
@@ -0,0 +1,117 @@
+import os
+import time
+from datetime import datetime, timezone
+
+from src import utils
+
+
+class FakeTimestamp:
+ def __init__(self, value: datetime):
+ self.value = value
+
+ def to_pydatetime(self) -> datetime:
+ return self.value
+
+
+class TrackingTimestamp:
+ def __init__(self, value: datetime):
+ self.value = value
+ self.calls = 0
+
+ def to_pydatetime(self) -> datetime:
+ self.calls += 1
+ return self.value
+
+
+class BadTimestamp:
+ def __init__(self, value):
+ self.value = value
+
+ def to_pydatetime(self):
+ return self.value
+
+
+def test_is_number_and_to_float_helpers():
+ assert utils.is_number(3.14) is True
+ assert utils.is_number("3.14") is False
+ assert utils.is_number(float("nan")) is False
+ assert utils.to_float(12.3456789) == 12.345679
+ assert utils.to_float("12") is None
+
+
+def test_to_timestamp_handles_datetime_like_objects():
+ timestamp = datetime(2024, 1, 1, 12, 0, tzinfo=timezone.utc)
+ naive_timestamp = datetime(2024, 1, 1, 12, 0)
+ assert utils.to_timestamp(timestamp) == int(timestamp.timestamp())
+ assert utils.to_timestamp(naive_timestamp) == int(
+ naive_timestamp.replace(tzinfo=timezone.utc).timestamp()
+ )
+ assert utils.to_timestamp(FakeTimestamp(timestamp)) == int(timestamp.timestamp())
+ assert utils.to_timestamp(None) is None
+ assert utils.to_timestamp("bad-value") is None
+
+
+def test_to_timestamp_calls_to_pydatetime_once_and_normalizes_naive_values():
+ naive_timestamp = datetime(2024, 1, 1, 12, 0)
+ wrapped = TrackingTimestamp(naive_timestamp)
+
+ assert utils.to_timestamp(wrapped) == int(
+ naive_timestamp.replace(tzinfo=timezone.utc).timestamp()
+ )
+ assert wrapped.calls == 1
+
+
+def test_to_timestamp_returns_none_when_to_pydatetime_is_not_a_datetime():
+ assert utils.to_timestamp(BadTimestamp("not-a-datetime")) is None
+
+
+def test_to_timestamp_preserves_timezone_aware_offsets():
+ offset_timestamp = datetime.fromisoformat("2024-01-01T12:00:00+02:00")
+
+ assert utils.to_timestamp(offset_timestamp) == int(offset_timestamp.timestamp())
+
+
+def test_to_timestamp_treats_naive_datetimes_as_utc_even_when_local_tz_differs():
+ original_tz = os.environ.get("TZ")
+ try:
+ os.environ["TZ"] = "America/Winnipeg"
+ time.tzset()
+ naive_timestamp = datetime(2024, 1, 1, 12, 0)
+
+ assert utils.to_timestamp(naive_timestamp) == int(
+ naive_timestamp.replace(tzinfo=timezone.utc).timestamp()
+ )
+ finally:
+ if original_tz is None:
+ os.environ.pop("TZ", None)
+ else:
+ os.environ["TZ"] = original_tz
+ time.tzset()
+
+
+def test_format_change_and_normalize_query():
+ assert utils.format_change(11.0, 10.0) == (1.0, 10.0)
+ assert utils.format_change(11.0, 0.0) == (11.0, None)
+ assert utils.format_change(None, 10.0) == (None, None)
+ assert utils.normalize_query("EUR/USD Inc.") == "eurusdinc"
+
+
+def test_is_number_rejects_infinity_values():
+ assert utils.is_number(float("inf")) is False
+ assert utils.is_number(float("-inf")) is False
+
+
+def test_format_change_returns_none_when_previous_missing():
+ assert utils.format_change(11.0, None) == (None, None)
+
+
+def test_format_change_handles_negative_moves():
+ assert utils.format_change(9.0, 10.0) == (-1.0, -10.0)
+
+
+def test_normalize_query_preserves_digits_while_removing_symbols():
+ assert utils.normalize_query("BTC-USD 2024!") == "btcusd2024"
+
+
+def test_format_change_applies_rounding_precision():
+ assert utils.format_change(10.1234567, 9.7654321) == (0.358025, 3.6662)
diff --git a/app/server/services/python/market/test/test_yahoo_client.py b/app/server/services/python/market/test/test_yahoo_client.py
new file mode 100644
index 0000000..ed00cf0
--- /dev/null
+++ b/app/server/services/python/market/test/test_yahoo_client.py
@@ -0,0 +1,783 @@
+from unittest.mock import MagicMock, patch
+
+import pytest
+from fastapi import HTTPException
+
+from src import yahoo_client
+
+
+def build_response(payload):
+ response = MagicMock()
+ response.raise_for_status.return_value = None
+ response.json.return_value = payload
+ return response
+
+
+def test_fetch_yahoo_chart_returns_first_result():
+ payload = {"chart": {"result": [{"meta": {"symbol": "AAPL"}}], "error": None}}
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get:
+ result = yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert result == {"meta": {"symbol": "AAPL"}}
+ mock_get.assert_called_once_with(
+ yahoo_client.YAHOO_CHART_URL.format(symbol="AAPL"),
+ params={
+ "range": "1mo",
+ "interval": "1d",
+ "includePrePost": "false",
+ "events": "div,splits",
+ },
+ headers=yahoo_client.REQUEST_HEADERS,
+ timeout=8,
+ )
+
+
+def test_fetch_yahoo_chart_handles_request_and_json_errors():
+ with patch("src.yahoo_client.requests.get", side_effect=ValueError("bad json")):
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert exc_info.value.status_code == 502
+ assert exc_info.value.detail == "Unable to reach Yahoo market data."
+
+ with patch(
+ "src.yahoo_client.requests.get",
+ side_effect=yahoo_client.requests.RequestException("network"),
+ ):
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert exc_info.value.status_code == 502
+ assert exc_info.value.detail == "Unable to reach Yahoo market data."
+
+
+def test_fetch_yahoo_chart_handles_raise_for_status_errors():
+ response = MagicMock()
+ response.raise_for_status.side_effect = yahoo_client.requests.RequestException("bad status")
+
+ with patch("src.yahoo_client.requests.get", return_value=response):
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert exc_info.value.status_code == 502
+ assert exc_info.value.detail == "Unable to reach Yahoo market data."
+
+
+def test_fetch_yahoo_chart_prefers_first_result_when_multiple_exist():
+ payload = {
+ "chart": {
+ "result": [{"meta": {"symbol": "FIRST"}}, {"meta": {"symbol": "SECOND"}}],
+ "error": None,
+ }
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ result = yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert result == {"meta": {"symbol": "FIRST"}}
+
+
+def test_fetch_yahoo_chart_quotes_symbols_with_reserved_characters():
+ payload = {"chart": {"result": [{"meta": {"symbol": "EUR/USD"}}], "error": None}}
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get:
+ yahoo_client.fetch_yahoo_chart("EUR/USD", "1mo", "1d")
+
+ assert mock_get.call_args.args[0].endswith("/EUR%2FUSD")
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get:
+ yahoo_client.fetch_yahoo_chart("EURUSD=X", "1mo", "1d")
+
+ assert mock_get.call_args.args[0].endswith("/EURUSD=X")
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get:
+ yahoo_client.fetch_yahoo_chart("^GSPC.-", "1mo", "1d")
+
+ assert mock_get.call_args.args[0].endswith("/^GSPC.-")
+
+
+def test_fetch_yahoo_chart_handles_yahoo_error_payload():
+ payload = {"chart": {"result": None, "error": {"description": "missing"}}}
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "missing"
+
+
+def test_fetch_yahoo_chart_uses_default_message_when_description_missing():
+ payload = {"chart": {"result": None, "error": {"description": None}}}
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Yahoo market data not found for symbol."
+
+
+def test_fetch_yahoo_chart_handles_missing_results():
+ payload = {"chart": {"result": [], "error": None}}
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Yahoo market data not found for symbol."
+
+
+def test_fetch_yahoo_chart_handles_missing_chart_payload():
+ with patch("src.yahoo_client.requests.get", return_value=build_response({})):
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Yahoo market data not found for symbol."
+
+
+def test_fetch_yahoo_chart_handles_chart_none_payload():
+ with patch("src.yahoo_client.requests.get", return_value=build_response({"chart": None})):
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.fetch_yahoo_chart("AAPL", "1mo", "1d")
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Yahoo market data not found for symbol."
+
+
+def test_extract_chart_points_success_and_failures():
+ result = yahoo_client.extract_chart_points(
+ {
+ "timestamp": [1700000000, 1700003600],
+ "indicators": {"quote": [{"close": [123.45, 124.0]}]},
+ }
+ )
+ assert result == [
+ {"timestamp": 1700000000, "price": 123.45},
+ {"timestamp": 1700003600, "price": 124.0},
+ ]
+
+ with pytest.raises(HTTPException):
+ yahoo_client.extract_chart_points({"indicators": {}})
+
+ with pytest.raises(HTTPException):
+ yahoo_client.extract_chart_points(
+ {"timestamp": [1], "indicators": {"quote": [{"close": [None]}]}}
+ )
+
+ with pytest.raises(HTTPException):
+ yahoo_client.extract_chart_points(
+ {"timestamp": [1], "indicators": {"quote": [{"open": [10.0]}]}}
+ )
+
+
+def test_extract_chart_points_uses_only_the_first_quote_entry():
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.extract_chart_points(
+ {
+ "timestamp": [1],
+ "indicators": {
+ "quote": [
+ {"close": [None]},
+ {"close": [123.45]},
+ ]
+ },
+ }
+ )
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Historical data not found for symbol."
+
+
+def test_extract_chart_points_handles_first_quote_none():
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.extract_chart_points(
+ {
+ "timestamp": [1],
+ "indicators": {"quote": [None]},
+ }
+ )
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Historical data not found for symbol."
+
+
+def test_extract_chart_points_preserves_zero_price_points():
+ result = yahoo_client.extract_chart_points(
+ {
+ "timestamp": [1, 2],
+ "indicators": {"quote": [{"close": [0.0, 1.5]}]},
+ }
+ )
+
+ assert result == [
+ {"timestamp": 1, "price": 0.0},
+ {"timestamp": 2, "price": 1.5},
+ ]
+
+
+def test_extract_chart_points_skips_non_numeric_values():
+ result = yahoo_client.extract_chart_points(
+ {
+ "timestamp": [1, 2, 3],
+ "indicators": {"quote": [{"close": [10.0, "bad", 12.0]}]},
+ }
+ )
+
+ assert result == [
+ {"timestamp": 1, "price": 10.0},
+ {"timestamp": 3, "price": 12.0},
+ ]
+
+
+def test_extract_chart_points_skips_invalid_prices_but_keeps_valid_points():
+ result = yahoo_client.extract_chart_points(
+ {
+ "timestamp": [1700000000, 1700003600, 1700007200],
+ "indicators": {"quote": [{"close": [123.45, None, 124.0]}]},
+ }
+ )
+
+ assert result == [
+ {"timestamp": 1700000000, "price": 123.45},
+ {"timestamp": 1700007200, "price": 124.0},
+ ]
+
+
+def test_extract_chart_points_casts_timestamps_to_ints():
+ result = yahoo_client.extract_chart_points(
+ {
+ "timestamp": ["1700000000"],
+ "indicators": {"quote": [{"close": [123.45]}]},
+ }
+ )
+
+ assert result == [{"timestamp": 1700000000, "price": 123.45}]
+
+
+def test_extract_chart_points_handles_missing_timestamps():
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.extract_chart_points(
+ {"indicators": {"quote": [{"close": [123.45]}]}}
+ )
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Historical data not found for symbol."
+
+
+def test_extract_chart_points_handles_missing_indicators():
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.extract_chart_points({"timestamp": [1]})
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Historical data not found for symbol."
+
+
+def test_extract_chart_points_truncates_to_shortest_series():
+ result = yahoo_client.extract_chart_points(
+ {
+ "timestamp": [1, 2, 3],
+ "indicators": {"quote": [{"close": [10.0, 11.0]}]},
+ }
+ )
+
+ assert result == [
+ {"timestamp": 1, "price": 10.0},
+ {"timestamp": 2, "price": 11.0},
+ ]
+
+
+def test_extract_chart_points_raises_when_quote_list_is_empty():
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.extract_chart_points(
+ {"timestamp": [1], "indicators": {"quote": []}}
+ )
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Historical data not found for symbol."
+
+
+def test_extract_chart_points_raises_when_all_numeric_points_are_filtered_out():
+ with pytest.raises(HTTPException) as exc_info:
+ yahoo_client.extract_chart_points(
+ {
+ "timestamp": [1, 2],
+ "indicators": {"quote": [{"close": ["bad", None]}]},
+ }
+ )
+
+ assert exc_info.value.status_code == 404
+ assert exc_info.value.detail == "Historical data not found for symbol."
+
+
+def test_search_catalog_helpers_cover_empty_and_filtered_paths():
+ assert len(yahoo_client.search_forex_catalog("", 2)) == 2
+ assert yahoo_client.search_forex_catalog("euro", 5)[0]["symbol"] == "EURUSD=X"
+ assert len(yahoo_client.search_stock_catalog("", 2)) == 2
+ assert yahoo_client.search_stock_catalog("tesla", 5)[0]["symbol"] == "TSLA"
+
+
+def test_search_catalog_helpers_honor_limit_for_matching_results():
+ assert yahoo_client.search_forex_catalog("usd", 1) == [yahoo_client.FOREX_CATALOG[0]]
+ assert yahoo_client.search_stock_catalog("inc", 2) == [
+ yahoo_client.FEATURED_STOCKS[0],
+ yahoo_client.FEATURED_STOCKS[3],
+ ]
+
+
+def test_search_catalog_helpers_return_empty_for_unknown_queries():
+ assert yahoo_client.search_forex_catalog("zzzz", 5) == []
+ assert yahoo_client.search_stock_catalog("zzzz", 5) == []
+
+
+def test_yahoo_search_filters_deduplicates_and_falls_back_on_error():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "longname": "Apple Inc.",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "longname": "Apple Duplicate",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "quoteType": "CURRENCY",
+ "symbol": "EURUSD=X",
+ "shortname": "Euro FX",
+ "currency": "USD",
+ "exchange": "CCY",
+ },
+ {
+ "quoteType": "MUTUALFUND",
+ "symbol": "SKIP",
+ },
+ {
+ "quoteType": "EQUITY",
+ "symbol": None,
+ },
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("apple", 10)
+
+ assert results == [
+ {
+ "symbol": "AAPL",
+ "displaySymbol": "AAPL",
+ "name": "Apple Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "EURUSD=X",
+ "displaySymbol": "EURUSD",
+ "name": "Euro FX",
+ "type": "forex",
+ "currency": "USD",
+ "exchange": "CCY",
+ },
+ ]
+
+ with patch("src.yahoo_client.requests.get", side_effect=RuntimeError("boom")):
+ with pytest.raises(RuntimeError):
+ yahoo_client.yahoo_search("apple", 10)
+
+ with patch(
+ "src.yahoo_client.requests.get",
+ side_effect=yahoo_client.requests.RequestException("network"),
+ ):
+ assert yahoo_client.yahoo_search("apple", 10) == []
+
+ with patch("src.yahoo_client.requests.get", side_effect=ValueError("bad json")):
+ assert yahoo_client.yahoo_search("apple", 10) == []
+
+
+def test_yahoo_search_skips_quotes_without_symbols_or_supported_types():
+ payload = {
+ "quotes": [
+ {"quoteType": "EQUITY", "symbol": None},
+ {"quoteType": "MUTUALFUND", "symbol": "SKIP"},
+ {"quoteType": None, "symbol": "ALSO_SKIP"},
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("apple", 10)
+
+ assert results == []
+
+
+def test_yahoo_search_continues_after_missing_symbol_and_unsupported_type():
+ payload = {
+ "quotes": [
+ {"quoteType": "EQUITY", "symbol": None},
+ {"quoteType": "MUTUALFUND", "symbol": "SKIP"},
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "longname": "Apple Inc.",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "quoteType": "CURRENCY",
+ "symbol": "EURUSD=X",
+ "shortname": "Euro FX",
+ "currency": "USD",
+ "exchange": "CCY",
+ },
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("mixed", 10)
+
+ assert results == [
+ {
+ "symbol": "AAPL",
+ "displaySymbol": "AAPL",
+ "name": "Apple Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "EURUSD=X",
+ "displaySymbol": "EURUSD",
+ "name": "Euro FX",
+ "type": "forex",
+ "currency": "USD",
+ "exchange": "CCY",
+ },
+ ]
+
+
+def test_yahoo_search_returns_empty_when_quotes_key_is_missing():
+ with patch("src.yahoo_client.requests.get", return_value=build_response({})):
+ results = yahoo_client.yahoo_search("apple", 10)
+
+ assert results == []
+
+
+def test_yahoo_search_returns_empty_when_quotes_is_none():
+ with patch(
+ "src.yahoo_client.requests.get",
+ return_value=build_response({"quotes": None}),
+ ):
+ results = yahoo_client.yahoo_search("apple", 10)
+
+ assert results == []
+
+
+def test_yahoo_search_uses_expected_request_shape():
+ payload = {"quotes": []}
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)) as mock_get:
+ results = yahoo_client.yahoo_search("apple", 3)
+
+ assert results == []
+ mock_get.assert_called_once_with(
+ yahoo_client.YAHOO_SEARCH_URL,
+ params={"q": "apple", "quotesCount": 6, "newsCount": 0},
+ headers=yahoo_client.REQUEST_HEADERS,
+ timeout=6,
+ )
+
+
+def test_yahoo_search_uses_shortname_when_longname_missing():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "shortname": "Apple Short",
+ "currency": "USD",
+ "exchange": "NMS",
+ }
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("apple", 10)
+
+ assert results == [
+ {
+ "symbol": "AAPL",
+ "displaySymbol": "AAPL",
+ "name": "Apple Short",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ }
+ ]
+
+
+def test_yahoo_search_skips_empty_string_symbol():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "EQUITY",
+ "symbol": "",
+ "longname": "Empty",
+ "currency": "USD",
+ "exchange": "NMS",
+ }
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("empty", 10)
+
+ assert results == []
+
+
+def test_yahoo_search_uses_symbol_when_names_are_missing_for_forex():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "CURRENCY",
+ "symbol": "USDJPY=X",
+ "currency": "JPY",
+ "exchange": "CCY",
+ }
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("yen", 10)
+
+ assert results == [
+ {
+ "symbol": "USDJPY=X",
+ "displaySymbol": "USDJPY",
+ "name": "USDJPY=X",
+ "type": "forex",
+ "currency": "JPY",
+ "exchange": "CCY",
+ }
+ ]
+
+
+def test_yahoo_search_formats_forex_display_symbol_and_preserves_exchange_none():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "CURRENCY",
+ "symbol": "EURUSD=X",
+ "shortname": "Euro FX",
+ "currency": "",
+ "exchange": None,
+ }
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("eur", 10)
+
+ assert results == [
+ {
+ "symbol": "EURUSD=X",
+ "displaySymbol": "EURUSD",
+ "name": "Euro FX",
+ "type": "forex",
+ "currency": "USD",
+ "exchange": None,
+ }
+ ]
+
+
+def test_yahoo_search_prefers_longname_over_shortname():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "longname": "Apple Long",
+ "shortname": "Apple Short",
+ "currency": "USD",
+ "exchange": "NMS",
+ }
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("apple", 10)
+
+ assert results[0]["name"] == "Apple Long"
+
+
+def test_yahoo_search_keeps_equity_display_symbol_even_when_symbol_ends_with_x():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "EQUITY",
+ "symbol": "ABC=X",
+ "longname": "Equity With X",
+ "currency": "USD",
+ "exchange": "NMS",
+ }
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("abc", 10)
+
+ assert results == [
+ {
+ "symbol": "ABC=X",
+ "displaySymbol": "ABC=X",
+ "name": "Equity With X",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ }
+ ]
+
+
+def test_yahoo_search_deduplicates_before_applying_limit():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "longname": "Apple Inc.",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "longname": "Apple Duplicate",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "quoteType": "EQUITY",
+ "symbol": "MSFT",
+ "longname": "Microsoft Corporation",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("tech", 2)
+
+ assert results == [
+ {
+ "symbol": "AAPL",
+ "displaySymbol": "AAPL",
+ "name": "Apple Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "symbol": "MSFT",
+ "displaySymbol": "MSFT",
+ "name": "Microsoft Corporation",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ ]
+
+
+def test_yahoo_search_preserves_result_order_across_supported_types():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "CURRENCY",
+ "symbol": "EURUSD=X",
+ "shortname": "Euro FX",
+ "currency": "USD",
+ "exchange": "CCY",
+ },
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "longname": "Apple Inc.",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("mixed", 10)
+
+ assert [item["symbol"] for item in results] == ["EURUSD=X", "AAPL"]
+
+
+def test_yahoo_search_stops_when_limit_is_reached():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "EQUITY",
+ "symbol": "AAPL",
+ "longname": "Apple Inc.",
+ "currency": "USD",
+ "exchange": "NMS",
+ },
+ {
+ "quoteType": "CURRENCY",
+ "symbol": "EURUSD=X",
+ "shortname": "Euro FX",
+ "currency": "USD",
+ "exchange": "CCY",
+ },
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("a", 1)
+
+ assert results == [
+ {
+ "symbol": "AAPL",
+ "displaySymbol": "AAPL",
+ "name": "Apple Inc.",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NMS",
+ }
+ ]
+
+
+def test_yahoo_search_falls_back_to_symbol_name_and_usd_currency():
+ payload = {
+ "quotes": [
+ {
+ "quoteType": "EQUITY",
+ "symbol": "BRK-B",
+ "exchange": "NYQ",
+ }
+ ]
+ }
+
+ with patch("src.yahoo_client.requests.get", return_value=build_response(payload)):
+ results = yahoo_client.yahoo_search("berkshire", 10)
+
+ assert results == [
+ {
+ "symbol": "BRK-B",
+ "displaySymbol": "BRK-B",
+ "name": "BRK-B",
+ "type": "stock",
+ "currency": "USD",
+ "exchange": "NYQ",
+ }
+ ]
diff --git a/app/server/services/ts/.gitignore b/app/server/services/ts/.gitignore
new file mode 100644
index 0000000..2faca44
--- /dev/null
+++ b/app/server/services/ts/.gitignore
@@ -0,0 +1,170 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Bun stuff
+**/node_modules
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/app/server/services/ts/README.md b/app/server/services/ts/README.md
new file mode 100644
index 0000000..10a7885
--- /dev/null
+++ b/app/server/services/ts/README.md
@@ -0,0 +1,16 @@
+### Typescript services
+
+Each service inherits from this directory's:
+
+- packages
+- tsconfig
+- eslint.config
+
+Unless a package will be used in multiple modules install it only in the subdirectory (by running `bun install` in the module directory, not this one).
+
+The `common` folder is home for modules that are used by one or more services. For example `common/port.ts` is used by all services to determine which port to bind their webserver to.
+
+### Tests
+
+To run tests use `bun run vitest`
+to see the coverage run `bun run vitest --coverage`
diff --git a/app/server/services/ts/api-gateway/.env.local b/app/server/services/ts/api-gateway/.env.local
new file mode 100644
index 0000000..a6637fc
--- /dev/null
+++ b/app/server/services/ts/api-gateway/.env.local
@@ -0,0 +1,4 @@
+MYSQL_HOST
+MYSQL_PORT
+MYSQL_PASSWORD
+DB_NAME
\ No newline at end of file
diff --git a/app/server/services/ts/api-gateway/Dockerfile b/app/server/services/ts/api-gateway/Dockerfile
new file mode 100644
index 0000000..7d8ca25
--- /dev/null
+++ b/app/server/services/ts/api-gateway/Dockerfile
@@ -0,0 +1,21 @@
+FROM oven/bun:1.3.10
+
+WORKDIR /project
+COPY package.json bun.lockb .
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+bun install
+
+WORKDIR /project/app
+COPY api-gateway/package.json api-gateway/bun.lockb .
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+bun install
+
+WORKDIR /project
+COPY tsconfig.json .
+COPY common ./common
+
+WORKDIR /project/app
+COPY api-gateway/src ./src
+
+CMD ["bun", "--no-env-file", "src/index.ts"]
+
diff --git a/app/server/services/ts/api-gateway/bun.lock b/app/server/services/ts/api-gateway/bun.lock
new file mode 100644
index 0000000..ca1bb46
--- /dev/null
+++ b/app/server/services/ts/api-gateway/bun.lock
@@ -0,0 +1,628 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "api-gateway-s",
+ "dependencies": {
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.6",
+ "cors": "^2.8.6",
+ "eslint": "^9.39.2",
+ "express": "^5.2.1",
+ "http-proxy-middleware": "^3.0.5",
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@types/bun": "latest",
+ "@types/node": "^25.2.0",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
+ "eslint-plugin-react": "^7.37.5",
+ "globals": "^17.3.0",
+ "jiti": "^2.6.1",
+ "typescript": "^5.3.3",
+ "typescript-eslint": "^8.54.0",
+ },
+ },
+ },
+ "packages": {
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="],
+
+ "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
+
+ "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
+
+ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
+
+ "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
+
+ "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
+
+ "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+
+ "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="],
+
+ "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
+
+ "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
+
+ "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
+
+ "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
+
+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
+
+ "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
+
+ "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
+
+ "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
+
+ "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
+
+ "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="],
+
+ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
+
+ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
+
+ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+
+ "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
+
+ "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
+
+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+ "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+ "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
+
+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
+ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
+
+ "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
+
+ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
+
+ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
+
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+ "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
+
+ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
+
+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="],
+
+ "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
+
+ "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
+
+ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
+
+ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
+
+ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+
+ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
+
+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
+
+ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
+
+ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="],
+
+ "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
+
+ "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+ "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="],
+
+ "http-proxy-middleware": ["http-proxy-middleware@3.0.5", "", { "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" } }, "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg=="],
+
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
+
+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+ "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
+
+ "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
+
+ "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
+
+ "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
+
+ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
+
+ "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+
+ "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
+
+ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
+
+ "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
+
+ "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
+
+ "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
+
+ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
+
+ "is-plain-object": ["is-plain-object@5.0.0", "", {}, "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q=="],
+
+ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+ "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
+
+ "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
+
+ "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
+
+ "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
+
+ "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
+
+ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
+
+ "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
+
+ "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
+
+ "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
+
+ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+ "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
+
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+ "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
+
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+ "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
+
+ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
+
+ "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="],
+
+ "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
+
+ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
+
+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+ "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+
+ "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
+
+ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+
+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="],
+
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+
+ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
+ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
+
+ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
+
+ "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
+
+ "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+ "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
+
+ "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
+
+ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
+
+ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+
+ "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
+
+ "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
+
+ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
+
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
+
+ "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
+
+ "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="],
+
+ "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
+
+ "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
+
+ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
+
+ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
+
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
+ "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
+
+ "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
+
+ "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
+
+ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
+
+ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
+
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
+
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
+
+ "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
+
+ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
+
+ "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+ "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+ }
+}
diff --git a/app/server/services/ts/api-gateway/bun.lockb b/app/server/services/ts/api-gateway/bun.lockb
new file mode 100755
index 0000000..d86e1f8
Binary files /dev/null and b/app/server/services/ts/api-gateway/bun.lockb differ
diff --git a/app/server/services/ts/api-gateway/package.json b/app/server/services/ts/api-gateway/package.json
new file mode 100644
index 0000000..62ab6c0
--- /dev/null
+++ b/app/server/services/ts/api-gateway/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "api-gateway",
+ "main": "src/index.ts",
+ "type": "module",
+ "scripts": {
+ "dev": "bun --watch src/index.ts",
+ "build": "tsc",
+ "start": "bun dist/index.js"
+ },
+ "dependencies": {
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.6",
+ "cors": "^2.8.6",
+ "eslint": "^9.39.2",
+ "express": "^5.2.1",
+ "http-proxy-middleware": "^3.0.5",
+ "mysql2": "^3.18.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@types/bun": "latest",
+ "@types/node": "^25.2.0",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
+ "eslint-plugin-react": "^7.37.5",
+ "globals": "^17.3.0",
+ "jiti": "^2.6.1",
+ "typescript": "^5.3.3",
+ "typescript-eslint": "^8.54.0"
+ }
+}
diff --git a/app/server/services/ts/api-gateway/src/index.ts b/app/server/services/ts/api-gateway/src/index.ts
new file mode 100644
index 0000000..092c564
--- /dev/null
+++ b/app/server/services/ts/api-gateway/src/index.ts
@@ -0,0 +1,83 @@
+import { PORT } from "@/port";
+import { onExit } from "@/hooks";
+import express from "express";
+import { createProxyMiddleware } from "http-proxy-middleware";
+import { buildCorsConfig } from "@/expressUtils.ts";
+
+const app = express();
+app.use(buildCorsConfig());
+
+const pathMatches = (path, valid): boolean => {
+ path = path.replace("/api", "");
+ return valid.includes(path);
+};
+
+const registerProxy = (target: string, paths: string[]): void => {
+ app.use(
+ createProxyMiddleware({
+ pathFilter: (path) => pathMatches(path, paths),
+ target: target,
+ changeOrigin: true,
+ pathRewrite: { "^/api": "" },
+ }),
+ );
+};
+
+registerProxy(process.env.AUTH_SERVICE_ADDR, ["/signup", "/login"]);
+
+registerProxy(process.env.MARKET_SERVICE_ADDR, [
+ "/markets/search",
+ "/markets/quote",
+ "/markets/history",
+]);
+
+registerProxy(process.env.USER_SERVICE_ADDR, [
+ "/accounts",
+ "/profiles",
+ "/goals",
+ "/debts",
+ "/savings",
+ "/transactions",
+ "/charts/expenses",
+ "/table/transactions",
+ "/table/transactions/accounts",
+ "/table/snapshot",
+ "/transactions/csvTransaction",
+]);
+
+registerProxy(process.env.ANALYTICS_SERVICE_ADDR, [
+ "/charts/savings",
+ "/charts/incomeflow",
+ "/charts/budget-expenditure",
+ "/compound-interest",
+ "/predict-debt-payoff",
+]);
+
+app.get("/health", async (req: express.Request, res: express.Response) => {
+ const services: string[] = Object.keys(process.env).filter((x) =>
+ /^.*_SERVICE_ADDR$/.test(x),
+ );
+
+ //parallel arrays
+ const serviceNames: string[] = services.map(
+ (service) => service.split("_")[0],
+ );
+ const serviceStatusPromises: [Promise] = services.map((service) =>
+ fetch(`${process.env[service]}/health`)
+ .then((res) => res.text())
+ .catch((err) => err.message),
+ );
+
+ const serviceStatuses = await Promise.all(serviceStatusPromises);
+ const results = {};
+ services.forEach(
+ (_, index) => (results[serviceNames[index]] = serviceStatuses[index]),
+ );
+
+ res.json(results);
+});
+
+const server = app.listen(PORT, () => {
+ console.log(`API Gateway running on port ${PORT}`);
+});
+onExit(async () => await server.close());
diff --git a/app/server/services/ts/auth/.env.local b/app/server/services/ts/auth/.env.local
new file mode 100644
index 0000000..e69de29
diff --git a/app/server/services/ts/auth/Dockerfile b/app/server/services/ts/auth/Dockerfile
new file mode 100644
index 0000000..0f0ed5e
--- /dev/null
+++ b/app/server/services/ts/auth/Dockerfile
@@ -0,0 +1,21 @@
+FROM oven/bun:1.3.10
+
+WORKDIR /project
+COPY package.json bun.lockb .
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+bun install
+
+WORKDIR /project/app
+COPY auth/package.json auth/bun.lockb .
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+bun install
+
+WORKDIR /project
+COPY tsconfig.json .
+COPY common ./common
+
+WORKDIR /project/app
+COPY auth/src ./src
+
+CMD ["bun", "--no-env-file", "src/index.ts"]
+
diff --git a/app/server/services/ts/auth/Dockerfile.integration.test b/app/server/services/ts/auth/Dockerfile.integration.test
new file mode 100644
index 0000000..97fb570
--- /dev/null
+++ b/app/server/services/ts/auth/Dockerfile.integration.test
@@ -0,0 +1,10 @@
+FROM measureonecodetwice/finus-auth:local
+
+COPY auth/integration ./integration
+
+WORKDIR /project
+COPY vitest.config.ts .
+
+ENV CI=true
+
+CMD ["bun", "test", "integration", "--coverage"]
diff --git a/app/server/services/ts/auth/Dockerfile.mutation.test b/app/server/services/ts/auth/Dockerfile.mutation.test
new file mode 100644
index 0000000..46704fd
--- /dev/null
+++ b/app/server/services/ts/auth/Dockerfile.mutation.test
@@ -0,0 +1,15 @@
+FROM measureonecodetwice/finus-auth:local
+
+RUN --mount=type=cache,target=/var/cache/apt \
+ --mount=type=cache,target=/var/lib/apt/lists \
+ apt-get update && apt-get install -y nodejs npm procps \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY auth/test ./test
+
+WORKDIR /project
+COPY vitest.config.ts .
+
+ENV CI=true
+
+CMD [ "bun", "stryker", "run"]
diff --git a/app/server/services/ts/auth/Dockerfile.test b/app/server/services/ts/auth/Dockerfile.test
new file mode 100644
index 0000000..e3de84b
--- /dev/null
+++ b/app/server/services/ts/auth/Dockerfile.test
@@ -0,0 +1,10 @@
+FROM measureonecodetwice/finus-auth:local
+
+COPY auth/test ./test
+
+WORKDIR /project
+COPY vitest.config.ts .
+
+ENV CI=true
+
+CMD [ "bun", "run", "vitest", "--run"]
diff --git a/app/server/services/ts/auth/bun.lock b/app/server/services/ts/auth/bun.lock
new file mode 100644
index 0000000..6ea5372
--- /dev/null
+++ b/app/server/services/ts/auth/bun.lock
@@ -0,0 +1,267 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "auth",
+ "dependencies": {
+ "@types/jsonwebtoken": "^9.0.10",
+ "express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
+ "mysql2": "^3.15.3",
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.9",
+ "@types/express": "^5.0.6",
+ "supertest": "^7.2.2",
+ },
+ },
+ },
+ "packages": {
+ "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
+
+ "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="],
+
+ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
+
+ "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
+
+ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
+
+ "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
+
+ "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
+
+ "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+
+ "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="],
+
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
+ "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
+
+ "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
+
+ "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
+
+ "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
+
+ "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
+
+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
+
+ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+
+ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
+
+ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
+
+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="],
+
+ "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
+
+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
+ "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
+
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+ "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
+
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+
+ "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
+
+ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+
+ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+
+ "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="],
+
+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+ "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
+
+ "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
+
+ "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
+
+ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
+
+ "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
+
+ "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
+
+ "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
+
+ "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
+
+ "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
+
+ "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
+
+ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
+
+ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
+
+ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+ "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
+
+ "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
+
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "mysql2": ["mysql2@3.18.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-3rupyOFks7Vq0jcjBpmg1gtgfGuCcmgrRJPEfpGzzrB/ydutupbjKkoDJGsGkrJRU6j44o2tb0McduL03/v/dQ=="],
+
+ "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
+
+ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+ "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+
+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+ "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
+
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+
+ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
+
+ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+ "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
+
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="],
+
+ "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="],
+
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+ }
+}
diff --git a/app/server/services/ts/auth/bun.lockb b/app/server/services/ts/auth/bun.lockb
new file mode 100755
index 0000000..55281ee
Binary files /dev/null and b/app/server/services/ts/auth/bun.lockb differ
diff --git a/app/server/services/ts/auth/integration/auth.test.ts b/app/server/services/ts/auth/integration/auth.test.ts
new file mode 100644
index 0000000..0823067
--- /dev/null
+++ b/app/server/services/ts/auth/integration/auth.test.ts
@@ -0,0 +1,157 @@
+import { describe, it, expect, beforeAll } from "vitest";
+import request from "supertest";
+
+const BASE_URL = process.env.API_GATEWAY_ADDR;
+
+describe("Authentication Integration (Docker)", () => {
+ let email: string;
+ let password: string;
+ const signupEndpoint = "/api/signup";
+ const loginEndpoint = "/api/login";
+
+ beforeAll(() => {
+ const unique = `auth${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
+ email = `${unique}@example.com`;
+ password = "12idontknow";
+ });
+
+ it("signs up a new user", async () => {
+ const signupBody = {
+ username: "anything",
+ email: email,
+ first_name: "Auth",
+ last_name: "Test",
+ age: 40,
+ password: password,
+ };
+ console.log("Signup body:", signupBody);
+ const res = await request(BASE_URL).post(signupEndpoint).send(signupBody);
+
+ expect(res.status).toBe(201);
+ });
+
+ it("failed signup due to duplicate email", async () => {
+ const signupBody = {
+ username: "anything",
+ email: email,
+ first_name: "Auth",
+ last_name: "Test",
+ age: 40,
+ password: "12idontknow",
+ };
+ const res = await request(BASE_URL).post(signupEndpoint).send(signupBody);
+
+ expect(res.status).toBe(409);
+ expect(res.body.error).toBe("An account with this email already exists.");
+ });
+
+ it("failed signup due to invalid email format", async () => {
+ const signupBody = {
+ username: "anything",
+ email: "invalidemail",
+ first_name: "Auth",
+ last_name: "Test",
+ age: 40,
+ password: "12idontknow",
+ };
+ const res = await request(BASE_URL).post(signupEndpoint).send(signupBody);
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe("invalid email format");
+ });
+
+ it("failed signup due to invalid password", async () => {
+ const signupBody = {
+ username: "anything",
+ email: "newaccount@example.com",
+ first_name: "Auth",
+ last_name: "Test",
+ age: 40,
+ password: "short",
+ };
+ const res = await request(BASE_URL).post(signupEndpoint).send(signupBody);
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe(
+ "Password must be at least 8 characters and include letters and numbers.",
+ );
+ });
+
+ it("failed signup due to under age", async () => {
+ const signupBody = {
+ username: "anything",
+ email: "newaccount@example.com",
+ first_name: "Auth",
+ last_name: "Test",
+ age: 15,
+ password: "longenoughpassword1",
+ };
+ const res = await request(BASE_URL).post(signupEndpoint).send(signupBody);
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe(
+ "Age must be a whole number between 16 and 120",
+ );
+ });
+
+ it("failed signup due to over age", async () => {
+ const signupBody = {
+ username: "anything",
+ email: "newaccount@example.com",
+ first_name: "Auth",
+ last_name: "Test",
+ age: 121,
+ password: "longenoughpassword1",
+ };
+ const res = await request(BASE_URL).post(signupEndpoint).send(signupBody);
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe(
+ "Age must be a whole number between 16 and 120",
+ );
+ });
+
+ it("logs in an existing user", async () => {
+ const loginBody = {
+ email: email,
+ password: password,
+ };
+ const res = await request(BASE_URL).post(loginEndpoint).send(loginBody);
+
+ expect(res.status).toBe(200);
+ expect(res.body.token).toBeDefined();
+ });
+
+ it("logs in with wrong password", async () => {
+ const loginBody = {
+ email: email,
+ password: "wrongpassword",
+ };
+ const res = await request(BASE_URL).post(loginEndpoint).send(loginBody);
+
+ expect(res.status).toBe(401);
+ expect(res.body.error).toBe("Invalid email or password.");
+ });
+
+ it("logs into non existing account", async () => {
+ const loginBody = {
+ email: "nonexist@exmaple.com",
+ password: "bskdfks312",
+ };
+ const res = await request(BASE_URL).post(loginEndpoint).send(loginBody);
+
+ expect(res.status).toBe(401);
+ expect(res.body.error).toBe("Invalid email or password.");
+ });
+
+ it("logs in with empty credentials", async () => {
+ const loginBody = {
+ email: "",
+ password: "",
+ };
+ const res = await request(BASE_URL).post(loginEndpoint).send(loginBody);
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe("Missing email");
+ });
+});
diff --git a/app/server/services/ts/auth/package.json b/app/server/services/ts/auth/package.json
new file mode 100644
index 0000000..c04db39
--- /dev/null
+++ b/app/server/services/ts/auth/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "auth",
+ "main": "src/index.ts",
+ "type": "module",
+ "dependencies": {
+ "@types/jsonwebtoken": "^9.0.10",
+ "express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
+ "mysql2": "^3.15.3"
+ },
+ "scripts": {
+ "dev": "bun --watch src/index.ts",
+ "build": "tsc",
+ "start": "bun dist/index.js",
+ "test": "vitest run"
+ },
+ "devDependencies": {
+ "@types/bun": "^1.3.9",
+ "@types/express": "^5.0.6",
+ "supertest": "^7.2.2"
+ }
+}
diff --git a/app/server/services/ts/auth/src/config.ts b/app/server/services/ts/auth/src/config.ts
new file mode 100644
index 0000000..0e88bb3
--- /dev/null
+++ b/app/server/services/ts/auth/src/config.ts
@@ -0,0 +1,4 @@
+export const JWT_SECRET = process.env.JWT_SECRET;
+export const MIN_PASSWORD_LENGTH = 8;
+export const MIN_AGE = 16;
+export const MAX_AGE = 120;
diff --git a/app/server/services/ts/auth/src/index.ts b/app/server/services/ts/auth/src/index.ts
new file mode 100644
index 0000000..a80bb3c
--- /dev/null
+++ b/app/server/services/ts/auth/src/index.ts
@@ -0,0 +1,53 @@
+import express from "express";
+import { PORT } from "@/port";
+import { onExit } from "@/hooks";
+import { buildCorsConfig, handleServerError } from "@/expressUtils";
+import { signup, login } from "./logic";
+import type { LoginBody, SignupBody } from "./types";
+import { parseLoginBody, parseSignupBody } from "./parsing";
+import { getConnectionPool, getDatabaseStatus } from "@/sqlUtil";
+
+const pool = getConnectionPool();
+const app = express();
+app.use(buildCorsConfig());
+app.use(express.json());
+
+app.post("/signup", async (req, res) => {
+ let body: SignupBody;
+ try {
+ body = parseSignupBody(req.body);
+ } catch (e) {
+ res.status(400).json({ error: e.message });
+ return;
+ }
+
+ console.log("In Index, calling for signup function");
+ handleServerError(() => signup(body, res, pool), res);
+});
+
+app.post("/login", async (req, res) => {
+ let body: LoginBody;
+ try {
+ body = parseLoginBody(req.body);
+ } catch (e) {
+ res.status(400).json({ error: e.message });
+ return;
+ }
+
+ handleServerError(() => login(body, res, pool), res);
+});
+
+app.get("/health", async (_, res) => {
+ const status = await getDatabaseStatus();
+ res.send(status);
+});
+
+const server = app.listen(PORT, () => {
+ console.log(`Auth Service running on port ${PORT}`);
+});
+
+onExit(async () => {
+ await new Promise((res) => server.close(res));
+ await pool.end();
+ process.exit(0);
+});
diff --git a/app/server/services/ts/auth/src/logic.ts b/app/server/services/ts/auth/src/logic.ts
new file mode 100644
index 0000000..18c0065
--- /dev/null
+++ b/app/server/services/ts/auth/src/logic.ts
@@ -0,0 +1,54 @@
+import { type LoginBody, type SignupBody } from "./types";
+import { createUser, getUserByEmail, accountWithEmailExists } from "./queries";
+import { validateSignupBody } from "./validation";
+import { passwordMatchesHash, generateJWT, hashPassword } from "./secrets.ts";
+import { type User } from "@/types";
+import express from "express";
+import { Pool } from "mysql2/promise";
+
+export async function signup(body: SignupBody, res, pool) {
+ try {
+ validateSignupBody(body);
+ } catch (e) {
+ res.status(400).json({
+ error: e.message,
+ });
+ return;
+ }
+
+ const existing = await accountWithEmailExists(body.email, pool);
+ if (existing) {
+ res.status(409).json({
+ error: "An account with this email already exists.",
+ });
+ return;
+ }
+
+ const hash = await hashPassword(body.password);
+ await createUser({ ...body, pw_hash: hash }, pool);
+
+ res.status(201).json({ ok: true });
+}
+
+export async function login(
+ body: LoginBody,
+ res: express.Response,
+ pool: Pool,
+): Promise {
+ let user: User;
+ try {
+ user = await getUserByEmail(body.email, pool);
+ } catch {
+ res.status(401).json({ error: "Invalid email or password." });
+ return;
+ }
+
+ if (!passwordMatchesHash(body.password, user.pw_hash)) {
+ res.status(401).json({ error: "Invalid email or password." });
+ return;
+ }
+
+ res.status(200).json({
+ token: generateJWT(user),
+ });
+}
diff --git a/app/server/services/ts/auth/src/parsing.ts b/app/server/services/ts/auth/src/parsing.ts
new file mode 100644
index 0000000..45baf48
--- /dev/null
+++ b/app/server/services/ts/auth/src/parsing.ts
@@ -0,0 +1,47 @@
+import type { LoginBody, SignupBody } from "./types.ts";
+import { validateType } from "@/types.ts";
+
+function normalizeEmail(email: string): boolean {
+ const normalized = email?.trim().toLowerCase(),
+ isValidFormat = /^\w+@\w+\.\w+$/.test(normalized);
+ if (!isValidFormat) {
+ throw new Error("invalid email format");
+ }
+ return normalized;
+}
+
+export function parseLoginBody(obj: unknown): LoginBody {
+ validateType(obj, ["email", "password"]);
+
+ return {
+ email: normalizeEmail(obj.email),
+ password: obj.password,
+ };
+}
+
+export function parseSignupBody(obj: unknown): SignupBody {
+ validateType(obj, [
+ "username",
+ "email",
+ "first_name",
+ "last_name",
+ "age",
+ "password",
+ ]);
+
+ const idInt = Number(obj.age);
+ if (Number.isNaN(idInt) || Math.floor(idInt) !== idInt) {
+ const error = new Error("age must be a number");
+ throw error;
+ }
+
+ return {
+ first_name: obj.first_name.trim(),
+ last_name: obj.last_name.trim(),
+ username: obj.username.trim(),
+ password: obj.password,
+ created: obj.created,
+ email: normalizeEmail(obj.email),
+ age: idInt,
+ };
+}
diff --git a/app/server/services/ts/auth/src/queries.ts b/app/server/services/ts/auth/src/queries.ts
new file mode 100644
index 0000000..a3a3896
--- /dev/null
+++ b/app/server/services/ts/auth/src/queries.ts
@@ -0,0 +1,92 @@
+import type { UserWithPassword } from "./types";
+import type { Pool } from "mysql2/promise";
+export async function getUserByEmail(
+ email: string,
+ pool: Pool,
+): Promise {
+ const [rows] = await pool.execute(
+ `
+ SELECT
+ a.id,
+ a.username,
+ a.email,
+ a.first_name,
+ a.last_name,
+ a.age,
+ c.pw_hash
+ FROM finusAccount a
+ JOIN credentials c ON c.finus_account_id = a.id
+ WHERE a.email = ?
+ LIMIT 1
+ `,
+ [email],
+ );
+
+ if (rows.length === 0) {
+ throw new Error("User not found");
+ }
+
+ rows[0].pw_hash = rows[0].pw_hash.toString();
+ return rows[0];
+}
+
+export async function accountWithEmailExists(
+ email: string,
+ pool,
+): Promise {
+ return await pool
+ .execute(`SELECT COUNT(*) as count FROM finusAccount WHERE email = ?`, [
+ email,
+ ])
+ .then((x) => {
+ return x[0][0].count > 0;
+ });
+}
+
+export async function createUser(user: UserWithPassword, pool): void {
+ const connection = await pool.getConnection();
+ try {
+ await connection.beginTransaction();
+
+ const [accountInsert] = await connection.execute(
+ `
+ INSERT INTO finusAccount (username, email, first_name, last_name, age)
+ VALUES (?, ?, ?, ?, ?)
+ `,
+ [user.username, user.email, user.first_name, user.last_name, user.age],
+ );
+
+ console.log("Executed insert into finus account");
+
+ await connection.execute(
+ `
+ INSERT INTO credentials (finus_account_id, pw_hash)
+ VALUES (?, ?)
+ `,
+ [accountInsert.insertId, user.pw_hash],
+ );
+
+ const [result] = await connection.query(
+ "INSERT INTO profile (name) VALUE (?)",
+ [user.first_name],
+ );
+
+ await connection.query(
+ `INSERT INTO finusAccount_profile (account_id, profile_id)
+ VALUE(?,?)`,
+ [accountInsert.insertId, result.insertId],
+ );
+
+ console.log("User created with ID:", accountInsert.insertId);
+
+ await connection.commit();
+ connection.release();
+ } catch (error) {
+ await connection.rollback();
+ connection?.release();
+
+ console.error(error);
+ //eslint-disable-next-line preserve-caught-error
+ throw new Error("Could not create account");
+ }
+}
diff --git a/app/server/services/ts/auth/src/secrets.ts b/app/server/services/ts/auth/src/secrets.ts
new file mode 100644
index 0000000..a98d426
--- /dev/null
+++ b/app/server/services/ts/auth/src/secrets.ts
@@ -0,0 +1,28 @@
+import type { AuthTokenClaims, User } from "./types";
+import { JWT_SECRET } from "./config";
+import jwt from "jsonwebtoken";
+
+export function passwordMatchesHash(
+ password: string,
+ pw_hash: string,
+): boolean {
+ return Bun.password.verifySync(password, pw_hash);
+}
+
+export async function hashPassword(password: string) {
+ return await Bun.password.hash(password, {
+ algorithm: "bcrypt",
+ });
+}
+
+export function generateJWT(user: User): string {
+ const payload: AuthTokenClaims = {
+ sub: String(user.id),
+ email: user.email,
+ name: user.name,
+ first_name: user.first_name,
+ last_name: user.last_name,
+ };
+
+ return jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" });
+}
diff --git a/app/server/services/ts/auth/src/types.ts b/app/server/services/ts/auth/src/types.ts
new file mode 100644
index 0000000..b995bcb
--- /dev/null
+++ b/app/server/services/ts/auth/src/types.ts
@@ -0,0 +1,20 @@
+import { type User } from "@/types";
+
+export type SignupBody = User & {
+ password: string;
+};
+
+export type LoginBody = {
+ email: string;
+ password: string;
+};
+
+export type UserWithPassword = User & {
+ pw_hash: string;
+};
+
+export type AuthTokenClaims = {
+ sub: string;
+ email: string;
+ name: string;
+};
diff --git a/app/server/services/ts/auth/src/validation.ts b/app/server/services/ts/auth/src/validation.ts
new file mode 100644
index 0000000..bf25979
--- /dev/null
+++ b/app/server/services/ts/auth/src/validation.ts
@@ -0,0 +1,22 @@
+import type { LoginBody } from "../src/types";
+import { MIN_AGE, MAX_AGE, MIN_PASSWORD_LENGTH } from "../src/config";
+
+export function validateSignupBody(body: LoginBody) {
+ if (body.age < MIN_AGE || body.age > MAX_AGE) {
+ throw new Error(
+ `Age must be a whole number between ${MIN_AGE} and ${MAX_AGE}`,
+ );
+ }
+
+ if (!validatePassword(body.password)) {
+ throw new Error(
+ `Password must be at least ${MIN_PASSWORD_LENGTH} characters and include letters and numbers.`,
+ );
+ }
+}
+
+function validatePassword(password: string): boolean {
+ const hasLetter = /[a-zA-Z]/.test(password);
+ const hasDigit = /\d/.test(password);
+ return password.length >= MIN_PASSWORD_LENGTH && hasLetter && hasDigit;
+}
diff --git a/app/server/services/ts/auth/stryker.conf.js b/app/server/services/ts/auth/stryker.conf.js
new file mode 100644
index 0000000..b2d8c03
--- /dev/null
+++ b/app/server/services/ts/auth/stryker.conf.js
@@ -0,0 +1,6 @@
+export default {
+ testRunner: "vitest",
+ vitest: {
+ configFile: "vitest.config.ts",
+ },
+};
diff --git a/app/server/services/ts/auth/test/logic.test.ts b/app/server/services/ts/auth/test/logic.test.ts
new file mode 100644
index 0000000..5681b99
--- /dev/null
+++ b/app/server/services/ts/auth/test/logic.test.ts
@@ -0,0 +1,104 @@
+vi.mock("../src/secrets.ts");
+vi.mock("../src/validation.ts");
+vi.mock("../src/queries.ts");
+
+import { vi, describe, beforeEach, afterEach, expect, it } from "vitest";
+import { login, signup } from "../src/logic";
+import type { SignupBody } from "../src/types";
+
+//mocked functions
+import { validateSignupBody } from "../src/validation";
+import {
+ accountWithEmailExists,
+ createUser,
+ getUserByEmail,
+} from "../src/queries";
+import {
+ hashPassword,
+ generateJWT,
+ passwordMatchesHash,
+} from "../src/secrets.ts";
+
+const resJson = vi.fn();
+let res;
+
+beforeEach(() => {
+ res = {
+ status: vi.fn(() => ({ json: resJson })),
+ };
+});
+
+afterEach(() => {
+ vi.resetAllMocks();
+});
+
+describe("Signup behaviour", () => {
+ const signupBody: SignupBody = vi.fn({
+ email: "dummy email",
+ password: "dummy password",
+ });
+
+ it("Should return 201 on happy path", async () => {
+ await signup(signupBody, res, undefined);
+ expect(res.status).toHaveBeenCalledWith(201);
+ });
+
+ it("Should insert use into DB", async () => {
+ vi.mocked(hashPassword).mockImplementation(() => "pwhash");
+ await signup(signupBody, res, undefined);
+
+ expect(createUser).toHaveBeenCalledOnce();
+ const params = vi.mocked(createUser).mock.calls[0];
+ expect(params[0]?.pw_hash).toEqual("pwhash");
+ });
+
+ it("Should 400 on invalid sign in body", async () => {
+ vi.mocked(validateSignupBody).mockImplementation(() => {
+ throw new Error();
+ });
+ await signup(signupBody, res, undefined);
+ expect(res.status).toHaveBeenCalledWith(400);
+ });
+
+ it("Should 409 on user already existing", async () => {
+ vi.mocked(accountWithEmailExists).mockImplementation(() => true);
+ await signup(signupBody, res, undefined);
+ expect(res.status).toHaveBeenCalledWith(409);
+ });
+});
+
+describe("Login behaviour", () => {
+ const MOCK_JWT = "JWT";
+ const MOCK_USER = { pw_hash: "dummy hash" };
+ beforeEach(() => {
+ vi.mocked(generateJWT).mockImplementation(() => MOCK_JWT);
+ vi.mocked(passwordMatchesHash).mockImplementation(() => true);
+ vi.mocked(getUserByEmail).mockImplementation(() => MOCK_USER);
+ });
+ const loginBody = {
+ email: "email",
+ password: "password",
+ };
+
+ it("Should return JWT on Happy path", async () => {
+ await login(loginBody, res, undefined);
+ expect(res.status).toHaveBeenCalledWith(200);
+ expect(resJson).toHaveBeenCalledOnce();
+ const params = vi.mocked(resJson).mock.calls[0];
+ expect(params[0]?.token).toEqual(MOCK_JWT);
+ });
+
+ it("Should 401 on no account w/ matching email", async () => {
+ vi.mocked(getUserByEmail).mockImplementation(() => {
+ throw new Error();
+ });
+ await login(loginBody, res, undefined);
+ expect(res.status).toHaveBeenCalledWith(401);
+ });
+
+ it("Should 401 on wrong password", async () => {
+ vi.mocked(passwordMatchesHash).mockImplementation(() => false);
+ await login(loginBody, res, undefined);
+ expect(res.status).toHaveBeenCalledWith(401);
+ });
+});
diff --git a/app/server/services/ts/auth/test/parsing.test.ts b/app/server/services/ts/auth/test/parsing.test.ts
new file mode 100644
index 0000000..50dca43
--- /dev/null
+++ b/app/server/services/ts/auth/test/parsing.test.ts
@@ -0,0 +1,62 @@
+import { describe, expect, it, beforeEach } from "vitest";
+import { parseLoginBody, parseSignupBody } from "../src/parsing.ts";
+
+let signupBody;
+let loginBody;
+beforeEach(() => {
+ signupBody = {
+ username: "username",
+ email: "e@ma.il",
+ first_name: "first_name",
+ last_name: "last_name",
+ age: "21",
+ password: "Password123123",
+ };
+
+ loginBody = {
+ email: "e@ma.il",
+ password: "password",
+ };
+});
+
+describe("loginBody tests", () => {
+ it("Should accept a valid signupBody", () => {
+ parseSignupBody(signupBody);
+ });
+
+ it("Should throw on non-numeric ages", () => {
+ signupBody.age = "aoue";
+ expect(() => parseSignupBody(signupBody)).toThrowError();
+ });
+
+ it("Should only accept integer ages", () => {
+ signupBody.age = "21.253";
+ expect(() => parseSignupBody(signupBody)).toThrowError();
+ });
+
+ it("Should throw on missing field", () => {
+ delete signupBody.username;
+ expect(() => parseSignupBody(signupBody)).toThrowError();
+ });
+});
+
+describe("loginBody tests", () => {
+ it("Should lowercase email", () => {
+ loginBody.email = "EMAIL@MAIL.COM";
+ const parsed = parseLoginBody(loginBody);
+
+ expect(parsed.email).toEqual(loginBody.email.toLowerCase());
+ });
+
+ it("Should trim email", () => {
+ loginBody.email = " email@mail.com ";
+ const parsed = parseLoginBody(loginBody);
+
+ expect(parsed.email).toEqual(loginBody.email.trim());
+ });
+
+ it("Should throw on invalid email", () => {
+ loginBody.email = "abc";
+ expect(() => parseLoginBody(loginBody)).toThrow();
+ });
+});
diff --git a/app/server/services/ts/auth/test/validation.test.ts b/app/server/services/ts/auth/test/validation.test.ts
new file mode 100644
index 0000000..5a5a996
--- /dev/null
+++ b/app/server/services/ts/auth/test/validation.test.ts
@@ -0,0 +1,53 @@
+import { vi, describe, expect, it, beforeEach } from "vitest";
+import { validateSignupBody } from "../src/validation";
+import type { SignupBody } from "../src/types";
+
+vi.mock("../src/config.ts", () => ({
+ MIN_PASSWORD_LENGTH: 8,
+ MIN_AGE: 12,
+ MAX_AGE: 100,
+}));
+
+let signupBody: SignupBody;
+beforeEach(() => {
+ signupBody = {
+ email: "testemail@domain.com",
+ password: "PASSWORPASSWORDDPASSWORD123!@#",
+ age: 15,
+ };
+});
+
+describe("Happy path", () => {
+ it("Should accept a valid signupBody", () => {
+ validateSignupBody(signupBody);
+ });
+});
+
+describe("Age validation tests", () => {
+ it("should reject ages below the minimum", () => {
+ signupBody.age = 11;
+ expect(() => validateSignupBody(signupBody)).toThrowError();
+ });
+
+ it("should reject above below the maximum", () => {
+ signupBody.age = 101;
+ expect(() => validateSignupBody(signupBody)).toThrowError();
+ });
+});
+
+describe("Password validation tests", () => {
+ it("should reject passwords that are too short", () => {
+ signupBody.password = "PA12";
+ expect(() => validateSignupBody(signupBody)).toThrowError();
+ });
+
+ it("should reject passwords that have no letters", () => {
+ signupBody.password = "11111111111";
+ expect(() => validateSignupBody(signupBody)).toThrowError();
+ });
+
+ it("should reject passwords that have no digits", () => {
+ signupBody.password = "aaaaaaaaaaa";
+ expect(() => validateSignupBody(signupBody)).toThrowError();
+ });
+});
diff --git a/app/server/services/ts/auth/vitest.config.ts b/app/server/services/ts/auth/vitest.config.ts
new file mode 100644
index 0000000..df9568a
--- /dev/null
+++ b/app/server/services/ts/auth/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "node",
+ include: ["test/**/*.ts"],
+ },
+});
diff --git a/app/server/services/ts/bun.lock b/app/server/services/ts/bun.lock
new file mode 100644
index 0000000..7455634
--- /dev/null
+++ b/app/server/services/ts/bun.lock
@@ -0,0 +1,956 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "dependencies": {
+ "@types/cors": "^2.8.19",
+ "@vitest/coverage-v8": "^4.1.1",
+ "cors": "^2.8.6",
+ "express": "^5.2.1",
+ "mysql2": "^3.18.1",
+ "stryker-mutator-bun-runner": "^0.4.0",
+ },
+ "devDependencies": {
+ "@eslint/css": "^0.14.1",
+ "@eslint/js": "^10.0.1",
+ "@eslint/json": "^1.0.0",
+ "eslint": "^10.1.0",
+ "globals": "^17.3.0",
+ "husky": "^9.1.7",
+ "jiti": "^2.6.1",
+ "minimatch": "5",
+ "prettier": "3.8.1",
+ "supertest": "^7.2.2",
+ "typescript-eslint": "^8.55.0",
+ "vitest": "^4.0.18",
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0",
+ },
+ },
+ },
+ "packages": {
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
+
+ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
+
+ "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
+
+ "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="],
+
+ "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
+ "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
+
+ "@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.29.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-syntax-decorators": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA=="],
+
+ "@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA=="],
+
+ "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
+
+ "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
+
+ "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/traverse": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw=="],
+
+ "@babel/plugin-transform-explicit-resource-management": ["@babel/plugin-transform-explicit-resource-management@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6", "@babel/plugin-transform-destructuring": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg=="],
+
+ "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="],
+
+ "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="],
+
+ "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
+
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
+
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "^1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/css": ["@eslint/css@0.14.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "@eslint/css-tree": "^3.6.6", "@eslint/plugin-kit": "^0.4.1" } }, "sha512-NXiteSacmpaXqgyIW3+GcNzexXyfC0kd+gig6WTjD4A74kBGJeNx1tV0Hxa0v7x0+mnIyKfGPhGNs1uhRFdh+w=="],
+
+ "@eslint/css-tree": ["@eslint/css-tree@3.6.9", "", { "dependencies": { "mdn-data": "2.23.0", "source-map-js": "^1.0.1" } }, "sha512-3D5/OHibNEGk+wKwNwMbz63NMf367EoR4mVNNpxddCHKEb2Nez7z62J2U6YjtErSsZDoY0CsccmoUpdEbkogNA=="],
+
+ "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
+
+ "@eslint/json": ["@eslint/json@1.0.1", "", { "dependencies": { "@eslint/core": "^1.1.0", "@eslint/plugin-kit": "^0.6.0", "@humanwhocodes/momoa": "^3.3.10", "natural-compare": "^1.4.0" } }, "sha512-bE2nGv8/U+uRvQEJWOgCsZCa65XsCBgxyyx/sXtTHVv0kqdauACLzyp7A1C3yNn7pRaWjIt5acxY+TAbSyIJXw=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/momoa": ["@humanwhocodes/momoa@3.3.10", "", {}, "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@inquirer/ansi": ["@inquirer/ansi@2.0.4", "", {}, "sha512-DpcZrQObd7S0R/U3bFdkcT5ebRwbTTC4D3tCc1vsJizmgPLxNJBo+AAFmrZwe8zk30P2QzgzGWZ3Q9uJwWuhIg=="],
+
+ "@inquirer/checkbox": ["@inquirer/checkbox@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PubpMPO2nJgMufkoB3P2wwxNXEMUXnBIKi/ACzDUYfaoPuM7gSTmuxJeMscoLVEsR4qqrCMf5p0SiYGWnVJ8kw=="],
+
+ "@inquirer/confirm": ["@inquirer/confirm@6.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-tiNyA73pgpQ0FQ7axqtoLUe4GDYjNCDcVsbgcA5anvwg2z6i+suEngLKKJrWKJolT//GFPZHwN30binDIHgSgQ=="],
+
+ "@inquirer/core": ["@inquirer/core@11.1.7", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-1BiBNDk9btIwYIzNZpkikIHXWeNzNncJePPqwDyVMhXhD1ebqbpn1mKGctpoqAbzywZfdG0O4tvmsGIcOevAPQ=="],
+
+ "@inquirer/editor": ["@inquirer/editor@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/external-editor": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-VJx4XyaKea7t8hEApTw5dxeIyMtWXre2OiyJcICCRZI4hkoHsMoCnl/KbUnJJExLbH9csLLHMVR144ZhFE1CwA=="],
+
+ "@inquirer/expand": ["@inquirer/expand@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-fC0UHJPXsTRvY2fObiwuQYaAnHrp3aDqfwKUJSdfpgv18QUG054ezGbaRNStk/BKD5IPijeMKWej8VV8O5Q/eQ=="],
+
+ "@inquirer/external-editor": ["@inquirer/external-editor@2.0.4", "", { "dependencies": { "chardet": "^2.1.1", "iconv-lite": "^0.7.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Prenuv9C1PHj2Itx0BcAOVBTonz02Hc2Nd2DbU67PdGUaqn0nPCnV34oDyyoaZHnmfRxkpuhh/u51ThkrO+RdA=="],
+
+ "@inquirer/figures": ["@inquirer/figures@2.0.4", "", {}, "sha512-eLBsjlS7rPS3WEhmOmh1znQ5IsQrxWzxWDxO51e4urv+iVrSnIHbq4zqJIOiyNdYLa+BVjwOtdetcQx1lWPpiQ=="],
+
+ "@inquirer/input": ["@inquirer/input@5.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-nvZ6qEVeX/zVtZ1dY2hTGDQpVGD3R7MYPLODPgKO8Y+RAqxkrP3i/3NwF3fZpLdaMiNuK0z2NaYIx9tPwiSegQ=="],
+
+ "@inquirer/number": ["@inquirer/number@4.0.10", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-Ht8OQstxiS3APMGjHV0aYAjRAysidWdwurWEo2i8yI5xbhOBWqizT0+MU1S2GCcuhIBg+3SgWVjEoXgfhY+XaA=="],
+
+ "@inquirer/password": ["@inquirer/password@5.0.10", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-QbNyvIE8q2GTqKLYSsA8ATG+eETo+m31DSR0+AU7x3d2FhaTWzqQek80dj3JGTo743kQc6mhBR0erMjYw5jQ0A=="],
+
+ "@inquirer/prompts": ["@inquirer/prompts@8.3.2", "", { "dependencies": { "@inquirer/checkbox": "^5.1.2", "@inquirer/confirm": "^6.0.10", "@inquirer/editor": "^5.0.10", "@inquirer/expand": "^5.0.10", "@inquirer/input": "^5.0.10", "@inquirer/number": "^4.0.10", "@inquirer/password": "^5.0.10", "@inquirer/rawlist": "^5.2.6", "@inquirer/search": "^4.1.6", "@inquirer/select": "^5.1.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-yFroiSj2iiBFlm59amdTvAcQFvWS6ph5oKESls/uqPBect7rTU2GbjyZO2DqxMGuIwVA8z0P4K6ViPcd/cp+0w=="],
+
+ "@inquirer/rawlist": ["@inquirer/rawlist@5.2.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-jfw0MLJ5TilNsa9zlJ6nmRM0ZFVZhhTICt4/6CU2Dv1ndY7l3sqqo1gIYZyMMDw0LvE1u1nzJNisfHEhJIxq5w=="],
+
+ "@inquirer/search": ["@inquirer/search@4.1.6", "", { "dependencies": { "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-3/6kTRae98hhDevENScy7cdFEuURnSpM3JbBNg8yfXLw88HgTOl+neUuy/l9W0No5NzGsLVydhBzTIxZP7yChQ=="],
+
+ "@inquirer/select": ["@inquirer/select@5.1.2", "", { "dependencies": { "@inquirer/ansi": "^2.0.4", "@inquirer/core": "^11.1.7", "@inquirer/figures": "^2.0.4", "@inquirer/type": "^4.0.4" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-kTK8YIkHV+f02y7bWCh7E0u2/11lul5WepVTclr3UMBtBr05PgcZNWfMa7FY57ihpQFQH/spLMHTcr0rXy50tA=="],
+
+ "@inquirer/type": ["@inquirer/type@4.0.4", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-PamArxO3cFJZoOzspzo6cxVlLeIftyBsZw/S9bKY5DzxqJVZgjoj1oP8d0rskKtp7sZxBycsoer1g6UeJV1BBA=="],
+
+ "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
+
+ "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/8IzqSu4/OWGRs7Fs2ROzGVwJMFTBQkgAp6sAthkBYoN7OiM4rY/CpPVs2X9w9N1W61CHSkEdNKi8HrLZKfK3g=="],
+
+ "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-TT7eUihnAzxM2tlZesusuC75PAOYKvUBgVU/Nm/lakZ/DpyuqhNkzUfcxSgmmK9IjVWzMmezLIGZl16XGCGJng=="],
+
+ "@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-CYjIHWaQG7T4phfjErHr6BiXRs0K/9DqMeiohJmuYSBF+H2m56vFslOenLCguGYQL9jeiiCZBeoVCpwjxZrMgQ=="],
+
+ "@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-8XMLyRNxHF4jfLajkWt+F8UDxsWbzysyxQVMZKUXwoeGvaxB0rVd07r3YbgDtG8U6khhRFM3oaGp+CQ0whwmdA=="],
+
+ "@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jBwYCLG5Eb+PqtFrc3Wp2WMYlw1Id75gUcsdP+ApCOpf5oQhHxkFWCjZmcDoioDmEhMWAiM3wtwSrTlPg+sI6Q=="],
+
+ "@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-z3GFCk1UBzDOOiEBHL32lVP7Edi26BhOjKb6bIc0nRyabbRiyON4++GR0zmd/H5zM5S0+UcXFgCGnD+b8avTLw=="],
+
+ "@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-KZlf1jKtf4jai8xiQv/0XRjxVVhHnw/HtUKtLdOeQpTOQ1fQFhLoz2FGGtVRd0LVa/yiRbSz9HlWIzWlmJClng=="],
+
+ "@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-ADImD4yCHNpqZu718E2chWcCaAHvua90yhmpzzV6fF4zOhwkGGbPCgUWmKyJ83uz+DXaPdYxX0ttDvtolrzx3Q=="],
+
+ "@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.11", "", { "os": "linux", "cpu": "x64" }, "sha512-J+qz4Al05PrNIOdj7xsWVTyx0c/gjUauG5nKV3Rrx0Q+5JO+1pPVlnfNmWbOF9pKG4f3IGad8KXJUfGMORld+Q=="],
+
+ "@oven/bun-windows-aarch64": ["@oven/bun-windows-aarch64@1.3.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UOdkwScHRkGPz+n9ZJU7sTkTvqV7rD1SLCLaru1xH8WRsV7tDorPqNCzEN1msOIiPRK825nvAtEm9UsomO1GsA=="],
+
+ "@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-E51tyWDP1l0CbjZYhiUxhDGPaY8Hf5YBREx0PHBff1LM1/q3qsJ6ZvRUa8YbbOO0Ax9QP6GHjD9vf3n6bXZ7QA=="],
+
+ "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.11", "", { "os": "win32", "cpu": "x64" }, "sha512-cCsXK9AQ9Zf18QlVnbrFu2IKfr4sf2sfbErkF2jfCzyCO9Bnhl0KRx63zlN+Ni1xU7gcBLAssgcui5R400N2eA=="],
+
+ "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
+
+ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
+
+ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@stryker-mutator/api": ["@stryker-mutator/api@9.6.0", "", { "dependencies": { "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "tslib": "~2.8.0", "typed-inject": "~5.0.0" } }, "sha512-kJEEwOVoWDXGEIXuM+9efT6LSJ7nyxnQQvjEoKg8GSZXbDUjfD0tqA0aBD06U1SzQLKCM7ffjgPffr154MHZKw=="],
+
+ "@stryker-mutator/core": ["@stryker-mutator/core@9.6.0", "", { "dependencies": { "@inquirer/prompts": "^8.0.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/instrumenter": "9.6.0", "@stryker-mutator/util": "9.6.0", "ajv": "~8.18.0", "chalk": "~5.6.0", "commander": "~14.0.0", "diff-match-patch": "1.0.5", "emoji-regex": "~10.6.0", "execa": "~9.6.0", "json-rpc-2.0": "^1.7.0", "lodash.groupby": "~4.6.0", "minimatch": "~10.2.4", "mutation-server-protocol": "~0.4.0", "mutation-testing-elements": "3.7.2", "mutation-testing-metrics": "3.7.2", "mutation-testing-report-schema": "3.7.2", "npm-run-path": "~6.0.0", "progress": "~2.0.3", "rxjs": "~7.8.1", "semver": "^7.6.3", "source-map": "~0.7.4", "tree-kill": "~1.2.2", "tslib": "2.8.1", "typed-inject": "~5.0.0", "typed-rest-client": "~2.2.0" }, "bin": { "stryker": "bin/stryker.js" } }, "sha512-oSbw01l6HXHt0iW9x5fQj7yHGGT8ZjCkXSkI7Bsu0juO7Q6vRMXk7XcvKpCBgRgzKXi1osg8+iIzj7acHuxepQ=="],
+
+ "@stryker-mutator/instrumenter": ["@stryker-mutator/instrumenter@9.6.0", "", { "dependencies": { "@babel/core": "~7.29.0", "@babel/generator": "~7.29.0", "@babel/parser": "~7.29.0", "@babel/plugin-proposal-decorators": "~7.29.0", "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/preset-typescript": "~7.28.0", "@stryker-mutator/api": "9.6.0", "@stryker-mutator/util": "9.6.0", "angular-html-parser": "~10.4.0", "semver": "~7.7.0", "tslib": "2.8.1", "weapon-regex": "~1.3.2" } }, "sha512-tWdRYfm9LF4Go7cNOos0xEIOEnN7ZOSj38rfXvGZS9IINlvYBrBCl2xcz/67v6l5A7xksMWWByZRIq2bgdnnUg=="],
+
+ "@stryker-mutator/util": ["@stryker-mutator/util@9.6.0", "", {}, "sha512-gw7fJOFNHEj9inAEOodD9RrrMEMhZmWJ46Ww/kDJAXlSsBBmdwCzeomNLngmLTvgp14z7Tfq85DHYwvmNMdOxA=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
+
+ "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.1", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.1", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.1.1", "vitest": "4.1.1" }, "optionalPeers": ["@vitest/browser"] }, "sha512-nZ4RWwGCoGOQRMmU/Q9wlUY540RVRxJZ9lxFsFfy0QV7Zmo5VVBhB6Sl9Xa0KIp2iIs3zWfPlo9LcY1iqbpzCw=="],
+
+ "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
+
+ "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
+
+ "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
+
+ "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
+
+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+
+ "angular-html-parser": ["angular-html-parser@10.4.0", "", {}, "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww=="],
+
+ "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="],
+
+ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+
+ "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
+
+ "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
+
+ "bun": ["bun@1.3.11", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.11", "@oven/bun-darwin-x64": "1.3.11", "@oven/bun-darwin-x64-baseline": "1.3.11", "@oven/bun-linux-aarch64": "1.3.11", "@oven/bun-linux-aarch64-musl": "1.3.11", "@oven/bun-linux-x64": "1.3.11", "@oven/bun-linux-x64-baseline": "1.3.11", "@oven/bun-linux-x64-musl": "1.3.11", "@oven/bun-linux-x64-musl-baseline": "1.3.11", "@oven/bun-windows-aarch64": "1.3.11", "@oven/bun-windows-x64": "1.3.11", "@oven/bun-windows-x64-baseline": "1.3.11" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-AvXWYFO6j/ZQ7bhGm4X6eilq2JHsDVC90ZM32k2B7/srhC2gs3Sdki1QTbwrdRCo8o7eT+167vcB1yzOvPdbjA=="],
+
+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="],
+
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
+ "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
+
+ "chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
+
+ "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
+
+ "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="],
+
+ "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
+
+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
+ "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="],
+
+ "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
+
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+ "des.js": ["des.js@1.1.0", "", { "dependencies": { "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" } }, "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg=="],
+
+ "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="],
+
+ "diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.325", "", {}, "sha512-PwfIw7WQSt3xX7yOf5OE/unLzsK9CaN2f/FvV3WjPR1Knoc1T9vePRVV4W1EM301JzzysK51K7FNKcusCr0zYA=="],
+
+ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@10.1.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.3", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA=="],
+
+ "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+ "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
+
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
+
+ "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="],
+
+ "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="],
+
+ "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+
+ "fast-wrap-ansi": ["fast-wrap-ansi@0.2.0", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
+
+ "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
+
+ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+
+ "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="],
+
+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
+
+ "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@17.3.0", "", {}, "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
+
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+ "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
+
+ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
+
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+ "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
+
+ "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
+
+ "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
+
+ "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
+
+ "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
+
+ "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "js-md4": ["js-md4@0.3.2", "", {}, "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA=="],
+
+ "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-rpc-2.0": ["json-rpc-2.0@1.7.1", "", {}, "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="],
+
+ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
+
+ "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
+
+ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="],
+
+ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "mdn-data": ["mdn-data@2.23.0", "", {}, "sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ=="],
+
+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+ "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
+
+ "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="],
+
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+ "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="],
+
+ "minimatch": ["minimatch@5.1.8", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ=="],
+
+ "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "mutation-server-protocol": ["mutation-server-protocol@0.4.1", "", { "dependencies": { "zod": "^4.1.12" } }, "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g=="],
+
+ "mutation-testing-elements": ["mutation-testing-elements@3.7.2", "", {}, "sha512-i7X2Q4X5eYon72W2QQ9HND7plVhQcqTnv+Xc3KeYslRZSJ4WYJoal8LFdbWm7dKWLNE0rYkCUrvboasWzF3MMA=="],
+
+ "mutation-testing-metrics": ["mutation-testing-metrics@3.7.2", "", { "dependencies": { "mutation-testing-report-schema": "3.7.2" } }, "sha512-ichXZSC4FeJbcVHYOWzWUhNuTJGogc0WiQol8lqEBrBSp+ADl3fmcZMqrx0ogInEUiImn+A8JyTk6uh9vd25TQ=="],
+
+ "mutation-testing-report-schema": ["mutation-testing-report-schema@3.7.2", "", {}, "sha512-fN5M61SDzIOeJyatMOhGPLDOFz5BQIjTNPjo4PcHIEUWrejO4i4B5PFuQ/2l43709hEsTxeiXX00H73WERKcDw=="],
+
+ "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="],
+
+ "mysql2": ["mysql2@3.19.0", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-760Ay9HViAUT7V8QkYxrIx/LHJPGWtE+D/31VuiDm1eu2IFaUIqMcK7+hVHnmgnC7JnnwreFKsZoa8K6BnGl8Q=="],
+
+ "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+ "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
+
+ "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
+
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
+
+ "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
+
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
+
+ "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
+
+ "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
+
+ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
+
+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
+
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
+
+ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+ "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
+
+ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
+ "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
+
+ "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
+
+ "stryker-mutator-bun-runner": ["stryker-mutator-bun-runner@0.4.0", "", { "dependencies": { "@stryker-mutator/api": "^9.0.1", "@stryker-mutator/util": "^9.0.1", "execa": "^9.6.0", "glob": "^11.0.3", "semver": "^7.5.4" }, "peerDependencies": { "@stryker-mutator/core": "^9.0.0", "bun": ">=1.0.0" } }, "sha512-wx2nfdfKFWRxE5fiAYzRBVJoUMtoeYIE2uAtWS1fu4rGeay6XVm3wBmqi2bKosVH6zHPShsciSCDKDGifZHvSA=="],
+
+ "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="],
+
+ "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
+
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+ "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
+
+ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
+ "typed-inject": ["typed-inject@5.0.0", "", {}, "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA=="],
+
+ "typed-rest-client": ["typed-rest-client@2.2.0", "", { "dependencies": { "des.js": "^1.1.0", "js-md4": "^0.3.2", "qs": "^6.14.1", "tunnel": "0.0.6", "underscore": "^1.12.1" } }, "sha512-/e2Rk9g20N0r44kaQLb3v6QGuryOD8SPb53t43Y5kqXXA+SqWuU7zLiMxetw61jNn/JFrxTdr5nPDhGY/eTNhQ=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
+
+ "underscore": ["underscore@1.13.8", "", {}, "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
+
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
+
+ "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
+
+ "weapon-regex": ["weapon-regex@1.3.6", "", {}, "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
+
+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
+ "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/config-array/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "@eslint/config-helpers/@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="],
+
+ "@eslint/json/@eslint/core": ["@eslint/core@1.1.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw=="],
+
+ "@eslint/json/@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.0", "", { "dependencies": { "@eslint/core": "^1.1.0", "levn": "^0.4.1" } }, "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ=="],
+
+ "@stryker-mutator/core/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
+
+ "@stryker-mutator/core/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.3", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-Rwi3pnapEqirPSbWbrZaa6N3nmqq4Xer/2XooiOKyV3q12ML06f7MOuc5DVH8ONZIFhwIYQ3yzPH4nt7iWHaTg=="],
+
+ "@vitest/coverage-v8/@vitest/utils": ["@vitest/utils@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ=="],
+
+ "@vitest/coverage-v8/std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
+
+ "eslint/@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="],
+
+ "eslint/@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="],
+
+ "eslint/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
+
+ "@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="],
+
+ "@stryker-mutator/core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+ "@stryker-mutator/core/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="],
+
+ "@vitest/coverage-v8/@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@4.1.1", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ=="],
+
+ "eslint/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="],
+
+ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "glob/minimatch/brace-expansion": ["brace-expansion@5.0.3", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA=="],
+
+ "@eslint/config-array/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "@stryker-mutator/core/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "eslint/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+ }
+}
diff --git a/app/server/services/ts/bun.lockb b/app/server/services/ts/bun.lockb
new file mode 100755
index 0000000..c41d30d
Binary files /dev/null and b/app/server/services/ts/bun.lockb differ
diff --git a/app/server/services/ts/common/expressUtils.ts b/app/server/services/ts/common/expressUtils.ts
new file mode 100644
index 0000000..1a56b0d
--- /dev/null
+++ b/app/server/services/ts/common/expressUtils.ts
@@ -0,0 +1,29 @@
+import cors from "cors";
+
+const defaultMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"];
+const origins = [
+ "http://localhost",
+ "http://localhost:8080",
+ process.env.CORS_ALLOWED_ORIGIN
+];
+
+export function buildCorsConfig(opts?: { methods?: string[] }) {
+ return cors({
+ origin: origins,
+ methods: opts?.methods ?? defaultMethods,
+ credentials: true,
+ allowedHeaders: ["Content-Type", "Authorization"],
+ });
+}
+
+export async function handleServerError(
+ func: () => void,
+ res,
+ message: string = "Error processing request",
+) {
+ await func().catch((e) =>
+ res.status(500).json({
+ message: e.message ?? message,
+ }),
+ );
+}
diff --git a/app/server/services/ts/common/hooks.ts b/app/server/services/ts/common/hooks.ts
new file mode 100644
index 0000000..d0f66dd
--- /dev/null
+++ b/app/server/services/ts/common/hooks.ts
@@ -0,0 +1,3 @@
+export function onExit(callback: () => Promise) {
+ process.on("SIGTERM", callback);
+}
diff --git a/app/server/services/ts/common/port.ts b/app/server/services/ts/common/port.ts
new file mode 100644
index 0000000..336e23b
--- /dev/null
+++ b/app/server/services/ts/common/port.ts
@@ -0,0 +1 @@
+export const PORT = Number(process.env.PORT) || 8000;
diff --git a/app/server/services/ts/common/sqlUtil.ts b/app/server/services/ts/common/sqlUtil.ts
new file mode 100644
index 0000000..1b6ae23
--- /dev/null
+++ b/app/server/services/ts/common/sqlUtil.ts
@@ -0,0 +1,37 @@
+import mysql from "mysql2/promise";
+
+const connectionParams = {
+ host: process.env.MYSQL_HOST,
+ port: Number(process.env.MYSQL_PORT),
+ user: process.env.MYSQL_USER,
+ password: process.env.MYSQL_PASSWORD,
+};
+
+export const getConnectionPool = () =>
+ mysql.createPool({
+ ...connectionParams,
+ database: process.env.DB_NAME,
+ waitForConnections: true,
+ connectionLimit: 10,
+ });
+
+export type DatabaseStatus = "unhealthy" | "ok";
+export const getDatabaseStatus = async (): DatabaseStatus => {
+ let status: DatabaseStatus;
+
+ try {
+ const connection = await mysql.createConnection({
+ ...connectionParams,
+ connectTimeout: 1000,
+ });
+ connection.connect();
+ connection.end();
+
+ status = "ok";
+ } catch (e) {
+ console.error(e.code);
+ status = "unhealthy";
+ }
+
+ return status;
+};
diff --git a/app/server/services/ts/common/types.ts b/app/server/services/ts/common/types.ts
new file mode 100644
index 0000000..e8097de
--- /dev/null
+++ b/app/server/services/ts/common/types.ts
@@ -0,0 +1,48 @@
+import type { RowDataPacket } from "mysql2";
+
+export type DatabaseStatus = "unhealthy" | "ok";
+
+export type User = {
+ id: number;
+ username: string;
+ email: string;
+ firstName: string;
+ lastName: string;
+ age: number;
+ created: string;
+ pw_hash: string;
+ salt: string;
+};
+
+export interface financialAccount extends RowDataPacket {
+ id: number;
+ name: string;
+ type: string;
+ balance: number;
+ value: number;
+ last_updated: Date;
+ subtype: string;
+}
+
+export interface Transaction extends RowDataPacket {
+ id: number;
+ financialAccount_id: number;
+ amount: number;
+ category: string;
+ sender: string;
+ recipient: string;
+ date: Date; // ISO format date string
+ description?: string;
+}
+
+export function validateType(obj: unknown, requiredKeys: string[]): void {
+ if (!obj) {
+ throw new Error("null object");
+ }
+
+ for (const key of requiredKeys) {
+ if (!obj[key]) {
+ throw new Error(`Missing ${key}`);
+ }
+ }
+}
diff --git a/app/server/services/ts/eslint.config.mts b/app/server/services/ts/eslint.config.mts
new file mode 100644
index 0000000..a6d7875
--- /dev/null
+++ b/app/server/services/ts/eslint.config.mts
@@ -0,0 +1,29 @@
+import js from "@eslint/js";
+import globals from "globals";
+import tseslint from "typescript-eslint";
+import json from "@eslint/json";
+import css from "@eslint/css";
+import { defineConfig, globalIgnores } from "eslint/config";
+
+export default defineConfig([
+ tseslint.configs.recommended,
+ globalIgnores(["tsconfig.json"]),
+ {
+ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"],
+ plugins: { js },
+ extends: ["js/recommended"],
+ languageOptions: { globals: { ...globals.node, Bun: "readonly" } },
+ },
+ {
+ files: ["**/*.json"],
+ plugins: { json },
+ language: "json/json",
+ extends: ["json/recommended"],
+ },
+ {
+ files: ["**/*.css"],
+ plugins: { css },
+ language: "css/css",
+ extends: ["css/recommended"],
+ },
+]);
diff --git a/app/server/services/ts/market/.gitignore b/app/server/services/ts/market/.gitignore
new file mode 100644
index 0000000..a14702c
--- /dev/null
+++ b/app/server/services/ts/market/.gitignore
@@ -0,0 +1,34 @@
+# dependencies (bun install)
+node_modules
+
+# output
+out
+dist
+*.tgz
+
+# code coverage
+coverage
+*.lcov
+
+# logs
+logs
+_.log
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# caches
+.eslintcache
+.cache
+*.tsbuildinfo
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/app/server/services/ts/market/Dockerfile b/app/server/services/ts/market/Dockerfile
new file mode 100644
index 0000000..600d6cb
--- /dev/null
+++ b/app/server/services/ts/market/Dockerfile
@@ -0,0 +1,21 @@
+FROM oven/bun:1.3.10
+
+WORKDIR /project
+COPY package.json bun.lockb .
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+bun install
+
+WORKDIR /project/app
+COPY market/package.json market/bun.lockb .
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+bun install
+
+WORKDIR /project
+COPY tsconfig.json .
+COPY common ./common
+
+WORKDIR /project/app
+COPY market/src ./src
+
+CMD ["bun", "--no-env-file", "src/index.ts"]
+
diff --git a/app/server/services/ts/market/Dockerfile.test b/app/server/services/ts/market/Dockerfile.test
new file mode 100644
index 0000000..a8718cd
--- /dev/null
+++ b/app/server/services/ts/market/Dockerfile.test
@@ -0,0 +1,11 @@
+FROM measureonecodetwice/finus-user:local
+
+COPY market /project/market
+
+COPY vitest.config.ts /project/vitest.config.ts
+
+WORKDIR /project
+
+ENV CI=true
+
+CMD ["bun", "x", "vitest", "--run"]
diff --git a/app/server/services/ts/market/README.md b/app/server/services/ts/market/README.md
new file mode 100644
index 0000000..6431ed1
--- /dev/null
+++ b/app/server/services/ts/market/README.md
@@ -0,0 +1,15 @@
+# market
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
diff --git a/app/server/services/ts/market/bun.lock b/app/server/services/ts/market/bun.lock
new file mode 100644
index 0000000..c8b4458
--- /dev/null
+++ b/app/server/services/ts/market/bun.lock
@@ -0,0 +1,179 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "market",
+ "dependencies": {
+ "vitest": "^4.1.1",
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ },
+ "peerDependencies": {
+ "typescript": "^5",
+ },
+ },
+ },
+ "packages": {
+ "@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
+
+ "@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
+
+ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
+
+ "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
+
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.11", "", { "os": "android", "cpu": "arm64" }, "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm" }, "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.11", "", { "os": "linux", "cpu": "x64" }, "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.11", "", { "os": "none", "cpu": "arm64" }, "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g=="],
+
+ "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.11", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.11", "", { "os": "win32", "cpu": "x64" }, "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.11", "", {}, "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
+
+ "@vitest/expect": ["@vitest/expect@4.1.1", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.1.1", "", { "dependencies": { "@vitest/spy": "4.1.1", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.1.1", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ=="],
+
+ "@vitest/runner": ["@vitest/runner@4.1.1", "", { "dependencies": { "@vitest/utils": "4.1.1", "pathe": "^2.0.3" } }, "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "@vitest/utils": "4.1.1", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg=="],
+
+ "@vitest/spy": ["@vitest/spy@4.1.1", "", {}, "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA=="],
+
+ "@vitest/utils": ["@vitest/utils@4.1.1", "", { "dependencies": { "@vitest/pretty-format": "4.1.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
+
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "rolldown": ["rolldown@1.0.0-rc.11", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.11" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-arm64": "1.0.0-rc.11", "@rolldown/binding-darwin-x64": "1.0.0-rc.11", "@rolldown/binding-freebsd-x64": "1.0.0-rc.11", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "vite": ["vite@8.0.2", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.11", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA=="],
+
+ "vitest": ["vitest@4.1.1", "", { "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", "@vitest/pretty-format": "4.1.1", "@vitest/runner": "4.1.1", "@vitest/snapshot": "4.1.1", "@vitest/spy": "4.1.1", "@vitest/utils": "4.1.1", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.1", "@vitest/browser-preview": "4.1.1", "@vitest/browser-webdriverio": "4.1.1", "@vitest/ui": "4.1.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+ }
+}
diff --git a/app/server/services/ts/market/index.ts b/app/server/services/ts/market/index.ts
new file mode 100644
index 0000000..2a5e4b8
--- /dev/null
+++ b/app/server/services/ts/market/index.ts
@@ -0,0 +1 @@
+console.log("Hello via Bun!");
diff --git a/app/server/services/ts/market/package.json b/app/server/services/ts/market/package.json
new file mode 100644
index 0000000..1ebfd03
--- /dev/null
+++ b/app/server/services/ts/market/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "market",
+ "module": "index.ts",
+ "type": "module",
+ "private": true,
+ "devDependencies": {
+ "@types/bun": "latest"
+ },
+ "peerDependencies": {
+ "typescript": "^5"
+ },
+ "dependencies": {
+ "vitest": "^4.1.1"
+ }
+}
diff --git a/app/server/services/ts/market/tests/market.test.ts b/app/server/services/ts/market/tests/market.test.ts
new file mode 100644
index 0000000..6e3667b
--- /dev/null
+++ b/app/server/services/ts/market/tests/market.test.ts
@@ -0,0 +1,141 @@
+import {
+ getIntervalForPeriod,
+ formatMarketPrice,
+ formatSignedValue,
+ formatSignedPercent,
+ formatMarketTimestamp,
+ isPositiveChange,
+ toChartSeries,
+ mergeInstrumentQuote,
+} from "../../../../../client/src/utils/market.ts";
+
+import { describe, it, expect } from "vitest";
+
+describe("getIntervalForPeriod", () => {
+ it("returns correct intervals", () => {
+ expect(getIntervalForPeriod("1d")).toBe("5m");
+ expect(getIntervalForPeriod("5d")).toBe("15m");
+ expect(getIntervalForPeriod("1y")).toBe("1wk");
+ expect(getIntervalForPeriod("5y")).toBe("1mo");
+ expect(getIntervalForPeriod("1mo")).toBe("1d");
+ });
+});
+
+describe("formatMarketPrice", () => {
+ it("returns N/A for null", () => {
+ expect(formatMarketPrice(null)).toBe("N/A");
+ });
+
+ it("formats USD with correct precision", () => {
+ expect(formatMarketPrice(123.45, "USD")).toBe("$123.45");
+ expect(formatMarketPrice(0.1234, "USD")).toBe("$0.1234");
+ });
+
+ it("formats JPY correctly", () => {
+ expect(formatMarketPrice(500, "JPY")).toBe("¥500");
+ });
+
+ it("treats any 3‑letter code as a currency (Intl formatting)", () => {
+ const result = formatMarketPrice(1.234567, "XYZ");
+ expect(result.endsWith("1.2346")).toBe(true);
+ expect(result.startsWith("XYZ")).toBe(true);
+ });
+});
+
+describe("formatSignedValue", () => {
+ it("handles null", () => {
+ expect(formatSignedValue(null)).toBe("N/A");
+ });
+
+ it("adds + for positive values", () => {
+ expect(formatSignedValue(1.23)).toBe("+1.23");
+ });
+
+ it("keeps negative sign", () => {
+ expect(formatSignedValue(-2.5)).toBe("-2.50");
+ });
+});
+
+describe("formatSignedPercent", () => {
+ it("handles null", () => {
+ expect(formatSignedPercent(null)).toBe("N/A");
+ });
+
+ it("formats positive percent", () => {
+ expect(formatSignedPercent(1.234)).toBe("+1.23%");
+ });
+
+ it("formats negative percent", () => {
+ expect(formatSignedPercent(-0.5)).toBe("-0.50%");
+ });
+});
+
+describe("formatMarketTimestamp", () => {
+ it("returns Unavailable for null", () => {
+ expect(formatMarketTimestamp(null)).toBe("Unavailable");
+ });
+
+ it("formats a valid timestamp", () => {
+ const ts = 1700000000;
+ const result = formatMarketTimestamp(ts);
+ expect(result).toBeTypeOf("string");
+ expect(result.length).toBeGreaterThan(5);
+ });
+});
+
+describe("isPositiveChange", () => {
+ it("treats null as zero", () => {
+ expect(isPositiveChange(null)).toBe(true);
+ });
+
+ it("detects positive", () => {
+ expect(isPositiveChange(5)).toBe(true);
+ });
+
+ it("detects negative", () => {
+ expect(isPositiveChange(-1)).toBe(false);
+ });
+});
+
+describe("toChartSeries", () => {
+ it("converts points to chart series", () => {
+ const points = [
+ { timestamp: 1700000000, price: 100 },
+ { timestamp: 1700003600, price: 105 },
+ ];
+
+ const series = toChartSeries(points);
+
+ expect(series.length).toBe(2);
+ expect(series[0]).toHaveProperty("date");
+ expect(series[0]).toHaveProperty("label");
+ expect(series[0]).toHaveProperty("price", 100);
+ });
+});
+
+describe("mergeInstrumentQuote", () => {
+ it("merges quote fields into instrument", () => {
+ const instrument = {
+ symbol: "AAPL",
+ name: "Apple",
+ price: 0,
+ change: 0,
+ changePercent: 0,
+ timestamp: 0,
+ };
+
+ const quote = {
+ price: 150,
+ change: 2,
+ changePercent: 1.5,
+ timestamp: 1700000000,
+ };
+
+ const merged = mergeInstrumentQuote(instrument, quote);
+
+ expect(merged.price).toBe(150);
+ expect(merged.change).toBe(2);
+ expect(merged.changePercent).toBe(1.5);
+ expect(merged.timestamp).toBe(1700000000);
+ });
+});
diff --git a/app/server/services/ts/market/tsconfig.json b/app/server/services/ts/market/tsconfig.json
new file mode 100644
index 0000000..72a9658
--- /dev/null
+++ b/app/server/services/ts/market/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "lib": ["ESNext"],
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}
diff --git a/app/server/services/ts/market/types/HistoricalPoint.ts b/app/server/services/ts/market/types/HistoricalPoint.ts
new file mode 100644
index 0000000..b714b8f
--- /dev/null
+++ b/app/server/services/ts/market/types/HistoricalPoint.ts
@@ -0,0 +1,5 @@
+// timestamp, price model for charts
+export interface HistoricalPoint {
+ timestamp: number;
+ price: number;
+}
diff --git a/app/server/services/ts/market/types/MarketModel.ts b/app/server/services/ts/market/types/MarketModel.ts
new file mode 100644
index 0000000..ad2b881
--- /dev/null
+++ b/app/server/services/ts/market/types/MarketModel.ts
@@ -0,0 +1,9 @@
+// model for current market data
+export interface MarketModel {
+ pair: string;
+ name: string;
+ type: "stock" | "forex";
+ price: number;
+ shift: number;
+ timestamp: number;
+}
diff --git a/app/server/services/ts/package.json b/app/server/services/ts/package.json
new file mode 100644
index 0000000..758b166
--- /dev/null
+++ b/app/server/services/ts/package.json
@@ -0,0 +1,32 @@
+{
+ "scripts": {
+ "format": "prettier --write \"**/src/*.ts|**/*.ts\"",
+ "prepare": "cd ../../../../ && husky hooks/.husky"
+ },
+ "devDependencies": {
+ "@eslint/css": "^0.14.1",
+ "@eslint/js": "^10.0.1",
+ "@eslint/json": "^1.0.0",
+ "eslint": "^10.1.0",
+ "globals": "^17.3.0",
+ "husky": "^9.1.7",
+ "jiti": "^2.6.1",
+ "minimatch": "5",
+ "prettier": "3.8.1",
+ "supertest": "^7.2.2",
+ "typescript-eslint": "^8.55.0",
+ "vitest": "^4.0.18"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "type": "module",
+ "dependencies": {
+ "@types/cors": "^2.8.19",
+ "@vitest/coverage-v8": "^4.1.1",
+ "cors": "^2.8.6",
+ "express": "^5.2.1",
+ "mysql2": "^3.18.1",
+ "stryker-mutator-bun-runner": "^0.4.0"
+ }
+}
diff --git a/app/server/services/ts/tsconfig.json b/app/server/services/ts/tsconfig.json
new file mode 100644
index 0000000..9fab895
--- /dev/null
+++ b/app/server/services/ts/tsconfig.json
@@ -0,0 +1,50 @@
+{
+ // Visit https://aka.ms/tsconfig to read more about this file
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
+ // File Layout
+ // "rootDir": "./src",
+ // "outDir": "./dist",
+
+ // Environment Settings
+ // See also https://aka.ms/tsconfig/module
+ "module": "nodenext",
+ "target": "esnext",
+ "types": ["bun"],
+ // For nodejs:
+ // "lib": ["esnext"],
+ // "types": ["node"],
+ // and npm install -D @types/node
+
+ "paths": {
+ "@/*": ["./common/*"]
+ },
+
+ // Other Outputs
+ "sourceMap": true,
+ "declaration": true,
+ "declarationMap": true,
+
+ // Stricter Typechecking Options
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+
+ // Style Options
+ // "noImplicitReturns": true,
+ // "noImplicitOverride": true,
+ // "noUnusedLocals": true,
+ // "noUnusedParameters": true,
+ // "noFallthroughCasesInSwitch": true,
+ // "noPropertyAccessFromIndexSignature": true,
+
+ // Recommended Options
+ "strict": true,
+ "jsx": "react-jsx",
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "noUncheckedSideEffectImports": true,
+ "moduleDetection": "force",
+ "skipLibCheck": true
+ }
+}
diff --git a/app/server/services/ts/user/.env.local b/app/server/services/ts/user/.env.local
new file mode 100644
index 0000000..e69de29
diff --git a/app/server/services/ts/user/Dockerfile b/app/server/services/ts/user/Dockerfile
new file mode 100644
index 0000000..70d938f
--- /dev/null
+++ b/app/server/services/ts/user/Dockerfile
@@ -0,0 +1,21 @@
+FROM oven/bun:1.3.10
+
+WORKDIR /project
+COPY package.json bun.lockb .
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+bun install
+
+WORKDIR /project/app
+COPY user/package.json user/bun.lockb .
+RUN --mount=type=cache,target=/root/.bun/install/cache \
+bun install
+
+WORKDIR /project
+COPY tsconfig.json .
+COPY common ./common
+
+WORKDIR /project/app
+COPY user/src ./src
+
+CMD ["bun", "--no-env-file", "src/index.ts"]
+
diff --git a/app/server/services/ts/user/Dockerfile.integration.test b/app/server/services/ts/user/Dockerfile.integration.test
new file mode 100644
index 0000000..7a68957
--- /dev/null
+++ b/app/server/services/ts/user/Dockerfile.integration.test
@@ -0,0 +1,12 @@
+FROM measureonecodetwice/finus-user:local
+
+COPY user/integration ./integration
+
+WORKDIR /project
+COPY vitest.config.ts .
+
+ENV CI=true
+
+CMD ["bun", "test", "integration", "--coverage"]
+
+
diff --git a/app/server/services/ts/user/Dockerfile.mutation.test b/app/server/services/ts/user/Dockerfile.mutation.test
new file mode 100644
index 0000000..ec890cd
--- /dev/null
+++ b/app/server/services/ts/user/Dockerfile.mutation.test
@@ -0,0 +1,10 @@
+FROM measureonecodetwice/finus-user:local
+
+COPY user/test ./test
+
+WORKDIR /project
+COPY vitest.config.ts .
+
+ENV CI=true
+
+CMD [ "bun", "stryker", "run"]
diff --git a/app/server/services/ts/user/Dockerfile.test b/app/server/services/ts/user/Dockerfile.test
new file mode 100644
index 0000000..da30765
--- /dev/null
+++ b/app/server/services/ts/user/Dockerfile.test
@@ -0,0 +1,10 @@
+FROM measureonecodetwice/finus-user:local
+
+COPY user/test ./test
+
+WORKDIR /project
+COPY vitest.config.ts .
+
+ENV CI=true
+
+CMD [ "bun", "run", "vitest", "--run"]
diff --git a/app/server/services/ts/user/Dockerfile.userInp.test b/app/server/services/ts/user/Dockerfile.userInp.test
new file mode 100644
index 0000000..7ea27e2
--- /dev/null
+++ b/app/server/services/ts/user/Dockerfile.userInp.test
@@ -0,0 +1,11 @@
+FROM measureonecodetwice/finus-user:local
+
+COPY user/userInput ./test
+
+WORKDIR /project
+
+COPY vitest.config.ts .
+
+ENV CI=true
+
+CMD [ "bun", "run", "vitest", "--run"]
diff --git a/app/server/services/ts/user/bun.lock b/app/server/services/ts/user/bun.lock
new file mode 100644
index 0000000..d88bf07
--- /dev/null
+++ b/app/server/services/ts/user/bun.lock
@@ -0,0 +1,846 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "user-service",
+ "dependencies": {
+ "@mysql/xdevapi": "^8.0.35",
+ "axios": "^1.13.6",
+ "express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
+ "mysql2": "^3.15.3",
+ "vitest": "^4.0.18",
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@types/bun": "latest",
+ "@types/express": "^5.0.6",
+ "@types/node": "^25.1.0",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
+ "eslint": "^9.39.2",
+ "eslint-plugin-react": "^7.37.5",
+ "globals": "^17.3.0",
+ "jiti": "^2.6.1",
+ "typescript-eslint": "^8.54.0",
+ },
+ "peerDependencies": {
+ "typescript": "^5.9.3",
+ },
+ },
+ },
+ "packages": {
+ "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
+
+ "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
+
+ "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
+
+ "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
+
+ "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
+
+ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
+
+ "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
+
+ "@eslint/eslintrc": ["@eslint/eslintrc@3.3.4", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" } }, "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ=="],
+
+ "@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@mysql/xdevapi": ["@mysql/xdevapi@8.0.35", "", { "dependencies": { "google-protobuf": "3.19.4", "lossless-json": "2.0.1", "parsimmon": "1.18.1" } }, "sha512-l7HoBt1l0GwdCBXBrqri4kPBbJrqsyvaa4eqXvq50aWWArOTc5XGV06FWF1epxT+RORBy7scZTWFANxpr4Sfuw=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
+
+ "@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="],
+
+ "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="],
+
+ "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
+
+ "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
+
+ "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
+
+ "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="],
+
+ "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
+
+ "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
+
+ "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
+
+ "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
+
+ "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
+
+ "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+
+ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
+
+ "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
+
+ "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
+
+ "array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
+
+ "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
+
+ "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
+
+ "array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="],
+
+ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
+
+ "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
+
+ "axios": ["axios@1.13.6", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ=="],
+
+ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+
+ "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+
+ "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
+
+ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
+
+ "bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
+
+ "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+ "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
+
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
+ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
+
+ "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
+
+ "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
+
+ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
+
+ "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
+
+ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
+
+ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
+
+ "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+ "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
+
+ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+ "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-iterator-helpers": ["es-iterator-helpers@1.2.2", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" } }, "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w=="],
+
+ "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
+
+ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
+
+ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
+
+ "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="],
+
+ "eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
+
+ "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
+
+ "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.3.4", "", {}, "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA=="],
+
+ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+
+ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
+
+ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+
+ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+ "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
+
+ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
+
+ "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
+
+ "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="],
+
+ "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
+
+ "google-protobuf": ["google-protobuf@3.19.4", "", {}, "sha512-OIPNCxsG2lkIvf+P5FNfJ/Km95CsXOBecS9ZcAU6m2Rq3svc0Apl9nB3GMDNKfQ9asNv4KjyAqGwPQFrVle3Yg=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
+
+ "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
+
+ "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
+
+ "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
+
+ "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+ "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
+
+ "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
+
+ "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
+
+ "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
+
+ "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
+
+ "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
+
+ "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
+
+ "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
+
+ "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
+
+ "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
+
+ "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
+
+ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+ "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
+
+ "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
+
+ "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
+
+ "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
+
+ "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
+
+ "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
+
+ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
+
+ "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
+
+ "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
+
+ "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
+
+ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
+
+ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
+
+ "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
+
+ "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
+
+ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="],
+
+ "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
+
+ "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="],
+
+ "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="],
+
+ "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
+
+ "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="],
+
+ "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
+
+ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="],
+
+ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
+
+ "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
+
+ "lossless-json": ["lossless-json@2.0.1", "", {}, "sha512-KW/FSL426qblKVvf4ImeMVGr0Je6J9aXvAMUOIU8AzelDj06q47mn6QJ+56lBBd+A8kjrncrxdKQs6ZssAXTmw=="],
+
+ "lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+ "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+ "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "mysql2": ["mysql2@3.18.2", "", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-UfEShBFAZZEAKjySnTUuE7BgqkYT4mx+RjoJ5aqtmwSSvNcJ/QxQPXz/y3jSxNiVRedPfgccmuBtiPCSiEEytw=="],
+
+ "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
+
+ "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+ "node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
+
+ "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+ "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+ "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
+
+ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
+
+ "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="],
+
+ "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
+
+ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
+
+ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+ "parsimmon": ["parsimmon@1.18.1", "", {}, "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
+
+ "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
+
+ "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
+
+ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+ "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
+
+ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+ "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+
+ "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
+
+ "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
+
+ "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
+
+ "resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
+
+ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
+
+ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
+
+ "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+ "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
+
+ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
+
+ "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+
+ "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
+
+ "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
+
+ "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
+
+ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+ "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "sql-escaper": ["sql-escaper@1.3.3", "", {}, "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
+
+ "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
+
+ "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
+
+ "string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="],
+
+ "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
+
+ "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
+
+ "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
+
+ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
+
+ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+ "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
+ "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
+
+ "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
+
+ "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
+
+ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
+
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
+
+ "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+ "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
+
+ "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
+
+ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
+
+ "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
+
+ "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
+
+ "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
+
+ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
+
+ "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
+
+ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "jsonwebtoken/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
+
+ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+ }
+}
diff --git a/app/server/services/ts/user/bun.lockb b/app/server/services/ts/user/bun.lockb
new file mode 100755
index 0000000..9f17393
Binary files /dev/null and b/app/server/services/ts/user/bun.lockb differ
diff --git a/app/server/services/ts/user/integration/accounts.test.ts b/app/server/services/ts/user/integration/accounts.test.ts
new file mode 100644
index 0000000..53b02b6
--- /dev/null
+++ b/app/server/services/ts/user/integration/accounts.test.ts
@@ -0,0 +1,111 @@
+import { describe, it, expect, beforeAll } from "vitest";
+import request from "supertest";
+import { createAuthenticatedAccount, createSignupBody } from "./setup";
+
+const BASE_URL = process.env.API_GATEWAY_ADDR;
+
+let token: string;
+let accountId: number;
+
+beforeAll(async () => {
+ const setup = await createAuthenticatedAccount(BASE_URL, "accounts", {
+ name: "Chequing",
+ type: "chequing",
+ balance: 1000,
+ value: 1000,
+ subtype: "na",
+ });
+
+ token = setup.token;
+ accountId = setup.accountId;
+}, 30000);
+
+describe("Accounts Integration (Docker)", () => {
+ it("lists accounts for the authenticated user", async () => {
+ const res = await request(BASE_URL)
+ .get("/api/accounts")
+ .set("Authorization", `Bearer ${token}`);
+
+ expect(res.status).toBe(200);
+ expect(Array.isArray(res.body)).toBe(true);
+ expect(res.body.length).toBeGreaterThan(0);
+ });
+
+ it("updates an account", async () => {
+ const res = await request(BASE_URL)
+ .put("/api/accounts")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ id: accountId,
+ name: "Updated Chequing",
+ type: "chequing",
+ balance: 1500,
+ value: 1500,
+ subtype: "na",
+ });
+
+ expect(res.status).toBe(200);
+ expect(res.body.message).toBe("Account successfully updated");
+ });
+
+ it("prevents unauthorized account updates", async () => {
+ const otherUser = createSignupBody("accounts-other");
+ otherUser.age = 22;
+ otherUser.password = "password123";
+
+ const signup = await request(BASE_URL).post("/api/signup").send(otherUser);
+ expect(signup.status).toBe(201);
+
+ const login2 = await request(BASE_URL)
+ .post("/api/login")
+ .send({ email: otherUser.email, password: otherUser.password });
+
+ expect(login2.status).toBe(200);
+
+ const token2 = login2.body.token;
+
+ const res = await request(BASE_URL)
+ .put("/api/accounts")
+ .set("Authorization", `Bearer ${token2}`)
+ .send({
+ id: accountId,
+ name: "Hacked Account",
+ type: "chequing",
+ balance: 9999,
+ value: 9999,
+ subtype: "na",
+ });
+
+ expect(res.status).toBe(401);
+ });
+
+ it("deletes an account", async () => {
+ const res = await request(BASE_URL)
+ .delete(`/api/accounts?id=${accountId}`)
+ .set("Authorization", `Bearer ${token}`);
+
+ expect(res.status).toBe(200);
+ expect(res.body.message).toBe("Account successfully deleted");
+ });
+
+ it("rejects account creation without auth", async () => {
+ const res = await request(BASE_URL).post("/api/accounts").send({
+ name: "Unauthorized",
+ type: "chequing",
+ });
+
+ expect(res.status).toBe(400);
+ });
+
+ it("validates account input", async () => {
+ const res = await request(BASE_URL)
+ .post("/api/accounts")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ name: "", // invalid
+ type: "chequing",
+ });
+
+ expect(res.status).toBe(400);
+ });
+});
diff --git a/app/server/services/ts/user/integration/goals.test.ts b/app/server/services/ts/user/integration/goals.test.ts
new file mode 100644
index 0000000..77404ba
--- /dev/null
+++ b/app/server/services/ts/user/integration/goals.test.ts
@@ -0,0 +1,238 @@
+import { describe, it, expect, beforeAll } from "vitest";
+import request from "supertest";
+import { setupUserWithGoals } from "./setup.ts";
+import type { Goal } from "../src/types/Goals.ts";
+
+const BASE_URL = process.env.API_GATEWAY_ADDR;
+
+let token: string;
+
+beforeAll(async () => {
+ const setup = await setupUserWithGoals(BASE_URL, "goals-test", [
+ {
+ name: "Reduce Grocery Spending",
+ type: "reduce_spending",
+ category: "groceries",
+ target: 400,
+ period: "m",
+ },
+ {
+ type: "save",
+ name: "New Goal",
+ category: "Unknown",
+ target: 100,
+ period: "m",
+ },
+ ]);
+ token = setup.token;
+}, 30000);
+
+describe("Goals Integration (Docker)", () => {
+ let createdGoalId: number;
+
+ it("creates a new spending reduction goal", async () => {
+ const goalData = {
+ name: "Reduce Grocery Spending",
+ type: "reduce_spending",
+ category: "groceries",
+ target: 400,
+ period: "m",
+ };
+
+ const res = await request(BASE_URL)
+ .post("/goals")
+ .set("Authorization", `Bearer ${token}`)
+ .send(goalData);
+
+ expect(res.status).toBe(201);
+ expect(res.body).toHaveProperty("id");
+ expect(res.body.name).toBe(goalData.name);
+ expect(res.body.type).toBe(goalData.type);
+ expect(res.body.target).toBe("400.00");
+ expect(res.body.period).toBe(goalData.period);
+
+ createdGoalId = res.body.id;
+ });
+
+ it("creates a savings goal", async () => {
+ const goalData = {
+ type: "save",
+ name: "New Goal",
+ category: "Unknown",
+ target: 100,
+ period: "m",
+ };
+
+ const res = await request(BASE_URL)
+ .post("/goals")
+ .set("Authorization", `Bearer ${token}`)
+ .send(goalData);
+
+ expect(res.status).toBe(201);
+ expect(res.body.name).toBe(goalData.name);
+ expect(res.body.type).toBe(goalData.type);
+ expect(res.body.target).toBe("100.00");
+ });
+
+ it("lists all goals for the user", async () => {
+ const res = await request(BASE_URL)
+ .get("/goals")
+ .set("Authorization", `Bearer ${token}`);
+
+ expect(res.status).toBe(200);
+ expect(Array.isArray(res.body)).toBe(true);
+ expect(res.body.length).toBeGreaterThan(0);
+ });
+
+ it("updates a goal (using query parameter gid)", async () => {
+ const updates = {
+ id: createdGoalId,
+ name: "Updated Grocery Goal",
+ profile_id: 1,
+ target: 350,
+ };
+
+ const res = await request(BASE_URL)
+ .patch(`/goals?gid=${createdGoalId}`)
+ .set("Authorization", `Bearer ${token}`)
+ .send(updates);
+
+ expect(res.status).toBe(200);
+ expect(res.body.name).toBe(updates.name);
+ expect(res.body.target).toBe("350.00");
+ });
+
+ it("deletes a goal (using query parameter gid)", async () => {
+ // Create a temporary goal to delete
+ const createRes = await request(BASE_URL)
+ .post("/goals")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ name: "Temp Goal",
+ type: "save",
+ category: "temp",
+ target: 100,
+ period: "m",
+ });
+
+ const tempGoalId = createRes.body.id;
+
+ const deleteRes = await request(BASE_URL)
+ .delete(`/goals?gid=${tempGoalId}`)
+ .set("Authorization", `Bearer ${token}`);
+
+ expect(deleteRes.status).toBe(204);
+
+ // Verify it's gone - should return 404
+ const getRes = await request(BASE_URL)
+ .get("/goals")
+ .set("Authorization", `Bearer ${token}`);
+
+ const stillExists = getRes.body.some(
+ (goal: Goal) => goal.id === tempGoalId,
+ );
+ expect(stillExists).toBe(false);
+ });
+
+ it("respects the 5-goal limit", async () => {
+ // First, delete all existing goals to start fresh
+ const listRes = await request(BASE_URL)
+ .get("/goals")
+ .set("Authorization", `Bearer ${token}`);
+
+ for (const goal of listRes.body) {
+ const result = await request(BASE_URL)
+ .delete(`/goals?gid=${goal.id}`)
+ .set("Authorization", `Bearer ${token}`);
+
+ expect(result.status).toBe(204);
+ }
+
+ const listResPostDelete = await request(BASE_URL)
+ .get("/goals")
+ .set("Authorization", `Bearer ${token}`);
+
+ expect(listResPostDelete.body.length).toBe(0);
+
+ // Create 5 goals
+ for (let i = 0; i < 5; i++) {
+ const res = await request(BASE_URL)
+ .post("/goals")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ name: `Goal ${i}`,
+ type: "save",
+ category: `category${i}`,
+ target: 100,
+ period: "m",
+ });
+ expect(res.status).toBe(201);
+ }
+
+ // Try to create a 6th
+ const res = await request(BASE_URL)
+ .post("/goals")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ name: "Sixth Goal",
+ profile_id: 1,
+ type: "save",
+ category: "extra",
+ target: 100,
+ period: "m",
+ });
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe("Maximum 5 goals allowed per profile");
+ });
+
+ it("rejects goal creation without required fields", async () => {
+ // Missing period for reduce_spending
+ const res = await request(BASE_URL)
+ .post("/goals")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ name: "Invalid Goal",
+ type: "reduce_spending",
+ category: "groceries",
+ target: 400,
+ // missing period
+ });
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toBe("Missing required fields");
+ });
+
+ it("rejects goal creation without auth", async () => {
+ const res = await request(BASE_URL).post("/goals").send({
+ name: "Unauthorized Goal",
+ profile_id: 1,
+ type: "save",
+ category: "test",
+ target: 100,
+ period: "m",
+ });
+
+ expect(res.status).toBe(401);
+ });
+
+ it("rejects update without gid parameter", async () => {
+ const res = await request(BASE_URL)
+ .patch("/goals")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ name: "Should Fail",
+ });
+
+ // Without gid, should return 400 or 404
+ expect(res.status).toBe(404);
+ });
+
+ it("rejects delete without gid parameter", async () => {
+ const res = await request(BASE_URL)
+ .delete("/goals")
+ .set("Authorization", `Bearer ${token}`);
+
+ expect(res.status).toBe(404);
+ });
+});
diff --git a/app/server/services/ts/user/integration/setup.ts b/app/server/services/ts/user/integration/setup.ts
new file mode 100644
index 0000000..d30dc30
--- /dev/null
+++ b/app/server/services/ts/user/integration/setup.ts
@@ -0,0 +1,175 @@
+import request from "supertest";
+
+function sleep(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+export function createSignupBody(prefix: string) {
+ const safePrefix = prefix.replace(/[^\w]/g, "");
+ const unique = `${safePrefix}${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
+
+ return {
+ username: unique,
+ email: `${unique}@example.com`,
+ first_name: "Test",
+ last_name: "User",
+ age: 30,
+ password: "123ABC!7",
+ };
+}
+
+export async function createAuthenticatedAccount(
+ baseUrl: string,
+ prefix: string,
+ accountOverrides?: {
+ name?: string;
+ type?: string;
+ balance?: number;
+ value?: number;
+ subtype?: string;
+ },
+) {
+ const deadline = Date.now() + 20000;
+ let lastFailure = "setup did not start";
+
+ while (Date.now() < deadline) {
+ const signupBody = createSignupBody(prefix);
+
+ try {
+ const signup = await request(baseUrl)
+ .post("/api/signup")
+ .send(signupBody);
+ if (signup.status !== 201) {
+ lastFailure = `signup=${signup.status} ${JSON.stringify(signup.body)}`;
+ await sleep(500);
+ continue;
+ }
+
+ const login = await request(baseUrl)
+ .post("/api/login")
+ .send({ email: signupBody.email, password: signupBody.password });
+ if (login.status !== 200 || !login.body.token) {
+ lastFailure = `login=${login.status} ${JSON.stringify(login.body)}`;
+ await sleep(500);
+ continue;
+ }
+
+ const token = login.body.token as string;
+
+ // Create the account
+ const account = await request(baseUrl)
+ .post("/api/accounts")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ name: accountOverrides?.name ?? "Chequing",
+ type: accountOverrides?.type ?? "chequing",
+ balance: accountOverrides?.balance ?? 1000,
+ value: accountOverrides?.value ?? 1000,
+ subtype: accountOverrides?.subtype ?? "na",
+ });
+
+ console.log("Account creation response:", account.status, account.body);
+
+ if (account.status !== 200 || !account.body.id) {
+ lastFailure = `account=${account.status} ${JSON.stringify(account.body)}`;
+ await sleep(500);
+ continue;
+ }
+
+ return {
+ token,
+ accountId: account.body.id as number,
+ };
+ } catch (error) {
+ lastFailure =
+ error instanceof Error ? error.message : "unknown setup error";
+ console.log("Setup error:", lastFailure);
+ await sleep(500);
+ }
+ }
+
+ throw new Error(`Timed out creating authenticated account: ${lastFailure}`);
+}
+
+export interface GoalData {
+ name: string;
+ type: "save" | "reduce_spending";
+ category: string;
+ target: number;
+ period?: "w" | "m";
+}
+
+export async function createGoal(
+ baseUrl: string,
+ token: string,
+ goalData: GoalData,
+) {
+ const response = await request(baseUrl)
+ .post("/api/goals")
+ .set("Authorization", `Bearer ${token}`)
+ .send(goalData);
+
+ if (response.status !== 201) {
+ throw new Error(
+ `Failed to create goal: ${response.status} ${JSON.stringify(response.body)}`,
+ );
+ }
+
+ return response.body;
+}
+
+export async function createMultipleGoals(
+ baseUrl: string,
+ token: string,
+ goalsData: GoalData[],
+) {
+ const createdGoals = [];
+ for (const goalData of goalsData) {
+ const goal = await createGoal(baseUrl, token, goalData);
+ createdGoals.push(goal);
+ }
+ return createdGoals;
+}
+
+export async function deleteAllGoals(baseUrl: string, token: string) {
+ // Fetch all goals
+ const listRes = await request(baseUrl)
+ .get("/goals")
+ .set("Authorization", `Bearer ${token}`);
+
+ if (listRes.status === 200 && Array.isArray(listRes.body)) {
+ // Delete each goal
+ for (const goal of listRes.body) {
+ await request(baseUrl)
+ .delete(`/goals?gid=${goal.id}`)
+ .set("Authorization", `Bearer ${token}`);
+ }
+ }
+}
+
+export async function setupUserWithGoals(
+ baseUrl: string,
+ prefix: string,
+ goalsData: GoalData[],
+) {
+ // First create the user account
+ const { token } = await createAuthenticatedAccount(baseUrl, prefix, {
+ name: "Test Account",
+ type: "chequing",
+ balance: 5000,
+ value: 5000,
+ subtype: "na",
+ });
+
+ // Clean up any existing goals (in case of leftover data)
+ await deleteAllGoals(baseUrl, token);
+
+ // Create the specified goals
+ const createdGoals = await createMultipleGoals(baseUrl, token, goalsData);
+
+ return {
+ token,
+ goals: createdGoals,
+ deleteAllGoals: () => deleteAllGoals(baseUrl, token),
+ };
+}
diff --git a/app/server/services/ts/user/integration/transaction.test.ts b/app/server/services/ts/user/integration/transaction.test.ts
new file mode 100644
index 0000000..9756dba
--- /dev/null
+++ b/app/server/services/ts/user/integration/transaction.test.ts
@@ -0,0 +1,166 @@
+import { describe, it, expect, beforeAll } from "vitest";
+import request from "supertest";
+import { createAuthenticatedAccount } from "./setup";
+
+const BASE_URL = process.env.API_GATEWAY_ADDR;
+
+let token: string;
+let accountId: number;
+
+beforeAll(async () => {
+ const setup = await createAuthenticatedAccount(BASE_URL, "transactions", {
+ name: "all my moola",
+ type: "chequing",
+ balance: 1000,
+ value: 1000,
+ subtype: "TFSA",
+ });
+
+ token = setup.token;
+ accountId = setup.accountId;
+}, 30000);
+
+describe("Transactions Integration (Docker)", () => {
+ it("creates a transaction", async () => {
+ const res = await request(BASE_URL)
+ .post("/api/transactions")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ financialAccount_id: accountId,
+ amount: 50,
+ description: "Groceries",
+ sender: "Me",
+ recipient: "Store",
+ date: "2024-01-01T12:00:00",
+ category: "Food",
+ });
+
+ expect(res.status).toBe(200);
+ expect(res.body.id).toBeDefined();
+ });
+
+ it("lists transactions for an account", async () => {
+ // Create a transaction first
+ await request(BASE_URL)
+ .post("/api/transactions")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ financialAccount_id: accountId,
+ amount: 20,
+ description: "Coffee",
+ sender: "Me",
+ recipient: "Cafe",
+ date: "2024-01-02T10:00:00",
+ category: "Food",
+ });
+
+ const res = await request(BASE_URL)
+ .get(`/api/transactions?financialAccount_id=${accountId}`)
+ .set("Authorization", `Bearer ${token}`);
+
+ expect(res.status).toBe(200);
+ expect(Array.isArray(res.body)).toBe(true);
+ expect(res.body.length).toBeGreaterThan(0);
+ });
+
+ it("updates a transaction", async () => {
+ // Create a transaction
+ const create = await request(BASE_URL)
+ .post("/api/transactions")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ financialAccount_id: accountId,
+ amount: 10,
+ description: "Snack",
+ sender: "Me",
+ recipient: "Vending",
+ date: "2024-01-03T09:00:00",
+ category: "Food",
+ });
+
+ const id = create.body.id;
+
+ const update = await request(BASE_URL)
+ .put("/api/transactions")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ id,
+ financialAccount_id: accountId,
+ amount: 15,
+ description: "Snack Updated",
+ sender: "Me",
+ recipient: "Vending",
+ date: "2024-01-03T09:30:00",
+ });
+
+ expect(update.status).toBe(200);
+ expect(update.body.message).toBe("Transaction successfully updated");
+ });
+
+ it("deletes a transaction", async () => {
+ // Create a transaction
+ const create = await request(BASE_URL)
+ .post("/api/transactions")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ financialAccount_id: accountId,
+ amount: 100,
+ description: "Test",
+ sender: "Me",
+ recipient: "Someone",
+ date: "2024-01-04T12:00:00",
+ category: "Misc",
+ });
+
+ const id = create.body.id;
+
+ const del = await request(BASE_URL)
+ .delete("/api/transactions")
+ .set("Authorization", `Bearer ${token}`)
+ .send({ id, financialAccount_id: accountId });
+
+ expect(del.status).toBe(200);
+ expect(del.body.message).toBe("Transaction deleted");
+ });
+
+ it("imports CSV transactions", async () => {
+ const res = await request(BASE_URL)
+ .post("/api/transactions/csvTransaction")
+ .set("Authorization", `Bearer ${token}`)
+ .send({
+ financialAccount_id: accountId,
+ transactions: [
+ {
+ amount: 12.5,
+ description: "Lunch",
+ sender: "Me",
+ recipient: "Restaurant",
+ date: "2024-01-05T13:00:00",
+ category: "Food",
+ errors: [],
+ },
+ {
+ amount: null,
+ description: null,
+ date: null,
+ errors: ["Missing amount"],
+ },
+ ],
+ });
+
+ expect(res.status).toBe(200);
+ expect(res.body.inserted).toBe(1);
+ expect(res.body.skipped).toBe(1);
+ expect(res.body.transactions.length).toBe(1);
+ });
+
+ it("rejects unauthorized access", async () => {
+ const res = await request(BASE_URL).post("/api/transactions").send({
+ financialAccount_id: accountId,
+ amount: 10,
+ date: "2024-01-01T12:00:00",
+ });
+
+ expect(res.status).toBe(401);
+ });
+});
diff --git a/app/server/services/ts/user/package.json b/app/server/services/ts/user/package.json
new file mode 100644
index 0000000..837e93e
--- /dev/null
+++ b/app/server/services/ts/user/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "user-service",
+ "main": "src/index.ts",
+ "module": "index.ts",
+ "type": "module",
+ "private": true,
+ "devDependencies": {
+ "@eslint/js": "^9.39.2",
+ "@types/bun": "latest",
+ "@types/express": "^5.0.6",
+ "@types/node": "^25.1.0",
+ "@types/supertest": "^7.2.0",
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
+ "@typescript-eslint/parser": "^8.54.0",
+ "eslint": "^10.1.0",
+ "eslint-plugin-react": "^7.37.5",
+ "globals": "^17.3.0",
+ "jiti": "^2.6.1",
+ "supertest": "^7.2.2",
+ "typescript-eslint": "^8.54.0"
+ },
+ "peerDependencies": {
+ "typescript": "^5.9.3"
+ },
+ "scripts": {
+ "dev": "bun --watch src/index.ts",
+ "build": "tsc",
+ "start": "bun dist/index.js",
+ "test:mutation": "npx stryker run",
+ "test": "vitest"
+ },
+ "dependencies": {
+ "@mysql/xdevapi": "^8.0.35",
+ "axios": "^1.13.6",
+ "express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
+ "mysql2": "^3.15.3",
+ "stryker-mutator-bun-runner": "^0.4.0",
+ "supertest": "^7.2.2",
+ "vitest": "^4.0.18"
+ }
+}
diff --git a/app/server/services/ts/user/reports/mutation/mutation.html b/app/server/services/ts/user/reports/mutation/mutation.html
new file mode 100644
index 0000000..e67e91a
--- /dev/null
+++ b/app/server/services/ts/user/reports/mutation/mutation.html
@@ -0,0 +1,11350 @@
+
+
+
+
+
+
+
+
+
+ Your browser doesn't support
+ custom elements. Please use a latest version of an evergreen browser (Firefox, Chrome,
+ Safari, Opera, Edge, etc).
+
+
+
+
diff --git a/app/server/services/ts/user/src/CheckUser.ts b/app/server/services/ts/user/src/CheckUser.ts
new file mode 100644
index 0000000..e7d8bc7
--- /dev/null
+++ b/app/server/services/ts/user/src/CheckUser.ts
@@ -0,0 +1,67 @@
+import type { RowDataPacket, Connection } from "mysql2/promise";
+
+//Checks the user is the owner of the account
+// export async function checkUserId(
+// db: Connection,
+// userId: number,
+// accountId: number,
+// ): Promise {
+// let result = false;
+// try {
+// //need the user account's profile id first. This should be just a single item returned unless stretch feature 7 is implemented
+// // const [profileRows] = await db.query(
+// // `SELECT profile_id FROM finusAccount_profile WHERE account_id = ?`,
+// // [userId],
+// // );
+
+// const [profileRows] = await db.query(
+// `SELECT uid from`
+
+// if (!profileRows || (profileRows as RowDataPacket[]).length === 0) {
+// throw new Error("User profile not found");
+// }
+
+// const profileId = (profileRows as RowDataPacket[])[0].profile_id;
+
+// //Checks if the profile has an account with that id
+// const [rows] = await db.query(
+// `SELECT 1 FROM profile_financialAccount
+// WHERE profile_id =? AND financialAccount_id =?`,
+// [profileId, accountId],
+// );
+
+// console.log(rows.length);
+// if (rows.length > 0) {
+// result = true;
+// }
+// } catch (error) {
+// console.error(error);
+// }
+
+// return result;
+// }
+
+export async function checkUserId(
+ db: Connection,
+ userId: number,
+ accountId: number,
+): Promise {
+ try {
+ //single query joining through the relationship chain
+ const [rows] = await db.query(
+ `SELECT 1
+ FROM profile_financialAccount pfa
+ INNER JOIN finusAccount_profile uap ON pfa.profile_id = uap.profile_id
+ WHERE uap.account_id = ? AND pfa.financialAccount_id = ?`,
+ [userId, accountId],
+ );
+ console.log(
+ `checking account id:${accountId} for user ${userId}, rows found: `,
+ rows.length,
+ );
+ return rows.length > 0;
+ } catch (error) {
+ console.error("Error in checkUserId:", error);
+ return false;
+ }
+}
diff --git a/app/server/services/ts/user/src/DataConversion.ts b/app/server/services/ts/user/src/DataConversion.ts
new file mode 100644
index 0000000..cd5fdff
--- /dev/null
+++ b/app/server/services/ts/user/src/DataConversion.ts
@@ -0,0 +1,4 @@
+//C0j
+export function convertToDateTime(date: string) {
+ return date.slice(0, 19).replace("T", " ");
+}
diff --git a/app/server/services/ts/user/src/db.ts b/app/server/services/ts/user/src/db.ts
new file mode 100644
index 0000000..ca32705
--- /dev/null
+++ b/app/server/services/ts/user/src/db.ts
@@ -0,0 +1,9 @@
+import mysql from "mysql2/promise";
+
+export const pool = mysql.createPool({
+ host: process.env.MYSQL_HOST,
+ port: Number(process.env.MYSQL_PORT),
+ user: process.env.MYSQL_USER,
+ password: process.env.MYSQL_PASSWORD,
+ database: process.env.DB_NAME,
+});
diff --git a/app/server/services/ts/user/src/handleJWT.ts b/app/server/services/ts/user/src/handleJWT.ts
new file mode 100644
index 0000000..fc55203
--- /dev/null
+++ b/app/server/services/ts/user/src/handleJWT.ts
@@ -0,0 +1,32 @@
+import jwt from "jsonwebtoken";
+import type { Request } from "express";
+import { UnauthorizedAccessError } from "./types/UnauthorizedAccess.js";
+const JWT_SECRET = process.env.JWT_SECRET;
+
+export const authenticateJWT = (req: Request) => {
+ const authHeader = req.headers.authorization;
+ if (!authHeader) {
+ throw new UnauthorizedAccessError("Authorization header missing");
+ }
+
+ const parts = authHeader.split(" ");
+ if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
+ throw new UnauthorizedAccessError("Invalid authorization header format");
+ }
+ const token = parts[1];
+ //add as any if it doesn't work
+ let decoded;
+ try {
+ decoded = jwt.verify(token, JWT_SECRET);
+ } catch {
+ throw new Error("Could not verify given JWT.");
+ }
+
+ const userId = decoded.sub;
+ //console.log("user id: " + userId);
+ if (!userId) {
+ throw new UnauthorizedAccessError("User ID not found in token");
+ }
+
+ return userId;
+};
diff --git a/app/server/services/ts/user/src/index.ts b/app/server/services/ts/user/src/index.ts
new file mode 100644
index 0000000..aabe250
--- /dev/null
+++ b/app/server/services/ts/user/src/index.ts
@@ -0,0 +1,530 @@
+import express from "express";
+import { getConnectionPool, getDatabaseStatus } from "@/sqlUtil";
+import { authenticateJWT } from "./utils/auth.ts";
+import { generateDateRange } from "./utils/dates.ts";
+import { validatePeriod } from "./validation.ts";
+import { getExpensesChartData } from "./logic/expenses.ts";
+import {
+ getAccountIDsForUser,
+ getTransactionsData,
+} from "./logic/transactions.ts";
+import { getSnapshotData } from "./logic/snapshot.ts";
+import { onExit } from "@/hooks";
+import { buildCorsConfig } from "@/expressUtils";
+import { PORT } from "@/port";
+import {
+ deleteUserGoal,
+ createUserGoal,
+ updateUserGoal,
+ getUserGoals,
+ getUserGoalById,
+} from "./logic/goals.ts";
+import { getUserProfileId } from "./queries/goals.ts";
+import type { GoalType, UpdateGoalInput } from "./types/Goals.ts";
+import { getGoalCountByProfileId } from "./queries/goals.ts";
+import { accountsRouter } from "./routes/account.js";
+import { profilesRouter } from "./routes/profile.js";
+import { transactionsRouter } from "./routes/transaction.js";
+import { debtRouter } from "./routes/debt.ts";
+import { savingRouter } from "./routes/saving.ts";
+import type { Transaction } from "./types/Transaction.ts";
+import {
+ createTransaction,
+ deleteTransactionQuery,
+ updateTransactionQuery,
+} from "./queries/transactions.ts";
+
+const pool = getConnectionPool();
+const app = express();
+app.use(buildCorsConfig());
+
+app.use(express.json());
+
+app.use((req, res, next) => {
+ console.log("USER Incoming request: " + req.method + " " + req.url);
+ next();
+});
+
+app.use("/accounts", accountsRouter);
+app.use("/transactions", transactionsRouter);
+app.use("/profiles", profilesRouter);
+app.use("/debts", debtRouter);
+app.use("/savings", savingRouter);
+
+const server = app.listen(PORT, () => {
+ console.log(`User Service running on port ${PORT}`);
+});
+onExit(async () => {
+ await new Promise((res) => server.close(res));
+ await pool.end();
+ process.exit(0);
+});
+
+app.get(
+ "/charts/expenses",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+ const period = req.query.period as string;
+
+ if (!validatePeriod(period)) {
+ return res
+ .status(400)
+ .json({ error: "Invalid period. Must be 'w', 'm', or 'y'." });
+ }
+
+ const chartData = await getExpensesChartData(pool, userId, period);
+ res.json(chartData);
+ } catch (error) {
+ console.error("Error fetching expenses chart data:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to fetch expenses chart data" });
+ }
+ }
+ },
+);
+
+app.get(
+ "/table/transactions",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+ const transactions = await getTransactionsData(pool, userId);
+
+ if (!transactions || transactions.length === 0) {
+ return res.json([]); // Return empty array for no transactions
+ }
+
+ // console.log("Fetched transactions for user:", transactions);
+ res.json(transactions);
+ } catch (error) {
+ console.error("Error fetching transactions:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to fetch transactions" });
+ }
+ }
+ },
+);
+
+app.get(
+ "/table/transactions/accounts",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+ const accountsMap = await getAccountIDsForUser(pool, userId);
+
+ //convert Map to array of objects
+ const accountsArray: { id: number; name: string }[] = [];
+ if (accountsMap instanceof Map) {
+ accountsMap.forEach((name, id) => {
+ accountsArray.push({ id: Number(id), name });
+ });
+ }
+
+ res.json(accountsArray);
+ } catch (error) {
+ console.error("Error fetching transactions:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to fetch transactions" });
+ }
+ }
+ },
+);
+
+//create a new transaction through the table
+app.post(
+ "/table/transactions",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+ if (!userId) {
+ return res.status(401).json({ error: "User not authenticated" });
+ }
+ //extract financial account id as query param
+ const financialAccountId = req.query.fid as string;
+ if (!financialAccountId) {
+ return res.status(400).json({ error: "Missing financial account ID" });
+ }
+
+ //validate request body
+ if (!req.body) {
+ return res.status(400).json({ error: "Invalid request body" });
+ }
+
+ let description = null;
+ let category = null;
+ let amount = null;
+ let date = null;
+ let sender = null;
+ let recipient = null;
+
+ if (req.body.description) description = req.body.description;
+ if (req.body.category) category = req.body.category;
+ if (req.body.amount) amount = req.body.amount;
+ if (req.body.date) date = req.body.date;
+ if (req.body.sender) sender = req.body.sender;
+ if (req.body.recipient) recipient = req.body.recipient;
+
+ if (
+ !description ||
+ !category ||
+ !amount ||
+ !date ||
+ !sender ||
+ !recipient
+ ) {
+ return res.status(400).json({
+ error: "Missing required fields for creating a transaction",
+ });
+ }
+
+ const transactionData: Transaction = {
+ id: 0, //this is ignored in creation because it is auto generated
+ financialAccount_id: financialAccountId as unknown as number,
+ amount: amount,
+ category: category,
+ description: description,
+ sender: sender,
+ recipient: recipient,
+ date: date,
+ constructor: {
+ name: "RowDataPacket",
+ },
+ };
+
+ // console.log("Creating transaction with data:", transactionData);
+
+ const transaction = await createTransaction(
+ pool,
+ financialAccountId as unknown as number,
+ transactionData,
+ );
+
+ res.json(transaction);
+ } catch (error) {
+ console.error("Error creating transaction:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to create transaction" });
+ }
+ }
+ },
+);
+
+//delete a transaction through the table
+app.delete(
+ "/table/transactions",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+ if (!userId) {
+ return res.status(401).json({ error: "User not authenticated" });
+ }
+
+ //extract transaction id as query param
+ const transactionId = req.query.tid as unknown as number;
+ if (!transactionId) {
+ return res.status(400).json({ error: "Missing transaction ID" });
+ }
+
+ //get user profile id
+ const userProfileId = await getUserProfileId(pool, userId);
+ if (!userProfileId) {
+ return res.status(404).json({ error: "User profile not found" });
+ }
+
+ //delete the transaciton
+ const deleted = await deleteTransactionQuery(
+ pool,
+ transactionId,
+ userProfileId,
+ );
+ if (!deleted) {
+ return res.status(404).json({ error: "Transaction not found" });
+ }
+ return res.status(200).send();
+ } catch (error) {
+ console.error("Error deleting transaction:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to delete transaction" });
+ }
+ }
+ },
+);
+
+//update a transaction through the table
+app.patch(
+ "/table/transactions",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+ if (!userId) {
+ return res.status(401).json({ error: "User not authenticated" });
+ }
+
+ //extract transaction id as query param
+ const transactionId = req.query.tid as unknown as number;
+ if (!transactionId) {
+ return res.status(400).json({ error: "Missing transaction ID" });
+ }
+
+ //validate request body
+ if (!req.body) {
+ return res.status(400).json({ error: "Invalid request body" });
+ }
+
+ //need profile id to verify ownership of the transaction via account
+ const userProfileId = await getUserProfileId(pool, userId);
+ if (!userProfileId) {
+ return res.status(404).json({ error: "User profile not found" });
+ }
+
+ const updates: Partial = {};
+ if (req.body.description !== undefined)
+ updates.description = req.body.description;
+ if (req.body.category !== undefined) updates.category = req.body.category;
+ if (req.body.amount !== undefined) updates.amount = req.body.amount;
+ if (req.body.date !== undefined) updates.date = req.body.date;
+ if (req.body.sender !== undefined) updates.sender = req.body.sender;
+ if (req.body.recipient !== undefined)
+ updates.recipient = req.body.recipient;
+ if (req.body.financialAccount_id !== undefined)
+ updates.financialAccount_id = req.body.financialAccount_id;
+
+ // console.log("Updating transaction with the following updates:", updates);
+
+ const transaction = await updateTransactionQuery(
+ pool,
+ transactionId,
+ userProfileId,
+ updates,
+ );
+ if (!transaction) {
+ return res.status(404).json({ error: "Transaction not found" });
+ }
+
+ res.json(transaction);
+ } catch (error) {
+ console.error("Error updating transaction:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to update transaction" });
+ }
+ }
+ },
+);
+
+app.get(
+ "/table/snapshot",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+
+ if (!userId) {
+ return res.status(401).json({ error: "User not authenticated" });
+ }
+
+ const snapshotData = await getSnapshotData(pool, userId);
+ res.json(snapshotData);
+ } catch (error) {
+ console.error("Error fetching snapshot data:", error);
+ if (error instanceof Error) {
+ if (error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else if (error.message.includes("No snapshot data")) {
+ res.status(404).json({ error: "No snapshot data found" });
+ } else {
+ res.status(500).json({ error: "Failed to fetch snapshot data" });
+ }
+ } else {
+ res.status(500).json({ error: "Failed to fetch snapshot data" });
+ }
+ }
+ },
+);
+
+//export { generateDateRange }; //for testing purposes only
+
+// Goal stuff is below------------------------------------------------------
+
+// GET all goals
+app.get("/goals", async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+
+ const goals = await getUserGoals(pool, userId);
+
+ res.json(goals);
+ } catch (error) {
+ console.error("Error fetching goals:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to fetch goals" });
+ }
+ }
+});
+
+// POST create new goal
+app.post("/goals", async (req: express.Request, res: express.Response) => {
+ try {
+ // const userId = authenticateJWT(req);
+ // const name = req.body.name;
+ // const type = req.body.type;
+ // const category = req.body.category;
+ // const target = req.body.target;
+ // const period = req.body.period;
+
+ const userId = authenticateJWT(req);
+ let name = null;
+ let type = null;
+ let category = null;
+ let target = null;
+ let period = null;
+
+ if (req.body.name) name = req.body.name;
+ if (req.body.type) type = req.body.type;
+ if (req.body.category) category = req.body.category;
+ if (req.body.target) target = req.body.target;
+ if (req.body.period) period = req.body.period;
+
+ if (!name || !type || !target || !period) {
+ return res.status(400).json({ error: "Missing required fields" });
+ }
+ //check goal limit (max 5)
+ const goalCount = await getGoalCountByProfileId(pool, userId);
+ if (goalCount >= 5) {
+ return res
+ .status(400)
+ .json({ error: "Maximum 5 goals allowed per profile" });
+ }
+
+ const newGoal = await createUserGoal(pool, userId, {
+ name,
+ type,
+ category,
+ target,
+ period,
+ });
+ res.status(201).json(newGoal);
+ } catch (error) {
+ console.error("Error creating goal:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to create goal" });
+ }
+ }
+});
+
+// PATCH update goal
+app.patch("/goals", async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+ // const goalId = parseInt(req.query.gid);
+ let goalId = null;
+
+ if (req.query.gid) goalId = req.query.gid as unknown as number; //parseInt()
+
+ if (!goalId) {
+ return res.status(404).json({ error: "Missing goal ID" });
+ }
+
+ //verify goal exists and belongs to user
+ const existingGoal = await getUserGoalById(pool, goalId, userId);
+ if (!existingGoal) {
+ return res.status(404).json({ error: "Goal not found" });
+ }
+
+ //build updates object with only provided fields - for security
+ const updates: UpdateGoalInput = {};
+ if (req.body.name !== undefined) updates.name = req.body.name;
+ if (req.body.category !== undefined)
+ updates.category = req.body.category.toLowerCase();
+ if (req.body.target !== undefined) updates.target = req.body.target;
+ if (req.body.period !== undefined) {
+ updates.period = req.body.period;
+ } //period can be 'na' if goal is savings
+ if (req.body.type !== undefined) updates.type = req.body.type;
+
+ //check that period exists if the goal type is reduce_spending, it doesn't matter if type is 'save' because period is ignored
+ if (
+ updates.type &&
+ updates.type === ("reduce_spending" as GoalType) &&
+ !updates.period
+ ) {
+ return res
+ .status(400)
+ .json({ error: "Period is required for reduce_spending goals" });
+ }
+
+ //validate updates
+ if (updates.target !== undefined && updates.target <= 0) {
+ return res
+ .status(400)
+ .json({ error: "Target amount must be greater than 0" });
+ }
+
+ const updatedGoal = await updateUserGoal(pool, goalId, userId, updates);
+
+ if (!updatedGoal) {
+ return res.status(404).json({ error: "Goal not found" });
+ }
+
+ res.json(updatedGoal);
+ } catch (error) {
+ console.error("Error updating goal:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to update goal" });
+ }
+ }
+});
+
+// DELETE goal
+app.delete("/goals", async (req: express.Request, res: express.Response) => {
+ try {
+ const userId = authenticateJWT(req);
+ // const goalId = parseInt(req.query.gid);
+
+ let goalId = null;
+
+ if (req.query.gid) goalId = req.query.gid as unknown as number; //parseInt()
+
+ if (!goalId) {
+ return res.status(404).json({ error: "Missing goal ID" });
+ }
+
+ const deleted = await deleteUserGoal(pool, goalId, userId);
+ if (!deleted) {
+ return res.status(404).json({ error: "Goal not found" });
+ }
+
+ res.status(204).send();
+ } catch (error) {
+ console.error("Error deleting goal:", error);
+ if (error instanceof Error && error.message.includes("Authorization")) {
+ res.status(401).json({ error: error.message });
+ } else {
+ res.status(500).json({ error: "Failed to delete goal" });
+ }
+ }
+});
+
+app.get("/health", async (_, res) => {
+ const status = await getDatabaseStatus();
+ res.send(status);
+});
+
+export { generateDateRange }; //for testing purposes only
diff --git a/app/server/services/ts/user/src/logic/debt.ts b/app/server/services/ts/user/src/logic/debt.ts
new file mode 100644
index 0000000..a2ee725
--- /dev/null
+++ b/app/server/services/ts/user/src/logic/debt.ts
@@ -0,0 +1,18 @@
+import type { FinancialAccountRequest } from "../types/FinancialAccountRequest.ts";
+import { addDebt, findDebtsBy } from "../queries/debt.ts";
+import type { DebtInfoResponse } from "../types/DebtInfoResponse.ts";
+import { BadRequestError } from "../types/BadRequestError.ts";
+import { getConnectionPool } from "@/sqlUtil.ts";
+
+const db = getConnectionPool()
+export async function getDebts(userId: string): Promise {
+ return await findDebtsBy(db, userId)
+}
+export async function createNewDebt(debt: FinancialAccountRequest, userId: string): Promise {
+ try{
+ return await addDebt(db, debt,userId)
+ } catch (err) {
+ console.error("Error creating a new saving account: ", err);
+ throw err;
+ }
+}
\ No newline at end of file
diff --git a/app/server/services/ts/user/src/logic/expenses.ts b/app/server/services/ts/user/src/logic/expenses.ts
new file mode 100644
index 0000000..6b4fac6
--- /dev/null
+++ b/app/server/services/ts/user/src/logic/expenses.ts
@@ -0,0 +1,70 @@
+import type { Pool } from "mysql2/promise";
+import { getExpensesQuery } from "../queries/expenses.ts";
+import { generateDateRange } from "../utils/dates.ts";
+
+export async function getExpensesChartData(
+ pool: Pool,
+ userId: string,
+ period: string,
+) {
+ const endDate = new Date();
+ const startDate = new Date();
+ let dateFormat: string = "%Y-%m-%d";
+ let selectFormat: string;
+
+ switch (period) {
+ case "w":
+ startDate.setDate(endDate.getDate() - 7);
+ selectFormat = "DATE(date)";
+ break;
+ case "m":
+ startDate.setDate(endDate.getDate() - 30);
+ selectFormat = "DATE(date)";
+ break;
+ case "y":
+ startDate.setDate(endDate.getDate() - 365);
+ dateFormat = "%Y-%m";
+ selectFormat = 'DATE_FORMAT(date, "%Y-%m-01")';
+ break;
+ default:
+ throw new Error("Invalid period");
+ }
+
+ const startDateStr = startDate.toISOString().slice(0, 10);
+ const endDateStr = endDate.toISOString().slice(0, 10);
+
+ const expenses = await getExpensesQuery(
+ pool,
+ userId,
+ dateFormat,
+ startDateStr,
+ endDateStr,
+ selectFormat,
+ );
+
+ const allLabels = generateDateRange(startDate, endDate, period);
+
+ const dataMap = new Map();
+
+ expenses.forEach((row) => {
+ dataMap.set(row.label, Number(row.total_expenses));
+ });
+
+ const data = allLabels.map((label) => dataMap.get(label) || 0);
+
+ const periodLabels = {
+ w: "Weekly Expenses",
+ m: "Monthly Expenses",
+ y: "Yearly Expenses",
+ };
+
+ return {
+ labels: allLabels,
+ datasets: [
+ {
+ label: periodLabels[period as keyof typeof periodLabels],
+ data,
+ },
+ ],
+ };
+}
diff --git a/app/server/services/ts/user/src/logic/goals.ts b/app/server/services/ts/user/src/logic/goals.ts
new file mode 100644
index 0000000..90ce360
--- /dev/null
+++ b/app/server/services/ts/user/src/logic/goals.ts
@@ -0,0 +1,260 @@
+import { Pool } from "mysql2/promise";
+import * as goalsQueries from "../queries/goals.ts";
+import * as transactionsQueries from "../queries/transactions.ts";
+import type {
+ Goal,
+ CreateGoalInput,
+ UpdateGoalInput,
+ GoalWithProgress,
+} from "../types/Goals.ts";
+
+const MAX_GOALS_PER_PROFILE = 5;
+
+//get current spending/achievement for a goal based on transactions
+export async function calculateCurrentAmount(
+ pool: Pool,
+ goal: Goal,
+ profileId: number | null,
+): Promise {
+ if (!profileId) {
+ // console.log("No profileId provided");
+ // throw new Error("No profileId provided");
+ return 0;
+ }
+
+ if (goal.type === "reduce_spending") {
+ const period = goal.period;
+ const now = new Date();
+ let startDate: Date;
+
+ if (period === "w") {
+ // Start of week (Sunday)
+ startDate = new Date(now);
+ startDate.setDate(now.getDate() - now.getDay());
+ startDate.setHours(0, 0, 0, 0);
+ // console.log(`Weekly period: ${startDate.toISOString()} to ${now.toISOString()}`);
+ } else {
+ // Start of month
+ startDate = new Date(now.getFullYear(), now.getMonth(), 1);
+ startDate.setHours(0, 0, 0, 0);
+ // console.log(`Monthly period: ${startDate.toISOString()} to ${now.toISOString()}`);
+ }
+
+ //get expenses (negative amounts) for this category in the period
+ const expenses = await transactionsQueries.getDateCategoryTransactionsQuery(
+ pool,
+ profileId,
+ goal.category!,
+ startDate,
+ now,
+ );
+
+ //sum the amounts (they are negative, so sum will be negative)
+ const totalSpending = expenses.reduce(
+ (total, transaction) => total + transaction.amount,
+ 0,
+ );
+
+ //return absolute value for display
+ // console.log(`Total spending for ${goal.category}: ${totalSpending}`);
+ return Math.abs(totalSpending);
+ } else if (goal.type === "save") {
+ // For savings goals, get all transactions for this category
+ const transactions =
+ await transactionsQueries.getProfileCategoryTransactionsQuery(
+ pool,
+ profileId,
+ goal.category!,
+ );
+
+ //sum all amounts (positive = savings, negative = expenses)
+ const total = transactions.reduce(
+ (sum, transaction) => sum + transaction.amount,
+ 0,
+ );
+
+ return total > 0 ? total : 0;
+ }
+
+ return 0;
+}
+
+export async function getGoalsByProfileId(
+ pool: Pool,
+ profileId: number,
+): Promise {
+ const goals = await goalsQueries.getGoalsByProfileId(pool, profileId);
+ return goals;
+}
+
+//enrich a goal with calculated progress
+export async function enrichGoalWithProgress(
+ pool: Pool,
+ goal: Goal,
+ profileId: number,
+): Promise {
+ const current_amount = await calculateCurrentAmount(pool, goal, profileId);
+ const progress_percentage = Math.min(
+ (current_amount / goal.target) * 100,
+ 100,
+ );
+
+ // console.log(
+ // "enriching goal with progress, cur amount:",
+ // current_amount,
+ // "progress percentage:",
+ // progress_percentage,
+ // " because target is:",
+ // goal.target,
+ // );
+
+ return {
+ ...goal,
+ current_amount,
+ progress_percentage,
+ };
+}
+
+export async function getUserGoals(
+ pool: Pool,
+ userId: number,
+): Promise {
+ const profileId = await goalsQueries.getUserProfileId(pool, userId);
+ if (!profileId) return [];
+
+ const goals = await goalsQueries.getGoalsByProfileId(pool, profileId);
+
+ const goalsWithProgress = await Promise.all(
+ goals.map((goal) => enrichGoalWithProgress(pool, goal, profileId)),
+ );
+
+ return goalsWithProgress;
+}
+
+//gets a single goal by id - this is used for patching goals
+export async function getUserGoalById(
+ pool: Pool,
+ goalId: number,
+ userId: number,
+): Promise {
+ const profileId = await goalsQueries.getUserProfileId(pool, userId);
+ if (!profileId) return null;
+
+ const goal = await goalsQueries.getGoalById(pool, goalId, profileId);
+ if (!goal) return null;
+
+ return enrichGoalWithProgress(pool, goal, profileId);
+}
+
+export async function createUserGoal(
+ pool: Pool,
+ userId: number,
+ goalData: CreateGoalInput,
+): Promise {
+ const profileId = await goalsQueries.getUserProfileId(pool, userId);
+ if (!profileId) {
+ throw new Error("User profile not found");
+ }
+
+ //validate goal count
+ const goalCount = await goalsQueries.getGoalCountByProfileId(pool, profileId);
+ if (goalCount >= MAX_GOALS_PER_PROFILE) {
+ throw new Error(
+ `Maximum ${MAX_GOALS_PER_PROFILE} goals allowed per profile`,
+ );
+ }
+
+ //validate input based on goal type
+ validateGoalInput(goalData);
+
+ const newGoal = await goalsQueries.createGoal(pool, profileId, goalData);
+ if (!newGoal) {
+ throw new Error("Failed to create goal.");
+ }
+
+ const enrichedGoal = await enrichGoalWithProgress(pool, newGoal, profileId);
+ if (!enrichedGoal) {
+ throw new Error("Failed to enrich goal with progress");
+ }
+
+ return enrichedGoal;
+}
+
+export async function updateUserGoal(
+ pool: Pool,
+ goalId: number,
+ userId: number,
+ updates: UpdateGoalInput,
+): Promise {
+ const profileId = await goalsQueries.getUserProfileId(pool, userId);
+ if (!profileId) {
+ throw new Error("User profile not found");
+ }
+
+ const existingGoal = await goalsQueries.getGoalById(pool, goalId, profileId);
+ if (!existingGoal) {
+ throw new Error("Goal not found");
+ }
+
+ //validate updates - maybe expand on this if needed
+ validateGoalUpdates(updates);
+
+ const updated = await goalsQueries.updateGoal(
+ pool,
+ goalId,
+ profileId,
+ updates,
+ );
+ if (!updated) {
+ throw new Error("Failed to update goal");
+ }
+
+ const updatedWithProgress = await enrichGoalWithProgress(
+ pool,
+ updated,
+ profileId,
+ );
+ if (!updatedWithProgress) {
+ throw new Error("Failed to enrich goal with progress");
+ }
+
+ return updatedWithProgress;
+}
+
+export async function deleteUserGoal(
+ pool: Pool,
+ goalId: number,
+ userId: number,
+): Promise {
+ const profileId = await goalsQueries.getUserProfileId(pool, userId);
+ if (!profileId) {
+ throw new Error("User profile not found");
+ }
+
+ const deleted = await goalsQueries.deleteGoal(pool, goalId, profileId);
+ if (!deleted) {
+ throw new Error("Goal not found or already deleted");
+ }
+ return true;
+}
+
+//validation functions (same as before, but updated for new fields)
+function validateGoalInput(input: CreateGoalInput): void {
+ if (!input.name || input.name.trim().length === 0) {
+ throw new Error("Goal name is required");
+ }
+
+ if (input.target <= 0) {
+ throw new Error("Target amount must be greater than 0");
+ }
+
+ if (input.type === "reduce_spending" && !input.category) {
+ throw new Error("Spending limit goals require a category");
+ }
+}
+
+function validateGoalUpdates(updates: UpdateGoalInput): void {
+ if (updates.target !== undefined && updates.target <= 0) {
+ throw new Error("Target amount must be greater than 0");
+ }
+}
diff --git a/app/server/services/ts/user/src/logic/saving.ts b/app/server/services/ts/user/src/logic/saving.ts
new file mode 100644
index 0000000..4148f3f
--- /dev/null
+++ b/app/server/services/ts/user/src/logic/saving.ts
@@ -0,0 +1,41 @@
+import { addSavingAccount, findSavingsBy } from "../queries/saving.ts";
+import type { SavingInfoResponse } from "../types/SavingInfoResponse.ts";
+import type { FinancialAccountRequest } from "../types/FinancialAccountRequest.ts";
+import { findTransactionsBy } from "../queries/transactions.ts";
+import type { TransactionDto } from "../types/TransactionDto.ts";
+import { BadRequestError } from "../types/BadRequestError.ts";
+import { getConnectionPool } from "@/sqlUtil.ts";
+const db = getConnectionPool();
+export async function getSavingAccount(userId: string): Promise {
+ return await findSavingsBy(db, userId);
+}
+export async function getSavingAccountTransactionBy(financialAccountId: string | undefined): Promise {
+ if (!financialAccountId)
+ throw new BadRequestError("Missing financial account id")
+
+ const transactions = await findTransactionsBy(db, financialAccountId);
+ const transationDtos : TransactionDto[] = transactions.map((transation) => ({
+ id: transation.id,
+ amount: transation.amount,
+ category: transation.category,
+ description: transation.description,
+ sender: transation.sender,
+ recipient: transation.recipient,
+ date: transation.date ?
+ new Date(transation.date)
+ .toISOString()
+ .replace("T", " ")
+ .replace(/\.\d{3}Z$/, "")
+ :
+ "N/A"
+ }))
+ return transationDtos;
+}
+export async function createSavingAccount(savingInfo: FinancialAccountRequest, userId: string) : Promise{
+ try {
+ return await addSavingAccount(db, savingInfo, userId);
+ } catch (err) {
+ console.error("Error creating a new saving account: ", err);
+ throw err;
+ }
+}
\ No newline at end of file
diff --git a/app/server/services/ts/user/src/logic/snapshot.ts b/app/server/services/ts/user/src/logic/snapshot.ts
new file mode 100644
index 0000000..60c3689
--- /dev/null
+++ b/app/server/services/ts/user/src/logic/snapshot.ts
@@ -0,0 +1,44 @@
+import type { Pool } from "mysql2/promise";
+import { getSnapshotQuery } from "../queries/snapshot.ts";
+import type { SnapshotResponse } from "../types/SnapshotResponse.ts";
+
+export async function getSnapshotData(
+ pool: Pool,
+ userId: string,
+): Promise {
+ const today = new Date();
+ const ytdStart = new Date(today.getFullYear(), 0, 1);
+ const ytdStartStr = ytdStart.toISOString().slice(0, 10);
+ const todayStr = today.toISOString().slice(0, 10);
+ const monthsPassed = today.getMonth() + 1;
+
+ const results = await getSnapshotQuery(pool, userId, ytdStartStr, todayStr);
+
+ if (!results || results.length === 0) {
+ throw new Error("No snapshot data found");
+ }
+
+ const data = results[0];
+ const avgMonthlyExpenses =
+ monthsPassed > 0 ? data.total_expenses / monthsPassed : 0;
+
+ return {
+ totalBalance: data.total_balance || 0,
+ currentIncome: data.current_income || 0,
+ averageExpenses: Math.round(avgMonthlyExpenses * 100) / 100,
+ currentDebt: data.current_debt || 0,
+ totalSavings: data.total_savings || 0,
+ };
+}
+
+export async function getSnapshotDataSafe(
+ pool: Pool,
+ userId: string,
+): Promise {
+ try {
+ return await getSnapshotData(pool, userId);
+ } catch (error) {
+ console.error("Error in getSnapshotData:", error);
+ return null;
+ }
+}
diff --git a/app/server/services/ts/user/src/logic/transactions.ts b/app/server/services/ts/user/src/logic/transactions.ts
new file mode 100644
index 0000000..fe286ff
--- /dev/null
+++ b/app/server/services/ts/user/src/logic/transactions.ts
@@ -0,0 +1,70 @@
+import type { Pool, RowDataPacket } from "mysql2/promise";
+import {
+ getAllTransactionsQuery,
+ getTransactionAccountNames,
+} from "../queries/transactions.ts";
+import type { Transaction } from "../types/Transaction.ts";
+
+//returns a key value map of account id to account name
+export async function getAccountIDsForUser(
+ pool: Pool,
+ userId: string,
+): Promise