diff --git a/client/.coderrabbit.yaml b/client/.coderrabbit.yaml deleted file mode 100644 index fa3f25484..000000000 --- a/client/.coderrabbit.yaml +++ /dev/null @@ -1,14 +0,0 @@ -language: "en-CA" -tone_instructions: "His palms are sweaty, knees weak, arms are heavy There’s vomit on his sweater already, mom’s spaghetti" -early_access: false -reviews: - profile: "chill" - request_changes_workflow: false - high_level_summary: false - poem: false - review_status: false - auto_review: - enabled: true - drafts: false -chat: - auto_reply: false diff --git a/client/.dockerignore b/client/.dockerignore deleted file mode 100644 index fdfa00585..000000000 --- a/client/.dockerignore +++ /dev/null @@ -1,3 +0,0 @@ -Client/node_modules/ -Server/node_modules/ -Server/*.env \ No newline at end of file diff --git a/client/.env.production b/client/.env.production index 25849c7b9..60b35ca71 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,4 +1,2 @@ -VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL -VITE_APP_API_V2_BASE_URL=UPTIME_APP_API_V2_BASE_URL -VITE_APP_CLIENT_HOST=UPTIME_APP_CLIENT_HOST -VITE_APP_LOG_LEVEL=UPTIME_APP_LOG_LEVEL \ No newline at end of file +VITE_APP_HOST="http://147.182.153.60" +VITE_APP_API_BASE_URL="http://147.182.153.60:52345/api/v1" diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs deleted file mode 100644 index 86d523a6f..000000000 --- a/client/.eslintrc.cjs +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - ], - ignorePatterns: ["dist", ".eslintrc.cjs"], - parserOptions: { ecmaVersion: "latest", sourceType: "module" }, - settings: { react: { version: "18.2" } }, - plugins: ["react-refresh"], - rules: { - "react/jsx-no-target-blank": "off", - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], - "react/no-unescaped-entities": "off", - }, - globals: { - __APP_VERSION__: "readonly", - process: "readonly", - }, -}; diff --git a/client/.gitignore b/client/.gitignore index 83f6fe9ed..e8e4414a2 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -24,7 +24,7 @@ dist-ssr *.sw? .env - +.env.* # Scripts *.sh diff --git a/client/.prettierrc b/client/.prettierrc deleted file mode 100644 index 5c345e37a..000000000 --- a/client/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "printWidth": 90, - "useTabs": true, - "tabWidth": 2, - "singleQuote": false, - "bracketSpacing": true, - "proseWrap": "preserve", - "bracketSameLine": false, - "singleAttributePerLine": true, - "semi": true, - "jsxSingleQuote": false, - "quoteProps": "as-needed", - "arrowParens": "always", - "trailingComma": "es5", - "htmlWhitespaceSensitivity": "css" -} diff --git a/client/Dockerfile b/client/Dockerfile new file mode 100644 index 000000000..0f0312395 --- /dev/null +++ b/client/Dockerfile @@ -0,0 +1,16 @@ +# Build +FROM node:20-slim AS build +ENV NODE_OPTIONS="--max-old-space-size=4096" +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Serve +FROM nginx:1.27.1-alpine +COPY ./nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/client/README.md b/client/README.md index 55bc1efe2..d2e77611f 100644 --- a/client/README.md +++ b/client/README.md @@ -1 +1,73 @@ -This directory contains the client side (frontend) of Checkmate. +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/client/env.sh b/client/env.sh deleted file mode 100755 index d95f8ee8f..000000000 --- a/client/env.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -for i in $(env | grep UPTIME_APP_) -do - key=$(echo $i | cut -d '=' -f 1) - value=$(echo $i | cut -d '=' -f 2-) - echo $key=$value - find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' + -done \ No newline at end of file diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 000000000..018b3fd9d --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,28 @@ +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["recommended-latest"], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-function-type": "off", + "@typescript-eslint/no-empty-object-type": "off", + }, + }, +]); diff --git a/client/i18n-unused.config.cjs b/client/i18n-unused.config.cjs new file mode 100644 index 000000000..a2b0e9057 --- /dev/null +++ b/client/i18n-unused.config.cjs @@ -0,0 +1,6 @@ +/** @type {import('i18n-unused').RunOptions} */ +module.exports = { + localesPath: "src/locales", + srcPath: "src", + translationKeyMatcher: /\bt\s*\(\s*['"`](.*?)['"`]/g, +}; diff --git a/client/index.html b/client/index.html index 0b0f675b4..ab7c560cd 100644 --- a/client/index.html +++ b/client/index.html @@ -1,24 +1,17 @@ - - - - - Checkmate - + + + + - -
- - - + Checkmate + + + +
+ + + + \ No newline at end of file diff --git a/client/nginx/conf.d/default.conf b/client/nginx/conf.d/default.conf new file mode 100644 index 000000000..c3f993a08 --- /dev/null +++ b/client/nginx/conf.d/default.conf @@ -0,0 +1,34 @@ +server { + listen 80 default_server; + + server_name localhost; # Set server name to localhost + server_tokens off; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + # location /api/ { + # proxy_pass http://server:52345/api/; + # proxy_http_version 1.1; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_set_header Connection ''; + # chunked_transfer_encoding off; + # proxy_buffering off; + # proxy_cache off; + # } + + # location /api-docs/ { + # proxy_pass http://server:52345/api-docs/; + # proxy_http_version 1.1; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } +} \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 7cf0e8631..e4c5de5b6 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,6784 +1,5509 @@ { - "name": "client", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "client", - "version": "0.0.0", - "dependencies": { - "@emotion/react": "^11.13.3", - "@emotion/styled": "^11.13.0", - "@hello-pangea/dnd": "^18.0.0", - "@hookform/resolvers": "5.2.2", - "@mui/icons-material": "6.4.11", - "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", - "@mui/material": "6.4.11", - "@mui/x-charts": "^7.5.1", - "@mui/x-date-pickers": "7.29.0", - "@reduxjs/toolkit": "2.7.0", - "axios": "^1.7.4", - "dayjs": "1.11.13", - "flag-icons": "7.3.2", - "html2canvas": "^1.4.1", - "human-interval": "2.0.1", - "i18next": "25.4.2", - "joi": "17.13.3", - "mui-color-input": "^6.0.0", - "pretty-ms": "9.3.0", - "react": "18.3.1", - "react-dom": "^18.2.0", - "react-hook-form": "7.63.0", - "react-i18next": "^15.4.0", - "react-icons": "5.5.0", - "react-redux": "9.2.0", - "react-router": "^6.23.0", - "react-router-dom": "^6.23.1", - "react-toastify": "^10.0.5", - "recharts": "2.15.2", - "redux-persist": "6.0.0", - "swr": "2.3.6", - "vite-plugin-svgr": "^4.2.0", - "zod": "4.1.11" - }, - "devDependencies": { - "@types/node": "24.5.2", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.57.0", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.6", - "prettier": "^3.3.3", - "typescript": "5.9.3", - "vite": "6.3.6" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", - "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/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@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-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", - "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "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/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.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", - "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.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", - "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", - "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/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "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/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", - "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", - "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.0", - "@floating-ui/utils": "^0.2.9" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", - "license": "MIT" - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@hello-pangea/dnd": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", - "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.26.7", - "css-box-model": "^1.2.1", - "raf-schd": "^4.0.3", - "react-redux": "^9.2.0", - "redux": "^5.0.1" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@hookform/resolvers": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", - "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", - "license": "MIT", - "dependencies": { - "@standard-schema/utils": "^0.3.0" - }, - "peerDependencies": { - "react-hook-form": "^7.55.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mui/base": { - "version": "5.0.0-beta.42", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.42.tgz", - "integrity": "sha512-fWRiUJVCHCPF+mxd5drn08bY2qRw3jj5f1SSQdUXmaJ/yKpk23ys8MgLO2KGVTRtbks/+ctRfgffGPbXifj0Ug==", - "deprecated": "This package has been replaced by @base-ui-components/react", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.4", - "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.14", - "@mui/utils": "^6.0.0-alpha.1", - "@popperjs/core": "^2.11.8", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.11.tgz", - "integrity": "sha512-CzAQs9CTzlwbsF9ZYB4o4lLwBv1/qNE264NjuYao+ctAXsmlPtYa8RtER4UsUXSMxNN9Qi+aQdYcKl2sUpnmAw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.11.tgz", - "integrity": "sha512-+jjJGIrB1awNbMv4ZVPPdN/p7O1UKFZ+xqRvNIQ8B1KnlID5hPMPBLM6UUbRF4bu3UDCbu79rn9Nye5LGNzmeA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^6.4.11", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab": { - "version": "6.0.0-dev.240424162023-9968b4889d", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz", - "integrity": "sha512-iKFAz7/EeWI4PaFsP4jK2FcYJmUYDBkn3XZwpQSAl5806yYq5J2U2mPQLuZBdhrH50gT2O98p95i3vwL4YBwAg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.24.4", - "@mui/base": "5.0.0-beta.42", - "@mui/system": "^6.0.0-dev.240424162023-9968b4889d", - "@mui/types": "^7.2.14", - "@mui/utils": "^6.0.0-alpha.3", - "clsx": "^2.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material": "^6.0.0-dev.240424162023-9968b4889d", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/private-theming": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz", - "integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.9", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/styled-engine": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.11.tgz", - "integrity": "sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/system": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.11.tgz", - "integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.4.11", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/lab/node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz", - "integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.4.11", - "@mui/system": "^6.4.11", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.12", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^19.0.0", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.4.11", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@mui/material-pigment-css": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/private-theming": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.9.tgz", - "integrity": "sha512-LktcVmI5X17/Q5SkwjCcdOLBzt1hXuc14jYa7NPShog0GBDCDvKtcnP0V7a2s6EiVRlv7BzbWEJzH6+l/zaCxw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.9", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/styled-engine": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.11.tgz", - "integrity": "sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/system": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.11.tgz", - "integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.4.11", - "@mui/types": "~7.2.24", - "@mui/utils": "^6.4.9", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material/node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz", - "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/utils": "^7.1.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming/node_modules/@mui/utils": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/types": "^7.4.2", - "@types/prop-types": "^15.7.14", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz", - "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.27.1", - "@emotion/cache": "^11.13.5", - "@emotion/serialize": "^1.3.3", - "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz", - "integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/private-theming": "^7.1.0", - "@mui/styled-engine": "^7.1.0", - "@mui/types": "^7.4.2", - "@mui/utils": "^7.1.0", - "clsx": "^2.1.1", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/system/node_modules/@mui/utils": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/types": "^7.4.2", - "@types/prop-types": "^15.7.14", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz", - "integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.1" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "6.4.9", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.9.tgz", - "integrity": "sha512-Y12Q9hbK9g+ZY0T3Rxrx9m2m10gaphDuUMgWxyV5kNJevVxXYCLclYUCC9vXaIk1/NdNDTcW2Yfr2OGvNFNmHg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.26.0", - "@mui/types": "~7.2.24", - "@types/prop-types": "^15.7.14", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^19.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils/node_modules/@mui/types": { - "version": "7.2.24", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.24.tgz", - "integrity": "sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/x-charts": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.29.1.tgz", - "integrity": "sha512-5s9PX51HWhpMa+DCDa4RgjtODSaMe+PlTZUqoGIil2vaW/+4ouDLREXvyuVvIF93KfZwrPKAL2SJKSQS4YYB2w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0", - "@mui/x-charts-vendor": "7.20.0", - "@mui/x-internals": "7.29.0", - "@react-spring/rafz": "^9.7.5", - "@react-spring/web": "^9.7.5", - "clsx": "^2.1.1", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", - "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/x-charts-vendor": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz", - "integrity": "sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg==", - "license": "MIT AND ISC", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@types/d3-color": "^3.1.3", - "@types/d3-delaunay": "^6.0.4", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-scale": "^4.0.8", - "@types/d3-shape": "^3.1.6", - "@types/d3-time": "^3.0.3", - "d3-color": "^3.1.0", - "d3-delaunay": "^6.0.4", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.2.0", - "d3-time": "^3.1.0", - "delaunator": "^5.0.1", - "robust-predicates": "^3.0.2" - } - }, - "node_modules/@mui/x-date-pickers": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.29.0.tgz", - "integrity": "sha512-6jpkbXB1/9/VLvlJ5wLs4M54fi9WeCpJSJ3I76MbZBKtZUDMFsuc3V5Mm4bsTd08xF6py1o+Qx2HapsE2gFhrg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0", - "@mui/x-internals": "7.29.0", - "@types/react-transition-group": "^4.4.11", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", - "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", - "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", - "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", - "dayjs": "^1.10.7", - "luxon": "^3.0.2", - "moment": "^2.29.4", - "moment-hijri": "^2.1.2 || ^3.0.0", - "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "date-fns": { - "optional": true - }, - "date-fns-jalali": { - "optional": true - }, - "dayjs": { - "optional": true - }, - "luxon": { - "optional": true - }, - "moment": { - "optional": true - }, - "moment-hijri": { - "optional": true - }, - "moment-jalaali": { - "optional": true - } - } - }, - "node_modules/@mui/x-internals": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.29.0.tgz", - "integrity": "sha512-+Gk6VTZIFD70XreWvdXBwKd8GZ2FlSCuecQFzm6znwqXg1ZsndavrhG9tkxpxo2fM1Zf7Tk8+HcOO0hCbhTQFA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "react": "^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "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/@react-spring/animated": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", - "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", - "license": "MIT", - "dependencies": { - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/core": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", - "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/rafz": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", - "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", - "license": "MIT" - }, - "node_modules/@react-spring/shared": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", - "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", - "license": "MIT", - "dependencies": { - "@react-spring/rafz": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", - "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", - "license": "MIT" - }, - "node_modules/@react-spring/web": { - "version": "9.7.5", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", - "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", - "license": "MIT", - "dependencies": { - "@react-spring/animated": "~9.7.5", - "@react-spring/core": "~9.7.5", - "@react-spring/shared": "~9.7.5", - "@react-spring/types": "~9.7.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.7.0.tgz", - "integrity": "sha512-XVwolG6eTqwV0N8z/oDlN93ITCIGIop6leXlGJI/4EKy+0POYkR+ABHRSdGXY+0MQvJBP8yAzh+EYFxTuvmBiQ==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^10.0.3", - "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/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", - "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", - "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", - "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", - "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/@svgr/babel-plugin-add-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", - "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", - "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", - "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", - "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", - "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", - "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", - "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", - "license": "MIT", - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", - "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", - "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", - "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", - "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", - "@svgr/babel-plugin-transform-svg-component": "8.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/core": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", - "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^8.1.3", - "snake-case": "^3.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/core/node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "license": "MIT", - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", - "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.3", - "entities": "^4.4.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", - "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "@svgr/hast-util-to-babel-ast": "8.0.0", - "svg-parser": "^2.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", - "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-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "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.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "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.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.12.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/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.22", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", - "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.12", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", - "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*" - } - }, - "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/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "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-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "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", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "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/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "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", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", - "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": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", - "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/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "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", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "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/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", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-box-model": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", - "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "license": "MIT", - "dependencies": { - "tiny-invariant": "^1.0.6" - } - }, - "node_modules/css-line-break": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", - "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "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-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "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.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "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/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "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", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "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/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "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.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", - "license": "ISC" - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.23.10", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", - "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", - "dev": true, - "license": "MIT", - "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-regex": "^1.2.1", - "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", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "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-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "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.4", - "safe-array-concat": "^1.1.3" - }, - "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-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "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.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": ">=8.40" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "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/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "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/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.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", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "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/flag-icons": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.3.2.tgz", - "integrity": "sha512-QkaZ6Zvai8LIjx+UNAHUJ5Dhz9OLZpBDwCRWxF6YErxIcR16jTkIFm3bFu54EkvKQy4+wicW+Gm7/0631wVQyQ==", - "license": "MIT" - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "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/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "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/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "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-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/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/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/html-parse-stringify": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", - "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", - "license": "MIT", - "dependencies": { - "void-elements": "3.1.0" - } - }, - "node_modules/html2canvas": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", - "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", - "license": "MIT", - "dependencies": { - "css-line-break": "^2.1.0", - "text-segmentation": "^1.0.3" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/human-interval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz", - "integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==", - "license": "MIT", - "dependencies": { - "numbered": "^1.1.0" - } - }, - "node_modules/i18next": { - "version": "25.4.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz", - "integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==", - "funding": [ - { - "type": "individual", - "url": "https://locize.com" - }, - { - "type": "individual", - "url": "https://locize.com/i18next.html" - }, - { - "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" - } - ], - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.27.6" - }, - "peerDependencies": { - "typescript": "^5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "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", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "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-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "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", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "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", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mui-color-input": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/mui-color-input/-/mui-color-input-6.0.0.tgz", - "integrity": "sha512-F9n/RqPLq7fnPWeF53/qssbIHvart7clpOfE+f+dsM+3ddfawwg98+PEhzv3cdAYlJLE9zfVViVTsp/ckUUd3g==", - "license": "MIT", - "dependencies": { - "@ctrl/tinycolor": "^4.1.0" - }, - "peerDependencies": { - "@emotion/react": "^11.13.0", - "@emotion/styled": "^11.13.0", - "@mui/material": "^6.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "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", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT" - }, - "node_modules/numbered": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz", - "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==", - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "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/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "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", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "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", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "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/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "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", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "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.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/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/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", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/raf-schd": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", - "license": "MIT" - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-hook-form": { - "version": "7.63.0", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", - "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, - "node_modules/react-i18next": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", - "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.0", - "html-parse-stringify": "^3.0.1" - }, - "peerDependencies": { - "i18next": ">= 23.2.3", - "react": ">= 16.8.0", - "typescript": "^5" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "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.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", - "license": "MIT" - }, - "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-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-toastify": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz", - "integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/recharts": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz", - "integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==", - "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, - "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-persist": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", - "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", - "license": "MIT", - "peerDependencies": { - "redux": ">4.0.0" - } - }, - "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/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "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", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, - "node_modules/rollup": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", - "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.0", - "@rollup/rollup-android-arm64": "4.41.0", - "@rollup/rollup-darwin-arm64": "4.41.0", - "@rollup/rollup-darwin-x64": "4.41.0", - "@rollup/rollup-freebsd-arm64": "4.41.0", - "@rollup/rollup-freebsd-x64": "4.41.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", - "@rollup/rollup-linux-arm-musleabihf": "4.41.0", - "@rollup/rollup-linux-arm64-gnu": "4.41.0", - "@rollup/rollup-linux-arm64-musl": "4.41.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-musl": "4.41.0", - "@rollup/rollup-linux-s390x-gnu": "4.41.0", - "@rollup/rollup-linux-x64-gnu": "4.41.0", - "@rollup/rollup-linux-x64-musl": "4.41.0", - "@rollup/rollup-win32-arm64-msvc": "4.41.0", - "@rollup/rollup-win32-ia32-msvc": "4.41.0", - "@rollup/rollup-win32-x64-msvc": "4.41.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "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", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "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", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "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/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "license": "MIT" - }, - "node_modules/swr": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", - "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/text-segmentation": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", - "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", - "license": "MIT", - "dependencies": { - "utrie": "^1.0.2" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, - "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.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "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", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/utrie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", - "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", - "license": "MIT", - "dependencies": { - "base64-arraybuffer": "^1.0.2" - } - }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "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": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "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/vite-plugin-svgr": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", - "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.3", - "@svgr/core": "^8.1.0", - "@svgr/plugin-jsx": "^8.1.0" - }, - "peerDependencies": { - "vite": ">=2.6.0" - } - }, - "node_modules/void-elements": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", - "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", - "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - } - } + "name": "checkmate", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "checkmate", + "version": "0.0.0", + "dependencies": { + "@emotion/react": "11.14.0", + "@emotion/styled": "11.14.1", + "@hookform/resolvers": "5.2.2", + "@mui/material": "7.3.4", + "@mui/x-date-pickers": "8.16.0", + "@reduxjs/toolkit": "2.9.0", + "axios": "1.12.2", + "dayjs": "1.11.18", + "flag-icons": "7.5.0", + "human-interval": "2.0.1", + "lucide-react": "0.553.0", + "ms": "2.1.3", + "pretty-ms": "9.3.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-i18next": "16.0.1", + "react-redux": "9.2.0", + "react-router": "7.9.4", + "react-router-dom": "7.9.4", + "react-toastify": "11.0.5", + "recharts": "2.15.2", + "redux-persist": "6.0.0", + "swr": "2.3.6", + "vite-plugin-svgr": "4.5.0", + "zod": "4.1.12" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/ms": "2.1.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "i18n-unused": "0.19.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@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/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@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.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@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", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.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/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", + "peer": true, + "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", + "peer": true, + "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/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "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", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "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", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "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", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "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.0", + "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", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "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", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "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", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "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/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "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", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "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", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "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", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.5.tgz", + "integrity": "sha512-kOLwlcDPnVz2QMhiBv0OQ8le8hTCqKM9cRXlfVPL91l3RGeOsxrIhNRsUt3Xb8wb+pTVUolW+JXKym93vRKxCw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz", + "integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/core-downloads-tracker": "^7.3.4", + "@mui/system": "^7.3.3", + "@mui/types": "^7.4.7", + "@mui/utils": "^7.3.3", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.1.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^7.3.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.5.tgz", + "integrity": "sha512-cTx584W2qrLonwhZLbEN7P5pAUu0nZblg8cLBlTrZQ4sIiw8Fbvg7GvuphQaSHxPxrCpa7FDwJKtXdbl2TSmrA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.5", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.5.tgz", + "integrity": "sha512-zbsZ0uYYPndFCCPp2+V3RLcAN6+fv4C8pdwRx6OS3BwDkRCN8WBehqks7hWyF3vj1kdQLIWrpdv/5Y0jHRxYXQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.5.tgz", + "integrity": "sha512-yPaf5+gY3v80HNkJcPi6WT+r9ebeM4eJzrREXPxMt7pNTV/1eahyODO4fbH3Qvd8irNxDFYn5RQ3idHW55rA6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.5", + "@mui/styled-engine": "^7.3.5", + "@mui/types": "^7.4.8", + "@mui/utils": "^7.3.5", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.4.8", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.8.tgz", + "integrity": "sha512-ZNXLBjkPV6ftLCmmRCafak3XmSn8YV0tKE/ZOhzKys7TZXUiE0mZxlH8zKDo6j6TTUaDnuij68gIG+0Ucm7Xhw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.5.tgz", + "integrity": "sha512-jisvFsEC3sgjUjcPnR4mYfhzjCDIudttSGSbe1o/IXFNu0kZuR+7vqQI0jg8qtcVZBHWrwTfvAZj9MNMumcq1g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.8", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-8.16.0.tgz", + "integrity": "sha512-zvUoO9ImWiKRaOWvQVbB1vCa6aUQIX5GM0tJ+nAyNNIVV0VqpXz3CvkRR6ovBBFzIcChc7FXlqrMKcJ//EhePQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "@mui/x-internals": "8.16.0", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", + "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", + "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0", + "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2 || ^3.0.0", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-8.16.0.tgz", + "integrity": "sha512-JR53WOFqmQYQzurOpB0H91K7/9uMcte1ooxHxTLGB+97PgB+rKY6siRWvUALGS56XyPV+1a2ALI33hd2E7+Rgg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.3", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "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/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "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/@rolldown/pluginutils": { + "version": "1.0.0-beta.43", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", + "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "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/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "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", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "devOptional": true, + "license": "MIT", + "peer": true, + "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/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", + "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.2", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", + "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, + "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.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.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.46.3", + "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", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4" + }, + "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.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", + "debug": "^4.3.4" + }, + "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.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" + }, + "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.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "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.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.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.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "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.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.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/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "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/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.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/visitor-keys": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "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": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", + "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.43", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "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", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "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.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "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", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", + "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "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", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "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", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "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", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "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/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "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.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "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.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "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/dayjs": { + "version": "1.11.18", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", + "integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==", + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "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", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "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/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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.247", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.247.tgz", + "integrity": "sha512-bCkfEJNE5EOhEsGpgbvgUAXYP/uAZeN9GCd9bPGjm2/uiLdpoiZnxrBTbWT1nbZs1y6UqN7hatM3hOB4X1kXUQ==", + "license": "ISC" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "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.1", + "@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": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "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.24", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "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", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "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.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", + "integrity": "sha512-6rxyATwPCkaFIL3JLqw8qXqMpIZ942pTX/tbQFkRsDGblS8tNGtlUauA/+mt6RUfqn/4MoEr+WDkYoIQbibWuQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "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/flag-icons": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/flag-icons/-/flag-icons-7.5.0.tgz", + "integrity": "sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==", + "license": "MIT" + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "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", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "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", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "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-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", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "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/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/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/human-interval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz", + "integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==", + "license": "MIT", + "dependencies": { + "numbered": "^1.1.0" + } + }, + "node_modules/i18n-unused": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/i18n-unused/-/i18n-unused-0.19.0.tgz", + "integrity": "sha512-g/zPenRzkE0NV0LWSCKfQ6h08lCSloh5MPHKPAw19oscPKMV5zEcyYF2QrwUvZF5CV96aeR8XQenJcsEWcLlIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^10.0.1", + "esm": "^3.2.25", + "ts-import": "^2.0.40" + }, + "bin": { + "i18n-unused": "bin/i18n-unused.cjs" + } + }, + "node_modules/i18next": { + "version": "25.6.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.1.tgz", + "integrity": "sha512-yUWvdXtalZztmKrKw3yz/AvSP3yKyqIkVPx/wyvoYy9lkLmwzItLxp0iHZLG5hfVQ539Jor4XLO+U+NHIXg7pw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "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", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "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", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "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", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "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", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.553.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.553.0.tgz", + "integrity": "sha512-BRgX5zrWmNy/lkVAe0dXBgd7XQdZ3HTf+Hwe3c9WK6dqgnj9h+hxV+MDncM88xDWlCq27+TKvHGE70ViODNILw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/numbered": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz", + "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "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/options-defaults": { + "version": "2.0.40", + "resolved": "https://registry.npmjs.org/options-defaults/-/options-defaults-2.0.40.tgz", + "integrity": "sha512-a0oW0AMaP/Uqk1gU7s3unE83wzs/MACy3wsCnNREn4wqp4KCcxRdulRjf0d2FeIxENbGJ4EBGtHTQ6J30XB6Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "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", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "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", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "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/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "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", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/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/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", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.66.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", + "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-i18next": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.0.1.tgz", + "integrity": "sha512-0S//bpYEkCPjzuVmxDf9Z6+Y+ArNvpAUk7eDL4qNCZXjDh6Z9j6MZ+NThU7kMCOsmYmDCun3GYEwkiOjjZo9Ug==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 25.5.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz", + "integrity": "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==", + "license": "MIT" + }, + "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", + "peer": true, + "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-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz", + "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==", + "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.9.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz", + "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-toastify": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz", + "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz", + "integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "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", + "peer": true + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "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", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "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", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "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", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "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/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "license": "MIT" + }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "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/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-import": { + "version": "2.0.40", + "resolved": "https://registry.npmjs.org/ts-import/-/ts-import-2.0.40.tgz", + "integrity": "sha512-yTP9fSDaBL1LvcN7/taRHbcQoazk7cLrDOEPyIDUVKLNYTMhaDkvZhfUwp41iXe+GlAFL6l87ZoGABWZRdIfjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "options-defaults": "^2.0.39" + }, + "engines": { + "node": ">=14.14.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.46.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", + "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.46.3", + "@typescript-eslint/parser": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.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/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "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", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "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": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.1.tgz", + "integrity": "sha512-qTl3VF7BvOupTR85Zc561sPEgxyUSNSvTQ9fit7DEMP7yPgvvIGm5Zfa1dOM+kOwWGNviK9uFM9ra77+OjK7lQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "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", + "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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/vite-plugin-svgr": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.5.0.tgz", + "integrity": "sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.2.0", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": ">=2.6.0" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "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", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } } diff --git a/client/package.json b/client/package.json index 376671942..8c4b428a5 100644 --- a/client/package.json +++ b/client/package.json @@ -1,63 +1,56 @@ { - "name": "client", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "build-dev": "tsc -b && vite build --mode development", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview", - "format": "prettier --write .", - "format-check": "prettier --check ." - }, - "dependencies": { - "@emotion/react": "^11.13.3", - "@emotion/styled": "^11.13.0", - "@hello-pangea/dnd": "^18.0.0", - "@hookform/resolvers": "5.2.2", - "@mui/icons-material": "6.4.11", - "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", - "@mui/material": "6.4.11", - "@mui/x-charts": "^7.5.1", - "@mui/x-date-pickers": "7.29.0", - "@reduxjs/toolkit": "2.7.0", - "axios": "^1.7.4", - "dayjs": "1.11.13", - "flag-icons": "7.3.2", - "html2canvas": "^1.4.1", - "human-interval": "2.0.1", - "i18next": "25.4.2", - "joi": "17.13.3", - "mui-color-input": "^6.0.0", - "pretty-ms": "9.3.0", - "react": "18.3.1", - "react-dom": "^18.2.0", - "react-hook-form": "7.63.0", - "react-i18next": "^15.4.0", - "react-icons": "5.5.0", - "react-redux": "9.2.0", - "react-router": "^6.23.0", - "react-router-dom": "^6.23.1", - "react-toastify": "^10.0.5", - "recharts": "2.15.2", - "redux-persist": "6.0.0", - "swr": "2.3.6", - "vite-plugin-svgr": "^4.2.0", - "zod": "4.1.11" - }, - "devDependencies": { - "@types/node": "24.5.2", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", - "@vitejs/plugin-react": "^4.2.1", - "eslint": "^8.57.0", - "eslint-plugin-react": "^7.34.1", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.6", - "prettier": "^3.3.3", - "typescript": "5.9.3", - "vite": "6.3.6" - } + "name": "checkmate", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "i18n:display": "i18n-unused display-unused > test.log", + "dev": "vite --mode development", + "build": "tsc -b && vite build --mode production", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@emotion/react": "11.14.0", + "@emotion/styled": "11.14.1", + "@hookform/resolvers": "5.2.2", + "@mui/material": "7.3.4", + "@mui/x-date-pickers": "8.16.0", + "@reduxjs/toolkit": "2.9.0", + "axios": "1.12.2", + "dayjs": "1.11.18", + "flag-icons": "7.5.0", + "human-interval": "2.0.1", + "lucide-react": "0.553.0", + "ms": "2.1.3", + "pretty-ms": "9.3.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-i18next": "16.0.1", + "react-redux": "9.2.0", + "react-router": "7.9.4", + "react-router-dom": "7.9.4", + "react-toastify": "11.0.5", + "recharts": "2.15.2", + "redux-persist": "6.0.0", + "swr": "2.3.6", + "vite-plugin-svgr": "4.5.0", + "zod": "4.1.12" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "@types/ms": "2.1.0", + "@types/node": "^24.6.0", + "@types/react": "^19.1.16", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react": "^5.0.4", + "eslint": "^9.36.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.22", + "globals": "^16.4.0", + "i18n-unused": "0.19.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.45.0", + "vite": "^7.1.7" + } } diff --git a/client/public/bulk_import_monitors_sample.csv b/client/public/bulk_import_monitors_sample.csv deleted file mode 100644 index 2a01ea483..000000000 --- a/client/public/bulk_import_monitors_sample.csv +++ /dev/null @@ -1,6 +0,0 @@ -type,name,url,interval,port,expectedValue,jsonPath,matchMethod -http,HTTP Monitor,https://www.google.com,60000,,,, -http,API Monitor,http://reqres.in/api/users/2,120000,,Janet,data.first_name,equal -ping,Ping monitor,127.0.0.1,60000,,,, -docker,Docker monitor,bbdd20b4e3757d63e070bc16e04d3f770c4a8f6e62e813de380f23b141e2ca01,60000,,,, -port,Port monitor,127.0.0.1,60000,5000,,, \ No newline at end of file diff --git a/client/public/bulk_import_monitors_template.csv b/client/public/bulk_import_monitors_template.csv deleted file mode 100644 index 2902010fc..000000000 --- a/client/public/bulk_import_monitors_template.csv +++ /dev/null @@ -1 +0,0 @@ -type,name,url,interval,port,expectedValue,jsonPath,matchMethod \ No newline at end of file diff --git a/client/public/checkmate_favicon.svg b/client/public/checkmate_favicon.svg deleted file mode 100644 index e0e6bf6f4..000000000 --- a/client/public/checkmate_favicon.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/renovate.json b/client/renovate.json deleted file mode 100644 index 67bfd7514..000000000 --- a/client/renovate.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended"], - "labels": ["dependencies"] -} diff --git a/client/src/App.jsx b/client/src/App.jsx deleted file mode 100644 index d21850ebb..000000000 --- a/client/src/App.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect } from "react"; -import { useSelector } from "react-redux"; -import "react-toastify/dist/ReactToastify.css"; -import { ToastContainer } from "react-toastify"; -import { ThemeProvider } from "@emotion/react"; -import lightTheme from "./Utils/Theme/lightTheme"; -import darkTheme from "./Utils/Theme/darkTheme"; -import { CssBaseline, GlobalStyles } from "@mui/material"; -import { logger } from "./Utils/Logger"; // Import the logger -import { networkService } from "./main"; -import { Routes } from "./Routes"; -import AppLayout from "@/Components/v1/Layouts/AppLayout"; - -function App() { - const mode = useSelector((state) => state.ui.mode); - - // Cleanup - useEffect(() => { - return () => { - logger.cleanup(); - networkService.cleanup(); - }; - }, []); - - return ( - - - - - - - - ); -} - -export default App; diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 000000000..c86e2b973 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,38 @@ +import CssBaseline from "@mui/material/CssBaseline"; +import AppRoutes from "@/routes/Router"; +import { ThemeProvider } from "@mui/material/styles"; +import { lightTheme, darkTheme } from "@/theme/theme"; +import { I18nLoader } from "@/components/i18nLoader"; +import { useAppSelector } from "./hooks/AppHooks"; +import { BrowserRouter } from "react-router-dom"; +import { AuthVerifier } from "@/components/auth-verifier"; +import { ToastContainer } from "react-toastify"; +import { colors } from "@/theme/palette"; + +function App() { + const mode = useAppSelector((state) => state.ui.mode); + + return ( + + + + + + + + + + + ); +} + +export default App; diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx deleted file mode 100644 index 9b46204e3..000000000 --- a/client/src/Components/LanguageSelector.jsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useTranslation } from "react-i18next"; -import { Box, MenuItem, Select, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import "flag-icons/css/flag-icons.min.css"; -import { useSelector } from "react-redux"; -import { useDispatch } from "react-redux"; -import { setLanguage } from "../Features/UI/uiSlice"; - -const langMap = { - cs: "cz", - ja: "jp", - uk: "ua", - vi: "vn", -}; - -const LanguageSelector = () => { - const { i18n } = useTranslation(); - const theme = useTheme(); - const { language } = useSelector((state) => state.ui); - const dispatch = useDispatch(); - const handleChange = (event) => { - const newLang = event.target.value; - dispatch(setLanguage(newLang)); - }; - - const languages = Object.keys(i18n.options.resources || {}); - - return ( - - ); -}; - -export default LanguageSelector; diff --git a/client/src/Components/WalletProvider/index.css b/client/src/Components/WalletProvider/index.css deleted file mode 100644 index f3da31efc..000000000 --- a/client/src/Components/WalletProvider/index.css +++ /dev/null @@ -1,53 +0,0 @@ -.wallet-adapter-dropdown { - width: 100%; - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: var(--env-var-spacing-1); -} - -.wallet-adapter-button { - display: flex; - align-items: center; - justify-content: center; - width: auto; - height: var(--env-var-height-2) !important; - font-size: var(--env-var-font-size-medium-plus) !important; - font-weight: 500 !important; - margin: 0; - padding: calc( - (var(--env-var-height-2) - var(--env-var-font-size-medium-plus) * 1.2) / 2 - ) - var(--env-var-spacing-1); - white-space: nowrap; -} - -.wallet-adapter-modal-title { - font-size: var(--font-size-h5) !important; -} - -/* Separator styling */ -.wallet-adapter-modal-divider { - background-color: var(--border-color) !important; - margin: var(--spacing-md) 0 !important; -} - -/* Responsive fixes */ -@media (max-width: 1200px) { - .wallet-adapter-button { - font-size: var(--env-var-font-size-medium) !important; - padding: calc((var(--env-var-height-2) - var(--env-var-font-size-medium) * 1.2) / 2) - var(--env-var-spacing-1-minus) !important; - } -} - -@media (max-width: 900px) { - .wallet-adapter-modal-wrapper { - flex-direction: column !important; - } - - .wallet-adapter-modal-divider { - margin: var(--spacing-sm) 0 !important; - } -} diff --git a/client/src/Components/WalletProvider/index.jsx b/client/src/Components/WalletProvider/index.jsx deleted file mode 100644 index 13cc83b07..000000000 --- a/client/src/Components/WalletProvider/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -// import { useMemo } from "react"; -// import { ConnectionProvider, WalletProvider } from "@solana/wallet-adapter-react"; -// import { WalletAdapterNetwork } from "@solana/wallet-adapter-base"; -// import { -// UnsafeBurnerWalletAdapter, -// PhantomWalletAdapter, -// } from "@solana/wallet-adapter-wallets"; - -// import { WalletModalProvider } from "@solana/wallet-adapter-react-ui"; -// import { clusterApiUrl } from "@solana/web3.js"; -// import PropTypes from "prop-types"; -// import "./index.css"; - -// // Default styles that can be overridden by your app -// import "@solana/wallet-adapter-react-ui/styles.css"; - -// export const Wallet = ({ children }) => { -// // The network can be set to 'devnet', 'testnet', or 'mainnet-beta'. -// const network = WalletAdapterNetwork.Mainnet; - -// // You can also provide a custom RPC endpoint. -// const endpoint = useMemo(() => clusterApiUrl(network), [network]); - -// const wallets = useMemo( -// () => [new PhantomWalletAdapter()], -// // eslint-disable-next-line react-hooks/exhaustive-deps -// [network] -// ); - -// return ( -// -// -// {children} -// -// -// ); -// }; - -// Wallet.propTypes = { -// children: PropTypes.node, -// }; - -const Wallet = ({ children }) => { - return children; -}; - -export default Wallet; diff --git a/client/src/Components/v1/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx deleted file mode 100644 index 229c7c875..000000000 --- a/client/src/Components/v1/ActionsMenu/index.jsx +++ /dev/null @@ -1,224 +0,0 @@ -// Components -import IconButton from "@mui/material/IconButton"; -import Menu from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import Settings from "../../../assets/icons/settings-bold.svg?react"; -import Dialog from "../Dialog/index.jsx"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils.jsx"; - -import PropTypes from "prop-types"; -import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/v1/monitorHooks.js"; - -const ActionsMenu = ({ - monitor, - isAdmin, - updateRowCallback, - pauseCallback, - setIsLoading = () => {}, -}) => { - const [anchorEl, setAnchorEl] = useState(null); - const [actions, setActions] = useState({}); - const [isOpen, setIsOpen] = useState(false); - const theme = useTheme(); - const [pauseMonitor, isPausing, error] = usePauseMonitor(); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - - const handleRemove = async (event) => { - event.preventDefault(); - event.stopPropagation(); - let monitor = { _id: actions.id }; - await deleteMonitor({ monitor }); - updateRowCallback(); - }; - - const handlePause = async () => { - try { - setIsLoading(true); - await pauseMonitor({ monitorId: monitor._id }); - pauseCallback(); - } catch (error) { - createToast({ body: "Failed to pause monitor." }); - } finally { - setIsLoading(false); - } - }; - - const openMenu = (event, id, url) => { - event.preventDefault(); - event.stopPropagation(); - setAnchorEl(event.currentTarget); - setActions({ id: id, url: url }); - }; - - const openRemove = (e) => { - closeMenu(e); - setIsOpen(true); - }; - - const closeMenu = (e) => { - e.stopPropagation(); - setAnchorEl(null); - }; - - const navigate = useNavigate(); - return ( - <> - { - event.stopPropagation(); - openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url); - }} - sx={{ - "&:focus": { - outline: "none", - }, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }} - > - - - - closeMenu(e)} - disableScrollLock - slotProps={{ - paper: { - sx: { - "& ul": { - p: theme.spacing(2.5), - backgroundColor: theme.palette.primary.main, - }, - "& li": { m: 0, color: theme.palette.primary.contrastTextSecondary }, - /* - This should not be set automatically on the last of type - "& li:last-of-type": { - color: theme.palette.error.main, - }, */ - }, - }, - }} - > - {actions.url !== null ? ( - { - closeMenu(e); - e.stopPropagation(); - window.open(actions.url, "_blank", "noreferrer"); - }} - > - Open site - - ) : ( - "" - )} - { - e.stopPropagation(); - navigate(`/uptime/${actions.id}`); - }} - > - Details - - {/* TODO - pass monitor id to Incidents page */} - { - e.stopPropagation(); - navigate(`/incidents/${actions.id}`); - }} - > - Incidents - - {isAdmin && ( - { - e.stopPropagation(); - - navigate(`/uptime/configure/${actions.id}`); - }} - > - Configure - - )} - {isAdmin && ( - { - e.stopPropagation(); - navigate(`/uptime/create/${actions.id}`); - }} - > - Clone - - )} - {isAdmin && ( - { - closeMenu(e); - - e.stopPropagation(); - handlePause(e); - }} - > - {monitor?.isActive === true ? "Pause" : "Resume"} - - )} - {isAdmin && ( - { - e.stopPropagation(); - openRemove(e); - }} - sx={{ "&.MuiButtonBase-root": { color: theme.palette.error.main } }} - > - Remove - - )} - - { - e.stopPropagation(); - setIsOpen(false); - }} - confirmationButtonLabel="Delete" - /* Do we need stop propagation? */ - onConfirm={(e) => { - e.stopPropagation(); - handleRemove(e); - }} - isLoading={isDeleting} - modelTitle="modal-delete-monitor" - modelDescription="delete-monitor-confirmation" - /> - - ); -}; - -ActionsMenu.propTypes = { - monitor: PropTypes.shape({ - _id: PropTypes.string, - url: PropTypes.string, - type: PropTypes.string, - isActive: PropTypes.bool, - }).isRequired, - isAdmin: PropTypes.bool, - updateRowCallback: PropTypes.func, - pauseCallback: PropTypes.func, - setIsLoading: PropTypes.func, -}; - -export default ActionsMenu; diff --git a/client/src/Components/v1/Alert/index.css b/client/src/Components/v1/Alert/index.css deleted file mode 100644 index 233b5c466..000000000 --- a/client/src/Components/v1/Alert/index.css +++ /dev/null @@ -1,9 +0,0 @@ -.alert { - margin: 0; - width: fit-content; -} -.alert, -.alert button, -.alert .MuiTypography-root { - font-size: var(--env-var-font-size-medium); -} diff --git a/client/src/Components/v1/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx deleted file mode 100644 index 921d4fd6e..000000000 --- a/client/src/Components/v1/Alert/index.jsx +++ /dev/null @@ -1,127 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Button, IconButton, Stack, Typography } from "@mui/material"; -import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; -import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; -import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; -import CloseIcon from "@mui/icons-material/Close"; -import "./index.css"; - -/** - * Icons mapping for different alert variants. - * @type {Object} - */ - -const icons = { - info: , - error: , - warning: , -}; - -/** - * @param {Object} props - * @param {'info' | 'error' | 'warning'} props.variant - The type of alert. - * @param {string} [props.title] - The title of the alert. - * @param {string} [props.body] - The body text of the alert. - * @param {boolean} [props.isToast] - Indicates if the alert is used as a toast notification. - * @param {boolean} [props.hasIcon] - Whether to display an icon in the alert. - * @param {function} props.onClick - Toast dismiss function. - * @returns {JSX.Element} - */ - -const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { - const theme = useTheme(); - /* TODO - Do we need other variants for alert? - */ - - const text = theme.palette.secondary.contrastText; - const border = theme.palette.alert.contrastText; - const bg = theme.palette.alert.main; - const icon = icons[variant]; - - return ( - - {hasIcon && {icon}} - - {title && ( - {title} - )} - {body && ( - {body} - )} - {hasIcon && isToast && ( - - )} - - {isToast && ( - - - - )} - - ); -}; - -Alert.propTypes = { - variant: PropTypes.oneOf(["info", "error", "warning"]).isRequired, - title: PropTypes.string, - body: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - isToast: PropTypes.bool, - hasIcon: PropTypes.bool, - onClick: function (props, propName, componentName) { - if (props.isToast && !props[propName]) { - return new Error( - `Prop '${propName}' is required when 'isToast' is true in '${componentName}'.` - ); - } - return null; - }, -}; - -export default Alert; diff --git a/client/src/Components/v1/Animated/PulseDot.jsx b/client/src/Components/v1/Animated/PulseDot.jsx deleted file mode 100644 index 83513d56e..000000000 --- a/client/src/Components/v1/Animated/PulseDot.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import PropTypes from "prop-types"; -import { Box, Stack, useTheme } from "@mui/material"; -// import useUtils from "../../Pages/Uptime/utils"; - -/** - * A component that renders a pulsating dot with a specified color. - * - * @component - * @example - * // Example usage: - * - * - * @param {Object} props - * @param {string} props.color - The color of the dot. - * @returns {JSX.Element} The PulseDot component. - */ - -const PulseDot = ({ color }) => { - const theme = useTheme(); - // const { statusToTheme } = useUtils(); - /* TODO refactor so it gets status and gets theme color. Then uses theme.palette.[themeColor].lowContrast */ - /* const themeColor = statusToTheme[status]; */ - - return ( - - - - ); -}; - -PulseDot.propTypes = { - color: PropTypes.string.isRequired, -}; - -export default PulseDot; diff --git a/client/src/Components/v1/ArrowLeft/index.jsx b/client/src/Components/v1/ArrowLeft/index.jsx deleted file mode 100644 index a327d6bb7..000000000 --- a/client/src/Components/v1/ArrowLeft/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import LeftArrow from "../../../assets/icons/left-arrow.svg?react"; -import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react"; -import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react"; -import PropTypes from "prop-types"; - -const ArrowLeft = ({ type, color = "#667085", ...props }) => { - if (type === "double") { - return ( - - ); - } else if (type === "long") { - return ( - - ); - } else { - return ( - - ); - } -}; - -ArrowLeft.propTypes = { - color: PropTypes.string, - type: PropTypes.oneOf(["double", "long", "default"]), -}; -export default ArrowLeft; diff --git a/client/src/Components/v1/ArrowRight/index.jsx b/client/src/Components/v1/ArrowRight/index.jsx deleted file mode 100644 index 08a2639b0..000000000 --- a/client/src/Components/v1/ArrowRight/index.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import RightArrow from "../../../assets/icons/right-arrow.svg?react"; -import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react"; -import PropTypes from "prop-types"; - -const ArrowRight = ({ type, color = "#667085", ...props }) => { - if (type === "double") { - return ( - - ); - } else { - return ( - - ); - } -}; - -ArrowRight.propTypes = { - type: PropTypes.oneOf(["double", "default"]), - color: PropTypes.string, -}; - -export default ArrowRight; diff --git a/client/src/Components/v1/Avatar/index.jsx b/client/src/Components/v1/Avatar/index.jsx deleted file mode 100644 index d089080ff..000000000 --- a/client/src/Components/v1/Avatar/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Avatar as MuiAvatar } from "@mui/material"; -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { useEffect, useState } from "react"; -import { useTheme } from "@emotion/react"; -/** - * @component - * @param {Object} props - * @param {string} props.src - Path to image for avatar - * @param {boolean} props.small - Specifies if avatar should be large - * @param {Object} [props.sx] - Additional styles to apply to the button. - * @returns {JSX.Element} - * @example - * // Render a red label - * - */ - -const Avatar = ({ src, small, sx, onClick = () => {} }) => { - const { user } = useSelector((state) => state.auth); - const theme = useTheme(); - - const style = small ? { width: 32, height: 32 } : { width: 64, height: 64 }; - const border = small ? 1 : 3; - - const [image, setImage] = useState(); - useEffect(() => { - if (user.avatarImage) { - setImage(`data:image/png;base64,${user.avatarImage}`); - } - }, [user?.avatarImage]); - - return ( - - {user.firstName?.charAt(0)} - {user.lastName?.charAt(0) || ""} - - ); -}; - -Avatar.propTypes = { - src: PropTypes.string, - small: PropTypes.bool, - sx: PropTypes.object, - onClick: PropTypes.func, -}; - -export default Avatar; diff --git a/client/src/Components/v1/Breadcrumbs/index.css b/client/src/Components/v1/Breadcrumbs/index.css deleted file mode 100644 index abb3c0a28..000000000 --- a/client/src/Components/v1/Breadcrumbs/index.css +++ /dev/null @@ -1,22 +0,0 @@ -.MuiBreadcrumbs-root { - min-height: 34px; -} -.MuiBreadcrumbs-root svg { - width: 16px; - min-height: 16px; -} -.MuiBreadcrumbs-root .MuiBreadcrumbs-li a { - font-size: var(--env-var-font-size-medium); - font-weight: 400; -} -.MuiBreadcrumbs-root .MuiBreadcrumbs-li:not(:last-child) { - cursor: pointer; -} -.MuiBreadcrumbs-root .MuiBreadcrumbs-li:last-child a { - font-weight: 500; - opacity: 1; - cursor: default; -} -.MuiBreadcrumbs-root .MuiBreadcrumbs-separator { - margin: 0; -} diff --git a/client/src/Components/v1/Breadcrumbs/index.jsx b/client/src/Components/v1/Breadcrumbs/index.jsx deleted file mode 100644 index 34e5d5e95..000000000 --- a/client/src/Components/v1/Breadcrumbs/index.jsx +++ /dev/null @@ -1,78 +0,0 @@ -import PropTypes from "prop-types"; -import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import ArrowRight from "../ArrowRight/index.jsx"; -import "./index.css"; - -/** - * Breadcrumbs component that displays a list of breadcrumb items. - * - * @param {Object} props - * @param {Array} props.list - Array of breadcrumb items. Each item should have `name` and `path` properties. - * @param {string} props.list.name - The name to display for the breadcrumb. - * @param {string} props.list.path - The path to navigate to when the breadcrumb is clicked. - * - * @returns {JSX.Element} The rendered Breadcrumbs component. - */ - -const Breadcrumbs = ({ list }) => { - const theme = useTheme(); - const navigate = useNavigate(); - - return ( - } - aria-label="breadcrumb" - px={theme.spacing(2)} - py={theme.spacing(3.5)} - width="fit-content" - backgroundColor={theme.palette.secondary.main} - borderRadius={theme.shape.borderRadius} - lineHeight="18px" - sx={{ - "& .MuiBreadcrumbs-li a": { - transition: "background-color 0.2s ease-in-out, color 0.2s ease-in-out", - }, - "& .MuiBreadcrumbs-li:not(:last-of-type):hover a": { - backgroundColor: theme.palette.secondary.contrastText, - color: theme.palette.secondary.main, - }, - }} - > - {list.map((item, index) => { - return ( - navigate(item.path)} - sx={{ - opacity: 0.8, - textTransform: "capitalize", - "&, &:hover": { - color: theme.palette.secondary.contrastText, - }, - }} - > - {item.name} - - ); - })} - - ); -}; - -Breadcrumbs.propTypes = { - list: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, - }).isRequired - ).isRequired, -}; - -export default Breadcrumbs; diff --git a/client/src/Components/v1/Buttons/RoundGradientButton.jsx b/client/src/Components/v1/Buttons/RoundGradientButton.jsx deleted file mode 100644 index ff22be955..000000000 --- a/client/src/Components/v1/Buttons/RoundGradientButton.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import Button from "@mui/material/Button"; -import { styled } from "@mui/material/styles"; - -const RoundGradientButton = styled(Button)(({ theme }) => ({ - position: "relative", - border: "5px solid transparent", - backgroundClip: "padding-box", - borderRadius: 30, - fontSize: "1.2rem", - color: theme.palette.primary.contrastText, - backgroundColor: theme.palette.background.main, - - "&:after": { - position: "absolute", - top: -3, - left: -3, - right: -3, - bottom: -3, - background: - theme.palette.mode === "dark" - ? "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)" - : "linear-gradient(90deg, #842bd2, #ff5451, #8c52ff)", - content: '""', - zIndex: -1, - borderRadius: 30, - }, -})); - -export default RoundGradientButton; diff --git a/client/src/Components/v1/Charts/AreaChart/index.jsx b/client/src/Components/v1/Charts/AreaChart/index.jsx deleted file mode 100644 index dd5a900d6..000000000 --- a/client/src/Components/v1/Charts/AreaChart/index.jsx +++ /dev/null @@ -1,214 +0,0 @@ -/** - * CustomAreaChart component for rendering an area chart with optional gradient and custom ticks. - * - * @param {Object} props - The properties object. - * @param {Array} props.data - The data array for the chart. - * @param {Array} props.dataKeys - An array of data keys to be plotted as separate areas. - * @param {string} props.xKey - The key for the x-axis data. - * @param {string} [props.yKey] - The key for the y-axis data (optional). - * @param {Object} [props.xTick] - Custom tick component for the x-axis. - * @param {Object} [props.yTick] - Custom tick component for the y-axis. - * @param {string} [props.strokeColor] - The base stroke color for the areas. - * If not provided, uses a predefined color palette. - * @param {string} [props.fillColor] - The base fill color for the areas. - * @param {boolean} [props.gradient=false] - Whether to apply a gradient fill to the areas. - * @param {string} [props.gradientDirection="vertical"] - The direction of the gradient. - * @param {string} [props.gradientStartColor] - The start color of the gradient. - * Defaults to the area's stroke color if not provided. - * @param {string} [props.gradientEndColor] - The end color of the gradient. - * @param {Object} [props.customTooltip] - Custom tooltip component for the chart. - * @param {string|number} [props.height="100%"] - Height of the chart container. - * - * @returns {JSX.Element} The rendered area chart component. - * - * @example - * // Single series chart - * - * - * @example - * // Multi-series chart with custom tooltip - * } - * yTick={} - * gradient={true} - * customTooltip={({ active, payload, label }) => ( - * - * )} - * /> - */ - -import { - AreaChart, - Area, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, -} from "recharts"; -import { createGradient } from "../Utils/gradientUtils.jsx"; -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; -import { useId } from "react"; -import { Fragment } from "react"; - -const CustomAreaChart = ({ - data, - dataKeys, - xKey, - xDomain, - yKey, - yDomain, - xTick, - yTick, - strokeColor, - fillColor, - gradient = false, - gradientDirection = "vertical", - gradientStartColor, - gradientEndColor, - customTooltip, - height = "100%", -}) => { - const theme = useTheme(); - const uniqueId = useId(); - - const AREA_COLORS = [ - // Blues - "#3182bd", // Deep blue - "#6baed6", // Medium blue - "#9ecae1", // Light blue - - // Greens - "#74c476", // Soft green - "#a1d99b", // Light green - "#c7e9c0", // Pale green - - // Oranges - "#fdae6b", // Warm orange - "#fdd0a2", // Light orange - "#feedde", // Pale orange - - // Purples - "#9467bd", // Lavender - "#a55194", // Deep magenta - "#c994c7", // Soft magenta - - // Reds - "#ff9896", // Soft red - "#de2d26", // Deep red - "#fc9272", // Medium red - - // Cyans/Teals - "#17becf", // Cyan - "#7fcdbb", // Teal - "#a1dab4", // Light teal - - // Yellows - "#fec44f", // Mustard - "#fee391", // Light yellow - "#ffffd4", // Pale yellow - - // Additional colors - "#e377c2", // Soft pink - "#bcbd22", // Olive - "#2ca02c", // Vibrant green - ]; - - return ( - - - - - - - {dataKeys.map((dataKey, index) => { - const gradientId = `gradient-${uniqueId}-${index}`; - - return ( - - {gradient === true && - createGradient({ - id: gradientId, - startColor: gradientStartColor || AREA_COLORS[index], - endColor: gradientEndColor, - direction: gradientDirection, - })} - - - ); - })} - {customTooltip ? ( - - ) : ( - - )} - - - ); -}; - -CustomAreaChart.propTypes = { - data: PropTypes.array.isRequired, - dataKeys: PropTypes.array.isRequired, - xTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself - yTick: PropTypes.object, // Recharts takes an instance of component, so we can't pass the component itself - xKey: PropTypes.string.isRequired, - xDomain: PropTypes.array, - yKey: PropTypes.string, - yDomain: PropTypes.array, - fillColor: PropTypes.string, - strokeColor: PropTypes.string, - gradient: PropTypes.bool, - gradientDirection: PropTypes.string, - gradientStartColor: PropTypes.string, - gradientEndColor: PropTypes.string, - customTooltip: PropTypes.object, - height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), -}; - -export default CustomAreaChart; diff --git a/client/src/Components/v1/Charts/BarChart/index.css b/client/src/Components/v1/Charts/BarChart/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Components/v1/Charts/BarChart/index.jsx b/client/src/Components/v1/Charts/BarChart/index.jsx deleted file mode 100644 index 2bf8e1798..000000000 --- a/client/src/Components/v1/Charts/BarChart/index.jsx +++ /dev/null @@ -1,181 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; -import { useEffect, useState } from "react"; -import "./index.css"; -import { useSelector } from "react-redux"; - -/* TODO add prop validation and jsdocs */ -const BarChart = ({ checks = [] }) => { - const theme = useTheme(); - const [animate, setAnimate] = useState(false); - const uiTimezone = useSelector((state) => state.ui.timezone); - - useEffect(() => { - setAnimate(true); - }); - - // set responseTime to average if there's only one check - if (checks.length === 1) { - checks[0] = { ...checks[0], responseTime: 50 }; - } - - if (checks.length !== 25) { - const placeholders = Array(25 - checks.length).fill("placeholder"); - checks = [...checks, ...placeholders]; - } - - return ( - event.stopPropagation()} - sx={{ - cursor: "default", - }} - > - {checks.map((check, index) => - check === "placeholder" ? ( - /* TODO what is the purpose of this box? */ - // CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars - // even if there are less than 25 checks - - ) : ( - - - {formatDateWithTz( - check.updatedAt, - "ddd, MMMM D, YYYY, HH:mm A", - uiTimezone - )} - - - - - - Response Time - - - {check.originalResponseTime} - - {" "} - ms - - - - - - } - placement="top" - key={`check-${check?._id}`} - slotProps={{ - popper: { - className: "bar-tooltip", - modifiers: [ - { - name: "offset", - options: { - offset: [0, -10], - }, - }, - ], - sx: { - "& .MuiTooltip-tooltip": { - backgroundColor: theme.palette.secondary.main, - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - boxShadow: theme.shape.boxShadow, - px: theme.spacing(4), - py: theme.spacing(3), - }, - "& .MuiTooltip-tooltip p": { - /* TODO Font size should point to theme */ - fontSize: 12, - color: theme.palette.secondary.contrastText, - fontWeight: 500, - }, - "& .MuiTooltip-tooltip span": { - /* TODO Font size should point to theme */ - fontSize: 11, - color: theme.palette.secondary.contrastText, - fontWeight: 600, - }, - }, - }, - }} - > - .MuiBox-root": { - filter: "brightness(0.8)", - }, */ - }} - > - - - - ) - )} - - ); -}; - -export default BarChart; diff --git a/client/src/Components/v1/Charts/ChartBox/EmptyView.jsx b/client/src/Components/v1/Charts/ChartBox/EmptyView.jsx deleted file mode 100644 index c1b60632d..000000000 --- a/client/src/Components/v1/Charts/ChartBox/EmptyView.jsx +++ /dev/null @@ -1,106 +0,0 @@ -// Components -import { Typography, Stack } from "@mui/material"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox/index.jsx"; - -/** - * `EmptyView` is a functional React component that displays an empty state view with an optional icon, header, and message. - * - * @component - * @param {Object} props - The properties that define the `EmptyView` component. - * @param {React.ReactNode} [props.icon] - An optional icon to display at the top of the empty view. - * @param {string} [props.header] - An optional header text displayed next to the icon. - * @param {string} [props.message="No Data"] - The message to be displayed in the empty view. - * @param {'h1' | 'h2' | 'h3'} [props.headingLevel="h2"] - The heading level for the message text. - * @param {string} [props.justifyContent="flex-start"] - The CSS `justify-content` value to align elements vertically. - * @param {string} [props.height="100%"] - The height of the empty view container. - * - * @example - * // Example usage of EmptyView component: - * } - * header="Average Response Time" - * message="No Response Time Available" - * headingLevel="h2" - * justifyContent="center" - * height="50%" - * /> - * - * @returns {React.Element} The `EmptyView` component with customizable icon, header, and message. - */ - -const EmptyView = ({ - icon, - header, - message = "No Data", - headingLevel = "h2", - justifyContent = "flex-start", - height = "100%", -}) => { - const theme = useTheme(); - return ( - - - - {icon && {icon}} - {header && {header}} - - - {message} - - - - ); -}; - -EmptyView.propTypes = { - message: PropTypes.string, - icon: PropTypes.node, - header: PropTypes.string, - headingLevel: PropTypes.oneOf(["h1", "h2", "h3"]), - justifyContent: PropTypes.string, - height: PropTypes.string, -}; - -export default EmptyView; diff --git a/client/src/Components/v1/Charts/ChartBox/index.jsx b/client/src/Components/v1/Charts/ChartBox/index.jsx deleted file mode 100644 index be2faf8ed..000000000 --- a/client/src/Components/v1/Charts/ChartBox/index.jsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox/index.jsx"; -import EmptyView from "./EmptyView.jsx"; -import PropTypes from "prop-types"; - -const ChartBox = ({ - children, - icon, - header, - height = "300px", - justifyContent = "space-between", - Legend, - borderRadiusRight = 4, - sx, - noDataMessage, - isEmpty = false, -}) => { - const theme = useTheme(); - if (isEmpty) { - return ( - - ); - } - return ( - - span": { - color: theme.palette.primary.contrastText, - fontSize: 20, - "& span": { - opacity: 0.8, - marginLeft: 2, - fontSize: 15, - }, - }, - - "& tspan, & text": { - fill: theme.palette.primary.contrastTextTertiary, - }, - "& path": { - transition: "fill 300ms ease, stroke-width 400ms ease", - }, - }} - > - - {icon && {icon}} - {header && {header}} - - {children} - - {Legend && Legend} - - ); -}; - -export default ChartBox; - -ChartBox.propTypes = { - children: PropTypes.node, - icon: PropTypes.node, - header: PropTypes.string, - height: PropTypes.string, - noDataMessage: PropTypes.string, - isEmpty: PropTypes.bool, -}; diff --git a/client/src/Components/v1/Charts/CustomGauge/index.css b/client/src/Components/v1/Charts/CustomGauge/index.css deleted file mode 100644 index 1b64a6b9a..000000000 --- a/client/src/Components/v1/Charts/CustomGauge/index.css +++ /dev/null @@ -1,14 +0,0 @@ -.radial-chart { - position: relative; - display: inline-block; -} - -.radial-chart-base { - opacity: 0.3; -} - -.radial-chart-progress { - transform: rotate(-90deg); - transform-origin: center; - transition: stroke-dashoffset 1.5s ease-in-out; -} diff --git a/client/src/Components/v1/Charts/CustomGauge/index.jsx b/client/src/Components/v1/Charts/CustomGauge/index.jsx deleted file mode 100644 index 614b3774c..000000000 --- a/client/src/Components/v1/Charts/CustomGauge/index.jsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { useEffect, useState, useMemo } from "react"; -import Stack from "@mui/material/Stack"; -import { Box, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -import "./index.css"; -import CircularProgress from "@mui/material/CircularProgress"; - -const MINIMUM_VALUE = 0; -const MAXIMUM_VALUE = 100; - -/** - * A Performant SVG based circular gauge - * - * @component - * @param {Object} props - Component properties - * @param {number} [props.progress=0] - Progress percentage (0-100) - * @param {number} [props.radius=60] - Radius of the gauge circle - * @param {number} [props.strokeWidth=15] - Width of the gauge stroke - * @param {number} [props.precision=1] - Precision of the progress percentage - * @param {string} [props.unit="%"] - Unit of progress - * - * @example - * - * - * @returns {React.ReactElement} Rendered CustomGauge component - */ -const CustomGauge = ({ - isLoading = false, - progress = 0, - radius = 70, - strokeWidth = 15, - precision = 1, - unit = "%", -}) => { - const theme = useTheme(); - // Calculate the length of the stroke for the circle - const { circumference, totalSize, strokeLength } = useMemo( - () => ({ - circumference: 2 * Math.PI * radius, - totalSize: radius * 2 + strokeWidth * 2, - strokeLength: (progress / 100) * (2 * Math.PI * radius), - }), - [radius, strokeWidth, progress] - ); - - // Handle initial animation - const [offset, setOffset] = useState(circumference); - useEffect(() => { - setOffset(circumference); - const timer = setTimeout(() => { - setOffset(circumference - strokeLength); - }, 100); - - return () => clearTimeout(timer); - }, [progress, circumference, strokeLength]); - - const progressWithinRange = Math.max(MINIMUM_VALUE, Math.min(progress, MAXIMUM_VALUE)); - - let fillColor; - if (progressWithinRange < 50) { - fillColor = theme.palette.success.main; - } else if (progressWithinRange < 80) { - fillColor = theme.palette.warning.lowContrast; - } else { - fillColor = theme.palette.error.lowContrast; - } - - if (isLoading) { - return ( - - - - ); - } - - return ( - - - - - - - - {`${progressWithinRange.toFixed(precision)}${unit}`} - - - ); -}; - -export default CustomGauge; - -CustomGauge.propTypes = { - isLoading: PropTypes.bool, - progress: PropTypes.number, - radius: PropTypes.number, - strokeWidth: PropTypes.number, - precision: PropTypes.number, - unit: PropTypes.string, -}; diff --git a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx deleted file mode 100644 index b37990e46..000000000 --- a/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx +++ /dev/null @@ -1,181 +0,0 @@ -// Components -import { Stack, Box, Tooltip, Typography } from "@mui/material"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useState, useEffect } from "react"; -import PropTypes from "prop-types"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; -import { useSelector } from "react-redux"; - -const PlaceholderCheck = ({ daysToShow }) => { - const theme = useTheme(); - - return ( - - ); -}; - -PlaceholderCheck.propTypes = { - daysToShow: PropTypes.number, -}; - -const Check = ({ check, daysToShow }) => { - const [animate, setAnimate] = useState(false); - - const theme = useTheme(); - useEffect(() => { - setAnimate(true); - }, []); - const uiTimezone = useSelector((state) => state.ui.timezone); - - return ( - - - {formatDateWithTz(check._id, "ddd, MMMM D, YYYY", uiTimezone)} - - - - - Uptime percentage - - - {check.upPercentage.toFixed(2)} - - {" "} - % - - - - - - } - placement="top" - key={`check-${check?._id}`} - slotProps={{ - popper: { - className: "bar-tooltip", - modifiers: [ - { - name: "offset", - options: { - offset: [0, -10], - }, - }, - ], - sx: { - "& .MuiTooltip-tooltip": { - backgroundColor: theme.palette.secondary.main, - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - boxShadow: theme.shape.boxShadow, - px: theme.spacing(4), - py: theme.spacing(3), - }, - "& .MuiTooltip-tooltip p": { - /* TODO Font size should point to theme */ - fontSize: 12, - color: theme.palette.secondary.contrastText, - fontWeight: 500, - }, - "& .MuiTooltip-tooltip span": { - /* TODO Font size should point to theme */ - fontSize: 11, - color: theme.palette.secondary.contrastText, - fontWeight: 600, - }, - }, - }, - }} - > - - - - - ); -}; - -Check.propTypes = { - check: PropTypes.object, - daysToShow: PropTypes.number, -}; - -const DePINStatusPageBarChart = ({ checks = [], daysToShow = 30 }) => { - if (checks.length !== daysToShow) { - const placeholders = Array(daysToShow - checks.length).fill("placeholder"); - checks = [...checks, ...placeholders]; - } - return ( - - {checks.map((check) => { - if (check === "placeholder") { - return ( - - ); - } - return ( - - ); - })} - - ); -}; - -DePINStatusPageBarChart.propTypes = { - checks: PropTypes.array, - daysToShow: PropTypes.number, -}; - -export default DePINStatusPageBarChart; diff --git a/client/src/Components/v1/Charts/LegendBox/index.jsx b/client/src/Components/v1/Charts/LegendBox/index.jsx deleted file mode 100644 index f19e09105..000000000 --- a/client/src/Components/v1/Charts/LegendBox/index.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox/index.jsx"; -import PropTypes from "prop-types"; - -const LegendBox = ({ children, icon, header, sx }) => { - const theme = useTheme(); - return ( - - - {icon} - {header} - - {children} - - ); -}; - -LegendBox.propTypes = { - children: PropTypes.node, - icon: PropTypes.node, - header: PropTypes.string, -}; - -export default LegendBox; diff --git a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.css b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx deleted file mode 100644 index a9ef04a53..000000000 --- a/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx +++ /dev/null @@ -1,222 +0,0 @@ -import PropTypes from "prop-types"; -import { - AreaChart, - Area, - XAxis, - Tooltip, - CartesianGrid, - ResponsiveContainer, - Text, -} from "recharts"; -import { Box, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useMemo, useState } from "react"; -import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; -import { - tooltipDateFormatLookup, - tickDateFormatLookup, -} from "../Utils/chartUtilFunctions.js"; - -import "./index.css"; -const CustomToolTip = ({ active, payload, label, dateRange }) => { - const format = tooltipDateFormatLookup(dateRange); - const uiTimezone = useSelector((state) => state.ui.timezone); - const theme = useTheme(); - if (active && payload && payload.length) { - const responseTime = payload[0]?.payload?.originalAvgResponseTime - ? payload[0]?.payload?.originalAvgResponseTime - : (payload[0]?.payload?.avgResponseTime ?? 0); - return ( - - - {formatDateWithTz(label, format, uiTimezone)} - - - - - - Response time: - - - {Math.floor(responseTime)} - - {" "} - ms - - - - - {/* Display original value */} - - ); - } - return null; -}; - -CustomToolTip.propTypes = { - active: PropTypes.bool, - payload: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.number, - payload: PropTypes.shape({ - _id: PropTypes.string, - avgResponseTime: PropTypes.number, - originalAvgResponseTime: PropTypes.number, - }), - }) - ), - label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - dateRange: PropTypes.string, -}; -const CustomTick = ({ x, y, payload, dateRange }) => { - const format = tickDateFormatLookup(dateRange); - const theme = useTheme(); - const uiTimezone = useSelector((state) => state.ui.timezone); - return ( - - {formatDateWithTz(payload?.value, format, uiTimezone)} - - ); -}; - -CustomTick.propTypes = { - x: PropTypes.number, - y: PropTypes.number, - payload: PropTypes.object, - index: PropTypes.number, - dateRange: PropTypes.string, -}; - -const MonitorDetailsAreaChart = ({ checks, dateRange }) => { - const theme = useTheme(); - const memoizedChecks = useMemo(() => checks, [checks[0]]); - const [isHovered, setIsHovered] = useState(false); - return ( - - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - - - - - - - - } - axisLine={false} - tickLine={false} - height={20} - /> - } - wrapperStyle={{ pointerEvents: "none" }} - /> - - - - ); -}; - -MonitorDetailsAreaChart.propTypes = { - checks: PropTypes.array, - dateRange: PropTypes.string, -}; - -export default MonitorDetailsAreaChart; diff --git a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx deleted file mode 100644 index 7da4d58ba..000000000 --- a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx +++ /dev/null @@ -1,217 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; -import { useEffect, useState, forwardRef } from "react"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -/** - * A customizable Bar component that renders a colored bar with optional children. - * @component - * - * @param {string} width The width of the bar (e.g., "100px"). - * @param {string} height The height of the bar (e.g., "50px"). - * @param {string} backgroundColor The background color of the bar (e.g., "#FF5733"). - * @param {string} [borderRadius] Optional border radius for the bar (e.g., "8px"). - * @param {node} children The content to be rendered inside the bar. - * @returns {JSX.Element} The Bar component. - */ - -const Bar = forwardRef( - ({ width, height, backgroundColor, borderRadius, children, ...otherProps }, ref) => { - const theme = useTheme(); - - return ( - - {children} - - ); - } -); - -Bar.displayName = "Bar"; - -Bar.propTypes = { - width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, - height: PropTypes.string.isRequired, - backgroundColor: PropTypes.string.isRequired, - borderRadius: PropTypes.string, - children: PropTypes.node, -}; - -/* TODO add prop validation and jsdocs */ -const StatusPageBarChart = ({ checks = [] }) => { - const theme = useTheme(); - const [animate, setAnimate] = useState(false); - const uiTimezone = useSelector((state) => state.ui.timezone); - - const barWidth = { - xs: "calc(60% / 25)", - xl: "calc(40% / 25)", - }; - - useEffect(() => { - setAnimate(true); - }, []); - - // set responseTime to average if there's only one check - if (checks.length === 1) { - checks[0] = { ...checks[0], responseTime: 50 }; - } - - if (checks.length !== 25) { - const placeholders = Array(25 - checks.length).fill("placeholder"); - checks = [...checks, ...placeholders]; - } - - return ( - event.stopPropagation()} - sx={{ - cursor: "default", - }} - > - {checks.map((check, index) => - check === "placeholder" ? ( - /* TODO what is the purpose of this box? */ - // CAIO_REVIEW the purpose of this box is to make sure there are always at least 25 bars - // even if there are less than 25 checks - - ) : ( - - - {formatDateWithTz( - check.createdAt, - "ddd, MMMM D, YYYY, HH:mm A", - uiTimezone - )} - - - - - - Response Time - - - {check.originalResponseTime} - - {" "} - ms - - - - - - } - placement="top" - key={`check-${check?._id}`} - slotProps={{ - popper: { - className: "bar-tooltip", - modifiers: [ - { - name: "offset", - options: { - offset: [0, -10], - }, - }, - ], - sx: { - "& .MuiTooltip-tooltip": { - backgroundColor: theme.palette.secondary.main, - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - boxShadow: theme.shape.boxShadow, - px: theme.spacing(4), - py: theme.spacing(3), - }, - "& .MuiTooltip-tooltip p": { - /* TODO Font size should point to theme */ - fontSize: 12, - color: theme.palette.secondary.contrastText, - fontWeight: 500, - }, - "& .MuiTooltip-tooltip span": { - /* TODO Font size should point to theme */ - fontSize: 11, - color: theme.palette.secondary.contrastText, - fontWeight: 600, - }, - }, - }, - }} - > - - - - - ) - )} - - ); -}; - -export default StatusPageBarChart; diff --git a/client/src/Components/v1/Charts/Utils/chartUtilFunctions.js b/client/src/Components/v1/Charts/Utils/chartUtilFunctions.js deleted file mode 100644 index 2851af93e..000000000 --- a/client/src/Components/v1/Charts/Utils/chartUtilFunctions.js +++ /dev/null @@ -1,27 +0,0 @@ -export const tooltipDateFormatLookup = (dateRange) => { - const dateFormatLookup = { - recent: "ddd. MMMM D, YYYY, hh:mm A", - day: "ddd. MMMM D, YYYY, hh:mm A", - week: "ddd. MMMM D, YYYY, hh:mm A", - month: "ddd. MMMM D, YYYY", - }; - const format = dateFormatLookup[dateRange]; - if (format === undefined) { - return ""; - } - return format; -}; - -export const tickDateFormatLookup = (dateRange) => { - const tickFormatLookup = { - recent: "h:mm A", - day: "h:mm A", - week: "MM/D, h:mm A", - month: "ddd. M/D", - }; - const format = tickFormatLookup[dateRange]; - if (format === undefined) { - return ""; - } - return format; -}; diff --git a/client/src/Components/v1/Charts/Utils/chartUtils.jsx b/client/src/Components/v1/Charts/Utils/chartUtils.jsx deleted file mode 100644 index 1bab80595..000000000 --- a/client/src/Components/v1/Charts/Utils/chartUtils.jsx +++ /dev/null @@ -1,337 +0,0 @@ -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { useTheme } from "@mui/material"; -import { Text } from "recharts"; -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; -import { Box, Stack, Typography } from "@mui/material"; -import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js"; -/** - * Custom tick component for rendering time with timezone. - * - * @param {Object} props - The properties object. - * @param {number} props.x - The x-coordinate for the tick. - * @param {number} props.y - The y-coordinate for the tick. - * @param {Object} props.payload - The payload object containing tick data. - * @param {number} props.index - The index of the tick. - * @returns {JSX.Element} The rendered tick component. - */ -export const TzTick = ({ x, y, payload, index, dateRange }) => { - const theme = useTheme(); - const uiTimezone = useSelector((state) => state.ui.timezone); - const format = tickDateFormatLookup(dateRange); - return ( - - {formatDateWithTz(payload?.value, format, uiTimezone)} - - ); -}; - -TzTick.propTypes = { - x: PropTypes.number, - y: PropTypes.number, - payload: PropTypes.object, - index: PropTypes.number, - dateRange: PropTypes.string, -}; - -/** - * Custom tick component for rendering percentage values. - * - * @param {Object} props - The properties object. - * @param {number} props.x - The x-coordinate for the tick. - * @param {number} props.y - The y-coordinate for the tick. - * @param {Object} props.payload - The payload object containing tick data. - * @param {number} props.index - The index of the tick. - * @returns {JSX.Element|null} The rendered tick component or null for the first tick. - */ -export const PercentTick = ({ x, y, payload, index }) => { - const theme = useTheme(); - if (index === 0) return null; - return ( - - {`${(payload?.value * 100).toFixed()}%`} - - ); -}; - -PercentTick.propTypes = { - x: PropTypes.number, - y: PropTypes.number, - payload: PropTypes.object, - index: PropTypes.number, -}; - -/** - * Converts a decimal value to a formatted percentage string. - * - * @param {number} value - The decimal value to convert (e.g., 0.75) - * @returns {string} Formatted percentage string (e.g., "75.00%") or original input if not a number - * - * @example - * getFormattedPercentage(0.7543) // Returns "75.43%" - * getFormattedPercentage(1) // Returns "100.00%" - * getFormattedPercentage("test") // Returns "test" - */ -const getFormattedPercentage = (value) => { - if (typeof value !== "number") return value; - return `${(value * 100).toFixed(2)}%`; -}; - -/** - * Custom tick component for rendering network bytes per second. - * - * @param {Object} props - The properties object. - * @param {number} props.x - The x-coordinate for the tick. - * @param {number} props.y - The y-coordinate for the tick. - * @param {Object} props.payload - The payload object containing tick data. - * @param {number} props.index - The index of the tick. - * @returns {JSX.Element|null} The rendered tick component or null for the first tick. - */ -export const NetworkTick = ({ x, y, payload, index, formatter }) => { - const theme = useTheme(); - if (index === 0) return null; - - if (formatter === undefined) { - formatter = (value, space = false) => { - if (typeof value !== "number") return value; - // need to add space between value and unit - return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`; - }; - } - - return ( - - {formatter(payload?.value, true)} - - ); -}; - -NetworkTick.propTypes = { - x: PropTypes.number, - y: PropTypes.number, - payload: PropTypes.object, - index: PropTypes.number, - formatter: PropTypes.func, -}; - -/** - * Custom tooltip component for displaying infrastructure data. - * - * @param {Object} props - The properties object. - * @param {boolean} props.active - Indicates if the tooltip is active. - * @param {Array} props.payload - The payload array containing tooltip data. - * @param {string} props.label - The label for the tooltip. - * @param {string} props.yKey - The key for the y-axis data. - * @param {string} props.yLabel - The label for the y-axis data. - * @param {string} props.dotColor - The color of the dot in the tooltip. - * @returns {JSX.Element|null} The rendered tooltip component or null if inactive. - */ -export const InfrastructureTooltip = ({ - active, - payload, - label, - yKey, - yIdx = -1, - yLabel, - dotColor, - dateRange, - formatter = getFormattedPercentage, -}) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - const theme = useTheme(); - - const format = tooltipDateFormatLookup(dateRange); - if (active && payload && payload.length) { - const [hardwareType, metric] = yKey.split("."); - return ( - - - {formatDateWithTz(label, format, uiTimezone)} - - - - - - {yIdx >= 0 - ? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}` - : `${yLabel} ${formatter(payload[0].payload[yKey])}`} - - - - {/* Display original value */} - - ); - } - return null; -}; - -InfrastructureTooltip.propTypes = { - active: PropTypes.bool, - payload: PropTypes.array, - label: PropTypes.oneOfType([ - PropTypes.instanceOf(Date), - PropTypes.string, - PropTypes.number, - ]), - yKey: PropTypes.string, - yIdx: PropTypes.number, - yLabel: PropTypes.string, - dotColor: PropTypes.string, - dateRange: PropTypes.string, -}; - -export const TemperatureTooltip = ({ - active, - payload, - label, - keys, - dotColor, - dateRange, -}) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - const theme = useTheme(); - const format = tooltipDateFormatLookup(dateRange); - const formatCoreKey = (key) => { - return key.replace(/^core(\d+)$/, "Core $1"); - }; - if (active && payload && payload.length) { - return ( - - - {formatDateWithTz(label, format, uiTimezone)} - - - - {keys.map((key) => { - return ( - - - - - - {`${formatCoreKey(key)}: ${payload[0].payload[key]} °C`} - - - - - ); - })} - - - ); - } - return null; -}; - -TemperatureTooltip.propTypes = { - active: PropTypes.bool, - keys: PropTypes.array, - payload: PropTypes.array, - label: PropTypes.oneOfType([ - PropTypes.instanceOf(Date), - PropTypes.string, - PropTypes.number, - ]), - dotColor: PropTypes.string, - dateRange: PropTypes.string, -}; diff --git a/client/src/Components/v1/Charts/Utils/gradientUtils.jsx b/client/src/Components/v1/Charts/Utils/gradientUtils.jsx deleted file mode 100644 index b5920374d..000000000 --- a/client/src/Components/v1/Charts/Utils/gradientUtils.jsx +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Creates an SVG gradient definition for use in charts - * @param {Object} params - The gradient parameters - * @param {string} [params.id="colorUv"] - Unique identifier for the gradient - * @param {string} params.startColor - Starting color of the gradient (hex, rgb, or color name) - * @param {string} params.endColor - Ending color of the gradient (hex, rgb, or color name) - * @param {number} [params.startOpacity=0.8] - Starting opacity (0-1) - * @param {number} [params.endOpacity=0] - Ending opacity (0-1) - * @param {('vertical'|'horizontal')} [params.direction="vertical"] - Direction of the gradient - * @returns {JSX.Element} SVG gradient definition element - * @example - * createCustomGradient({ - * startColor: "#1976D2", - * endColor: "#42A5F5", - * direction: "horizontal" - * }) - */ - -export const createGradient = ({ - id, - startColor, - endColor, - startOpacity = 0.8, - endOpacity = 0, - direction = "vertical", // or "horizontal" -}) => ( - - - - - - -); diff --git a/client/src/Components/v1/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx deleted file mode 100644 index 53971316b..000000000 --- a/client/src/Components/v1/Check/Check.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import "./check.css"; -import PropTypes from "prop-types"; -import CheckGrey from "../../../assets/icons/check.svg?react"; -import CheckOutlined from "../../../assets/icons/check-outlined.svg?react"; -import { Box, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * `Check` is a functional React component that displays a check icon and a label. - * - * @component - * @param {Object} props - The properties that define the `Check` component. - * @param {string} props.text - The text to be displayed as the label next to the check icon. - * @param {'info' | 'error' | 'success'} [props.variant='info'] - The variant of the check component, affecting its styling. - * @param {boolean} [props.outlined] - Whether the check icon should be outlined or not. - * - * @example - * // To use this component, import it and use it in your JSX like this: - * - * - * @returns {React.Element} The `Check` component with a check icon and a label, defined by the `text` prop. - */ -const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => { - const theme = useTheme(); - const colors = { - success: theme.palette.success.main, - error: theme.palette.error.main, - info: theme.palette.info.border, - }; - return ( - - {outlined ? ( - - ) : ( - path": { fill: colors[variant] }, - }} - > - - - )} - - {noHighlightText && {noHighlightText}}{" "} - {text} - - - ); -}; - -Check.propTypes = { - text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - noHighlightText: PropTypes.string, - variant: PropTypes.oneOf(["info", "error", "success"]), - outlined: PropTypes.bool, -}; - -export default Check; diff --git a/client/src/Components/v1/Check/check.css b/client/src/Components/v1/Check/check.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Components/v1/CircularCount/index.jsx b/client/src/Components/v1/CircularCount/index.jsx deleted file mode 100644 index 8350a8c44..000000000 --- a/client/src/Components/v1/CircularCount/index.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Box } from "@mui/material"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -const CircularCount = ({ count }) => { - const theme = useTheme(); - return ( - - {count} - - ); -}; - -CircularCount.propTypes = { - count: PropTypes.number, -}; - -export default CircularCount; diff --git a/client/src/Components/v1/Common/AppBar.jsx b/client/src/Components/v1/Common/AppBar.jsx deleted file mode 100644 index 1d5a38ddd..000000000 --- a/client/src/Components/v1/Common/AppBar.jsx +++ /dev/null @@ -1,213 +0,0 @@ -import { useState } from "react"; -import { styled, alpha } from "@mui/material/styles"; -import Box from "@mui/material/Box"; -import AppBar from "@mui/material/AppBar"; -import Toolbar from "@mui/material/Toolbar"; -import Button from "@mui/material/Button"; -import IconButton from "@mui/material/IconButton"; -import Container from "@mui/material/Container"; -import MenuItem from "@mui/material/MenuItem"; -import Drawer from "@mui/material/Drawer"; -import MenuIcon from "@mui/icons-material/Menu"; -import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; -import ThemeSwitch from "../ThemeSwitch/index.jsx"; -import { useTheme } from "@mui/material/styles"; -import { useSelector } from "react-redux"; -import { useLocation, useNavigate } from "react-router-dom"; - -const StyledToolbar = styled(Toolbar)(({ theme, mode }) => ({ - display: "flex", - alignItems: "center", - justifyContent: "space-between", - flexShrink: 0, - borderRadius: `calc(${theme.shape.borderRadius}px + 4px)`, - backdropFilter: "blur(24px)", - border: "1px solid", - borderColor: - mode === "light" - ? alpha(theme.palette.common.black, 0.1) - : alpha(theme.palette.common.white, 0.1), - backgroundColor: - mode === "light" - ? alpha(theme.palette.common.white, 0.4) - : alpha(theme.palette.common.black, 0.4), - boxShadow: theme.shadows[3], - padding: "8px 12px", -})); - -const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ - fontSize: "1.1rem", - margin: theme.spacing(4, 2), -})); - -const AppAppBar = () => { - const [open, setOpen] = useState(false); - const theme = useTheme(); - const mode = useSelector((state) => state.ui.mode); - const location = useLocation(); - const navigate = useNavigate(); - - const logoSrc = - mode === "light" ? "/images/prism-black.png" : "/images/prism-white.png"; - - const toggleDrawer = (newOpen) => () => { - setOpen(newOpen); - }; - - const handleScroll = (id) => { - if (location.pathname === "/") { - const element = document.getElementById(id); - if (element) { - element.scrollIntoView({ behavior: "smooth" }); - } - } else { - navigate(`/#${id}`); - } - }; - - return ( - - - - - Prism Logo - - - - - - - - - {/* - */} - - - - - - - - - - - - - - Features - Testimonials - Highlights - FAQ - - Blog - - {/* - - - - - */} - - - - - - - - ); -}; - -export default AppAppBar; diff --git a/client/src/Components/v1/Common/Footer.jsx b/client/src/Components/v1/Common/Footer.jsx deleted file mode 100644 index 92f01a9ae..000000000 --- a/client/src/Components/v1/Common/Footer.jsx +++ /dev/null @@ -1,255 +0,0 @@ -import * as React from "react"; -import Box from "@mui/material/Box"; -import Container from "@mui/material/Container"; -import IconButton from "@mui/material/IconButton"; -import Link from "@mui/material/Link"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import { FaFacebook, FaLinkedin, FaGithub, FaTwitter, FaEnvelope } from "react-icons/fa"; - -function Copyright() { - return ( - - {"Copyright © "} - - UpRock - -   - {new Date().getFullYear()} - - ); -} - -export default function Footer() { - return ( - - -
- - Privacy Policy - - -  •  - - - Terms of Service - - -
- - - - - - - - - - - - - - - - - -
- - - - Made with ❤️ by  - - UpRock  - - &  - - Bluewave Labs - - - - - Built on  - - - - - - - - - - - - - - - - - - - - -  Solana - - - -
- ); -} diff --git a/client/src/Components/v1/ConfigBox/index.jsx b/client/src/Components/v1/ConfigBox/index.jsx deleted file mode 100644 index 01b35729e..000000000 --- a/client/src/Components/v1/ConfigBox/index.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Stack, styled } from "@mui/material"; - -const ConfigBox = styled(Stack)(({ theme }) => ({ - display: "flex", - flexDirection: "row", - justifyContent: "space-between", - backgroundColor: theme.palette.primary.main, - border: 1, - borderStyle: "solid", - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.spacing(2), - "& > *": { - paddingTop: theme.spacing(15), - paddingBottom: theme.spacing(15), - }, - "& > div:first-of-type": { - flex: 0.7, - borderRight: 1, - borderRightStyle: "solid", - borderRightColor: theme.palette.primary.lowContrast, - paddingRight: theme.spacing(15), - paddingLeft: theme.spacing(15), - backgroundColor: theme.palette.tertiary.background, - "& :is(h1, h2):first-of-type": { - fontWeight: 600, - marginBottom: theme.spacing(4), - }, - }, - "& > div:last-of-type": { - flex: 1, - paddingRight: theme.spacing(20), - paddingLeft: theme.spacing(20), - }, - "& h1, & h2": { - color: theme.palette.primary.contrastTextSecondary, - }, -})); - -export default ConfigBox; diff --git a/client/src/Components/v1/ConfigRow/index.jsx b/client/src/Components/v1/ConfigRow/index.jsx deleted file mode 100644 index 12fea923e..000000000 --- a/client/src/Components/v1/ConfigRow/index.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Box, Stack, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import ConfigBox from "../ConfigBox/index.jsx"; - -/** - * ConfigRow is a styled container used to layout content in a row format with specific padding, border, and spacing. - * It serves as the wrapper for ConfigBox, with the left section displaying the title and description, - * and the right section displaying the children. - * - * @component - * @example - * return ( - * - *
Left content (Title + Description)
- *
Right content (Children)
- *
- * ); - */ - -const ConfigRow = ({ title, description, children }) => { - const theme = useTheme(); - - return ( - - - - {title} - - {description && ( - - {description} - - )} - - - {children} - - - ); -}; - -ConfigRow.propTypes = { - title: PropTypes.string.isRequired, - description: PropTypes.string, - children: PropTypes.node, -}; - -export default ConfigRow; diff --git a/client/src/Components/v1/Dialog/genericDialog.jsx b/client/src/Components/v1/Dialog/genericDialog.jsx deleted file mode 100644 index a13eba849..000000000 --- a/client/src/Components/v1/Dialog/genericDialog.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useId } from "react"; -import PropTypes from "prop-types"; -import { Modal, Stack, Typography } from "@mui/material"; - -const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => { - const titleId = useId(); - const descriptionId = useId(); - const ariaDescribedBy = description?.length > 0 ? descriptionId : ""; - return ( - e.stopPropagation()} - > - - - {title} - - {description && ( - - {description} - - )} - {children} - - - ); -}; - -GenericDialog.propTypes = { - title: PropTypes.string.isRequired, - description: PropTypes.string, - open: PropTypes.bool.isRequired, - onClose: PropTypes.func.isRequired, - theme: PropTypes.object.isRequired, - children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]) - .isRequired, - width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]), -}; - -export { GenericDialog }; diff --git a/client/src/Components/v1/Dialog/index.jsx b/client/src/Components/v1/Dialog/index.jsx deleted file mode 100644 index b8cf07439..000000000 --- a/client/src/Components/v1/Dialog/index.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import PropTypes from "prop-types"; -import { Button, Stack } from "@mui/material"; -import { GenericDialog } from "./genericDialog.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -const Dialog = ({ - title, - description, - open, - onCancel, - confirmationButtonLabel, - onConfirm, - isLoading, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - - - - - ); -}; - -Dialog.propTypes = { - title: PropTypes.string.isRequired, - description: PropTypes.string, - open: PropTypes.bool.isRequired, - onCancel: PropTypes.func.isRequired, - confirmationButtonLabel: PropTypes.string.isRequired, - onConfirm: PropTypes.func.isRequired, - isLoading: PropTypes.bool, -}; - -export default Dialog; diff --git a/client/src/Components/v1/Dot/index.jsx b/client/src/Components/v1/Dot/index.jsx deleted file mode 100644 index 0676d0a6f..000000000 --- a/client/src/Components/v1/Dot/index.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import PropTypes from "prop-types"; - -const Dot = ({ color = "gray", size = "4px", style }) => { - return ( - - ); -}; - -Dot.propTypes = { - color: PropTypes.string, - size: PropTypes.string, -}; - -export default Dot; diff --git a/client/src/Components/v1/Fallback/FallBackActionButtons.jsx b/client/src/Components/v1/Fallback/FallBackActionButtons.jsx deleted file mode 100644 index 144b16501..000000000 --- a/client/src/Components/v1/Fallback/FallBackActionButtons.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Button, Stack } from "@mui/material"; -import { useTheme } from "@mui/material/styles"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -const FallbackActionButtons = ({ link, type }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const { t } = useTranslation(); - - return ( - - - {type === "uptimeMonitor" && ( - - )} - - ); -}; -FallbackActionButtons.propTypes = { - title: PropTypes.string.isRequired, - link: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, -}; - -export default FallbackActionButtons; diff --git a/client/src/Components/v1/Fallback/FallbackBackground.jsx b/client/src/Components/v1/Fallback/FallbackBackground.jsx deleted file mode 100644 index 2182082fd..000000000 --- a/client/src/Components/v1/Fallback/FallbackBackground.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useTheme } from "@emotion/react"; -import Box from "@mui/material/Box"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import OutputAnimation from "../../../assets/Animations/output.gif"; -import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif"; -import { useSelector } from "react-redux"; -const FallbackBackground = () => { - const theme = useTheme(); - const mode = useSelector((state) => state.ui.mode); - return ( - <> - - - - - - ); -}; - -export default FallbackBackground; diff --git a/client/src/Components/v1/Fallback/FallbackCheckList.jsx b/client/src/Components/v1/Fallback/FallbackCheckList.jsx deleted file mode 100644 index befb95185..000000000 --- a/client/src/Components/v1/Fallback/FallbackCheckList.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material/styles"; -import Box from "@mui/material/Box"; -import Stack from "@mui/material/Stack"; -import Check from "../Check/Check.jsx"; -const FallbackCheckList = ({ checks, type }) => { - const theme = useTheme(); - return ( - - {checks?.map((check, index) => ( - - ))} - - ); -}; - -FallbackCheckList.propTypes = { - checks: PropTypes.arrayOf(PropTypes.string).isRequired, - title: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, -}; - -export default FallbackCheckList; diff --git a/client/src/Components/v1/Fallback/FallbackContainer.jsx b/client/src/Components/v1/Fallback/FallbackContainer.jsx deleted file mode 100644 index 7a2ec5fe0..000000000 --- a/client/src/Components/v1/Fallback/FallbackContainer.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { Box, Stack } from "@mui/material"; -import PropTypes from "prop-types"; - -const FallbackContainer = ({ children, type }) => { - const theme = useTheme(); - return ( - - - {children} - - - ); -}; - -FallbackContainer.propTypes = { - children: PropTypes.node, - type: PropTypes.string, -}; - -export default FallbackContainer; diff --git a/client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx b/client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx deleted file mode 100644 index 2cf52bd88..000000000 --- a/client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import Box from "@mui/material/Box"; -import { useTheme } from "@mui/material/styles"; -import Link from "@mui/material/Link"; -import { Link as RouterLink } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import Alert from "../Alert/index.jsx"; -import PropTypes from "prop-types"; - -const renderWarningMessage = (t) => { - return ( - <> - {t("pageSpeedWarning")}{" "} - - {t("pageSpeedLearnMoreLink")} - {" "} - {t("pageSpeedAddApiKey")} - - ); -}; - -const FallbackPageSpeedWarning = ({ settingsData }) => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( - - svg": { - color: theme.palette.warningSecondary.contrastText, - }, - }, - }} - > - {settingsData?.pagespeedKeySet === false && ( - - )} - - - ); -}; - -FallbackPageSpeedWarning.propTypes = { - settingsData: PropTypes.shape({ - pagespeedKeySet: PropTypes.bool, - }), -}; - -export default FallbackPageSpeedWarning; diff --git a/client/src/Components/v1/Fallback/FallbackTitle.jsx b/client/src/Components/v1/Fallback/FallbackTitle.jsx deleted file mode 100644 index 69fee05c6..000000000 --- a/client/src/Components/v1/Fallback/FallbackTitle.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useTheme } from "@mui/material/styles"; -import { Typography } from "@mui/material"; -import PropTypes from "prop-types"; - -const FallbackTitle = ({ title }) => { - const theme = useTheme(); - return ( - - {title} - - ); -}; -FallbackTitle.propTypes = { - title: PropTypes.string.isRequired, -}; -export default FallbackTitle; diff --git a/client/src/Components/v1/Fallback/index.css b/client/src/Components/v1/Fallback/index.css deleted file mode 100644 index 6988fa5bf..000000000 --- a/client/src/Components/v1/Fallback/index.css +++ /dev/null @@ -1,40 +0,0 @@ -[class*="fallback__"] { - width: fit-content; - margin: auto; -} - -[class*="fallback__"] h1.MuiTypography-root { - font-size: var(--env-var-font-size-large); - font-weight: 600; -} - -[class*="fallback__"] button.MuiButtonBase-root, -[class*="fallback__"] .check { - width: max-content; -} - -[class*="fallback__"] button.MuiButtonBase-root { - min-height: 34px; -} - -[class*="fallback__"] .check span.MuiTypography-root, -[class*="fallback__"] button.MuiButtonBase-root { - font-size: var(--env-var-font-size-medium); -} - -[class*="fallback__"] .background-pattern-svg { - position: absolute; - top: 0; - left: 50%; - transform: translate(-50%, -50%); - z-index: 0; - - width: 100%; - max-width: 800px; - height: 100%; - max-height: 800px; - - background-position: center; - background-size: cover; - background-repeat: no-repeat; -} diff --git a/client/src/Components/v1/Fallback/index.jsx b/client/src/Components/v1/Fallback/index.jsx deleted file mode 100644 index 40113f882..000000000 --- a/client/src/Components/v1/Fallback/index.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Stack } from "@mui/material"; -import "./index.css"; -import FallbackTitle from "./FallbackTitle.jsx"; -import FallbackCheckList from "./FallbackCheckList.jsx"; -import FallbackActionButtons from "./FallBackActionButtons.jsx"; -import FallbackBackground from "./FallbackBackground.jsx"; -import FallbackContainer from "./FallbackContainer.jsx"; -/** - * Fallback component to display a fallback UI with a title, a list of checks, and a navigation button. - * - * @param {Object} props - The component props. - * @param {string} props.title - The title to be displayed in the fallback UI. - * @param {string} props.type - The type of the fallback (e.g., "pageSpeed", "notifications"). - * @param {Array} props.checks - An array of strings representing the checks to display. - * @param {string} [props.link="/"] - The link to navigate to. - * @param {boolean} [props.showPageSpeedWarning=false] - Whether to show the PageSpeed API warning. - * @returns {JSX.Element} The rendered fallback UI. - */ - -const Fallback = ({ title, checks, link = "/", isAdmin, type, children }) => { - const theme = useTheme(); - return ( - - - - - - - - {isAdmin && ( - - - {children} - - )} - - - ); -}; -Fallback.propTypes = { - title: PropTypes.string.isRequired, - checks: PropTypes.arrayOf(PropTypes.string).isRequired, - link: PropTypes.string, - isAdmin: PropTypes.bool, - showPageSpeedWarning: PropTypes.bool, - type: PropTypes.string.isRequired, - children: PropTypes.node, -}; -export default Fallback; diff --git a/client/src/Components/v1/FilterHeader/index.jsx b/client/src/Components/v1/FilterHeader/index.jsx deleted file mode 100644 index 36dd04081..000000000 --- a/client/src/Components/v1/FilterHeader/index.jsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; -import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline"; - -/** - * A reusable filter header component that displays a dropdown menu with selectable options. - * - * @component - * @param {Object} props - The component props. - * @param {string} props.header - The header text to display when no options are selected. - * @param {Array} props.options - An array of options to display in the dropdown menu. Each option should have a `value` and `label`. - * @param {Array} [props.value] - The currently selected values. - * @param {Function} props.onChange - The callback function to handle changes in the selected values. - * @param {boolean} [props.multiple=true] - Whether multiple options can be selected. - * @returns {JSX.Element} The rendered FilterHeader component. - */ - -const FilterHeader = ({ header, options, value, onChange, multiple = true }) => { - const theme = useTheme(); - - const controlledValue = value === undefined ? [] : value; // Ensure value is always treated as an array for controlled component purposes - - return ( - - - - ); -}; - -FilterHeader.propTypes = { - header: PropTypes.string.isRequired, - options: PropTypes.arrayOf( - PropTypes.shape({ - value: PropTypes.string.isRequired, - label: PropTypes.string.isRequired, - }) - ).isRequired, - value: PropTypes.arrayOf(PropTypes.string), - onChange: PropTypes.func.isRequired, - multiple: PropTypes.bool, -}; - -export default FilterHeader; diff --git a/client/src/Components/v1/GenericFallback/NetworkError.jsx b/client/src/Components/v1/GenericFallback/NetworkError.jsx deleted file mode 100644 index ddffa1c31..000000000 --- a/client/src/Components/v1/GenericFallback/NetworkError.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -const NetworkError = () => { - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - <> - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); -}; - -export default NetworkError; diff --git a/client/src/Components/v1/GenericFallback/index.jsx b/client/src/Components/v1/GenericFallback/index.jsx deleted file mode 100644 index 137cdd1da..000000000 --- a/client/src/Components/v1/GenericFallback/index.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { Box, Stack } from "@mui/material"; -import OutputAnimation from "../../../assets/Animations/output.gif"; -import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import { useSelector } from "react-redux"; - -/** - * Fallback component to display a fallback UI for network errors - * - * @returns {JSX.Element} The rendered fallback UI. - */ - -const GenericFallback = ({ children }) => { - const theme = useTheme(); - const mode = useSelector((state) => state.ui.mode); - - return ( - - - - - - - - {children} - - - - ); -}; - -export default GenericFallback; diff --git a/client/src/Components/v1/HOC/withAdminCheck.jsx b/client/src/Components/v1/HOC/withAdminCheck.jsx deleted file mode 100644 index 8646c99b6..000000000 --- a/client/src/Components/v1/HOC/withAdminCheck.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import { logger } from "../../../Utils/Logger.js"; -import { networkService } from "../../../main.jsx"; - -const withAdminCheck = (WrappedComponent) => { - const WithAdminCheck = (props) => { - const navigate = useNavigate(); - const [isChecking, setIsChecking] = useState(true); - const [superAdminExists, setSuperAdminExists] = useState(false); - - useEffect(() => { - networkService - .doesSuperAdminExist() - .then((response) => { - if (response?.data?.data === true) { - navigate("/login"); - } else { - setSuperAdminExists(false); - } - }) - .catch((error) => { - logger.error(error); - }) - .finally(() => { - setIsChecking(false); - }); - }, [navigate]); - - if (isChecking) { - return null; - } - - return ( - - ); - }; - const wrappedComponentName = - WrappedComponent.displayName || WrappedComponent.name || "Component"; - WithAdminCheck.displayName = `WithAdminCheck(${wrappedComponentName})`; - - return WithAdminCheck; -}; - -export default withAdminCheck; diff --git a/client/src/Components/v1/Heading/index.jsx b/client/src/Components/v1/Heading/index.jsx deleted file mode 100644 index 1a5f1a53b..000000000 --- a/client/src/Components/v1/Heading/index.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Typography } from "@mui/material"; -import PropTypes from "prop-types"; - -/** - * A heading component that renders text with a specific heading level. - * - * @param {Object} props - The properties passed to the Heading component. - * @param {('h1'|'h2'|'h3')} props.component - The heading level for the component. - * @param {Object} props.style - Custom styles to apply to the heading. - * @param {string} props.children - The content to display inside the heading. - * @returns {JSX.Element} The Typography component with specified heading properties. - */ - -function Heading({ component, style, children }) { - return ( - - {children} - - ); -} - -Heading.propTypes = { - component: PropTypes.oneOf(["h1", "h2", "h3"]).isRequired, - style: PropTypes.object, - children: PropTypes.string.isRequired, -}; -export { Heading }; diff --git a/client/src/Components/v1/Host/index.jsx b/client/src/Components/v1/Host/index.jsx deleted file mode 100644 index 772f5b61c..000000000 --- a/client/src/Components/v1/Host/index.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import Dot from "../Dot/index.jsx"; -/** - * Host component. - * This subcomponent receives a params object and displays the host details. - * - * @component - * @param {Object} params - An object containing the following properties: - * @param {string} params.url - The URL of the host. - * @param {string} params.title - The name of the host. - * @param {string} params.percentageColor - The color of the percentage text. - * @param {number} params.percentage - The percentage to display. - * @returns {React.ElementType} Returns a div element with the host details. - */ -const Host = ({ url, title, percentageColor, percentage, showURL }) => { - const theme = useTheme(); - return ( - - - {title} - {percentageColor && percentage && ( - <> - - - {percentage}% - - - )} - - {showURL && {url}} - - ); -}; - -Host.propTypes = { - title: PropTypes.string, - percentageColor: PropTypes.string, - percentage: PropTypes.string, - url: PropTypes.string, - showURL: PropTypes.bool, -}; - -export default Host; diff --git a/client/src/Components/v1/HttpStatusLabel/index.jsx b/client/src/Components/v1/HttpStatusLabel/index.jsx deleted file mode 100644 index 2253e323e..000000000 --- a/client/src/Components/v1/HttpStatusLabel/index.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; -import { BaseLabel } from "../Label/index.jsx"; - -/** - * @component - * @param {Object} props - * @param {number} props.status - The http status for the label - * @param {Styles} props.customStyles - CSS Styles passed from parent component - * @returns {JSX.Element} - * @example - * // Render a http status label - * - */ - -const DEFAULT_CODE = 9999; // Default code for unknown status - -const handleStatusCode = (status) => { - if (status) { - return status; - } - return DEFAULT_CODE; -}; - -const getRoundedStatusCode = (status) => { - return Math.floor(status / 100) * 100; -}; - -const HttpStatusLabel = ({ status, customStyles }) => { - const theme = useTheme(); - const colors = { - 400: { - color: theme.palette.warning.main, - borderColor: theme.palette.warning.lowContrast, - }, - 500: { - color: theme.palette.error.main, - borderColor: theme.palette.error.lowContrast, - }, - default: { - color: theme.palette.primary.contrastText, - borderColor: theme.palette.primary.contrastText, - }, - }; - - const statusCode = handleStatusCode(status); - - const { borderColor, color } = - colors[getRoundedStatusCode(statusCode)] || colors.default; - return ( - - ); -}; - -HttpStatusLabel.propTypes = { - status: PropTypes.number, - customStyles: PropTypes.object, -}; - -export { HttpStatusLabel }; diff --git a/client/src/Components/v1/I18nLoader/index.jsx b/client/src/Components/v1/I18nLoader/index.jsx deleted file mode 100644 index 2a6a4966d..000000000 --- a/client/src/Components/v1/I18nLoader/index.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import i18n from "../../../Utils/i18n.js"; -import { useSelector } from "react-redux"; -import { useEffect } from "react"; -const I18nLoader = () => { - const language = useSelector((state) => state.ui.language); - - useEffect(() => { - if (language && i18n.language !== language) { - i18n.changeLanguage(language); - } - }, [language]); - - return null; -}; - -export default I18nLoader; diff --git a/client/src/Components/v1/IconBox/index.jsx b/client/src/Components/v1/IconBox/index.jsx deleted file mode 100644 index bb9770e6a..000000000 --- a/client/src/Components/v1/IconBox/index.jsx +++ /dev/null @@ -1,77 +0,0 @@ -import { Box, styled } from "@mui/material"; -import PropTypes from "prop-types"; - -/** - * IconBox - A styled box component for rendering icons with consistent sizing and styling - * - * @component - * @param {Object} [props] - Configuration options for the IconBox - * @param {number} [props.height=34] - Height of the icon box - * @param {number} [props.width=34] - Width of the icon box - * @param {number} [props.minWidth=34] - Minimum width of the icon box - * @param {number} [props.borderRadius=4] - Border radius of the icon box - * @param {number} [props.svgWidth=20] - Width of the SVG icon - * @param {number} [props.svgHeight=20] - Height of the SVG icon - * - * @example - * // Basic usage - * - * - * - * - * @example - * // Customized usage - * - * - * - * - * @returns {React.ReactElement} A styled box containing an icon - */ -const IconBox = styled(Box)( - ({ - theme, - height = 34, - width = 34, - minWidth = 34, - borderRadius = 4, - svgWidth = 20, - svgHeight = 20, - }) => ({ - height: height, - minWidth: minWidth, - width: width, - position: "relative", - border: 1, - borderStyle: "solid", - borderColor: theme.palette.primary.lowContrast, - borderRadius: borderRadius, - backgroundColor: theme.palette.tertiary.main, - "& svg": { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: svgWidth, - height: svgHeight, - "& path": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }, - }) -); - -IconBox.propTypes = { - height: PropTypes.number, - width: PropTypes.number, - minWidth: PropTypes.number, - borderRadius: PropTypes.number, - svgWidth: PropTypes.number, - svgHeight: PropTypes.number, -}; - -export default IconBox; diff --git a/client/src/Components/v1/Image/index.jsx b/client/src/Components/v1/Image/index.jsx deleted file mode 100644 index 8f30f8a9e..000000000 --- a/client/src/Components/v1/Image/index.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Box } from "@mui/material"; -import PropTypes from "prop-types"; - -const isValidBase64Image = (data) => { - return /^[A-Za-z0-9+/=]+$/.test(data); -}; - -const Image = ({ - shouldRender = true, - src, - alt, - width = "auto", - height = "auto", - minWidth = "auto", - minHeight = "auto", - maxWidth = "auto", - maxHeight = "auto", - base64, - placeholder, - sx, -}) => { - if (shouldRender === false) { - return null; - } - - if (typeof src !== "undefined" && typeof base64 !== "undefined") { - console.warn("base64 takes precedence over src and overwrites it"); - } - - if (typeof base64 !== "undefined" && isValidBase64Image(base64)) { - src = `data:image/png;base64,${base64}`; - } - - if ( - typeof src === "undefined" && - typeof base64 === "undefined" && - typeof placeholder !== "undefined" - ) { - src = placeholder; - } - - return ( - - ); -}; - -Image.propTypes = { - shouldRender: PropTypes.bool, - src: PropTypes.string, - alt: PropTypes.string.isRequired, - width: PropTypes.string, - height: PropTypes.string, - minWidth: PropTypes.string, - minHeight: PropTypes.string, - maxWidth: PropTypes.string, - maxHeight: PropTypes.string, - base64: PropTypes.string, - sx: PropTypes.object, -}; - -export default Image; diff --git a/client/src/Components/v1/InfoBox/index.jsx b/client/src/Components/v1/InfoBox/index.jsx deleted file mode 100644 index 96e67975d..000000000 --- a/client/src/Components/v1/InfoBox/index.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import { RowContainer } from "../StandardContainer/index.jsx"; -import { Stack, Typography } from "@mui/material"; -import Image from "../Image/index.jsx"; - -import { useTheme } from "@mui/material/styles"; - -const InfoBox = ({ - img, - icon: Icon, - alt, - heading, - headingLevel = 2, - subHeading, - subHeadingLevel = "", - sx, -}) => { - const theme = useTheme(); - return ( - - {img && ( - {alt} - )} - {Icon && ( - - )} - - {heading} - - {subHeading} - - - - ); -}; - -export default InfoBox; diff --git a/client/src/Components/v1/Inputs/Checkbox/index.jsx b/client/src/Components/v1/Inputs/Checkbox/index.jsx deleted file mode 100644 index 64f3cc3db..000000000 --- a/client/src/Components/v1/Inputs/Checkbox/index.jsx +++ /dev/null @@ -1,117 +0,0 @@ -import PropTypes from "prop-types"; -import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react"; -import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react"; - -/** - * Checkbox Component - * - * A customized checkbox component using Material-UI that supports custom sizing, - * disabled states, and custom icons. - * - * @component - * @param {Object} props - Component properties - * @param {string} props.id - Unique identifier for the checkbox input - * @param {string} [props.name] - Optional name attribute for the checkbox - * @param {(string|React.ReactNode)} props.label - Label text or node for the checkbox - * @param {('small'|'medium'|'large')} [props.size='medium'] - Size of the checkbox icon - * @param {boolean} props.isChecked - Current checked state of the checkbox - * @param {string} [props.value] - Optional value associated with the checkbox - * @param {Function} [props.onChange] - Callback function triggered when checkbox state changes - * @param {boolean} [props.isDisabled] - Determines if the checkbox is disabled - * - * @returns {React.ReactElement} Rendered Checkbox component - * - * @example - * // Basic usage - * - * - * @example - * // With custom size and disabled state - * - */ -const Checkbox = ({ - id, - name, - label, - size = "medium", - isChecked, - value, - onChange, - isDisabled, -}) => { - /* TODO move sizes to theme */ - const sizes = { small: "14px", medium: "16px", large: "18px" }; - const theme = useTheme(); - return ( - } - checkedIcon={} - inputProps={{ - "aria-label": "controlled checkbox", - id: id, - }} - sx={{ - "&:hover": { backgroundColor: "transparent" }, - "& svg": { width: sizes[size], height: sizes[size] }, - }} - /> - } - label={label} - disabled={isDisabled} - sx={{ - borderRadius: theme.shape.borderRadius, - p: theme.spacing(2.5), - "& .MuiButtonBase-root": { - width: theme.spacing(10), - p: 0, - mr: theme.spacing(6), - }, - "&:not(:has(.Mui-disabled)):hover": { - backgroundColor: theme.palette.tertiary.main, - }, - "& span.MuiTypography-root": { - fontSize: 13, - color: theme.palette.primary.contrastTextTertiary, - }, - ".MuiFormControlLabel-label.Mui-disabled": { - color: theme.palette.primary.contrastTextTertiary, - opacity: 0.25, - }, - }} - /> - ); -}; - -Checkbox.propTypes = { - id: PropTypes.string, - name: PropTypes.string, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - size: PropTypes.oneOf(["small", "medium", "large"]), - isChecked: PropTypes.bool.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - onChange: PropTypes.func, - isDisabled: PropTypes.bool, -}; - -export default Checkbox; diff --git a/client/src/Components/v1/Inputs/ColorPicker/index.jsx b/client/src/Components/v1/Inputs/ColorPicker/index.jsx deleted file mode 100644 index e64c936fc..000000000 --- a/client/src/Components/v1/Inputs/ColorPicker/index.jsx +++ /dev/null @@ -1,63 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { MuiColorInput } from "mui-color-input"; - -/** - * - * @param {*} id The ID of the component - * @param {*} value The color value of the component - * @param {*} error The error of the component - * @param {*} onChange The Change handler function - * @param {*} onBlur The Blur handler function - * @returns The ColorPicker component - * Example usage: - * - * - */ -const ColorPicker = ({ id, name, value, error, onChange, onBlur }) => { - const theme = useTheme(); - return ( - - onChange({ target: { name, value: color } })} - onBlur={onBlur} - /> - {error && ( - - {error} - - )} - - ); -}; - -ColorPicker.propTypes = { - id: PropTypes.string.isRequired, - value: PropTypes.string, - error: PropTypes.string, - onChange: PropTypes.func.isRequired, - onBlur: PropTypes.func, - name: PropTypes.string, -}; - -export default ColorPicker; diff --git a/client/src/Components/v1/Inputs/FieldWrapper/index.jsx b/client/src/Components/v1/Inputs/FieldWrapper/index.jsx deleted file mode 100644 index cc5061af8..000000000 --- a/client/src/Components/v1/Inputs/FieldWrapper/index.jsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; - -const DEFAULT_GAP = 6; -const FieldWrapper = ({ - label, - children, - gap, - labelMb, - labelFontWeight = 500, - labelVariant = "h3", - labelSx = {}, - sx = {}, -}) => { - const theme = useTheme(); - return ( - - {label && ( - - {label} - - )} - {children} - - ); -}; - -FieldWrapper.propTypes = { - label: PropTypes.node, - children: PropTypes.node.isRequired, - gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]), - labelMb: PropTypes.number, - labelFontWeight: PropTypes.number, - labelVariant: PropTypes.string, - labelSx: PropTypes.object, - sx: PropTypes.object, -}; - -export default FieldWrapper; diff --git a/client/src/Components/v1/Inputs/ImageUpload/index.jsx b/client/src/Components/v1/Inputs/ImageUpload/index.jsx deleted file mode 100644 index e7415bf8f..000000000 --- a/client/src/Components/v1/Inputs/ImageUpload/index.jsx +++ /dev/null @@ -1,242 +0,0 @@ -// Components -import { Box, Stack, Typography } from "@mui/material"; -import CloudUploadIcon from "@mui/icons-material/CloudUpload"; -import Image from "../../Image/index.jsx"; -import TextField from "@mui/material/TextField"; -import IconButton from "@mui/material/IconButton"; -import ProgressUpload from "../../ProgressBars/index.jsx"; -import ImageIcon from "@mui/icons-material/Image"; - -// Utils -import PropTypes from "prop-types"; -import { useCallback, useState, useRef, useEffect } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -/** - * ImageUpload component allows users to upload images with drag-and-drop functionality. - * It supports file size and format validation. - * - * @component - * @param {Object} props - Component props - * @param {boolean} [props.previewIsRound=false] - Determines if the image preview should be round - * @param {string} [props.src] - Source URL of the image to display - * @param {function} props.onChange - Callback function to handle file change, takes a file as an argument - * @param {number} [props.maxSize=3145728] - Maximum file size allowed in bytes (default is 3MB) - * @param {Array} [props.accept=['jpg', 'jpeg', 'png']] - Array of accepted file formats - * @param {Object} [props.errors] - Object containing error messages - * @returns {JSX.Element} The rendered component - */ -const ImageUpload = ({ - previewIsRound = false, - src, - onChange, - maxSize = 3 * 1024 * 1024, - accept = ["jpg", "jpeg", "png"], - error, -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - const [uploadComplete, setUploadComplete] = useState(false); - const [completedFile, setCompletedFile] = useState(null); - const [file, setFile] = useState(null); - const [progress, setProgress] = useState({ value: 0, isLoading: false }); - const intervalRef = useRef(null); - const [localError, setLocalError] = useState(null); - const [isDragging, setIsDragging] = useState(false); - - const roundStyle = previewIsRound ? { borderRadius: "50%" } : {}; - - const handleImageChange = useCallback( - (file) => { - if (!file) return; - - const isValidType = accept.some((type) => file.type.includes(type)); - const isValidSize = file.size <= maxSize; - - if (!isValidType) { - setLocalError(t("invalidFileFormat")); - return; - } - if (!isValidSize) { - setLocalError(t("invalidFileSize")); - return; - } - - setLocalError(null); - - const previewFile = { - src: URL.createObjectURL(file), - name: file.name, - file, - }; - - setFile(previewFile); - setProgress({ value: 0, isLoading: true }); - - intervalRef.current = setInterval(() => { - setProgress((prev) => { - const buffer = 12; - if (prev.value + buffer >= 100) { - clearInterval(intervalRef.current); - setUploadComplete(true); - setCompletedFile(previewFile); - return { value: 100, isLoading: false }; - } - return { value: prev.value + buffer, isLoading: true }; - }); - }, 120); - }, - [maxSize, accept] - ); - - useEffect(() => { - if (uploadComplete && completedFile) { - onChange?.(completedFile); - setUploadComplete(false); - setCompletedFile(null); - } - }, [uploadComplete, completedFile, onChange]); - - return ( - <> - {src ? ( - - Uploaded preview - - ) : ( - <> - setIsDragging(true)} - onDragLeave={() => setIsDragging(false)} - onDrop={() => setIsDragging(false)} - sx={{ - position: "relative", - height: "fit-content", - border: "dashed", - borderRadius: theme.shape.borderRadius, - borderColor: isDragging - ? theme.palette.primary.main - : theme.palette.primary.lowContrast, - backgroundColor: isDragging ? "hsl(215, 87%, 51%, 0.05)" : "transparent", - borderWidth: "2px", - transition: "0.2s", - "&:hover": { - borderColor: theme.palette.primary.main, - backgroundColor: "hsl(215, 87%, 51%, 0.05)", - }, - }} - > - handleImageChange(e?.target?.files?.[0])} - sx={{ - width: "100%", - "& .MuiInputBase-input[type='file']": { - opacity: 0, - cursor: "pointer", - maxWidth: "500px", - minHeight: "175px", - zIndex: 1, - }, - "& fieldset": { - padding: 0, - border: "none", - }, - }} - /> - - - - - - - {t("ClickUpload")} - {" "} - or {t("DragandDrop")} - - - ({t("MaxSize")}: {Math.round(maxSize / 1024 / 1024)}MB) - - - - {(localError || progress.isLoading || progress.value !== 0) && ( - } - label={file?.name || "Upload failed"} - size={file?.size} - progress={progress.value} - onClick={() => { - clearInterval(intervalRef.current); - setFile(null); - setProgress({ value: 0, isLoading: false }); - setLocalError(null); - onChange(undefined); - }} - error={localError || error} - /> - )} - - {t("SupportedFormats")}: {accept.join(", ").toUpperCase()} - - - )} - - ); -}; - -ImageUpload.propTypes = { - previewIsRound: PropTypes.bool, - src: PropTypes.string, - onChange: PropTypes.func, - maxSize: PropTypes.number, - accept: PropTypes.array, - error: PropTypes.string, -}; - -export default ImageUpload; diff --git a/client/src/Components/v1/Inputs/Radio/index.css b/client/src/Components/v1/Inputs/Radio/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Components/v1/Inputs/Radio/index.jsx b/client/src/Components/v1/Inputs/Radio/index.jsx deleted file mode 100644 index 575c1af74..000000000 --- a/client/src/Components/v1/Inputs/Radio/index.jsx +++ /dev/null @@ -1,112 +0,0 @@ -import PropTypes from "prop-types"; -import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import RadioChecked from "../../../../assets/icons/radio-checked.svg?react"; -import "./index.css"; - -/** - * Radio component. - * - * @component - * @example - * // Usage: - * - * - * @param {Object} props - The component - * @param {string} id - The id of the radio button. - * @param {string} title - The title of the radio button. - * @param {string} [desc] - The description of the radio button. - * @param {string} [size="small"] - The size of the radio button. - * @returns {JSX.Element} - The rendered Radio component. - */ - -const Radio = ({ - name, - checked, - value, - id, - size, - title, - desc, - onChange, - labelSpacing, -}) => { - const theme = useTheme(); - - return ( - } - sx={{ - color: "transparent", - width: 16, - height: 16, - boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`, - "&:not(.Mui-checked)": { - boxShadow: `inset 0 0 0 1px ${theme.palette.primary.contrastText}70`, // Use theme text color for the outline - }, - mt: theme.spacing(0.5), - }} - /> - } - onChange={onChange} - label={ - <> - - {title} - - - {desc} - - - } - labelPlacement="end" - sx={{ - alignItems: "flex-start", - p: theme.spacing(2.5), - m: theme.spacing(-2.5), - borderRadius: theme.shape.borderRadius, - "&:hover": { - backgroundColor: theme.palette.tertiary.main, - }, - "& .MuiButtonBase-root": { - p: 0, - mr: theme.spacing(6), - }, - }} - /> - ); -}; - -Radio.propTypes = { - title: PropTypes.string, - desc: PropTypes.string, - size: PropTypes.string, - name: PropTypes.string, - checked: PropTypes.bool, - value: PropTypes.string, - id: PropTypes.string, - onChange: PropTypes.func, -}; - -export default Radio; diff --git a/client/src/Components/v1/Inputs/Search/index.jsx b/client/src/Components/v1/Inputs/Search/index.jsx deleted file mode 100644 index 7a3771d47..000000000 --- a/client/src/Components/v1/Inputs/Search/index.jsx +++ /dev/null @@ -1,315 +0,0 @@ -import PropTypes from "prop-types"; -import { - Box, - ListItem, - Autocomplete, - TextField, - Typography, - Checkbox, -} from "@mui/material"; -import { useTheme } from "@emotion/react"; -import SearchIcon from "../../../../assets/icons/search.svg?react"; -import React, { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import FieldWrapper from "../FieldWrapper/index.jsx"; - -/** - * Search component using Material UI's Autocomplete. - * - * @param {Object} props - * @param {string} props.id - Unique identifier for the Autocomplete component - * @param {Array} props.options - Options to display in the Autocomplete dropdown - * @param {string} props.filteredBy - Key to access the option label from the options - * @param {string} props.value - Current input value for the Autocomplete - * @param {Function} props.handleChange - Function to call when the input changes - * @param {Function} Prop.onBlur - Function to call when the input is blured - * @param {Object} props.sx - Additional styles to apply to the component - * @param {string} props.unit - Label to identify type of options - * @returns {JSX.Element} The rendered Search component - */ - -const SearchAdornment = () => { - const theme = useTheme(); - return ( - - - - ); -}; - -//TODO keep search state inside of component. -const Search = ({ - label, - id, - options, - filteredBy, - secondaryLabel, - value, - inputValue, - handleInputChange, - handleChange, - sx, - multiple = false, - isAdorned = true, - error, - disabled, - startAdornment, - endAdornment, - onBlur, - //FieldWrapper's props - gap, - labelMb, - labelFontWeight, - labelVariant, - labelSx = {}, - unit = "option", - maxWidth = "100%", -}) => { - const theme = useTheme(); - const { t } = useTranslation(); - const [selectAll, setSelectAll] = React.useState(false); - - const [open, setOpen] = React.useState(false); - const enhancedOptions = React.useMemo(() => { - return multiple && isAdorned - ? [ - { [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" }, - ...options, - ] - : options; - }, [multiple, isAdorned, options, filteredBy]); - const isOptionSelected = (option) => { - if (!multiple && !isAdorned) return false; - if (Array.isArray(value)) { - return value.some((item) => item._id === option._id); - } - return false; - }; - const handleSelectAll = (isSelectAll) => { - const newValue = isSelectAll ? [...options] : []; - handleChange(newValue); - setSelectAll(isSelectAll); - }; - useEffect(() => { - const allSelected = - Array.isArray(value) && Array.isArray(options) && value.length === options.length; - if (selectAll !== allSelected) setSelectAll(allSelected); - }, [value, options]); - return ( - setOpen(true)} - onClose={(event, reason) => { - if (reason === "blur" || reason === "escape") { - setOpen(false); - } - }} - inputValue={inputValue} - onInputChange={(_, newValue) => { - handleInputChange(newValue); - }} - onChange={(_, newValue) => { - if (multiple && isAdorned) { - const hasSelectAllSelected = - Array.isArray(newValue) && newValue.some((item) => item.isSelectAll); - if (hasSelectAllSelected) { - handleSelectAll(!selectAll); - } else { - handleChange(newValue); - setSelectAll(Array.isArray(newValue) && newValue.length === options.length); - } - } else { - handleChange(newValue); - setOpen(false); - } - }} - fullWidth - freeSolo - disabled={disabled} - disableClearable - options={enhancedOptions} - getOptionLabel={(option) => option[filteredBy]} - isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier - renderInput={(params) => ( - - }), - ...(startAdornment && { startAdornment: startAdornment }), - ...(endAdornment && { endAdornment: endAdornment }), - }, - }} - sx={{}} - /> - {error && ( - - {error} - - )} - - )} - filterOptions={(options, { inputValue }) => { - if (inputValue.trim() === "" && multiple && isAdorned) { - return enhancedOptions; - } - const filtered = options.filter((option) => - option[filteredBy].toLowerCase().includes(inputValue.toLowerCase()) - ); - - if (filtered.length === 0) { - return [ - { - [filteredBy]: t("general.noOptionsFound", { unit: unit }), - noOptions: true, - }, - ]; - } - return filtered; - }} - getOptionKey={(option) => { - return option._id; - }} - renderOption={(props, option) => { - const { key, ...optionProps } = props; - const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined; - const port = option["port"]; - const selected = isOptionSelected(option); - return ( - - {multiple && isAdorned && !option.noOptions && ( - - )} - {option[filteredBy] + - (hasSecondaryLabel - ? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})` - : "")} - - ); - }} - slotProps={{ - popper: { - keepMounted: true, - sx: { - "& ul": { p: 2, backgroundColor: theme.palette.primary.main }, - "& li.MuiAutocomplete-option": { - color: theme.palette.primary.contrastTextSecondary, - px: 4, - borderRadius: theme.shape.borderRadius, - }, - - "& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover": - { - backgroundColor: theme.palette.primary.lowContrast, - color: "red", - }, - "& li.MuiAutocomplete-option:hover:not([aria-selected='true'])": { - color: theme.palette.secondary.contrastText, - backgroundColor: theme.palette.secondary.main, - }, - "& .MuiAutocomplete-noOptions": { - px: theme.spacing(6), - py: theme.spacing(5), - }, - }, - }, - }} - sx={{ - /* height: 34,*/ - "&.MuiAutocomplete-root .MuiAutocomplete-input": { - padding: `0 ${theme.spacing(5)}`, - }, - ...sx, - }} - /> - ); -}; - -Search.propTypes = { - label: PropTypes.string, - id: PropTypes.string, - multiple: PropTypes.bool, - options: PropTypes.array.isRequired, - filteredBy: PropTypes.string.isRequired, - secondaryLabel: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), - inputValue: PropTypes.string.isRequired, - handleInputChange: PropTypes.func.isRequired, - handleChange: PropTypes.func, - isAdorned: PropTypes.bool, - sx: PropTypes.object, - error: PropTypes.string, - disabled: PropTypes.bool, - startAdornment: PropTypes.object, - endAdornment: PropTypes.object, - onBlur: PropTypes.func, - unit: PropTypes.string, -}; - -export default Search; diff --git a/client/src/Components/v1/Inputs/Select/index.css b/client/src/Components/v1/Inputs/Select/index.css deleted file mode 100644 index b40cf8564..000000000 --- a/client/src/Components/v1/Inputs/Select/index.css +++ /dev/null @@ -1,7 +0,0 @@ -.select-wrapper .select-component > .MuiSelect-select { - padding: 0 10px; - min-height: 34px; - display: flex; - align-items: center; - line-height: 1; -} diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx deleted file mode 100644 index 37ce30dc1..000000000 --- a/client/src/Components/v1/Inputs/Select/index.jsx +++ /dev/null @@ -1,199 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import FieldWrapper from "../FieldWrapper/index.jsx"; - -import "./index.css"; - -/** - * @component - * @param {object} props - * @param {string} props.id - The ID attribute for the select element. - * @param {string} props.placeholder - The label of the select element. - * @param {string} props.placeholder - The placeholder text when no option is selected. - * @param {boolean} props.isHidden - Whether the placeholder should be hidden. - * @param {(string | number | boolean)} props.value - The currently selected value. - * @param {object[]} props.items - The array of items to populate in the select dropdown. - * @param {(string | number | boolean)} props.items._id - The unique identifier of each item. - * @param {string} props.items.name - The display name of each item. - * @param {function} props.onChange - The function to handle onChange event. - * @param {object} props.sx - The custom styles object for MUI Select component. - * @param {number} props.maxWidth - Maximum width in pixels for the select component. Enables responsive text truncation. - * @returns {JSX.Element} - * - * @example - * const frequencies = [ - * { _id: 1, name: "1 minute" }, - * { _id: 2, name: "2 minutes" }, - * { _id: 3, name: "3 minutes" }, - * ]; - * - * - {languagesForDisplay} - - ); -}; diff --git a/client/src/Components/v2/Inputs/RadioInput.tsx b/client/src/Components/v2/Inputs/RadioInput.tsx deleted file mode 100644 index d903a7b58..000000000 --- a/client/src/Components/v2/Inputs/RadioInput.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import Radio from "@mui/material/Radio"; -import type { RadioProps } from "@mui/material/Radio"; -import { useTheme } from "@mui/material/styles"; -import RadioChecked from "@/assets/icons/radio-checked.svg?react"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Typography from "@mui/material/Typography"; - -interface RadioInputProps extends RadioProps {} - -export const RadioInput: React.FC = ({ ...props }) => { - const theme = useTheme(); - return ( - } - sx={{ - color: "transparent", - boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`, - "&:not(.Mui-checked)": { - boxShadow: `inset 0 0 0 1px ${theme.palette.primary.contrastText}70`, // Use theme text color for the outline - }, - mt: theme.spacing(0.5), - padding: 0, - "& .MuiSvgIcon-root": { - fontSize: 16, - }, - }} - /> - ); -}; - -export const RadioWithDescription: React.FC< - RadioInputProps & { label: string; description: string } -> = ({ label, description, ...props }) => { - const theme = useTheme(); - return ( - } - label={ - <> - {label} - - {description} - - - } - sx={{ - alignItems: "flex-start", - p: theme.spacing(2.5), - m: theme.spacing(-2.5), - borderRadius: theme.shape.borderRadius, - - "&:hover": { - backgroundColor: theme.palette.tertiary.main, - }, - "& .MuiButtonBase-root": { - p: 0, - mr: theme.spacing(6), - }, - }} - /> - ); -}; diff --git a/client/src/Components/v2/Inputs/Select.tsx b/client/src/Components/v2/Inputs/Select.tsx deleted file mode 100644 index 54aacf68a..000000000 --- a/client/src/Components/v2/Inputs/Select.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Typography, Select } from "@mui/material"; -import MenuItem from "@mui/material/MenuItem"; -import type { SelectProps } from "@mui/material/Select"; -import { useTheme } from "@mui/material/styles"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; - -export const SelectInput: React.FC = ({ ...props }) => { - const theme = useTheme(); - return ( - setRole([event.target.value])} - items={[ - { _id: "admin", name: t("roles.admin") }, - { _id: "user", name: t("roles.teamMember") }, - ]} - /> - - - } - sx={{ mb: theme.spacing(5) }} - /> - - } - sx={{ mb: theme.spacing(5) }} - /> - - - - - - - - ); -}; -AddTeamMember.propTypes = { - handleIsRegisterOpen: PropTypes.func.isRequired, - isRegisterOpen: PropTypes.bool.isRequired, - onMemberAdded: PropTypes.func.isRequired, -}; - -export default AddTeamMember; diff --git a/client/src/Pages/v1/Account/components/PasswordPanel.jsx b/client/src/Pages/v1/Account/components/PasswordPanel.jsx deleted file mode 100644 index ca2af9205..000000000 --- a/client/src/Pages/v1/Account/components/PasswordPanel.jsx +++ /dev/null @@ -1,269 +0,0 @@ -import TabPanel from "@mui/lab/TabPanel"; -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { Box, Stack, Typography, Button } from "@mui/material"; -import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; -import Alert from "@/Components/v1/Alert/index.jsx"; -import { update } from "../../../../Features/Auth/authSlice.js"; -import { useDispatch, useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { getTouchedFieldErrors } from "../../../../Validation/error.js"; -import { useTranslation } from "react-i18next"; - -const defaultPasswordsState = { - password: "", - newPassword: "", - confirm: "", -}; - -/** - * PasswordPanel component manages the form for editing password. - * - * @returns {JSX.Element} - */ - -const PasswordPanel = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const SPACING_GAP = theme.spacing(12); - - //redux state - const { isLoading } = useSelector((state) => state.auth); - - const idToName = { - "edit-current-password": "password", - "edit-new-password": "newPassword", - "edit-confirm-password": "confirm", - }; - - const [localData, setLocalData] = useState(defaultPasswordsState); - const [errors, setErrors] = useState(defaultPasswordsState); - const [touchedFields, setTouchedFields] = useState({ - password: false, - newPassword: false, - confirm: false, - }); - - const handleChange = (event) => { - const { value, id } = event.target; - const name = idToName[id]; - - const updatedData = { - ...localData, - [name]: value, - }; - const updatedTouchedFields = { - ...touchedFields, - [name]: true, - }; - - const validation = newOrChangedCredentials.validate( - { ...updatedData }, - { abortEarly: false, context: { password: updatedData.newPassword } } - ); - - const updatedErrors = getTouchedFieldErrors(validation, updatedTouchedFields); - - if (!touchedFields[name]) { - setTouchedFields(updatedTouchedFields); - } - - setLocalData(updatedData); - setErrors(updatedErrors); - }; - - const handleSubmit = async (event) => { - event.preventDefault(); - - const { error } = newOrChangedCredentials.validate(localData, { - abortEarly: false, - context: { password: localData.newPassword }, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - } else { - const action = await dispatch(update({ localData })); - if (action.payload.success) { - createToast({ - body: t( - "passwordPanel.passwordChangedSuccess", - "Your password was changed successfully." - ), - }); - setLocalData({ - password: "", - newPassword: "", - confirm: "", - }); - } else { - // TODO: Check for other errors? - createToast({ - body: t( - "passwordPanel.passwordInputIncorrect", - "Your password input was incorrect." - ), - }); - setErrors({ password: "*" + action.payload.msg + "." }); - } - } - }; - - return ( - - - - - ); -}; - -PasswordPanel.propTypes = { - // No props are being passed to this component, hence no specific PropTypes are defined. -}; - -export default PasswordPanel; diff --git a/client/src/Pages/v1/Account/components/ProfilePanel.jsx b/client/src/Pages/v1/Account/components/ProfilePanel.jsx deleted file mode 100644 index 2035861a2..000000000 --- a/client/src/Pages/v1/Account/components/ProfilePanel.jsx +++ /dev/null @@ -1,433 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { useState } from "react"; -import TabPanel from "@mui/lab/TabPanel"; -import { Box, Button, Divider, Stack, Typography } from "@mui/material"; -import Avatar from "@/Components/v1/Avatar/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; -import { useDispatch, useSelector } from "react-redux"; -import { - clearAuthState, - deleteUser, - update, -} from "../../../../Features/Auth/authSlice.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { logger } from "../../../../Utils/Logger.js"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import { useTranslation } from "react-i18next"; - -/** - * ProfilePanel component displays a form for editing user profile information - * and allows for actions like updating profile picture, credentials, - * and deleting account. - * - * @returns {JSX.Element} - */ - -const ProfilePanel = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const SPACING_GAP = theme.spacing(12); - - //redux state - const { user, isLoading } = useSelector((state) => state.auth); - - const idToName = { - "edit-first-name": "firstName", - "edit-last-name": "lastName", - // Disabled for now, will revisit in the future - // "edit-email": "email", - }; - - // Local state for form data, errors, and file handling - const [localData, setLocalData] = useState({ - firstName: user.firstName, - lastName: user.lastName, - // email: user.email, // Disabled for now - }); - const [errors, setErrors] = useState({}); - const [file, setFile] = useState(); - - // Handles input field changes and performs validation - const handleChange = (event) => { - errors["unchanged"] && clearError("unchanged"); - const { value, id } = event.target; - const name = idToName[id]; - setLocalData((prev) => ({ - ...prev, - [name]: value, - })); - - validateField({ [name]: value }, newOrChangedCredentials, name); - }; - - // Validates input against provided schema and updates error state - const validateField = (toValidate, schema, name = "picture") => { - const { error } = schema.validate(toValidate, { abortEarly: false }); - setErrors((prev) => { - const prevErrors = { ...prev }; - if (error) prevErrors[name] = error.details[0].message; - else delete prevErrors[name]; - return prevErrors; - }); - if (error) return true; - }; - - // Clears specific error from errors state - const clearError = (err) => { - setErrors((prev) => { - const updatedErrors = { ...prev }; - if (updatedErrors[err]) delete updatedErrors[err]; - return updatedErrors; - }); - }; - - // Resets picture-related states and clears interval - const removePicture = () => { - errors["picture"] && clearError("picture"); - setFile(undefined); - setLocalData((prev) => ({ - ...prev, - file: undefined, - deleteProfileImage: true, - })); - }; - - // Opens the picture update modal - const openPictureModal = () => { - setIsOpen("picture"); - setFile(undefined); - }; - - // Closes the picture update modal and resets related states - const closePictureModal = () => { - if (errors["picture"]) clearError("picture"); - setFile(undefined); - setIsOpen(""); - }; - - // Updates profile image displayed on UI - const handleUpdatePicture = () => { - setLocalData((prev) => ({ - ...prev, - file: file?.src, - deleteProfileImage: false, - })); - setIsOpen(""); - if (errors["unchanged"]) clearError("unchanged"); - }; - - // Handles form submission to update user profile - const handleSaveProfile = async (event) => { - event.preventDefault(); - const nameChanged = - localData.firstName !== user.firstName || localData.lastName !== user.lastName; - - const avatarChanged = - localData.deleteProfileImage === true || - (localData.file && localData.file !== `data:image/png;base64,${user.avatarImage}`); - - if (!nameChanged && !avatarChanged) { - createToast({ - body: "Unable to update profile — no changes detected.", - }); - setErrors({ unchanged: "unable to update profile" }); - return; - } - - const action = await dispatch(update({ localData })); - if (action.payload.success) { - createToast({ - body: "Your profile data was changed successfully.", - }); - } else { - createToast({ - body: "There was an error updating your profile data.", - }); - } - }; - - // Removes current profile image from UI - const handleDeletePicture = () => { - setLocalData((prev) => ({ - ...prev, - deleteProfileImage: true, - })); - setFile(undefined); - errors["unchanged"] && clearError("unchanged"); - }; - - // Initiates the account deletion process - const handleDeleteAccount = async () => { - const action = await dispatch(deleteUser()); - if (action.payload.success) { - dispatch(clearAuthState()); - } else { - if (action.payload) { - // dispatch errors - createToast({ - body: action.payload.msg, - }); - } else { - // unknown errors - createToast({ - body: "Unknown error.", - }); - } - } - }; - - // Modal state and control functions - const [isOpen, setIsOpen] = useState(""); - const isModalOpen = (name) => isOpen === name; - - return ( - - - - {/* This 0.9 is a bit magic numbering, refactor */} - - {t("FirstName")} - - - - - - {t("LastName")} - - - - - - {t("auth.common.inputs.email.label")} - - {t("EmailDescriptionText")} - - - logger.warn("disabled")} - disabled={true} - flex={1} - /> - - - - {t("YourPhoto")} - - {t("PhotoDescriptionText")} - - - - - - - - - - - ); -}; - -ProfilePanel.propTypes = { - // No props are being passed to this component, hence no specific PropTypes are defined. -}; - -export default ProfilePanel; diff --git a/client/src/Pages/v1/Account/components/RoleTable/index.jsx b/client/src/Pages/v1/Account/components/RoleTable/index.jsx deleted file mode 100644 index 02dcaaf0d..000000000 --- a/client/src/Pages/v1/Account/components/RoleTable/index.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import Typography from "@mui/material/Typography"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; - -import { useTranslation } from "react-i18next"; -import { ROLES } from "../../../../../Utils/roleUtils.js"; - -const RoleTable = ({ roles, handleDeleteRole }) => { - const { t } = useTranslation(); - const HEADERS = [ - { - id: "name", - content: {t("editUserPage.table.roleHeader")}, - render: (row) => { - return row; - }, - }, - { - id: "delete", - content: {t("editUserPage.table.actionHeader")}, - render: (row) => { - if (row === ROLES.SUPERADMIN) return null; - return ( - { - handleDeleteRole(row); - }} - sx={{ cursor: "pointer" }} - /> - ); - }, - }, - ]; - return ( - - ); -}; - -export default RoleTable; diff --git a/client/src/Pages/v1/Account/components/TeamPanel.jsx b/client/src/Pages/v1/Account/components/TeamPanel.jsx deleted file mode 100644 index 02918eb30..000000000 --- a/client/src/Pages/v1/Account/components/TeamPanel.jsx +++ /dev/null @@ -1,360 +0,0 @@ -import { useTheme } from "@emotion/react"; -import TabPanel from "@mui/lab/TabPanel"; -import { Button, ButtonGroup, Stack, Typography } from "@mui/material"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; -import { networkService } from "../../../../main.jsx"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import AddTeamMember from "./AddTeamMember/index.jsx"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import { useGetInviteToken } from "../../../../Hooks/v1/inviteHooks.js"; -import { useNavigate } from "react-router-dom"; -import { useIsSuperAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; -import AddMemberMenu from "./AddMemberMenu/index.jsx"; -/** - * TeamPanel component manages the organization and team members, - * providing functionalities like renaming the organization, managing team members, - * and inviting new members. - * - * @returns {JSX.Element} - */ - -const TeamPanel = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const SPACING_GAP = theme.spacing(12); - - const [toInvite, setToInvite] = useState({ - email: "", - role: ["user"], - }); - const [data, setData] = useState([]); - const [members, setMembers] = useState([]); - const [filter, setFilter] = useState("all"); - const [isDisabled, setIsDisabled] = useState(true); - const [errors, setErrors] = useState({}); - const [isSendingInvite, setIsSendingInvite] = useState(false); - const navigate = useNavigate(); - const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken(); - const isSuperAdmin = useIsSuperAdmin(); - - const headers = [ - { - id: "name", - content: t("teamPanel.table.name"), - render: (row) => { - return ( - - - {row.firstName + " " + row.lastName} - - - {t("teamPanel.table.created")}{" "} - {new Date(row.createdAt).toLocaleDateString()} - - - ); - }, - }, - { id: "email", content: t("teamPanel.table.email"), render: (row) => row.email }, - { - id: "role", - content: t("teamPanel.table.role"), - render: (row) => row.role, - }, - ]; - const [refreshTrigger, setRefreshTrigger] = useState(false); - const refreshTeamList = () => { - setRefreshTrigger((prev) => !prev); - }; - useEffect(() => { - const fetchTeam = async () => { - try { - const response = await networkService.getAllUsers(); - setMembers(response.data.data); - } catch (error) { - createToast({ - body: error.message || "Error fetching team members.", - }); - } - }; - - fetchTeam(); - }, [refreshTrigger]); - - useEffect(() => { - const ROLE_MAP = { - superadmin: t("roles.superAdmin"), - admin: t("roles.admin"), - user: t("roles.teamMember"), - demo: t("roles.demoUser"), - }; - let team = members; - if (filter !== "all") - team = members.filter((member) => { - if (filter === "admin") { - return member.role.includes("admin") || member.role.includes("superadmin"); - } - return member.role.includes(filter); - }); - - team = team.map((member) => ({ - ...member, - id: member._id, - role: member.role.map((role) => ROLE_MAP[role]).join(","), - })); - setData(team); - }, [members, filter, t]); - - useEffect(() => { - setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === ""); - }, [errors, toInvite.email]); - const [isOpen, setIsOpen] = useState(false); - const [isRegisterOpen, setIsRegisterOpen] = useState(false); - const handleIsRegisterOpen = (open) => { - setIsRegisterOpen(open); - }; - - const handleChange = (event) => { - const { value } = event.target; - const newEmail = value?.toLowerCase() || value; - setToInvite((prev) => ({ - ...prev, - email: newEmail, - })); - - const validation = newOrChangedCredentials.validate( - { email: newEmail }, - { abortEarly: false } - ); - - setErrors((prev) => { - const updatedErrors = { ...prev }; - - if (validation.error) { - updatedErrors.email = validation.error.details[0].message; - } else { - delete updatedErrors.email; - } - return updatedErrors; - }); - }; - - const handleGetToken = async () => { - await getInviteToken({ email: toInvite.email, role: toInvite.role }); - }; - const handleInviteMember = async () => { - if (!toInvite.email) { - setErrors((prev) => ({ ...prev, email: "Email is required." })); - return; - } - setIsSendingInvite(true); - if (!toInvite.role.includes("user") || !toInvite.role.includes("admin")) - setToInvite((prev) => ({ ...prev, role: ["user"] })); - - const { error } = newOrChangedCredentials.validate( - { email: toInvite.email }, - { - abortEarly: false, - } - ); - - if (error) { - setErrors((prev) => ({ ...prev, email: error.details[0].message })); - return; - } - - try { - await networkService.sendInvitationToken({ - email: toInvite.email, - role: toInvite.role, - }); - closeInviteModal(); - createToast({ - body: "Member invited. They will receive an email with details on how to create their account.", - }); - } catch (error) { - createToast({ - body: error?.response?.data?.msg || error.message || "Unknown error.", - }); - } finally { - setIsSendingInvite(false); - } - }; - - const closeInviteModal = () => { - setIsOpen(false); - clearToken(); - setToInvite({ email: "", role: ["0"] }); - setErrors({}); - }; - - return ( - - - {t("teamPanel.teamMembers")} - - - - - - - - - - - setIsOpen(true)} - handleIsRegisterOpen={handleIsRegisterOpen} - /> - - - { - if (isSuperAdmin) { - navigate(`/account/team/${row.id}`); - } - }, - }} - /> - - - - setSelectedMonitor(e.target.value)} - items={monitorNames} - sx={{ - backgroundColor: theme.palette.primary.main, - color: theme.palette.primary.contrastTextSecondary, - }} - maxWidth={250} - /> - - - - {t("incidentsOptionsHeaderFilterBy")} - - - - - - - - - - ); -}; - -export default CreateInfrastructureMonitor; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx deleted file mode 100644 index ee100bb0e..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx +++ /dev/null @@ -1,37 +0,0 @@ -// Components -import { Typography } from "@mui/material"; -import BaseContainer from "../BaseContainer/index.jsx"; -import AreaChart from "@/Components/v1/Charts/AreaChart/index.jsx"; -// Utils -import { useTheme } from "@emotion/react"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; -const InfraAreaChart = ({ config }) => { - const theme = useTheme(); - const { getDimensions } = useHardwareUtils(); - return ( - - - {config.heading} - - - - ); -}; - -export default InfraAreaChart; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx deleted file mode 100644 index 3465683d9..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx +++ /dev/null @@ -1,140 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import InfraAreaChart from "./InfraAreaChart.jsx"; -import SkeletonLayout from "./skeleton.jsx"; - -// Utils -import { - PercentTick, - TzTick, - InfrastructureTooltip, - TemperatureTooltip, -} from "@/Components/v1/Charts/Utils/chartUtils.jsx"; -import { useTheme } from "@emotion/react"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; -import { useTranslation } from "react-i18next"; -const AreaChartBoxes = ({ shouldRender, monitor, dateRange }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const { buildTemps } = useHardwareUtils(); - - if (!shouldRender) { - return ; - } - - const { stats } = monitor ?? {}; - const { checks } = stats; - - let latestCheck = checks[0]; - const { temps, tempKeys } = buildTemps(checks); - - const configs = [ - { - type: "memory", - data: checks, - dataKeys: ["avgMemoryUsage"], - heading: t("memoryUsage"), - strokeColor: theme.palette.accent.main, // CAIO_REVIEW - gradientStartColor: theme.palette.accent.main, // CAIO_REVIEW - yLabel: t("memoryUsage"), - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - }, - { - type: "cpu", - data: checks, - dataKeys: ["avgCpuUsage"], - heading: t("cpuUsage"), - strokeColor: theme.palette.success.main, - gradientStartColor: theme.palette.success.main, - yLabel: t("cpuUsage"), - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - }, - { - type: "temperature", - data: temps, - dataKeys: tempKeys, - strokeColor: theme.palette.error.main, - gradientStartColor: theme.palette.error.main, - heading: t("cpuTemperature"), - yLabel: "Temperature", - xTick: , - yDomain: [ - 0, - Math.max(Math.max(...temps.flatMap((t) => tempKeys.map((k) => t[k]))) * 1.1, 200), - ], - toolTip: ( - - ), - }, - ...(latestCheck?.disks?.map((disk, idx) => ({ - type: "disk", - data: checks, - diskIndex: idx, - dataKeys: [`disks[${idx}].usagePercent`], - heading: `Disk${idx} usage`, - strokeColor: theme.palette.warning.main, - gradientStartColor: theme.palette.warning.main, - yLabel: t("diskUsage"), - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - })) || []), - ]; - - return ( - *": { - flexBasis: `calc(50% - ${theme.spacing(8)})`, - maxWidth: `calc(50% - ${theme.spacing(8)})`, - }, - }} - > - {configs.map((config) => ( - - ))} - - ); -}; - -export default AreaChartBoxes; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx deleted file mode 100644 index f787b88ec..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; -import { useTheme } from "@emotion/react"; -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx deleted file mode 100644 index dd73d1a65..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Renders a base box with consistent styling - * @param {Object} props - Component properties - * @param {React.ReactNode} props.children - Child components to render inside the box - * @param {Object} props.sx - Additional styling for the box - * @returns {React.ReactElement} Styled box component - */ - -// Components -import { Box } from "@mui/material"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; -import PropTypes from "prop-types"; - -const BaseContainer = ({ children, sx = {} }) => { - const theme = useTheme(); - const { getDimensions } = useHardwareUtils(); - return ( - - {children} - - ); -}; - -BaseContainer.propTypes = { - children: PropTypes.node.isRequired, - sx: PropTypes.object, -}; - -export default BaseContainer; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx deleted file mode 100644 index cd61b7109..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx +++ /dev/null @@ -1,88 +0,0 @@ -// Components -import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx"; -import BaseContainer from "../BaseContainer/index.jsx"; -import { Stack, Typography, Box } from "@mui/material"; -// Utils -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; - -const Gauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { - const theme = useTheme(); - - const valueStyle = { - borderRadius: theme.spacing(2), - backgroundColor: theme.palette.tertiary.main, - width: "40%", - mb: theme.spacing(2), - mt: theme.spacing(2), - pr: theme.spacing(2), - textAlign: "right", - }; - - return ( - - - - - - {heading} - - - - - {metricOne} - {valueOne} - - - {metricTwo} - {valueTwo} - - - - - ); -}; - -Gauge.propTypes = { - value: PropTypes.number, - heading: PropTypes.string, - metricOne: PropTypes.string, - valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), - metricTwo: PropTypes.string, - valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), -}; - -export default Gauge; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx deleted file mode 100644 index 432881e7e..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx +++ /dev/null @@ -1,89 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Gauge from "./Gauge.jsx"; -import SkeletonLayout from "./skeleton.jsx"; -import PropTypes from "prop-types"; - -// Utils -import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -const Gauges = ({ isLoading = false, monitor }) => { - const { decimalToPercentage, formatBytes } = useHardwareUtils(); - const theme = useTheme(); - const { t } = useTranslation(); - - if (isLoading) { - return ; - } - - const { stats } = monitor ?? {}; - let latestCheck = stats?.aggregateData?.latestCheck; - const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0; - const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0; - const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; - const cpuUsagePercent = latestCheck?.cpu?.usage_percent ?? 0; - const cpuPhysicalCores = latestCheck?.cpu?.physical_core ?? 0; - const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; - - const gauges = [ - { - type: "memory", - value: decimalToPercentage(memoryUsagePercent), - heading: t("memoryUsage"), - metricOne: t("used"), - valueOne: formatBytes(memoryUsedBytes, true), - metricTwo: t("total"), - valueTwo: formatBytes(memoryTotalBytes, true), - }, - { - type: "cpu", - value: decimalToPercentage(cpuUsagePercent), - heading: t("cpuUsage"), - metricOne: t("cores"), - valueOne: cpuPhysicalCores ?? 0, - metricTwo: t("frequency"), - valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`, - }, - ...(latestCheck?.disk ?? []).map((disk, idx) => ({ - type: "disk", - diskIndex: idx, - value: decimalToPercentage(disk.usage_percent), - heading: `Disk${idx} usage`, - metricOne: t("used"), - valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true), - metricTwo: t("total"), - valueTwo: formatBytes(disk.total_bytes, true), - })), - ]; - - return ( - - {gauges.map((gauge) => { - return ( - - ); - })} - - ); -}; - -Gauges.propTypes = { - isLoading: PropTypes.bool, - monitor: PropTypes.object, -}; - -export default Gauges; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx deleted file mode 100644 index 347a52901..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - - {Array.from({ length: 3 }).map((_, idx) => { - return ( - - ); - })} - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx deleted file mode 100644 index d9f93311c..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ /dev/null @@ -1,132 +0,0 @@ -import PropTypes from "prop-types"; -import { Stack, Typography } from "@mui/material"; -import InfraAreaChart from "../AreaChartBoxes/InfraAreaChart.jsx"; - -import { - TzTick, - InfrastructureTooltip, - NetworkTick, -} from "@/Components/v1/Charts/Utils/chartUtils.jsx"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; - -const NetworkCharts = ({ ethernetData, dateRange }) => { - const theme = useTheme(); - const { t } = useTranslation(); - - const { formatBytesPerSecondString, formatPacketsPerSecondString } = useHardwareUtils(); - - if (!ethernetData?.length) { - return {t("noNetworkStatsAvailable")}; - } - - const configs = [ - { - type: "network-bytes", - data: ethernetData, - dataKeys: ["bytesPerSec"], - heading: t("dataReceived"), - strokeColor: theme.palette.info.main, - gradientStartColor: theme.palette.info.main, - yLabel: t("rate"), - xTick: , - yTick: , - toolTip: ( - - ), - }, - { - type: "network-packets", - data: ethernetData, - dataKeys: ["packetsPerSec"], - heading: t("packetsReceivedRate"), - strokeColor: theme.palette.success.main, - gradientStartColor: theme.palette.success.main, - yLabel: t("rate"), - xTick: , - yTick: , - toolTip: ( - - ), - }, - { - type: "network-errors", - data: ethernetData, - dataKeys: ["errors"], - heading: t("networkErrors"), - strokeColor: theme.palette.error.main, - gradientStartColor: theme.palette.error.main, - yLabel: t("errors"), - xTick: , - toolTip: ( - Math.round(value).toLocaleString()} - /> - ), - }, - { - type: "network-drops", - data: ethernetData, - dataKeys: ["drops"], - heading: t("networkDrops"), - strokeColor: theme.palette.warning.main, - gradientStartColor: theme.palette.warning.main, - yLabel: t("drops"), - xTick: , - toolTip: ( - Math.round(value).toLocaleString()} - /> - ), - }, - ]; - - return ( - *": { - flexBasis: `calc(50% - ${theme.spacing(8)})`, - maxWidth: `calc(50% - ${theme.spacing(8)})`, - }, - }} - > - {configs.map((config) => ( - - ))} - - ); -}; - -NetworkCharts.propTypes = { - ethernetData: PropTypes.array.isRequired, - dateRange: PropTypes.string.isRequired, -}; - -export default NetworkCharts; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx deleted file mode 100644 index 0747dfcd1..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import PropTypes from "prop-types"; -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; -import { Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; - -function formatNumber(num) { - return num != null ? num.toLocaleString() : "0"; -} - -const NetworkStatBoxes = ({ shouldRender, net, ifaceName }) => { - const { t } = useTranslation(); - const { formatBytes } = useHardwareUtils(); - - const filtered = net?.filter((iface) => iface.name === ifaceName) || []; - - if (!net?.length) { - return {t("noNetworkStatsAvailable")}; - } - - return ( - - {filtered - .map((iface) => [ - , - , - , - , - , - , - ]) - .flat()} - - ); -}; - -NetworkStatBoxes.propTypes = { - shouldRender: PropTypes.bool.isRequired, - net: PropTypes.arrayOf( - PropTypes.shape({ - name: PropTypes.string.isRequired, - bytes_sent: PropTypes.number, - bytes_recv: PropTypes.number, - packets_sent: PropTypes.number, - packets_recv: PropTypes.number, - err_in: PropTypes.number, - err_out: PropTypes.number, - }) - ), - ifaceName: PropTypes.string.isRequired, -}; - -export default NetworkStatBoxes; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx deleted file mode 100644 index 78db333e6..000000000 --- a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from "prop-types"; -import { useState, useEffect } from "react"; -import { Box } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import NetworkStatBoxes from "./NetworkStatBoxes.jsx"; -import NetworkCharts from "./NetworkCharts.jsx"; -import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; - -const getAvailableInterfaces = (net) => { - return (net || []).map((iface) => iface.name).filter(Boolean); -}; - -const getNetworkInterfaceData = (checks, ifaceName) => { - if (!ifaceName) return []; - - // Transform backend data structure for the selected interface - // Backend already calculates deltas, we just reshape the data - return (checks || []) - .map((check) => { - const networkInterface = (check.net || []).find( - (iface) => iface.name === ifaceName - ); - if (!networkInterface) return null; - return { - _id: check._id, - bytesPerSec: networkInterface.deltaBytesRecv, - packetsPerSec: networkInterface.deltaPacketsRecv, - errors: networkInterface.deltaErrOut ?? 0, - drops: networkInterface.deltaDropOut ?? 0, - }; - }) - .filter(Boolean); -}; - -const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { - const { t } = useTranslation(); - const theme = useTheme(); - - const availableInterfaces = getAvailableInterfaces(net); - const [selectedInterface, setSelectedInterface] = useState(""); - - // Set default interface when data loads - useEffect(() => { - if (availableInterfaces.length > 0 && !selectedInterface) { - setSelectedInterface(availableInterfaces[0]); - } - }, [availableInterfaces, selectedInterface]); - - const ethernetData = getNetworkInterfaceData(checks, selectedInterface); - - return ( - <> - - - {availableInterfaces.length > 0 && ( - { - setLogLevel(e.target.value); - }} - /> - - - - - {logs - ?.filter((log) => { - if (logLevel === "all") return true; - return log.level === logLevel; - }) - .reverse() - .map((log, idx) => formatLog(theme, log, idx))} - - - - ); -}; - -export default Logs; diff --git a/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx deleted file mode 100644 index 743f52639..000000000 --- a/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import DataTable from "@/Components/v1/Table/index.jsx"; - -import { useNavigate } from "react-router-dom"; -import { useTheme } from "@emotion/react"; -import { TypeToPathMap } from "../../../../../../Utils/monitorUtils.js"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -const FailedJobTable = ({ metrics = {} }) => { - const { t } = useTranslation(); - const theme = useTheme(); - const jobsWithFailures = metrics?.jobsWithFailures; - const navigate = useNavigate(); - - const headers = [ - { - id: "monitorId", - content: t("queuePage.failedJobTable.monitorIdHeader"), - render: (row) => { - return row.monitorId; - }, - }, - { - id: "monitorUrl", - content: t("queuePage.failedJobTable.monitorUrlHeader"), - render: (row) => { - return row.monitorUrl; - }, - }, - { - id: "failCount", - content: t("queuePage.failedJobTable.failCountHeader"), - render: (row) => { - return row.failCount; - }, - }, - { - id: "failedAt", - content: t("queuePage.failedJobTable.failedAtHeader"), - render: (row) => { - return row.failedAt; - }, - }, - { - id: "failReason", - content: t("queuePage.failedJobTable.failReasonHeader"), - render: (row) => { - return row.failReason; - }, - }, - ]; - return ( - - {t("queuePage.failedJobTable.title")} - { - const path = TypeToPathMap[row.monitorType]; - navigate(`/${path}/${row.monitorId}`); - }, - rowSX: { - cursor: "pointer", - "&:hover td": { - backgroundColor: theme.palette.tertiary.main, - transition: "background-color .3s ease", - }, - }, - }} - /> - - ); -}; - -FailedJobTable.propTypes = { - metrics: PropTypes.object, -}; - -export default FailedJobTable; diff --git a/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx deleted file mode 100644 index 7d309e845..000000000 --- a/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx +++ /dev/null @@ -1,130 +0,0 @@ -import Stack from "@mui/material/Stack"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import Typography from "@mui/material/Typography"; -// Utils -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { TypeToPathMap } from "../../../../../../Utils/monitorUtils.js"; -import { useTranslation } from "react-i18next"; -import { createHeaderFactory } from "@/Components/v1/Table/TableUtils.js"; - -const JobTable = ({ jobs = [] }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const buildSx = (row) => { - if (row.lockedAt) { - return { - color: `${theme.palette.success.main} !important`, - }; - } - if (!row.active) { - return { - color: `${theme.palette.warning.main} !important`, - }; - } - - if (row.failCount > 0 && row.lastFailedAt >= row.lastFinishedAt) { - return { - color: `${theme.palette.error.main} !important`, - }; - } - - return {}; - }; - - const createHeader = createHeaderFactory(buildSx); - const headersData = [ - { - id: "id", - content: t("queuePage.jobTable.idHeader"), - render: (row) => row.monitorId, - }, - { - id: "url", - content: t("queuePage.jobTable.urlHeader"), - render: (row) => row.monitorUrl, - }, - { - id: "interval", - content: t("queuePage.jobTable.intervalHeader"), - render: (row) => `${row.monitorInterval} ms`, - }, - { - id: "type", - content: t("queuePage.jobTable.typeHeader"), - render: (row) => row.monitorType, - }, - { - id: "active", - content: t("queuePage.jobTable.activeHeader"), - render: (row) => row.active.toString(), - }, - { - id: "runCount", - content: t("queuePage.jobTable.runCountHeader"), - render: (row) => row.runCount, - }, - { - id: "failCount", - content: t("queuePage.jobTable.failCountHeader"), - render: (row) => row.failCount, - }, - { - id: "lastRun", - content: t("queuePage.jobTable.lastRunHeader"), - render: (row) => row.lastRunAt || "-", - }, - { - id: "lockedAt", - content: t("queuePage.jobTable.lockedAtHeader"), - render: (row) => row.lockedAt || "-", - }, - - { - id: "lastFinish", - content: t("queuePage.jobTable.lastFinishedAtHeader"), - render: (row) => row.lastFinishedAt || "-", - }, - { - id: "lastRunTook", - content: t("queuePage.jobTable.lastRunTookHeader"), - render: (row) => { - const value = row.lastRunTook ? row.lastRunTook + " ms" : "-"; - return value; - }, - }, - ]; - - const headers = headersData.map((header) => createHeader(header)); - - return ( - - {t("queuePage.jobTable.title")} - { - const path = TypeToPathMap[row.monitorType]; - navigate(`/${path}/${row.monitorId}`); - }, - rowSX: { - cursor: "pointer", - "&:hover td": { - backgroundColor: theme.palette.tertiary.main, - transition: "background-color .3s ease", - }, - }, - }} - /> - - ); -}; - -JobTable.propTypes = { - jobs: PropTypes.array, -}; - -export default JobTable; diff --git a/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx b/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx deleted file mode 100644 index c4f4389c3..000000000 --- a/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import Stack from "@mui/material/Stack"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; - -const camelToTitle = (str) => { - return str - .replace(/([A-Z])/g, " $1") - .toLowerCase() - .replace(/^./, (m) => m.toUpperCase()); -}; - -const Metrics = ({ metrics = {} }) => { - const { t } = useTranslation(); - const theme = useTheme(); - - const data = Object.keys(metrics) - .filter((key) => key !== "jobsWithFailures") - .map((key) => { - return { key, title: camelToTitle(key), value: metrics[key] }; - }); - - return ( - - {data.map((metric) => { - return ( - - ); - })} - - ); -}; -export default Metrics; diff --git a/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx deleted file mode 100644 index d96f37a20..000000000 --- a/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import DataTable from "@/Components/v1/Table/index.jsx"; - -// Utils -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; -import PropTypes from "prop-types"; - -const camelToTitle = (str) => { - return str - .replace(/([A-Z])/g, " $1") - .toLowerCase() - .replace(/^./, (m) => m.toUpperCase()); -}; - -const Metrics = ({ metrics = {} }) => { - const { t } = useTranslation(); - const theme = useTheme(); - const keys = Object.keys(metrics); - - const headers = [ - { - id: "metric", - content: t("queuePage.metricsTable.metricHeader"), - render: (row) => { - return {row.key}; - }, - }, - { - id: "value", - content: t("queuePage.metricsTable.valueHeader"), - render: (row) => { - return {row.value}; - }, - }, - ]; - - const data = keys - .filter((key) => key !== "jobsWithFailures") - .map((key) => { - return { key: camelToTitle(key), value: metrics[key] }; - }); - - return ( - - {t("queuePage.metricsTable.title")} - - - ); -}; - -Metrics.propTypes = { - metrics: PropTypes.object, -}; - -export default Metrics; diff --git a/client/src/Pages/v1/Logs/Queue/index.jsx b/client/src/Pages/v1/Logs/Queue/index.jsx deleted file mode 100644 index 10eca4d3b..000000000 --- a/client/src/Pages/v1/Logs/Queue/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import JobTable from "./components/JobTable/index.jsx"; -import Metrics from "./components/Metrics/index.jsx"; -import FailedJobTable from "./components/FailedJobTable/index.jsx"; -import ButtonGroup from "@mui/material/ButtonGroup"; -import Button from "@mui/material/Button"; -import Typography from "@mui/material/Typography"; -import Divider from "@mui/material/Divider"; - -// Utils -import { useState } from "react"; -import { useFetchQueueData, useFlushQueue } from "../../../../Hooks/v1/logHooks.js"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; - -const QueueDetails = () => { - // Local state - const [trigger, setTrigger] = useState(false); - - // Hooks - const { t } = useTranslation(); - const theme = useTheme(); - const [jobs, metrics, isLoading, error] = useFetchQueueData(trigger); - const [flushQueue, isFlushing, flushError] = useFlushQueue(); - - if (isLoading) return
Loading...
; - if (error || flushError) return
Error: {error.message}
; - - return ( - - - - - - - - - - - - - ); -}; - -export default QueueDetails; diff --git a/client/src/Pages/v1/Logs/index.jsx b/client/src/Pages/v1/Logs/index.jsx deleted file mode 100644 index e5bdf623d..000000000 --- a/client/src/Pages/v1/Logs/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Tabs from "@mui/material/Tabs"; -import Tab from "@mui/material/Tab"; -import Queue from "./Queue/index.jsx"; -import LogsComponent from "./Logs/index.jsx"; -import Diagnostics from "./Diagnostics/index.jsx"; - -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { useState } from "react"; - -const Logs = () => { - const { t } = useTranslation(); - const theme = useTheme(); - - // Local state - const [value, setValue] = useState(0); - - // Handlers - const handleChange = (event, newValue) => { - setValue(newValue); - }; - - const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }]; - return ( - - - - - - - - {value === 0 && } - {value === 1 && } - {value === 2 && } - - ); -}; - -export default Logs; diff --git a/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx deleted file mode 100644 index 8d4c0ae53..000000000 --- a/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import PropTypes from "prop-types"; -const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => { - const getValueById = (config, id) => { - const item = config.find((config) => config._id === id); - return item ? (item.value ? item.value : item.name) : null; - }; - - const getIdByValue = (config, name) => { - const item = config.find((config) => { - if (config.value) { - return config.value === name; - } else { - return config.name === name; - } - }); - return item ? item._id : null; - }; - - return ( - - - - - - - {t(TITLE_MAP[type])} - - {t(DESCRIPTION_MAP[type])} - - - {type === "matrix" ? ( - <> - - - - - ) : ( - - )} - - {" "} - - - {notificationId && ( - - )} - - - - setIsOpen(false)} - onCancel={() => setIsOpen(false)} - title={t("createNotifications.dialogDeleteTitle")} - confirmationButtonLabel={t("createNotifications.dialogDeleteConfirm")} - onConfirm={onDelete} - isLoading={isDeleting} - /> - - ); -}; - -export default CreateNotifications; diff --git a/client/src/Pages/v1/Notifications/index.jsx b/client/src/Pages/v1/Notifications/index.jsx deleted file mode 100644 index 78ec6e9bb..000000000 --- a/client/src/Pages/v1/Notifications/index.jsx +++ /dev/null @@ -1,121 +0,0 @@ -// Components -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Button from "@mui/material/Button"; -import DataTable from "@/Components/v1/Table/index.jsx"; -import ActionMenu from "./components/ActionMenu.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { - useGetNotificationsByTeamId, - useDeleteNotification, -} from "../../../Hooks/v1/useNotifications.js"; -import { useTranslation } from "react-i18next"; - -const Notifications = () => { - const navigate = useNavigate(); - const theme = useTheme(); - const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }]; - const [updateTrigger, setUpdateTrigger] = useState(false); - const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger); - const [deleteNotification, isDeleting, deleteError] = useDeleteNotification(); - const { t } = useTranslation(); - // Handlers - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const onDelete = (id) => { - deleteNotification(id, triggerUpdate); - }; - - const headers = [ - { - id: "name", - content: "Name", - render: (row) => { - return row.notificationName; - }, - }, - { - id: "target", - content: "Target", - render: (row) => { - return row.address; - }, - }, - { - id: "platform", - content: "Platform", - render: (row) => { - return row?.config?.platform || row.type; - }, - }, - - { - id: "actions", - content: "Actions", - onClick: (e) => { - e.stopPropagation(); - }, - render: (row) => { - return ( - - ); - }, - }, - ]; - - return ( - <> - - - - - - - {t("notifications.createTitle")} - navigate(`/notifications/${row._id}`), - rowSX: { - cursor: "pointer", - "&:hover td": { - backgroundColor: theme.palette.tertiary.main, - transition: "background-color .3s ease", - }, - }, - }} - headers={headers} - data={notifications} - /> - - - - ); -}; - -export default Notifications; diff --git a/client/src/Pages/v1/Notifications/utils.js b/client/src/Pages/v1/Notifications/utils.js deleted file mode 100644 index 2a3d32f4d..000000000 --- a/client/src/Pages/v1/Notifications/utils.js +++ /dev/null @@ -1,44 +0,0 @@ -export const NOTIFICATION_TYPES = [ - { _id: 1, name: "E-mail", value: "email" }, - { _id: 2, name: "Slack", value: "slack" }, - { _id: 3, name: "PagerDuty", value: "pager_duty" }, - { _id: 4, name: "Webhook", value: "webhook" }, - { _id: 5, name: "Discord", value: "discord" }, - { _id: 6, name: "Matrix", value: "matrix" }, -]; - -export const TITLE_MAP = { - email: "createNotifications.emailSettings.title", - slack: "createNotifications.slackSettings.title", - pager_duty: "createNotifications.pagerdutySettings.title", - webhook: "createNotifications.webhookSettings.title", - discord: "createNotifications.discordSettings.title", - matrix: "createNotifications.matrixSettings.title", -}; - -export const DESCRIPTION_MAP = { - email: "createNotifications.emailSettings.description", - slack: "createNotifications.slackSettings.description", - pager_duty: "createNotifications.pagerdutySettings.description", - webhook: "createNotifications.webhookSettings.description", - discord: "createNotifications.discordSettings.description", - matrix: "createNotifications.matrixSettings.description", -}; - -export const LABEL_MAP = { - email: "createNotifications.emailSettings.emailLabel", - slack: "createNotifications.slackSettings.webhookLabel", - pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel", - webhook: "createNotifications.webhookSettings.webhookLabel", - discord: "createNotifications.discordSettings.webhookLabel", - matrix: "createNotifications.matrixSettings.homeserverLabel", -}; - -export const PLACEHOLDER_MAP = { - email: "createNotifications.emailSettings.emailPlaceholder", - slack: "createNotifications.slackSettings.webhookPlaceholder", - pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder", - webhook: "createNotifications.webhookSettings.webhookPlaceholder", - discord: "createNotifications.discordSettings.webhookPlaceholder", - matrix: "createNotifications.matrixSettings.homeserverPlaceholder", -}; diff --git a/client/src/Pages/v1/PageSpeed/Create/index.css b/client/src/Pages/v1/PageSpeed/Create/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/v1/PageSpeed/Create/index.jsx b/client/src/Pages/v1/PageSpeed/Create/index.jsx deleted file mode 100644 index cb856bd37..000000000 --- a/client/src/Pages/v1/PageSpeed/Create/index.jsx +++ /dev/null @@ -1,509 +0,0 @@ -// Components -import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; -import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; -import SkeletonLayout from "./skeleton.jsx"; -import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; - -// Utils -import { useState, useEffect } from "react"; -import { useSelector } from "react-redux"; -import { monitorValidation } from "../../../../Validation/validation.js"; -import { parseDomainName } from "../../../../Utils/monitorUtils.js"; -import { useTranslation } from "react-i18next"; -import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; -import { useTheme } from "@emotion/react"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; - -import { useParams } from "react-router"; -import { useMonitorUtils } from "../../../../Hooks/v1/useMonitorUtils.js"; -import { - useCreateMonitor, - useFetchMonitorById, - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, -} from "../../../../Hooks/v1/monitorHooks.js"; - -const PageSpeedSetup = () => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined"; - const CRUMBS = [ - { name: "pagespeed", path: "/pagespeed" }, - ...(isCreate - ? [{ name: "create", path: `/pagespeed/create` }] - : [ - { name: "details", path: `/pagespeed/${monitorId}` }, - { name: "configure", path: `/pagespeed/configure/${monitorId}` }, - ]), - ]; - - // States - const [monitor, setMonitor] = useState( - isCreate - ? { - url: "", - name: "", - type: "pagespeed", - notifications: [], - interval: 180000, - } - : {} - ); - const [https, setHttps] = useState(true); - const [errors, setErrors] = useState({}); - const [isOpen, setIsOpen] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - - // Setup - const { t } = useTranslation(); - const theme = useTheme(); - // Constants - const MS_PER_MINUTE = 60000; - const FREQUENCIES = [ - { _id: 3, name: t("time.threeMinutes") }, - { _id: 5, name: t("time.fiveMinutes") }, - { _id: 10, name: t("time.tenMinutes") }, - { _id: 20, name: t("time.twentyMinutes") }, - { _id: 60, name: t("time.oneHour") }, - { _id: 1440, name: t("time.oneDay") }, - { _id: 10080, name: t("time.oneWeek") }, - ]; - - const { user } = useSelector((state) => state.auth); - const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); - const [notifications, notificationsAreLoading, notificationsError] = - useGetNotificationsByTeamId(); - - // Hooks for API actions - const [isLoading] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger }); - const [createMonitor, isCreating] = useCreateMonitor(); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - const [pauseMonitor, isPausing] = usePauseMonitor(); - - // Handlers - const onSubmit = async (event) => { - event.preventDefault(); - if (isCreate) { - let form = { - url: `http${https ? "s" : ""}://` + monitor.url, - name: monitor.name === "" ? monitor.url : monitor.name, - type: monitor.type, - interval: monitor.interval, - }; - - const { error } = monitorValidation.validate(form, { abortEarly: false }); - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: t("checkFormError") }); - return; - } - - form = { - ...form, - description: form.name, - notifications: monitor.notifications, - }; - await createMonitor({ monitor: form, redirect: "/pagespeed" }); - } else { - const monitorParams = { - url: monitor.url, - name: monitor.name === "" ? monitor.url : monitor.name, - type: monitor.type, - interval: monitor.interval, - }; - const { error } = monitorValidation.validate(monitorParams, { abortEarly: false }); - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: t("checkFormError") }); - return; - } - await updateMonitor({ monitor, redirect: "/pagespeed" }); - } - }; - - const handleChange = (event) => { - let { value, name } = event.target; - - if (name === "interval") { - value = value * MS_PER_MINUTE; - } - - setMonitor({ - ...monitor, - [name]: value, - }); - - const { error } = monitorValidation.validate( - { [name]: value, type: monitor.type }, - { abortEarly: false } - ); - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - const handleBlur = (event) => { - const { name, value } = event.target; - if (name === "url" && monitor.name === "") { - setMonitor((prev) => ({ ...prev, name: parseDomainName(value) })); - } - }; - - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/pagespeed" }); - }; - - const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; - - if (Object.keys(monitor).length === 0) { - return ; - } - - return ( - - - - - - - - {!isCreate ? monitor.name : t("createYour") + " "} - - {isCreate ? ( - - {t("pageSpeedMonitor")} - - ) : ( - <> - )} - - {!isCreate && ( - - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - )} - - {!isCreate && ( - - - - - )} - - - - - {t("settingsGeneralSettings")} - - - {t("pageSpeedConfigureSettingsDescription")} - - - .Mui-disabled)": { - backgroundColor: theme.palette.tertiary.main, - }, - }} - > - : undefined} - placeholder="random.website.com" - value={monitor.url || ""} - onChange={handleChange} - onBlur={isCreate ? handleBlur : undefined} - error={!!errors["url"]} - helperText={errors["url"]} - disabled={!isCreate} - /> - - - - {isCreate && ( - - - - {t("distributedUptimeCreateChecks")} - - - {t("distributedUptimeCreateChecksDescription")} - - - - - - - - - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - - )} - - - - {t("notificationConfig.title")} - - {t("notificationConfig.description")} - - - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - - - - - ); -}; - -SettingsUI.propTypes = { - HEADING_SX: PropTypes.object, - handleChange: PropTypes.func, - mode: PropTypes.string, - language: PropTypes.string, -}; - -export default SettingsUI; diff --git a/client/src/Pages/v1/Settings/SettingsURL.jsx b/client/src/Pages/v1/Settings/SettingsURL.jsx deleted file mode 100644 index c52dcf317..000000000 --- a/client/src/Pages/v1/Settings/SettingsURL.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import Box from "@mui/material/Box"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import { PropTypes } from "prop-types"; -import { useTranslation } from "react-i18next"; - -const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => { - const { t } = useTranslation(); - const theme = useTheme(); - return ( - - - - {t("settingsPage.urlSettings.title")} - - - {t("settingsPage.urlSettings.description")} - - - - - - - ); -}; - -SettingsURL.propTypes = { - HEADING_SX: PropTypes.object, - handleChange: PropTypes.func, - showURL: PropTypes.bool, -}; - -export default SettingsURL; diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx deleted file mode 100644 index 2732e331a..000000000 --- a/client/src/Pages/v1/Settings/index.jsx +++ /dev/null @@ -1,267 +0,0 @@ -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import SettingsTimeZone from "./SettingsTimeZone.jsx"; -import SettingsUI from "./SettingsUI.jsx"; -import SettingsURL from "./SettingsURL.jsx"; -import SettingsPagespeed from "./SettingsPagespeed.jsx"; -import SettingsDemoMonitors from "./SettingsDemoMonitors.jsx"; -import SettingsAbout from "./SettingsAbout.jsx"; -import SettingsEmail from "./SettingsEmail.jsx"; -import SettingsGlobalThresholds from "./SettingsGlobalThresholds.jsx"; -import Button from "@mui/material/Button"; -// Utils -import { settingsValidation } from "../../../Validation/validation.js"; -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { useSelector, useDispatch } from "react-redux"; -import { - setTimezone, - setMode, - setLanguage, - setShowURL, -} from "../../../Features/UI/uiSlice.js"; -import SettingsStats from "./SettingsStats.jsx"; - -import { useFetchSettings, useSaveSettings } from "../../../Hooks/v1/settingsHooks.js"; -import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js"; -import { - useAddDemoMonitors, - useDeleteAllMonitors, - useDeleteMonitorStats, -} from "../../../Hooks/v1/monitorHooks.js"; -// Constants -const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; - -const Settings = () => { - // Redux state - const { mode, language, timezone, showURL } = useSelector((state) => state.ui); - - // Local state - const [settingsData, setSettingsData] = useState({}); - const [errors, setErrors] = useState({}); - const [isApiKeySet, setIsApiKeySet] = useState(settingsData?.pagespeedKeySet ?? false); - const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false); - const [isEmailPasswordSet, setIsEmailPasswordSet] = useState( - settingsData?.emailPasswordSet ?? false - ); - const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false); - - // Network - const [isSettingsLoading, settingsError] = useFetchSettings({ - setSettingsData, - setIsApiKeySet, - setIsEmailPasswordSet, - }); - - const [addDemoMonitors, isAddingDemoMonitors] = useAddDemoMonitors(); - - const [isSaving, saveError, saveSettings] = useSaveSettings({ - setSettingsData, - setIsApiKeySet, - setApiKeyHasBeenReset, - setIsEmailPasswordSet, - setEmailPasswordHasBeenReset, - }); - const [deleteAllMonitors, isDeletingMonitors] = useDeleteAllMonitors(); - const [deleteMonitorStats, isDeletingMonitorStats] = useDeleteMonitorStats(); - - // Setup - const isAdmin = useIsAdmin(); - const theme = useTheme(); - const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) }; - const { t } = useTranslation(); - const dispatch = useDispatch(); - // Handlers - const handleChange = async (e) => { - const { name, value, checked } = e.target; - - // Special case for showURL until handled properly in the backend - if (name === "showURL") { - dispatch(setShowURL(value)); - return; - } - let newValue; - if ( - name === "systemEmailIgnoreTLS" || - name === "systemEmailRequireTLS" || - name === "systemEmailRejectUnauthorized" || - name === "systemEmailSecure" || - name === "systemEmailPool" - ) { - newValue = checked; - } - // Build next state early - const newSettingsData = { - ...settingsData, - settings: { ...settingsData.settings, [name]: newValue ?? value }, - }; - - if (name === "timezone") { - dispatch(setTimezone({ timezone: value })); - return; - } - - if (name === "mode") { - dispatch(setMode(value)); - return; - } - - if (name === "language") { - dispatch(setLanguage(value)); - return; - } - - if (name === "deleteStats") { - await deleteMonitorStats(); - return; - } - - if (name === "demo") { - await addDemoMonitors(); - return; - } - - if (name === "deleteMonitors") { - await deleteAllMonitors(); - return; - } - - // Validate - const { error } = settingsValidation.validate(newSettingsData.settings, { - abortEarly: false, - }); - if (!error || error.details.length === 0) { - setErrors({}); - } else { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - } - - setSettingsData(newSettingsData); - }; - - const handleSave = () => { - const { error } = settingsValidation.validate(settingsData.settings, { - abortEarly: false, - }); - if (!error || error.details.length === 0) { - setErrors({}); - } else { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - - setErrors(newErrors); - } - saveSettings(settingsData?.settings); - }; - - return ( - - - {t("settingsPage.title")} - - - - - - - - - - - - - - - - ); -}; - -export default Settings; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx deleted file mode 100644 index 5484202d5..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx +++ /dev/null @@ -1,106 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded"; -import DeleteIcon from "@mui/icons-material/Delete"; - -// Utils -import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; -import { useTheme } from "@emotion/react"; -const MonitorListItem = ({ - monitor, - innerRef, - draggableProps, - dragHandleProps, - onDelete, -}) => { - const theme = useTheme(); - return ( - - - {monitor.name} - { - onDelete(monitor); - }} - /> - - ); -}; - -const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { - const onDelete = (monitorToDelete) => { - const newMonitors = selectedMonitors.filter( - (monitor) => monitor._id !== monitorToDelete._id - ); - setSelectedMonitors(newMonitors); - }; - const reorder = (list, startIndex, endIndex) => { - const result = Array.from(list); - const [removed] = result.splice(startIndex, 1); - result.splice(endIndex, 0, removed); - - return result; - }; - - const onDragEnd = (result) => { - // dropped outside the list - if (!result.destination) { - return; - } - - const reorderedMonitors = reorder( - selectedMonitors, - result.source.index, - result.destination.index - ); - - setSelectedMonitors(reorderedMonitors); - }; - - return ( - - - {(provided, snapshot) => ( - - {selectedMonitors?.map((monitor, index) => ( - - {(provided, snapshot) => ( - - )} - - ))} - {provided.placeholder} - - )} - - - ); -}; - -export default MonitorList; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx deleted file mode 100644 index 232a12f1d..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Button, Box } from "@mui/material"; -import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx"; -import ImageIcon from "@mui/icons-material/Image"; -import { useTranslation } from "react-i18next"; -import { formatBytes } from "../../../../../../Utils/fileUtils.js"; -const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => { - const { t } = useTranslation(); - if (isLoading) { - return ( - } - label={logo?.name} - size={formatBytes(logo?.size)} - progress={progressValue} - onClick={removeLogo} - /> - ); - } - - if (logo && logoType) { - return ( - - - - ); - } -}; - -export default Progress; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx deleted file mode 100644 index 482c490fb..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx deleted file mode 100644 index f31c456ef..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; - -// This can be used to add any extra/additional section/stacks on top of existing sections on the tab -const ConfigStack = ({ title, description, children }) => { - const theme = useTheme(); - return ( - - - - {title} - - {description} - - {children} - - ); -}; - -ConfigStack.propTypes = { - title: PropTypes.string.isRequired, // Title must be a string and is required - description: PropTypes.string.isRequired, // Description must be a string and is required - children: PropTypes.node.isRequired, -}; - -export default ConfigStack; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx deleted file mode 100644 index d11825e69..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx +++ /dev/null @@ -1,107 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import { TabPanel } from "@mui/lab"; -import MonitorList from "../MonitorList/index.jsx"; -import Search from "@/Components/v1/Inputs/Search/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import ConfigStack from "./ConfigStack.jsx"; -const Content = ({ - tabValue, - form, - monitors, - handleFormChange, - errors, - selectedMonitors, - setSelectedMonitors, -}) => { - // Local state - const [search, setSearch] = useState(""); - - // Handlers - const handleMonitorsChange = (selectedMonitors) => { - handleFormChange({ - target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor._id) }, - }); - setSelectedMonitors(selectedMonitors); - }; - - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - - return ( - - - - - - - - - {errors["monitors"]} - - - - - - - - - - - - - - ); -}; - -export default Content; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx deleted file mode 100644 index a668a9db8..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx +++ /dev/null @@ -1,187 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import { TabPanel } from "@mui/lab"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Search from "@/Components/v1/Inputs/Search/index.jsx"; -import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; -import ColorPicker from "@/Components/v1/Inputs/ColorPicker/index.jsx"; -import Progress from "../Progress/index.jsx"; - -// Utils -import { useTheme } from "@emotion/react"; -import timezones from "../../../../../../Utils/timezones.json"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; -import { useMemo, useState, useCallback } from "react"; - -const TabSettings = ({ - isCreate, - tabValue, - form, - handleFormChange, - handleImageChange, - progress, - removeLogo, - errors, -}) => { - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - const [rawInput, setRawInput] = useState(""); - - const selectedTimezone = useMemo( - () => timezones.find((tz) => tz._id === form.timezone) ?? null, - [form.timezone] - ); - - const handleTimezoneChange = useCallback( - (newValue) => { - setRawInput(""); - handleFormChange({ - target: { - name: "timezone", - value: newValue?._id ?? "", - }, - }); - }, - [handleFormChange] - ); - - return ( - - - - - - {t("access")} - - {t("statusPageCreateSettings")} - - - - - - - - - {t("basicInformation")} - - - {t("statusPageCreateBasicInfoDescription")} - - - - - - - - - - - {t("timezone")} - - - {t("statusPageCreateSelectTimeZoneDescription")} - - - - setRawInput(newVal)} - handleChange={handleTimezoneChange} - isAdorned={true} - unit="timezone" - /> - - - - - - {t("statusPageCreateAppearanceTitle")} - - - {t("statusPageCreateAppearanceDescription")} - - - - - - - - - - - ); -}; - -TabSettings.propTypes = { - isCreate: PropTypes.bool, - tabValue: PropTypes.string, - form: PropTypes.object, - handleFormChange: PropTypes.func, - handleImageChange: PropTypes.func, - progress: PropTypes.object, - removeLogo: PropTypes.func, - errors: PropTypes.object, -}; - -export default TabSettings; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx deleted file mode 100644 index e8669c474..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx +++ /dev/null @@ -1,99 +0,0 @@ -// Components -import { TabContext } from "@mui/lab"; -import { Tab } from "@mui/material"; -import Settings from "./Settings.jsx"; -import Content from "./Content.jsx"; - -// Utils -import PropTypes from "prop-types"; -import CustomTabList from "@/Components/v1/Tab/index.jsx"; - -const Tabs = ({ - isCreate, - form, - errors, - monitors, - selectedMonitors, - setSelectedMonitors, - handleFormChange, - handleImageChange, - progress, - removeLogo, - tab, - setTab, - TAB_LIST, - handleDelete, - isDeleteOpen, - setIsDeleteOpen, - isDeleting, - isLoading, -}) => { - return ( - - { - setTab(TAB_LIST.indexOf(selected)); - }} - aria-label="status page tabs" - > - {TAB_LIST.map((tabLabel) => ( - - ))} - - {tab === 0 ? ( - - ) : ( - - )} - - ); -}; - -Tabs.propTypes = { - isCreate: PropTypes.bool, - form: PropTypes.object, - errors: PropTypes.object, - monitors: PropTypes.array, - selectedMonitors: PropTypes.array, - setSelectedMonitors: PropTypes.func, - handleFormChange: PropTypes.func, - handleImageChange: PropTypes.func, - progress: PropTypes.object, - removeLogo: PropTypes.func, - tab: PropTypes.number, - setTab: PropTypes.func, - TAB_LIST: PropTypes.array, - handleDelete: PropTypes.func, - isDeleteOpen: PropTypes.bool, - setIsDeleteOpen: PropTypes.func, - isDeleting: PropTypes.bool, - isLoading: PropTypes.bool, -}; - -export default Tabs; diff --git a/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx deleted file mode 100644 index 808598052..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../../../../../main.jsx"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; - -const useCreateStatusPage = (isCreate, url) => { - const [isLoading, setIsLoading] = useState(false); - const [networkError, setNetworkError] = useState(false); - const createStatusPage = async ({ form }) => { - setIsLoading(true); - try { - await networkService.createStatusPage({ form, isCreate, url }); - return true; - } catch (error) { - setNetworkError(true); - createToast({ body: error?.response?.data?.msg ?? error.message }); - return false; - } finally { - setIsLoading(false); - } - }; - - return [createStatusPage, isLoading, networkError]; -}; - -export { useCreateStatusPage }; diff --git a/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx deleted file mode 100644 index 81fefd65c..000000000 --- a/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useState } from "react"; -import { networkService } from "../../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; - -const useMonitorsFetch = () => { - const { user } = useSelector((state) => state.auth); - - const [monitors, setMonitors] = useState(undefined); - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - useEffect(() => { - const fetchMonitors = async () => { - try { - const response = await networkService.getMonitorsByTeamId({ - limit: null, // donot return any checks for the monitors - types: ["http", "ping", "port", "game"], // include game servers in status page monitor selection - }); - setMonitors(response.data.data.monitors); - } catch (error) { - setNetworkError(true); - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [user]); - - return [monitors, isLoading, networkError]; -}; - -export { useMonitorsFetch }; diff --git a/client/src/Pages/v1/StatusPage/Create/index.jsx b/client/src/Pages/v1/StatusPage/Create/index.jsx deleted file mode 100644 index d9fafed70..000000000 --- a/client/src/Pages/v1/StatusPage/Create/index.jsx +++ /dev/null @@ -1,312 +0,0 @@ -// Components -import { Stack, Button, Typography } from "@mui/material"; -import Tabs from "./Components/Tabs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import SkeletonLayout from "./Components/Skeleton/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -//Utils -import { useTheme } from "@emotion/react"; -import { useState, useEffect, useRef, useCallback } from "react"; -import { statusPageValidation } from "../../../../Validation/validation.js"; -import { buildErrors } from "../../../../Validation/error.js"; -import { useMonitorsFetch } from "./Hooks/useMonitorsFetch.jsx"; -import { useCreateStatusPage } from "./Hooks/useCreateStatusPage.jsx"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { useNavigate } from "react-router-dom"; -import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx"; -import { useParams } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete.jsx"; -//Constants -const ERROR_TAB_MAPPING = [ - ["companyName", "url", "timezone", "color", "isPublished", "logo"], - ["monitors", "showUptimePercentage", "showCharts", "showAdminLoginLink"], -]; - -const CreateStatusPage = () => { - const { url } = useParams(); - //Local state - const [tab, setTab] = useState(0); - const [progress, setProgress] = useState({ value: 0, isLoading: false }); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); - const [form, setForm] = useState({ - isPublished: false, - companyName: "", - url: url ?? Math.floor(Math.random() * 1000000).toFixed(0), - logo: undefined, - timezone: "America/Toronto", - color: "#4169E1", - type: "uptime", - monitors: [], - showCharts: true, - showUptimePercentage: true, - showAdminLoginLink: false, - }); - const [errors, setErrors] = useState({}); - const [selectedMonitors, setSelectedMonitors] = useState([]); - // Refs - const intervalRef = useRef(null); - - // Setup - const isCreate = typeof url === "undefined"; - - //Utils - const theme = useTheme(); - const [monitors, isLoading, networkError] = useMonitorsFetch(); - const [createStatusPage] = useCreateStatusPage(isCreate); - const navigate = useNavigate(); - const { t } = useTranslation(); - - const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] = - useStatusPageFetch(isCreate, url); - const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url); - - console.log(JSON.stringify(form, null, 2)); - // Handlers - const handleFormChange = (e) => { - let { type, name, value, checked } = e.target; - // Handle errors - const { error } = statusPageValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => { - return buildErrors(prev, name, error); - }); - - //Handle checkbox - if (type === "checkbox") { - setForm((prev) => ({ - ...prev, - [name]: checked, - })); - return; - } - - // Handle other inputs - setForm((prev) => ({ - ...prev, - [name]: value, - })); - }; - - const handleImageChange = useCallback((fileObj) => { - if (!fileObj || !fileObj.file) return; - - setForm((prev) => ({ - ...prev, - logo: { - src: fileObj.src, - name: fileObj.name, - type: fileObj.file.type, - size: fileObj.file.size, - }, - })); - - intervalRef.current = setInterval(() => { - const buffer = 12; - setProgress((prev) => { - if (prev.value + buffer >= 100) { - clearInterval(intervalRef.current); - return { value: 100, isLoading: false }; - } - return { ...prev, value: prev.value + buffer }; - }); - }, 120); - }, []); - - const removeLogo = () => { - setForm((prev) => ({ - ...prev, - logo: undefined, - })); - // interrupt interval if image upload is canceled prior to completing the process - clearInterval(intervalRef.current); - setProgress({ value: 0, isLoading: false }); - }; - - /** - * Handle status page deletion with optimistic UI update - * Immediately navigates away without waiting for the deletion to complete - * to prevent unnecessary network requests for the deleted page - */ - const handleDelete = async () => { - setIsDeleteOpen(false); - // Start deletion process but don't wait for it - deleteStatusPage(); - // Immediately navigate away to prevent additional fetches for the deleted page - navigate("/status"); - }; - - const handleSubmit = async () => { - let toSubmit = { - ...form, - logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null }, - }; - const { error } = statusPageValidation.validate(toSubmit, { - abortEarly: false, - }); - - if (typeof error === "undefined") { - const success = await createStatusPage({ form }); - if (success) { - createToast({ - body: isCreate ? t("statusPage.createSuccess") : t("statusPage.updateSuccess"), - }); - navigate(`/status/uptime/${form.url}`); - } - return; - } - - const newErrors = {}; - error?.details?.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors((prev) => ({ ...prev, ...newErrors })); - const errorTabs = Object.keys(newErrors).map((err) => { - return ERROR_TAB_MAPPING.findIndex((tab) => tab.includes(err)); - }); - - // If there's an error in the current tab, don't change the tab - if (errorTabs.some((errorTab) => errorTab === tab)) { - return; - } - - // If we get -1, there's an unknown error - if (errorTabs[0] === -1) { - createToast({ body: t("common.toasts.unknownError") }); - return; - } - - // Otherwise go to tab with error - setTab(errorTabs[0]); - }; - - // If we are configuring, populate fields - useEffect(() => { - if (isCreate) return; - if (typeof statusPage === "undefined") { - return; - } - - let newLogo = undefined; - if (statusPage.logo && Object.keys(statusPage.logo).length > 0) { - newLogo = { - src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`, - name: "logo", - type: statusPage.logo.contentType, - size: null, - }; - } - setForm((prev) => { - return { - ...prev, - companyName: statusPage?.companyName, - isPublished: statusPage?.isPublished, - timezone: statusPage?.timezone, - monitors: statusPageMonitors.map((monitor) => monitor._id), - color: statusPage?.color, - logo: newLogo, - showCharts: statusPage?.showCharts ?? true, - showUptimePercentage: statusPage?.showUptimePercentage ?? true, - showAdminLoginLink: statusPage?.showAdminLoginLink ?? false, - }; - }); - setSelectedMonitors(statusPageMonitors); - }, [isCreate, statusPage, statusPageMonitors]); - - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - if (isLoading) return ; - - // Load fields - return ( - - - {!isCreate && ( - - - setIsDeleteOpen(false)} - open={isDeleteOpen} - confirmationButtonLabel={t("deleteStatusPageConfirm")} - description={t("deleteStatusPageDescription")} - isLoading={isDeleting || statusPageIsLoading} - /> - - )} - - - - - - ); -}; - -export default CreateStatusPage; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx deleted file mode 100644 index 816fbd375..000000000 --- a/client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx +++ /dev/null @@ -1,36 +0,0 @@ -// Components -import { Box, Typography } from "@mui/material"; - -// Utils -import { useTheme } from "@mui/material/styles"; -import { useNavigate } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -const AdminLink = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - - return ( - - - {t("administrator")} - - navigate("/login")} - > - {t("loginHere")} - - - ); -}; - -export default AdminLink; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx deleted file mode 100644 index c56aa40cc..000000000 --- a/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx +++ /dev/null @@ -1,134 +0,0 @@ -// Components -import { Box, Stack, Typography, Button } from "@mui/material"; -import Image from "@/Components/v1/Image/index.jsx"; -import SettingsIcon from "../../../../../../assets/icons/settings-bold.svg?react"; -import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; -import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; -//Utils -import { useTheme } from "@mui/material/styles"; -import { useNavigate } from "react-router-dom"; -import { useLocation } from "react-router-dom"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -const Controls = ({ url, type }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const location = useLocation(); - const currentPath = location.pathname; - const navigate = useNavigate(); - - if (currentPath.startsWith("/status/uptime/public")) { - return null; - } - - return ( - - - - - - ); -}; - -Controls.propTypes = { - type: PropTypes.string, - url: PropTypes.string, -}; - -const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const publicUrl = `/status/uptime/public/${url}`; - - return ( - - - {"Company - - {statusPage?.companyName} - - {statusPage?.isPublished && !isPublic && ( - { - window.open(publicUrl, "_blank", "noopener,noreferrer"); - }} - sx={{ - display: "inline-flex", - ":hover": { - cursor: "pointer", - borderBottom: 1, - }, - }} - > - {t("publicLink")} - - - )} - - - {isPublic && } - - ); -}; - -ControlsHeader.propTypes = { - url: PropTypes.string, - statusPage: PropTypes.object, - isPublic: PropTypes.bool, - type: PropTypes.string, -}; - -export default ControlsHeader; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx deleted file mode 100644 index 59ad4d522..000000000 --- a/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx +++ /dev/null @@ -1,73 +0,0 @@ -// Components -import { Stack, Box } from "@mui/material"; -import Host from "@/Components/v1/Host/index.jsx"; -import StatusPageBarChart from "@/Components/v1/Charts/StatusPageBarChart/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; - -//Utils -import { useTheme } from "@mui/material/styles"; -import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; -import PropTypes from "prop-types"; - -import { useSelector } from "react-redux"; - -const MonitorsList = ({ - isLoading = false, - shouldRender = true, - monitors = [], - statusPage = {}, -}) => { - const theme = useTheme(); - const { determineState } = useMonitorUtils(); - - const { showURL } = useSelector((state) => state.ui); - - return ( - <> - {monitors?.map((monitor) => { - const status = determineState(monitor); - return ( - - - - {statusPage.showCharts !== false && ( - - - - )} - - - - - - ); - })} - - ); -}; - -MonitorsList.propTypes = { - monitors: PropTypes.array.isRequired, - statusPage: PropTypes.object, -}; - -export default MonitorsList; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx deleted file mode 100644 index 482c490fb..000000000 --- a/client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx deleted file mode 100644 index 1898ff3d7..000000000 --- a/client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx +++ /dev/null @@ -1,74 +0,0 @@ -// Components -import { Stack, Typography } from "@mui/material"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; -import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; - -// Utils -import { useTheme } from "@mui/material/styles"; -import PropTypes from "prop-types"; - -const getMonitorStatus = (monitors, theme) => { - const monitorsStatus = { - icon: ( - - ), - }; - if (monitors.every((monitor) => monitor.status === true)) { - monitorsStatus.msg = "All systems operational"; - monitorsStatus.color = theme.palette.success.lowContrast; - monitorsStatus.icon = ( - - ); - } - - if (monitors.every((monitor) => monitor.status === false)) { - monitorsStatus.msg = "All systems down"; - monitorsStatus.color = theme.palette.error.lowContrast; - } - - if (monitors.some((monitor) => monitor.status === false)) { - monitorsStatus.msg = "Degraded performance"; - monitorsStatus.color = theme.palette.warning.lowContrast; - } - - // Paused or unknown - if (monitors.some((monitor) => typeof monitor.status === "undefined")) { - monitorsStatus.msg = "Unknown status"; - monitorsStatus.color = theme.palette.warning.lowContrast; - } - return monitorsStatus; -}; - -const StatusBar = ({ monitors }) => { - const theme = useTheme(); - - if (typeof monitors === "undefined") return; - - const monitorsStatus = getMonitorStatus(monitors, theme); - return ( - - {monitorsStatus.icon} - {/* CAIO_REVIEW */} - {monitorsStatus.msg} - - ); -}; - -export default StatusBar; - -StatusBar.propTypes = { - status: PropTypes.object, -}; diff --git a/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx deleted file mode 100644 index 038a771ba..000000000 --- a/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../../../../../Utils/NetworkService.js"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; -import { useTranslation } from "react-i18next"; - -/** - * Hook for deleting a status page with optimistic UI update - * @param {Function} fetchStatusPage - Function to fetch status page data - * @param {string} url - URL of the status page - * @returns {Array} - [deleteStatusPage function, isLoading state] - */ -const useStatusPageDelete = (fetchStatusPage, url) => { - const [isLoading, setIsLoading] = useState(false); - const { t } = useTranslation(); - - /** - * Delete a status page with optimistic UI update - * @returns {Promise} - Success status - */ - const deleteStatusPage = async () => { - // We don't need to call fetchStatusPage after deletion - // This prevents the 404 error when trying to fetch a deleted status page - try { - setIsLoading(true); - await networkService.deleteStatusPage({ url }); - createToast({ - body: t("statusPage.deleteSuccess", "Status page deleted successfully"), - }); - return true; - } catch (error) { - createToast({ - body: t("statusPage.deleteFailed", "Failed to delete status page"), - }); - return false; - } finally { - setIsLoading(false); - } - }; - - return [deleteStatusPage, isLoading]; -}; - -export { useStatusPageDelete }; diff --git a/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx deleted file mode 100644 index 506082887..000000000 --- a/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/v1/useMonitorUtils.js"; - -const useStatusPageFetch = (isCreate = false, url) => { - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [statusPage, setStatusPage] = useState(undefined); - const [monitors, setMonitors] = useState(undefined); - const theme = useTheme(); - const { getMonitorWithPercentage } = useMonitorUtils(); - const fetchStatusPage = useCallback(async () => { - try { - const response = await networkService.getStatusPageByUrl({ - url, - type: "uptime", - }); - if (!response?.data?.data) return; - const { statusPage, monitors } = response.data.data; - setStatusPage(statusPage); - - const monitorsWithPercentage = monitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - - setMonitors(monitorsWithPercentage); - } catch (error) { - // If there is a 404, status page is not found - if (error?.response?.status === 404) { - setStatusPage(undefined); - return; - } - createToast({ body: error.message }); - setNetworkError(true); - } finally { - setIsLoading(false); - } - }, [theme, getMonitorWithPercentage, url]); - - useEffect(() => { - if (isCreate === true) { - return; - } - fetchStatusPage(); - }, [isCreate, fetchStatusPage]); - - return [statusPage, monitors, isLoading, networkError, fetchStatusPage]; -}; - -export { useStatusPageFetch }; diff --git a/client/src/Pages/v1/StatusPage/Status/index.jsx b/client/src/Pages/v1/StatusPage/Status/index.jsx deleted file mode 100644 index e700bbbd9..000000000 --- a/client/src/Pages/v1/StatusPage/Status/index.jsx +++ /dev/null @@ -1,166 +0,0 @@ -// Components -import { Typography, Stack } from "@mui/material"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import AdminLink from "./Components/AdminLink/index.jsx"; -import ControlsHeader from "./Components/ControlsHeader/index.jsx"; -import SkeletonLayout from "./Components/Skeleton/index.jsx"; -import StatusBar from "./Components/StatusBar/index.jsx"; -import MonitorsList from "./Components/MonitorsList/index.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import TextLink from "@/Components/v1/TextLink/index.jsx"; - -// Utils -import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx"; -import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; -import { useLocation } from "react-router-dom"; -import { useParams } from "react-router-dom"; -import { useTranslation } from "react-i18next"; - -const PublicStatus = () => { - const { url } = useParams(); - - // Utils - const theme = useTheme(); - const { t } = useTranslation(); - const location = useLocation(); - const isAdmin = useIsAdmin(); - - const [statusPage, monitors, isLoading, networkError] = useStatusPageFetch(false, url); - - // Breadcrumbs - const crumbs = [ - { name: t("statusBreadCrumbsStatusPages"), path: "/status" }, - { name: t("statusBreadCrumbsDetails"), path: `/status/uptime/${statusPage?.url}` }, - ]; - - // Setup - let sx = { paddingLeft: theme.spacing(20), paddingRight: theme.spacing(20) }; - let link = undefined; - const isPublic = location.pathname.startsWith("/status/uptime/public"); - // Public status page - if (isPublic && statusPage && statusPage.showAdminLoginLink === true) { - sx = { - paddingTop: theme.spacing(20), - paddingLeft: "20vw", - paddingRight: "20vw", - }; - link = ; - } - - // Loading - if (isLoading) { - return ; - } - - if (monitors?.length === 0) { - return ( - - - {"Theres nothing here yet"} - - {isAdmin && ( - - )} - - ); - } - - // Error fetching data - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - // Public status page fallback - if (!isLoading && typeof statusPage === "undefined" && isPublic) { - return ( - - - - {t("statusPageStatus")} - - {t("statusPageStatusContactAdmin")} - - - ); - } - - // Finished loading, but status page is not public - if (!isLoading && isPublic && statusPage.isPublished === false) { - return ( - - - - {t("statusPageStatusNotPublic")} - - {t("statusPageStatusContactAdmin")} - - - ); - } - - // Status page doesn't exist - if (!isLoading && typeof statusPage === "undefined") { - return ( - - - {t("statusPageStatusNoPage")} - - {t("statusPageStatusContactAdmin")} - - ); - } - - return ( - - {!isPublic && } - - {t("statusPageStatusServiceStatus")} - - - {link} - - ); -}; - -export default PublicStatus; diff --git a/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx deleted file mode 100644 index 1afe25409..000000000 --- a/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ /dev/null @@ -1,102 +0,0 @@ -import DataTable from "@/Components/v1/Table/index.jsx"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; -import { Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -const StatusPagesTable = ({ data }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const headers = [ - { - id: "name", - content: t("statusPageName"), - render: (row) => { - return row.companyName; - }, - }, - { - id: "url", - content: t("publicURL"), - onClick: (e, row) => { - if (row.isPublished) { - e.stopPropagation(); - const url = `/status/uptime/public/${row.url}`; - window.open(url, "_blank", "noopener,noreferrer"); - } - }, - render: (row) => { - const content = row.isPublished ? `/${row.url}` : "Unpublished"; - return ( - - {content} - {row.isPublished && } - - ); - }, - }, - { - id: "type", - content: t("type"), - render: (row) => { - return row.type; - }, - }, - { - id: "status", - content: t("status"), - render: (row) => { - const status = row.isPublished ? "published" : "unpublished"; - return ( - - ); - }, - }, - ]; - - const handleRowClick = (statusPage) => { - navigate(`/status/uptime/${statusPage.url}`); - }; - - return ( - { - handleRowClick(row); - }, - }} - headers={headers} - data={data} - /> - ); -}; - -export default StatusPagesTable; diff --git a/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx deleted file mode 100644 index 89df1b84d..000000000 --- a/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx +++ /dev/null @@ -1,32 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../../../../../main.jsx"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; - -const useStatusPagesFetch = () => { - const { user } = useSelector((state) => state.auth); - - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [statusPages, setStatusPages] = useState(undefined); - - useEffect(() => { - const fetchStatusPages = async () => { - try { - const res = await networkService.getStatusPagesByTeamId(); - setStatusPages(res?.data?.data); - } catch (error) { - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - fetchStatusPages(); - }, [user]); - return [isLoading, networkError, statusPages]; -}; - -export { useStatusPagesFetch }; diff --git a/client/src/Pages/v1/StatusPage/StatusPages/index.jsx b/client/src/Pages/v1/StatusPage/StatusPages/index.jsx deleted file mode 100644 index 3c60fe950..000000000 --- a/client/src/Pages/v1/StatusPage/StatusPages/index.jsx +++ /dev/null @@ -1,43 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; -import StatusPagesTable from "./Components/StatusPagesTable/index.jsx"; -import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; -// Utils -import { useTheme } from "@emotion/react"; -import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; -const BREADCRUMBS = [{ name: `Status Pages`, path: "" }]; - -const StatusPages = () => { - // Utils - const theme = useTheme(); - const isAdmin = useIsAdmin(); - const [isLoading, networkError, statusPages] = useStatusPagesFetch(); - - return ( - <> - - - - - - - - - ); -}; - -export default StatusPages; diff --git a/client/src/Pages/v1/Uptime/BulkImport/Upload.jsx b/client/src/Pages/v1/Uptime/BulkImport/Upload.jsx deleted file mode 100644 index 24bc0ec38..000000000 --- a/client/src/Pages/v1/Uptime/BulkImport/Upload.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useTheme } from "@emotion/react"; -import { useState, useRef } from "react"; -import { Button, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import PropTypes from "prop-types"; - -const UploadFile = ({ onFileSelect }) => { - // Changed prop to onFileSelect - const theme = useTheme(); - const [file, setFile] = useState(); - const [error, setError] = useState(""); - const inputRef = useRef(); - const { t } = useTranslation(); - - const handleSelectFile = () => { - inputRef.current.click(); - }; - - const handleFileChange = (e) => { - setError(""); - const selectedFile = e.target.files[0]; - - // Basic file validation - if (!selectedFile) return; - - if (!selectedFile.name.toLowerCase().endsWith(".csv")) { - setError(t("bulkImport.invalidFileType")); - return; - } - - setFile(selectedFile); - onFileSelect(selectedFile); // Pass the file directly to parent - }; - - return ( -
- - - {file?.name || t("bulkImport.noFileSelected")} - - - {error} - - -
- ); -}; - -UploadFile.prototype = { - onFileSelect: PropTypes.func.isRequired, -}; - -export default UploadFile; diff --git a/client/src/Pages/v1/Uptime/BulkImport/index.jsx b/client/src/Pages/v1/Uptime/BulkImport/index.jsx deleted file mode 100644 index 9f3efcd67..000000000 --- a/client/src/Pages/v1/Uptime/BulkImport/index.jsx +++ /dev/null @@ -1,117 +0,0 @@ -// React, Redux, Router -import { useTheme } from "@emotion/react"; -import { useState } from "react"; -// MUI -import { Box, Stack, Typography, Button, Link } from "@mui/material"; - -//Components -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import UploadFile from "./Upload.jsx"; -import { useSelector } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import { Trans, useTranslation } from "react-i18next"; -import { useCreateBulkMonitors } from "../../../../Hooks/v1/monitorHooks.js"; - -const BulkImport = () => { - const theme = useTheme(); - const { user } = useSelector((state) => state.auth); - const navigate = useNavigate(); - const { t } = useTranslation(); - const [selectedFile, setSelectedFile] = useState(null); - - const crumbs = [ - { name: t("uptime"), path: "/uptime" }, - { name: t("bulkImport.title"), path: `/uptime/bulk-import` }, - ]; - - const [createBulkMonitors, hookLoading] = useCreateBulkMonitors(); - - const handleSubmit = async () => { - if (!selectedFile) { - createToast({ body: t("bulkImport.noFileSelected") }); - return; - } - - const [success, data, error] = await createBulkMonitors(selectedFile, user); - - if (success) { - // You can use `data` here if needed - createToast({ body: t("bulkImport.uploadSuccess") }); - navigate("/uptime"); - } else { - createToast({ body: error || t("bulkImport.uploadFailed") }); - } - }; - - return ( - - - - - {t("bulkImport.title")} - - - - - {t("bulkImport.selectFileTips")} - - - - ), - sample: ( - - ), - }} - /> - - - - - setSelectedFile(file)} /> - - - - - - - - - ); -}; - -export default BulkImport; diff --git a/client/src/Pages/v1/Uptime/Create/index.css b/client/src/Pages/v1/Uptime/Create/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/v1/Uptime/Create/index.jsx b/client/src/Pages/v1/Uptime/Create/index.jsx deleted file mode 100644 index 202f64d85..000000000 --- a/client/src/Pages/v1/Uptime/Create/index.jsx +++ /dev/null @@ -1,812 +0,0 @@ -//Components -import { - Box, - Button, - ButtonGroup, - FormControlLabel, - Stack, - Switch, - Tooltip, - Typography, -} from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; -import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; -import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import Dialog from "@/Components/v1/Dialog/index.jsx"; -import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; -import SkeletonLayout from "./skeleton.jsx"; - -// Utils -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { monitorValidation } from "../../../../Validation/validation.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { - PauseOutlined as PauseOutlinedIcon, - PlayArrowOutlined as PlayArrowOutlinedIcon, -} from "@mui/icons-material"; -import { useMonitorUtils } from "../../../../Hooks/v1/useMonitorUtils.js"; -import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; -import { useParams } from "react-router-dom"; -import { - useCreateMonitor, - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, - useFetchMonitorById, - useFetchMonitorGames, -} from "../../../../Hooks/v1/monitorHooks.js"; - -/** - * Create page renders monitor creation or configuration views. - * @component - */ -const UptimeCreate = ({ isClone = false }) => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined" || isClone; - - // States - const [monitor, setMonitor] = useState({ - type: "http", - statusWindowSize: 5, - statusWindowThreshold: 60, - matchMethod: "equal", - expectedValue: "", - jsonPath: "", - notifications: [], - interval: 60000, - ignoreTlsErrors: false, - ...(isCreate ? { url: "", name: "" } : { port: undefined }), - }); - const [errors, setErrors] = useState({}); - const [https, setHttps] = useState(true); - const [isOpen, setIsOpen] = useState(false); - const [useAdvancedMatching, setUseAdvancedMatching] = useState(false); - const [updateTrigger, setUpdateTrigger] = useState(false); - const [games, setGames] = useState({}); - const triggerUpdate = () => { - setUpdateTrigger(!updateTrigger); - }; - - // Hooks - const [notifications, notificationsAreLoading, notificationsError] = - useGetNotificationsByTeamId(); - const { determineState, statusColor } = useMonitorUtils(); - // Fetch monitor details - const [isFetchingMonitor] = useFetchMonitorById({ - monitorId, - setMonitor, - updateTrigger: true, - }); - - // Fetch games - const [isFetchingGames] = useFetchMonitorGames({ - setGames, - triggerUpdate: true, - }); - - // Combine the loading states - const isLoading = isFetchingMonitor || isFetchingGames; - - const [createMonitor, isCreating] = useCreateMonitor(); - const [pauseMonitor, isPausing] = usePauseMonitor({}); - const [deleteMonitor, isDeleting] = useDeleteMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); - - // Setup - const theme = useTheme(); - const { t } = useTranslation(); - - // Constants - const MS_PER_MINUTE = 60000; - const FREQUENCIES = [ - { _id: 1, name: t("time.oneMinute") }, - { _id: 2, name: t("time.twoMinutes") }, - { _id: 3, name: t("time.threeMinutes") }, - { _id: 4, name: t("time.fourMinutes") }, - { _id: 5, name: t("time.fiveMinutes") }, - ]; - - const GAMELIST = Object.entries(games).map(([key, value]) => ({ - _id: key, - name: value.name, - })); - - const CRUMBS = [ - { name: "uptime", path: "/uptime" }, - ...(isCreate - ? [{ name: "create", path: `/uptime/create` }] - : [ - { name: "details", path: `/uptime/${monitorId}` }, - { name: "configure", path: `/uptime/configure/${monitorId}` }, - ]), - ]; - const matchMethodOptions = [ - { _id: "equal", name: t("matchMethodOptions.equal") }, - { _id: "include", name: t("matchMethodOptions.include") }, - { _id: "regex", name: t("matchMethodOptions.regex") }, - ]; - const expectedValuePlaceholders = { - regex: t("matchMethodOptions.regexPlaceholder"), - equal: t("matchMethodOptions.equalPlaceholder"), - include: t("matchMethodOptions.includePlaceholder"), - }; - const monitorTypeMaps = { - http: { - label: t("monitorType.http.label"), - placeholder: t("monitorType.http.placeholder"), - namePlaceholder: t("monitorType.http.namePlaceholder"), - }, - ping: { - label: t("monitorType.ping.label"), - placeholder: t("monitorType.ping.placeholder"), - namePlaceholder: t("monitorType.ping.namePlaceholder"), - }, - docker: { - label: t("monitorType.docker.label"), - placeholder: t("monitorType.docker.placeholder"), - namePlaceholder: t("monitorType.docker.namePlaceholder"), - }, - port: { - label: t("monitorType.port.label"), - placeholder: t("monitorType.port.placeholder"), - namePlaceholder: t("monitorType.port.namePlaceholder"), - }, - game: { - label: t("monitorType.game.label"), - placeholder: t("monitorType.game.placeholder"), - namePlaceholder: t("monitorType.game.namePlaceholder"), - }, - }; - - // Handlers - const onSubmit = async (event) => { - event.preventDefault(); - const { notifications, ...rest } = monitor; - - let form = {}; - if (isCreate) { - form = { - url: - monitor.type === "http" && !isClone - ? `http${https ? "s" : ""}://` + monitor.url - : monitor.url, - name: monitor.name || monitor.url.substring(0, 50), - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - type: monitor.type, - - port: - monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, - interval: monitor.interval, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - jsonPath: monitor.jsonPath, - ignoreTlsErrors: monitor.ignoreTlsErrors, - gameId: monitor.gameId || undefined, - }; - } else { - form = { - _id: monitor._id, - url: monitor.url, - name: monitor.name || monitor.url.substring(0, 50), - statusWindowSize: monitor.statusWindowSize, - statusWindowThreshold: monitor.statusWindowThreshold, - type: monitor.type, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - jsonPath: monitor.jsonPath, - interval: monitor.interval, - teamId: monitor.teamId, - userId: monitor.userId, - port: - monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, - ignoreTlsErrors: monitor.ignoreTlsErrors, - gameId: monitor.gameId || undefined, - }; - } - if (!useAdvancedMatching) { - form.matchMethod = isCreate ? undefined : ""; - form.expectedValue = isCreate ? undefined : ""; - form.jsonPath = isCreate ? undefined : ""; - } - - const { error } = monitorValidation.validate(form, { - abortEarly: false, - }); - - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: t("checkFormError") }); - return; - } - - form = { - ...form, - description: monitor.name || monitor.url, - notifications: monitor.notifications, - }; - - if (isCreate) { - await createMonitor({ monitor: form, redirect: "/uptime" }); - } else { - await updateMonitor({ monitor: form, redirect: "/uptime" }); - } - }; - - const onChange = (event) => { - let { name, value, checked } = event.target; - - if (name === "ignoreTlsErrors") { - value = checked; - } - - if (name === "useAdvancedMatching") { - setUseAdvancedMatching(checked); - return; - } - - if (name === "interval") { - value = value * MS_PER_MINUTE; - } - - setMonitor((prev) => ({ ...prev, [name]: value })); - - if (name === "type") { - setErrors({}); - return; - } - - const { error } = monitorValidation.validate( - { type: monitor.type, [name]: value }, - { abortEarly: false } - ); - - setErrors((prev) => ({ - ...prev, - ...(error && error.details[0].path[0] === name - ? { [name]: error.details[0].message } - : { [name]: undefined }), - })); - }; - - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - - const handleRemove = async (event) => { - event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/uptime" }); - }; - - const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; - const displayInterval = monitor?.interval / MS_PER_MINUTE || 1; - const parsedUrl = monitor?.url; - const protocol = parsedUrl?.protocol?.replace(":", "") || ""; - - useEffect(() => { - if (!isCreate || isClone) { - if (monitor.matchMethod) { - setUseAdvancedMatching(true); - } else { - setUseAdvancedMatching(false); - } - } - }, [monitor, isCreate]); - - if (Object.keys(monitor).length === 0) { - return ; - } - - return ( - - - - - - - - - {!isCreate ? monitor.name : t("createYour") + " "} - - {isCreate && ( - - {t("monitor")} - - )} - - {!isCreate && ( - - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - - )} - - {!isCreate && ( - - - - - )} - - {isCreate && ( - - - - {t("distributedUptimeCreateChecks")} - - - {t("distributedUptimeCreateChecksDescription")} - - - - - - {monitor.type === "http" ? ( - - - - - ) : ( - "" - )} - - - - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - - )} - - - - {t("settingsGeneralSettings")} - - - {isCreate - ? t(`uptimeGeneralInstructions.${monitor.type}`) - : t("distributedUptimeCreateSelectURL")} - - - - - ) - } - helperText={errors["url"]} - onChange={onChange} - disabled={!isCreate} - /> - + } + > + {children} + + ); +}; diff --git a/client/src/components/monitors/HistogramStatus.tsx b/client/src/components/monitors/HistogramStatus.tsx new file mode 100644 index 000000000..f3aeac2f7 --- /dev/null +++ b/client/src/components/monitors/HistogramStatus.tsx @@ -0,0 +1,156 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BaseChart } from "@/components/monitors/Chart"; +import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; +import { TrendingUp, AlertTriangle } from "lucide-react"; + +import type { GroupedCheck } from "@/types/check"; +import type { MonitorStatus } from "@/types/monitor"; + +import { normalizeResponseTimes } from "@/utils/DataUtils"; +import { useState } from "react"; +import { formatDateWithTz } from "@/utils/TimeUtils"; +import { useAppSelector } from "@/hooks/AppHooks"; +import { useTheme } from "@mui/material/styles"; +import { getResponseTimeColor } from "@/utils/MonitorUtils"; + +const XLabel = ({ + p1, + p2, + range, +}: { + p1: GroupedCheck; + p2: GroupedCheck; + range: string; +}) => { + const theme = useTheme(); + const uiTimezone = useAppSelector((state: any) => state.ui.timezone); + const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D"; + return ( + <> + + {formatDateWithTz(p1._id, dateFormat, uiTimezone)} + + + {formatDateWithTz(p2._id, dateFormat, uiTimezone)} + + + ); +}; + +export const HistogramStatus = ({ + checks, + status, + range, + title, +}: { + checks: GroupedCheck[]; + status: MonitorStatus; + range: string; + title: string; +}) => { + const uiTimezone = useAppSelector((state: any) => state.ui.timezone); + + const icon = status === "up" ? : ; + const theme = useTheme(); + const [idx, setIdx] = useState(null); + const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D"; + const normalChecks = normalizeResponseTimes(checks, "avgResponseTime"); + + if (normalChecks.length === 0) { + return ( + + + + {status === "up" ? "No checks yet" : "Great, no downtime yet!"} + + + + ); + } + + const totalChecks = normalChecks.reduce((count, check) => { + return count + check.count; + }, 0); + + return ( + + + + + Total checks + {idx ? ( + + + {normalChecks?.[idx]?.count} + + + {formatDateWithTz( + normalChecks?.[idx]?._id, + dateFormat, + uiTimezone + )} + + + ) : ( + {totalChecks} + )} + + + + + + } + /> + + {normalChecks?.map((groupedCheck, idx) => { + const fillColor = getResponseTimeColor( + groupedCheck.normalResponseTime + ); + return ( + setIdx(idx)} + onMouseLeave={() => setIdx(null)} + key={groupedCheck._id} + fill={theme.palette[fillColor].main} + /> + ); + })} + + + + + + ); +}; diff --git a/client/src/components/monitors/MonitorStatus.tsx b/client/src/components/monitors/MonitorStatus.tsx new file mode 100644 index 000000000..e0c555bae --- /dev/null +++ b/client/src/components/monitors/MonitorStatus.tsx @@ -0,0 +1,60 @@ +import type { IMonitor } from "@/types/monitor"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { PulseDot, Dot } from "@/components/design-elements"; +import { getStatusColor, formatUrl } from "@/utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; +import prettyMilliseconds from "pretty-ms"; +import { typographyLevels } from "@/theme/palette"; +import useMediaQuery from "@mui/material/useMediaQuery"; +export const MonitorStatus = ({ monitor }: { monitor: IMonitor }) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + if (!monitor) { + return null; + } + return ( + + + {monitor.name} + + + + + {formatUrl(monitor?.url)} + + {!isSmall && ( + <> + + + Checking every{" "} + {prettyMilliseconds(monitor?.interval, { verbose: true })} + + + )} + + + ); +}; diff --git a/client/src/components/monitors/index.tsx b/client/src/components/monitors/index.tsx new file mode 100644 index 000000000..3fec60d2b --- /dev/null +++ b/client/src/components/monitors/index.tsx @@ -0,0 +1,9 @@ +export { HeaderCreate } from "./HeaderCreate"; +export { HistogramStatus } from "./HistogramStatus"; +export { ChartAvgResponse } from "./ChartAvgResponse"; +export { HeaderRange } from "../common/HeaderRange"; +export { HeaderControls } from "./HeaderControls"; +export { ChartResponseTime } from "./ChartResponseTime"; +export { HistogramResponseTime } from "./HistogramResponseTime"; +export { HistogramPageSpeed } from "./HistogramPageSpeed"; +export { HistogramInfrastructure } from "./HistogramInfrastructure"; diff --git a/client/src/components/notification-channels/HeaderCreate.tsx b/client/src/components/notification-channels/HeaderCreate.tsx new file mode 100644 index 000000000..543ada4d3 --- /dev/null +++ b/client/src/components/notification-channels/HeaderCreate.tsx @@ -0,0 +1 @@ +export { HeaderCreate } from "@/components/monitors/HeaderCreate"; diff --git a/client/src/components/protected-route/index.tsx b/client/src/components/protected-route/index.tsx new file mode 100644 index 000000000..fcea31f38 --- /dev/null +++ b/client/src/components/protected-route/index.tsx @@ -0,0 +1,18 @@ +import { useAppSelector } from "@/hooks/AppHooks"; +import { Navigate } from "react-router"; + +export const ProtectedRoute: React.FC = ({ + children, +}) => { + const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated); + + if (!isAuthenticated) { + return ; + } + + if (!isAuthenticated) { + return null; + } + + return <>{children}; +}; diff --git a/client/src/components/status-pages/HistogramStatusPage.tsx b/client/src/components/status-pages/HistogramStatusPage.tsx new file mode 100644 index 000000000..b0668151d --- /dev/null +++ b/client/src/components/status-pages/HistogramStatusPage.tsx @@ -0,0 +1,72 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import type { ICheck } from "@/types/check"; +import { HistogramResponseTimeTooltip } from "@/components/monitors/HistogramResponseTimeTooltip"; +import { normalizeResponseTimes } from "@/utils/DataUtils"; + +const BAR_HEIGHT = "50px"; + +export const HistogramStatusPage = ({ checks }: { checks: ICheck[] }) => { + const theme = useTheme(); + const normalChecks = normalizeResponseTimes(checks, "responseTime"); + let data = Array(); + + if (!normalChecks || normalChecks.length === 0) { + return null; + } + if (normalChecks.length !== 25) { + const placeholders = Array(25 - normalChecks.length).fill({ + status: "placeholder", + }); + data = [...normalChecks, ...placeholders]; + } else { + data = normalChecks; + } + + return ( + event.stopPropagation()} + sx={{ + cursor: "default", + }} + > + {data.map((check, index) => { + return ( + + + + + + ); + })} + + ); +}; diff --git a/client/src/components/status-pages/NameHeader.tsx b/client/src/components/status-pages/NameHeader.tsx new file mode 100644 index 000000000..63f57ced8 --- /dev/null +++ b/client/src/components/status-pages/NameHeader.tsx @@ -0,0 +1,33 @@ +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import { ExternalLink } from "lucide-react"; + +import type { IStatusPageWithMonitors } from "@/types/status-page"; +import { useTheme } from "@mui/material/styles"; +const HOST = import.meta.env.VITE_APP_HOST; + +export const NameHeader = ({ + statusPage, +}: { + statusPage: IStatusPageWithMonitors; +}) => { + const theme = useTheme(); + return ( + + {statusPage?.name} + {statusPage?.isPublished && ( + + Public link + + + )} + + ); +}; diff --git a/client/src/components/status-pages/StatusHeader.tsx b/client/src/components/status-pages/StatusHeader.tsx new file mode 100644 index 000000000..ccf3d3b74 --- /dev/null +++ b/client/src/components/status-pages/StatusHeader.tsx @@ -0,0 +1,28 @@ +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; + +import { getStatusPageHeaderConfig } from "@/utils/MonitorUtils"; +import type { IStatusPageWithMonitors } from "@/types/status-page"; +import { useTheme } from "@mui/material/styles"; + +export const StatusHeader = ({ + statusPage, +}: { + statusPage: IStatusPageWithMonitors; +}) => { + const theme = useTheme(); + const headerConfig = getStatusPageHeaderConfig(statusPage?.monitors || []); + + return ( + + + {headerConfig.message} + + + ); +}; diff --git a/client/src/components/status-pages/StatusPageRow.tsx b/client/src/components/status-pages/StatusPageRow.tsx new file mode 100644 index 000000000..ec3c38af5 --- /dev/null +++ b/client/src/components/status-pages/StatusPageRow.tsx @@ -0,0 +1,30 @@ +import Grid from "@mui/material/Grid"; +import Typography from "@mui/material/Typography"; +import { HistogramStatusPage } from "./HistogramStatusPage"; +import { StatusLabel } from "@/components/design-elements"; + +import type { IMonitor } from "@/types/monitor"; +import { useTheme } from "@mui/material/styles"; + +export const StatusPageRow = ({ monitor }: { monitor: IMonitor }) => { + const theme = useTheme(); + return ( + + + {monitor?.name} + + + + + + + + + ); +}; diff --git a/client/src/components/teams/HeaderCreate.tsx b/client/src/components/teams/HeaderCreate.tsx new file mode 100644 index 000000000..543ada4d3 --- /dev/null +++ b/client/src/components/teams/HeaderCreate.tsx @@ -0,0 +1 @@ +export { HeaderCreate } from "@/components/monitors/HeaderCreate"; diff --git a/client/src/features/authSlice.ts b/client/src/features/authSlice.ts new file mode 100644 index 000000000..234e44de8 --- /dev/null +++ b/client/src/features/authSlice.ts @@ -0,0 +1,39 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import type { IUser } from "@/types/user"; +interface AuthState { + isAuthenticated: boolean; + user: IUser | null; + selectedTeamId: string | null; +} + +const initialState: AuthState = { + isAuthenticated: false, + user: null, + selectedTeamId: null, +}; + +export const authSlice = createSlice({ + name: "auth", + initialState, + reducers: { + setAuthenticated: (state, action: PayloadAction) => { + state.isAuthenticated = action.payload; + }, + setUser: (state, action: PayloadAction) => { + state.user = action.payload; + }, + setSelectedTeamId: (state, action: PayloadAction) => { + state.selectedTeamId = action.payload; + }, + logout: (state) => { + state.isAuthenticated = false; + state.user = null; + state.selectedTeamId = null; + }, + }, +}); + +export const { setAuthenticated, setUser, setSelectedTeamId, logout } = + authSlice.actions; +export default authSlice.reducer; diff --git a/client/src/features/uiSlice.ts b/client/src/features/uiSlice.ts new file mode 100644 index 000000000..a701c3ada --- /dev/null +++ b/client/src/features/uiSlice.ts @@ -0,0 +1,39 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; + +interface UiState { + language: string; + mode: string; + sidebarOpen: boolean; + timezone: string; +} + +const initialState: UiState = { + language: "en", + mode: "dark", + sidebarOpen: true, + timezone: "America/Vancouver", +}; + +export const uiSlice = createSlice({ + name: "ui", + initialState, + reducers: { + setLanguage: (state, action: PayloadAction) => { + state.language = action.payload; + }, + setMode: (state, action: PayloadAction) => { + state.mode = action.payload; + }, + setTimezone: (state, action: PayloadAction) => { + state.timezone = action.payload; + }, + setSidebarOpen: (state, action: PayloadAction) => { + state.sidebarOpen = action.payload; + }, + }, +}); + +export const { setLanguage, setMode, setSidebarOpen, setTimezone } = + uiSlice.actions; +export default uiSlice.reducer; diff --git a/client/src/hooks/AppHooks.tsx b/client/src/hooks/AppHooks.tsx new file mode 100644 index 000000000..577a5a387 --- /dev/null +++ b/client/src/hooks/AppHooks.tsx @@ -0,0 +1,5 @@ +import { useDispatch, useSelector } from "react-redux"; +import type { RootState, AppDispatch } from "@/store"; + +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); diff --git a/client/src/hooks/UseApi.tsx b/client/src/hooks/UseApi.tsx new file mode 100644 index 000000000..04353e25d --- /dev/null +++ b/client/src/hooks/UseApi.tsx @@ -0,0 +1,179 @@ +import { useState } from "react"; +import useSWR from "swr"; +import type { SWRConfiguration } from "swr"; +import type { AxiosRequestConfig } from "axios"; +import { get, post, patch, deleteOp } from "@/utils/ApiClient"; +import { useAppSelector } from "@/hooks/AppHooks"; +import { useToast } from "@/hooks//UseToast"; + +export type ApiResponse = { + message: string; + data: T; +}; + +export interface IExtraConfig { + useTeamIdAsKey?: boolean; +} + +// Generic fetcher for GET requests +const fetcher = async (url: string, config?: AxiosRequestConfig) => { + const res = await get(url, config); + return res.data; +}; +export const useGet = ( + url: string, + axiosConfig?: AxiosRequestConfig, + swrConfig?: SWRConfiguration, + extraConfig?: IExtraConfig +) => { + const { toastError } = useToast(); + const currentTeamId = useAppSelector((state) => state.auth.selectedTeamId); + const { data, error, isLoading, isValidating, mutate } = useSWR( + // Key + extraConfig?.useTeamIdAsKey && currentTeamId ? [url, currentTeamId] : url, + // Fetcher + (key) => { + // Fetcher receives the key as an argument + const targetUrl = Array.isArray(key) ? key[0] : key; + return fetcher(targetUrl, { + ...axiosConfig, + headers: { + ...axiosConfig?.headers, + "x-team-id": currentTeamId, + }, + }); + }, + // Config + { + onError: (err) => { + console.log(err); + toastError( + err?.response?.data?.message || + err.message || + "An unexpected error occurred" + ); + }, + ...swrConfig, + } + ); + + return { + response: data ?? null, + loading: isLoading, + isValidating, + error: error?.message ?? null, + refetch: mutate, + }; +}; + +export const usePost = () => { + const currentTeamId = useAppSelector((state) => state.auth.selectedTeamId); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError } = useToast(); + + const postFn = async ( + endpoint: string, + body: B, + config?: AxiosRequestConfig + ): Promise | null> => { + setLoading(true); + setError(null); + + try { + const res = await post>(endpoint, body, { + ...config, + headers: { + ...config?.headers, + "x-team-id": currentTeamId, + }, + }); + return res.data; + } catch (err: any) { + const errMsg = + err?.response?.data?.msg || err.message || "An error occurred"; + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { post: postFn, loading, error }; +}; + +export const usePatch = () => { + const currentTeamId = useAppSelector((state) => state.auth.selectedTeamId); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError, toastSuccess } = useToast(); + + const patchFn = async ( + endpoint: string, + body?: B, + config?: AxiosRequestConfig + ): Promise | null> => { + setLoading(true); + setError(null); + + try { + const res = await patch>(endpoint, body, { + ...config, + headers: { + ...config?.headers, + "x-team-id": currentTeamId, + }, + }); + toastSuccess(res.data?.message || "Operation successful"); + return res.data; + } catch (err: any) { + const errMsg = + err?.response?.data?.msg || err.message || "An error occurred"; + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { patch: patchFn, loading, error }; +}; + +export const useDelete = () => { + const currentTeamId = useAppSelector((state) => state.auth.selectedTeamId); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError, toastSuccess } = useToast(); + + const deleteFn = async ( + endpoint: string, + config?: AxiosRequestConfig + ): Promise | null> => { + setLoading(true); + setError(null); + + try { + const res = await deleteOp>(endpoint, { + ...config, + headers: { + ...config?.headers, + "x-team-id": currentTeamId, + }, + }); + toastSuccess(res.data?.message || "Operation successful"); + return res.data; + } catch (err: any) { + const errMsg = + err?.response?.data?.msg || err.message || "An error occurred"; + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { deleteFn, loading, error }; +}; diff --git a/client/src/hooks/UseToast.ts b/client/src/hooks/UseToast.ts new file mode 100644 index 000000000..6a4884741 --- /dev/null +++ b/client/src/hooks/UseToast.ts @@ -0,0 +1,16 @@ +import { toast, type ToastOptions } from "react-toastify"; +export const useToast = () => { + const showToast = (message: string, options?: ToastOptions) => { + toast.dismiss(); + toast(message, options); + }; + + const toastSuccess = (msg: string, opts?: ToastOptions) => + showToast(msg, { ...opts, type: "success" }); + const toastError = (msg: string, opts?: ToastOptions) => + showToast(msg, { ...opts, type: "error" }); + const toastInfo = (msg: string, opts?: ToastOptions) => + showToast(msg, { ...opts, type: "info" }); + + return { showToast, toastSuccess, toastError, toastInfo }; +}; diff --git a/client/src/hooks/forms/UseInitInfraForm.tsx b/client/src/hooks/forms/UseInitInfraForm.tsx new file mode 100644 index 000000000..d5f740f5b --- /dev/null +++ b/client/src/hooks/forms/UseInitInfraForm.tsx @@ -0,0 +1,31 @@ +import { monitorSchemaInfra } from "@/validation/zod"; +import ms from "ms"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + let humanInterval = "1 minute"; + if (initialData?.interval) { + const parsed = Number(initialData.interval); + if (!isNaN(parsed)) { + humanInterval = ms(parsed, { long: true }); + } + } + + const defaults: z.infer = { + type: "infrastructure", + url: initialData?.url || "", + secret: initialData?.secret || "", + n: initialData?.n || 3, + notificationChannels: initialData?.notificationChannels || [], + name: initialData?.name || "", + interval: humanInterval, + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitMaintenanceForm.tsx b/client/src/hooks/forms/UseInitMaintenanceForm.tsx new file mode 100644 index 000000000..e82d99543 --- /dev/null +++ b/client/src/hooks/forms/UseInitMaintenanceForm.tsx @@ -0,0 +1,24 @@ +import { maintenanceSchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + name: initialData?.name || "", + repeat: initialData?.repeat || "no repeat", + startTime: initialData?.startTime + ? new Date(initialData.startTime) + : new Date(), + endTime: initialData?.endTime + ? new Date(initialData.endTime) + : new Date(), + monitors: initialData?.monitors || [], + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitMonitorFrom.tsx b/client/src/hooks/forms/UseInitMonitorFrom.tsx new file mode 100644 index 000000000..65e06edec --- /dev/null +++ b/client/src/hooks/forms/UseInitMonitorFrom.tsx @@ -0,0 +1,30 @@ +import { monitorSchema } from "@/validation/zod"; +import ms from "ms"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + let humanInterval = "1 minute"; + if (initialData?.interval) { + const parsed = Number(initialData.interval); + if (!isNaN(parsed)) { + humanInterval = ms(parsed, { long: true }); + } + } + + const defaults: z.infer = { + type: initialData?.type || "https", + url: initialData?.url || "", + n: initialData?.n || 3, + notificationChannels: initialData?.notificationChannels || [], + name: initialData?.name || "", + interval: humanInterval, + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitNotificationsChannelForm.tsx b/client/src/hooks/forms/UseInitNotificationsChannelForm.tsx new file mode 100644 index 000000000..c0b1ba95c --- /dev/null +++ b/client/src/hooks/forms/UseInitNotificationsChannelForm.tsx @@ -0,0 +1,21 @@ +import { notificationChannelSchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + name: initialData?.name || "", + type: initialData?.type || "email", + config: { + url: initialData?.config?.url || "", + emailAddress: initialData?.config?.emailAddress || "", + }, + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitPageSpeedForm.tsx b/client/src/hooks/forms/UseInitPageSpeedForm.tsx new file mode 100644 index 000000000..c5438860e --- /dev/null +++ b/client/src/hooks/forms/UseInitPageSpeedForm.tsx @@ -0,0 +1,30 @@ +import { monitorSchemaPageSpeed } from "@/validation/zod"; +import ms from "ms"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + let humanInterval = "3 minutes"; + if (initialData?.interval) { + const parsed = Number(initialData.interval); + if (!isNaN(parsed)) { + humanInterval = ms(parsed, { long: true }); + } + } + + const defaults: z.infer = { + type: "pagespeed", + url: initialData?.url || "", + n: initialData?.n || 3, + notificationChannels: initialData?.notificationChannels || [], + name: initialData?.name || "", + interval: humanInterval, + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitProfileForm.tsx b/client/src/hooks/forms/UseInitProfileForm.tsx new file mode 100644 index 000000000..5be1c3d3b --- /dev/null +++ b/client/src/hooks/forms/UseInitProfileForm.tsx @@ -0,0 +1,19 @@ +import { profileSchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + firstName: initialData?.firstName || "", + lastName: initialData?.lastName || "", + password: undefined, + confirmPassword: undefined, + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitRecoveryForm.tsx b/client/src/hooks/forms/UseInitRecoveryForm.tsx new file mode 100644 index 000000000..4429d4b5a --- /dev/null +++ b/client/src/hooks/forms/UseInitRecoveryForm.tsx @@ -0,0 +1,16 @@ +import { recoverySchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + email: initialData?.email || "", + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitRegisterForm.tsx b/client/src/hooks/forms/UseInitRegisterForm.tsx new file mode 100644 index 000000000..60a1e0c1e --- /dev/null +++ b/client/src/hooks/forms/UseInitRegisterForm.tsx @@ -0,0 +1,20 @@ +import { registerSchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + email: initialData?.email || "", + password: initialData?.password || "", + firstName: initialData?.firstName || "", + lastName: initialData?.lastName || "", + confirmPassword: initialData?.confirmPassword || "", + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitResetForm.tsx b/client/src/hooks/forms/UseInitResetForm.tsx new file mode 100644 index 000000000..f44f15e4c --- /dev/null +++ b/client/src/hooks/forms/UseInitResetForm.tsx @@ -0,0 +1,17 @@ +import { resetSchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + password: initialData?.password || "", + confirmPassword: initialData?.confirmPassword || "", + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitStatusPageForm.tsx b/client/src/hooks/forms/UseInitStatusPageForm.tsx new file mode 100644 index 000000000..1a258ee0c --- /dev/null +++ b/client/src/hooks/forms/UseInitStatusPageForm.tsx @@ -0,0 +1,20 @@ +import { statusPageSchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + name: initialData?.name || "", + description: initialData?.description || "", + url: initialData?.url || Math.floor(Math.random() * 1000000).toFixed(0), + isPublished: initialData?.isPublished || false, + monitors: initialData?.monitors || [], + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitTeamMemberForm.tsx b/client/src/hooks/forms/UseInitTeamMemberForm.tsx new file mode 100644 index 000000000..3830601b4 --- /dev/null +++ b/client/src/hooks/forms/UseInitTeamMemberForm.tsx @@ -0,0 +1,18 @@ +import { teamMemberSchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + userId: initialData?.userId || "", + teamId: initialData?.teamId || "", + roleId: initialData?.roleId || "", + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/hooks/forms/UseInitTeamsForm.tsx b/client/src/hooks/forms/UseInitTeamsForm.tsx new file mode 100644 index 000000000..eabc71f45 --- /dev/null +++ b/client/src/hooks/forms/UseInitTeamsForm.tsx @@ -0,0 +1,18 @@ +import { teamSchema } from "@/validation/zod"; +import { useMemo } from "react"; + +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + return useMemo(() => { + const defaults: z.infer = { + name: initialData?.name || "", + description: initialData?.description || "", + roleId: initialData?.roleId || "", + }; + return { defaults }; + }, [initialData]); +}; diff --git a/client/src/index.css b/client/src/index.css deleted file mode 100644 index d58a7e872..000000000 --- a/client/src/index.css +++ /dev/null @@ -1,81 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); - -* { - box-sizing: border-box; -} - -html { - scroll-behavior: smooth; -} - -:root { - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - /* Generalized Stylings */ - --env-var-radius-2: 8px; - - --env-var-width-2: 360px; - --env-var-width-4: 100px; - - --env-var-height-2: 34px; - - --env-var-nav-bar-height: 70px; - --env-var-side-bar-width: 250px; - --env-var-side-bar-collapsed-width: 64px; - --env-var-side-bar-auth-footer-height: 50px; - - --env-var-spacing-1: 12px; - --env-var-spacing-1-plus: 16px; - --env-var-spacing-1-minus: 10px; - --env-var-spacing-2: 24px; - - --env-var-font-size-small: 11px; - --env-var-font-size-small-plus: 12px; - --env-var-font-size-medium: 13px; - --env-var-font-size-medium-plus: 14px; - --env-var-font-size-large: 16px; - --env-var-font-size-large-plus: 22px; - --env-var-font-size-xlarge: 30px; -} - -.MuiInputBase-root.Mui-disabled input { - cursor: not-allowed; -} - -.Toastify__toast-container { - width: auto; -} -.Toastify__toast-body .alert { - min-width: 150px; - padding: 5px 10px; - align-items: center; -} -.Toastify [class^="Toastify__toast"] { - padding: 0; - margin: 0; -} -.Toastify__toast { - min-height: 0; - border-radius: 4px; -} -.Toastify [class*="Toastify__toast-theme"] { - background-color: transparent; -} - -.MuiTouchRipple-root { - display: none; -} - -@keyframes ripple { - from { - opacity: 1; - transform: scale(0); - } - to { - opacity: 0; - transform: scale(2); - } -} diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json index 6cec253d8..d8019d5de 100644 --- a/client/src/locales/ar.json +++ b/client/src/locales/ar.json @@ -421,13 +421,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "", @@ -489,8 +483,7 @@ "ping": "", "docker": "", "port": "", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "", @@ -1078,11 +1071,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json index 837b49803..71c7e24b9 100644 --- a/client/src/locales/cs.json +++ b/client/src/locales/cs.json @@ -421,13 +421,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "", @@ -489,8 +483,7 @@ "ping": "", "docker": "", "port": "", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "Checkmate", @@ -1078,11 +1071,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/de.json b/client/src/locales/de.json index da5a3ee61..ef8df5df4 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -382,56 +382,50 @@ "created": "Angelegt" }, "addTeamMember": { - "addMemberMenu": "Teammitglied hinzufügen", - "title": "Neues Teammitglied registrieren", - "description": "Einen neuen Benutzer anlegen und Zugangsdaten mit ihm teilen. Diese Methode gibt dem Mitglied direkt Zugriff auf alle Überwachungen.", - "addButton": "Mitglied hinzufügen" + "addMemberMenu": "", + "title": "", + "description": "", + "addButton": "" }, - "register": "Teammitglied registrieren", + "register": "", "registerToast": { - "success": "Benutzer erstellt, Zugangsdaten auf sichere Weise weitergeben.", - "dbUserExists": "Der Benutzer existiert bereits.", - "unknownError": "Unbekannter Fehler aufgetreten." + "success": "", + "dbUserExists": "", + "unknownError": "" }, "registerTeamMember": { - "title": "Teammitglied registrieren", + "title": "", "auth": { "common": { "inputs": { "firstName": { "errors": { - "empty": "Bitte einen Namen eingeben", - "pattern": "Der Name darf nur Buchstaben, Leerzeichen, Apostrophe oder Bindestriche enthalten" + "empty": "", + "pattern": "" } }, "lastName": { "errors": { - "empty": "Bitte einen Vornamen eingeben", - "pattern": "Der Vorname darf nur Buchstaben, Leerzeichen, Apostrophe oder Bindestriche enthalten" + "empty": "", + "pattern": "" } }, "email": { "errors": { - "empty": "Zum Fortsetzen bitte eine E-Mail-Adresse eingeben", - "invalid": "Bitte die Gültigkeit der E-Mail-Adresse erneut prüfen" + "empty": "", + "invalid": "" } }, "role": { "errors": { - "empty": "Eine Rolle wird benötigt" + "empty": "" } } } } } }, - "role": "Rolle", - "changeTeamPassword": { - "changePasswordMenu": "Passwort zurücksetzen", - "title": "Passwort des Teammitgliedes zurücksetzen", - "description": "Neues Passwort für dieses Teammitglied erstellen. Es muss auf einem Sicheren weg zur Verfügung gestellt werden.", - "success": "Passwort erfolgreich zurückgesetzt. Bitte sicherstellen, dass die Zugangsdaten auf sichere Weise weitergegeben werden." - } + "role": "" }, "monitorState": { "paused": "Pausiert", @@ -493,8 +487,7 @@ "ping": "Geben Sie die IP-Adresse oder den Hostnamen ein, der angepingt werden soll (z. B. 192.168.1.100 oder example.com), und fügen Sie einen eindeutigen Anzeigenamen hinzu, der auf dem Dashboard angezeigt wird.", "docker": "Geben Sie die Docker-ID Ihres Containers ein. Docker-IDs müssen die vollständige 64-Zeichen-ID sein. Sie können docker inspect ausführen, um die vollständige Container-ID abzurufen.", "port": "Geben Sie die URL oder IP des Servers, die Portnummer und einen eindeutigen Anzeigenamen ein, der auf dem Dashboard angezeigt wird.", - "game": "IP Adresse oder Hostnamen und Port für Ping eingeben (z.B. 192.168.1.100 oder beispiel.de) und Spieletyp auswählen.", - "https": "Zu überwachende Adresse oder IP (z.B. https://beispiel.de/ oder 192.168.1.100) eingeben und einen klaren Anzeigenamen, der im Dashboard erscheint." + "game": "" }, "common": { "appName": "Checkmate", @@ -614,7 +607,7 @@ "forgotPassword": "Passwort vergessen?", "register": "Noch keinen Account?", "forgotPasswordLink": "Passwort zurücksetzen", - "registerLink": "Hier registrieren" + "registerLink": "Hier resistrieren" }, "toasts": { "success": "Willkommen zurück. Erfolgreich angemeldet.", @@ -625,7 +618,7 @@ "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" } }, - "welcome": "Willkommen zurück bei Checkmate!" + "welcome": "" }, "registration": { "heading": { @@ -652,7 +645,7 @@ "toasts": { "success": "Willkommen! Benutzer erfolgreich angelegt" }, - "welcome": "Willkommen bei Checkmate!" + "welcome": "" }, "forgotPassword": { "heading": "Passwort vergessen?", @@ -753,8 +746,8 @@ "monitorStatus": { "checkingEvery": "Überprüfung alle {{interval}}", "withCaptureAgent": "mit Capture-Agent {{version}}", - "up": "Online", - "down": "Offline", + "up": "hoch", + "down": "runter", "paused": "pausiert" }, "advancedMatching": "Erweiterter Abgleich", @@ -799,7 +792,7 @@ "lastRunHeader": "Letzter Lauf um", "lastFinishedAtHeader": "Zuletzt abgeschlossen um", "lastRunTookHeader": "Letzter Lauf dauerte", - "intervalHeader": "Invervall" + "intervalHeader": "" }, "metricsTable": { "title": "Warteschlangenmetriken", @@ -907,8 +900,8 @@ "title": "Überwachen Sie IP/URL auf der Statusseite" }, "globalThresholds": { - "title": "Globale Schwellenwerte", - "description": "Globale CPU-, Speicher-, Festplatten- und Temperaturschwellenwerte festlegen. Wenn ein Wert angegeben ist, wird er automatisch für die Überwachung genutzt." + "title": "", + "description": "" } }, "statusPageCreate": { @@ -982,11 +975,11 @@ "oneHour": "1 Stunde", "oneDay": "1 Tag", "oneWeek": "1 Woche", - "fourMinutes": "4 Minuten", - "oneMinute": "Eine Minute", - "twoMinutes": "Zwei Minuten", - "fifteenSeconds": "15 Sekunden", - "thirtySeconds": "30 Sekunden" + "fourMinutes": "", + "oneMinute": "", + "twoMinutes": "", + "fifteenSeconds": "", + "thirtySeconds": "" }, "general": { "noOptionsFound": "Keine {{unit}} gefunden" @@ -1057,77 +1050,73 @@ "incidentsPageActionResolveMonitor": "Monitor-Vorfälle beheben", "incidentsPageActionResolveAll": "Alle Vorfälle lösen", "matchMethodOptions": { - "equal": "Gleich", - "equalPlaceholder": "Erfolg", - "include": "Enthält", - "includePlaceholder": "OK", - "regex": "Regex", - "regexPlaceholder": "^(success|ok)$", - "text": "Übereinstummungsmethode" + "equal": "", + "equalPlaceholder": "", + "include": "", + "includePlaceholder": "", + "regex": "", + "regexPlaceholder": "", + "text": "" }, "monitorType": { "docker": { - "label": "Container Name/ID", - "namePlaceholder": "Mein Container", - "placeholder": "Meine-App" + "label": "", + "namePlaceholder": "", + "placeholder": "" }, "http": { - "label": "Zu überwachende URL", - "namePlaceholder": "Google", - "placeholder": "google.de" + "label": "", + "namePlaceholder": "", + "placeholder": "" }, "ping": { - "label": "Zu überwachende IP-Adresse", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" + "label": "", + "namePlaceholder": "", + "placeholder": "" }, "port": { - "label": "Zu überwachende URL", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" + "label": "", + "namePlaceholder": "", + "placeholder": "" }, "game": { - "label": "Zu überwachende URL", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" + "label": "", + "namePlaceholder": "", + "placeholder": "" } }, "uptimeAdvancedMatching": { - "jsonPath": "JSON Pfad" + "jsonPath": "" }, - "bytesPerSecond": "Bytes pro Sekunde", - "bytesReceived": "Empfangene Bytes", - "bytesSent": "Gesendete Bytes", - "chooseGame": "Spiel auswählen", + "bytesPerSecond": "", + "bytesReceived": "", + "bytesSent": "", + "chooseGame": "", "createMonitorPage": { - "incidentConfigDescription": "Mithilfe eines gleitenden Fensters wird ermittelt, wann ein Monitor ausfällt. Der Status eines Monitors ändert sich nur, wenn der Prozentsatz der Prüfungen im gleitenden Fenster den angegebenen Wert erreicht.", - "incidentConfigStatusWindowLabel": "Wie viele Prüfungen sollen im gleitenden Fenster ausgeführt werden?", - "incidentConfigStatusWindowThresholdLabel": "Wie viel Prozent der Prüfungen im gleitenden Fenster sollen fehlschlagen/erfolgreich sein, bevor sich der Status ändert?", - "incidentConfigTitle": "Vorfälle", - "incidentConfigDescriptionV2": "Anzahl der aufeinanderfolgenden Prüfungen, die zum Ändern des Monitorstatus erforderlich sind. Maximal 25", - "incidentConfigStatusCheckNumber": "Anzahl der Prüfungen bevor sich er Status ändert", - "intervalTitle": "Prüfintervall", - "intervalDescription": "Wie oft soll die Überwachung geprüft werden." + "incidentConfigDescription": "", + "incidentConfigStatusWindowLabel": "", + "incidentConfigStatusWindowThresholdLabel": "", + "incidentConfigTitle": "" }, - "dataRate": "Datenrate", - "dataReceived": "Empfangene Daten", - "dataSent": "Gesendete Daten", - "details": "Details", - "drops": "verloren", - "errors": "Fehler", - "errorsIn": "Fehler eingehend", - "errorsOut": "Fehler ausgehend", - "gameServerMonitoring": "Spiele Server Überwachung", - "gameServerMonitoringDescription": "Prüfen ob der Spiele-Server läuft oder nicht", - "network": "Netzwerk", - "networkDrops": "Netzwerkabbrüche", - "networkErrors": "Netzwerkfehler", - "networkInterface": "Netzwerkschnittstelle", - "noNetworkStatsAvailable": "Keine Netzwerkstatistiken verfügbar.", - "packetsPerSecond": "Pakete pro Sekunde", - "packetsReceived": "Empfangene Pakete", - "packetsReceivedRate": "Rate empfangener Pakete", - "packetsSent": "Gesendete Pakete", - "rate": "Rate", - "selectInterface": "Schnittstelle auswählen" + "dataRate": "", + "dataReceived": "", + "dataSent": "", + "details": "", + "drops": "", + "errors": "", + "errorsIn": "", + "errorsOut": "", + "gameServerMonitoring": "", + "gameServerMonitoringDescription": "", + "network": "", + "networkDrops": "", + "networkErrors": "", + "networkInterface": "", + "noNetworkStatsAvailable": "", + "packetsPerSecond": "", + "packetsReceived": "", + "packetsReceivedRate": "", + "packetsSent": "", + "rate": "", + "selectInterface": "" } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index e2e1a4faf..8c3c9a7b5 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1,1140 +1,1181 @@ { - "submit": "Submit", - "title": "Title", - "distributedStatusHeaderText": "Real-time, real-device coverage", - "distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city", - "settingsDisabled": "Disabled", - "settingsSuccessSaved": "Settings saved successfully", - "settingsFailedToSave": "Failed to save settings", - "settingsStatsCleared": "Stats cleared successfully", - "settingsFailedToClearStats": "Failed to clear stats", - "settingsMonitorsDeleted": "Successfully deleted all monitors", - "settingsFailedToDeleteMonitors": "Failed to delete all monitors", - "starPromptTitle": "Star Checkmate", - "starPromptDescription": "See the latest releases and help grow the community on GitHub", - "https": "HTTPS", - "http": "HTTP", - "monitor": "monitor", - "aboutus": "About Us", - "now": "Now", - "delete": "Delete", - "configure": "Configure", - "responseTime": "Response time", - "responseTimeBreakdown": "Response time breakdown", - "dnsLookup": "DNS lookup", - "tcpConnection": "TCP connection", - "tlsHandshake": "TLS handshake", - "waitTime": "Wait time", - "timeToFirstByte": "Time to first byte", - "download": "Download", - "ms": "ms", - "bar": "Bar", - "area": "Area", - "country": "COUNTRY", - "city": "CITY", - "response": "RESPONSE", - "monitorStatusUp": "Monitor {name} ({url}) is now UP and responding", - "monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding", - "webhookSendSuccess": "Webhook notification sent successfully", - "webhookSendError": "Error sending webhook notification to {platform}", - "webhookUnsupportedPlatform": "Unsupported platform: {platform}", - "distributedRightCategoryTitle": "Monitor", - "distributedStatusServerMonitors": "Server Monitors", - "distributedStatusServerMonitorsDescription": "Monitor status of related servers", - "distributedUptimeCreateSelectURL": "Here you can select the URL of the host, together with the type of monitor.", - "distributedUptimeCreateChecks": "Checks to perform", - "distributedUptimeCreateChecksDescription": "You can always add or remove checks after adding your site.", - "distributedUptimeCreateIncidentNotification": "Incident notifications", - "distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.", - "distributedUptimeCreateAdvancedSettings": "Advanced settings", - "distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.", - "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated", - "notifications": { - "enableNotifications": "Enable {{platform}} notifications", - "testNotification": "Test notification", - "addOrEditNotifications": "Add or edit notifications", - "slack": { - "label": "Slack", - "description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "Slack webhook URL is required" - }, - "discord": { - "label": "Discord", - "description": "To send data to a Discord channel from Checkmate via Discord notifications using webhooks, you can use Discord's incoming Webhooks feature.", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "Discord webhook URL is required" - }, - "telegram": { - "label": "Telegram", - "description": "To enable Telegram notifications, create a Telegram bot using BotFather, an official bot for creating and managing Telegram bots. Then, get the API token and chat ID and write them down here.", - "tokenLabel": "Your bot token", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "Your Chat ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "Telegram token and chat ID are required" - }, - "webhook": { - "label": "Webhooks", - "description": "You can set up a custom webhook to receive notifications when incidents occur.", - "urlLabel": "Webhook URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "Webhook URL is required" - }, - "testNotificationDevelop": "Test notification 2", - "integrationButton": "Notification Integration", - "testSuccess": "Test notification sent successfully!", - "testFailed": "Failed to send test notification", - "unsupportedType": "Unsupported notification type", - "networkError": "Network error occurred", - "fallback": { - "title": "A notification channel is used to:", - "checks": [ - "Alert teams about downtime or performance issues", - "Let engineers know when incidents happen", - "Keep administrators informed of system changes" - ], - "actionButton": "Let's create your first notification channel!" - }, - "createButton": "Create notification channel", - "createTitle": "Notification channel", - "create": { - "success": "Notification created successfully", - "failed": "Failed to create notification" - }, - "fetch": { - "success": "Notifications fetched successfully", - "failed": "Failed to fetch notifications" - }, - "delete": { - "success": "Notification deleted successfully", - "failed": "Failed to delete notification" - }, - "edit": { - "success": "Notification updated successfully", - "failed": "Failed to update notification" - }, - "test": { - "success": "Test notification sent successfully", - "failed": "Failed to send test notification" - } - }, - "testLocale": "testLocale", - "add": "Add", - "monitors": "monitors", - "distributedUptimeStatusCreateStatusPage": "status page", - "distributedUptimeStatusCreateStatusPageAccess": "Access", - "distributedUptimeStatusCreateStatusPageReady": "If your status page is ready, you can mark it as published.", - "distributedUptimeStatusBasicInfoHeader": "Basic Information", - "distributedUptimeStatusBasicInfoDescription": "Define company name and the subdomain that your status page points to.", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "Upload a logo for your status page", - "distributedUptimeStatusLogoUploadButton": "Upload logo", - "distributedUptimeStatusStandardMonitorsHeader": "Standard Monitors", - "distributedUptimeStatusStandardMonitorsDescription": "Attach standard monitors to your status page.", - "distributedUptimeStatusCreateYour": "Create your", - "distributedUptimeStatusEditYour": "Edit your", - "distributedUptimeStatusPublishedLabel": "Published and visible to the public", - "distributedUptimeStatusCompanyNameLabel": "Company name", - "distributedUptimeStatusPageAddressLabel": "Your status page address", - "distributedUptimeStatus30Days": "30 days", - "distributedUptimeStatus60Days": "60 days", - "distributedUptimeStatus90Days": "90 days", - "distributedUptimeStatusPageNotSetUp": "A status page is not set up.", - "distributedUptimeStatusContactAdmin": "Please contact your administrator", - "distributedUptimeStatusPageNotPublic": "This status page is not public.", - "distributedUptimeStatusPageDeleteDialog": "Do you want to delete this status page?", - "distributedUptimeStatusPageDeleteConfirm": "Yes, delete status page", - "distributedUptimeStatusPageDeleteDescription": "Once deleted, your status page cannot be retrieved.", - "distributedUptimeStatusDevices": "Devices", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "UPT Burned", - "distributedUptimeStatusUptLogo": "Upt Logo", - "incidentsTableNoIncidents": "No incidents recorded", - "incidentsTablePaginationLabel": "incidents", - "incidentsTableMonitorName": "Monitor Name", - "incidentsTableStatus": "Status", - "incidentsTableDateTime": "Date & Time", - "incidentsTableStatusCode": "Status Code", - "incidentsTableMessage": "Message", - "incidentsOptionsHeader": "Incidents for:", - "incidentsOptionsHeaderFilterBy": "Filter by:", - "incidentsOptionsHeaderFilterAll": "All", - "incidentsOptionsHeaderFilterDown": "Down", - "incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve", - "incidentsOptionsHeaderShow": "Show:", - "incidentsOptionsHeaderLastHour": "Last hour", - "incidentsOptionsHeaderLastDay": "Last day", - "incidentsOptionsHeaderLastWeek": "Last week", - "incidentsOptionsPlaceholderAllServers": "All servers", - "infrastructureCreateYour": "Create your", - "infrastructureCreateGeneralSettingsDescription": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent.", - "infrastructureServerRequirement": "The server you are monitoring must be running the", - "infrastructureCustomizeAlerts": "Customize alerts", - "infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.", - "infrastructureCreateMonitor": "Create Infrastructure Monitor", - "infrastructureProtocol": "Protocol", - "infrastructureServerUrlLabel": "Server URL", - "infrastructureDisplayNameLabel": "Display name", - "infrastructureAuthorizationSecretLabel": "Authorization secret", - "gb": "GB", - "mb": "MB", - "mem": "Mem", - "memoryUsage": "Memory usage", - "cpu": "CPU", - "cpuUsage": "CPU usage", - "cpuTemperature": "CPU Temperature", - "diskUsage": "Disk Usage", - "used": "Used", - "total": "Total", - "cores": "Cores", - "frequency": "Frequency", - "status": "Status", - "cpuPhysical": "CPU (Physical)", - "cpuLogical": "CPU (Logical)", - "cpuFrequency": "CPU Frequency", - "avgCpuTemperature": "Average CPU Temperature", - "memory": "Memory", - "disk": "Disk", - "uptime": "Uptime", - "os": "OS", - "host": "Host", - "actions": "Actions", - "integrations": "Integrations", - "integrationsPrism": "Connect Prism to your favorite service.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Connect with Slack and see incidents in a channel", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere", - "commonSave": "Save", - "createYour": "Create your", - "createMonitor": "Create monitor", - "pause": "Pause", - "resume": "Resume", - "editing": "Editing...", - "url": "URL", - "access": "Access", - "timezone": "Timezone", - "features": "Features", - "administrator": "Administrator?", - "loginHere": "Login here", - "displayName": "Display name", - "urlMonitor": "URL to monitor", - "portToMonitor": "Port to monitor", - "websiteMonitoring": "Website monitoring", - "websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.", - "pingMonitoring": "Ping monitoring", - "pingMonitoringDescription": "Check whether your server is available or not.", - "dockerContainerMonitoring": "Docker container monitoring", - "dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.", - "portMonitoring": "Port monitoring", - "portMonitoringDescription": "Check whether your port is open or not.", - "createMaintenanceWindow": "Create maintenance window", - "createMaintenance": "Create maintenance", - "editMaintenance": "Edit maintenance", - "maintenanceWindowName": "Maintenance Window Name", - "friendlyNameInput": "Friendly name", - "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", - "maintenanceRepeat": "Maintenance Repeat", - "maintenance": "maintenance", - "duration": "Duration", - "addMonitors": "Add monitors", - "window": "window", - "cancel": "Cancel", - "message": "Message", - "low": "low", - "high": "high", - "statusCode": "Status code", - "date&Time": "Date & Time", - "type": "Type", - "statusPageName": "Status page name", - "publicURL": "Public URL", - "repeat": "Repeat", - "edit": "Edit", - "createA": "Create a", - "remove": "Remove", - "maintenanceWindowDescription": "During maintenance windows, all monitoring is suspended for selected monitors. No network checks will be performed, preventing any status updates or notifications from being triggered. Your monitors will appear frozen at their last known status, and status pages will display a maintenance indicator. Once the maintenance window ends, monitoring automatically resumes, and alerts will trigger if issues are detected. Maintenance periods do not count against uptime calculations.", - "startTime": "Start time", - "timeZoneInfo": "All dates and times are in GMT+0 time zone.", - "monitorsToApply": "Monitors to apply maintenance window to", - "nextWindow": "Next window", - "notFoundButton": "Go to the main dashboard", - "pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.", - "monitorDisplayName": "Monitor display name", - "whenNewIncident": "When there is a new incident,", - "notifySMS": "Notify via SMS (coming soon)", - "notifyEmails": "Also notify via email to multiple addresses (coming soon)", - "seperateEmails": "You can separate multiple emails with a comma", - "checkFrequency": "Check frequency", - "matchMethod": "Match Method", - "expectedValue": "Expected value", - "deleteDialogTitle": "Do you really want to delete this monitor?", - "deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.", - "pageSpeedMonitor": "PageSpeed monitor", - "shown": "Shown", - "ago": "ago", - "companyName": "Company name", - "pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.", - "pageSpeedDetailsPerformanceReportCalculator": "See calculator", - "checkingEvery": "Checking every", - "statusPageCreateSettings": "If your status page is ready, you can mark it as published.", - "basicInformation": "Basic Information", - "statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.", - "statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.", - "statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.", - "statusPageCreateSettingsCheckboxLabel": "Published and visible to the public", - "statusPageCreateBasicInfoStatusPageAddress": "Your status page address", - "statusPageCreateTabsContent": "Status page servers", - "statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.", - "statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page", - "showCharts": "Show charts", - "showUptimePercentage": "Show uptime percentage", - "removeLogo": "Remove Logo", - "statusPageStatus": "A public status page is not set up.", - "statusPageStatusContactAdmin": "Please contact to your administrator", - "statusPageStatusNotPublic": "This status page is not public.", - "statusPageStatusNoPage": "There's no status page here.", - "statusPageStatusServiceStatus": "Service status", - "deleteStatusPage": "Do you want to delete this status page?", - "deleteStatusPageConfirm": "Yes, delete status page", - "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", - "uptimeCreate": "The expected value is used to match against response result, and the match determines the status.", - "uptimeCreateJsonPath": "This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See", - "uptimeCreateJsonPathQuery": "for query language documentation.", - "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", - "infrastructureEditYour": "Edit your", - "infrastructureEditMonitor": "Save Infrastructure Monitor", - "infrastructureMonitorCreated": "Infrastructure monitor created successfully!", - "infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!", - "errorInvalidTypeId": "Invalid notification type provided", - "errorInvalidFieldId": "Invalid field ID provided", - "inviteNoTokenFound": "No invite token found", - "pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.", - "pageSpeedLearnMoreLink": "Click here", - "pageSpeedAddApiKey": "to add your API key.", - "update": "Update", - "invalidFileFormat": "Unsupported file format!", - "invalidFileSize": "File size is too large!", - "ClickUpload": "Click to upload", - "DragandDrop": "drag and drop", - "MaxSize": "Maximum Size", - "SupportedFormats": "Supported formats", - "FirstName": "First name", - "LastName": "Last name", - "EmailDescriptionText": "This is your current email address — it cannot be changed.", - "YourPhoto": "Profile photo", - "PhotoDescriptionText": "This photo will be displayed in your profile page.", - "save": "Save", - "DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.", - "DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.", - "DeleteWarningTitle": "Really remove this account?", - "bulkImport": { - "title": "Bulk Import", - "selectFileTips": "Select CSV file to upload", - "selectFileDescription": "You can download our or sample", - "selectFile": "Select File", - "parsingFailed": "Parsing failed", - "uploadSuccess": "Monitors created successfully!", - "validationFailed": "Validation failed", - "noFileSelected": "No file selected", - "fallbackPage": "Import a file to upload a list of servers in bulk", - "invalidFileType": "Invalid file type", - "uploadFailed": "Upload failed" - }, - "DeleteAccountTitle": "Remove account", - "DeleteAccountButton": "Remove account", - "publicLink": "Public link", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "Reset", - "ignoreTLSError": "Ignore TLS/SSL error", - "tlsErrorIgnored": "TLS/SSL errors ignored", - "ignoreTLSErrorDescription": "Ignore TLS/SSL errors and continue checking the website's availability", - "createNew": "Create new", - "greeting": { - "prepend": "Hey there", - "append": "The afternoon is your playground—let's make it epic!", - "overview": "Here's an overview of your {{type}} monitors." - }, - "roles": { - "superAdmin": "Super admin", - "admin": "Admin", - "teamMember": "Team member", - "demoUser": "Demo user" - }, - "teamPanel": { - "teamMembers": "Team members", - "filter": { - "all": "All", - "member": "Member" - }, - "inviteTeamMember": "Invite a team member", - "inviteNewTeamMember": "Invite new team member", - "inviteDescription": "When you add a new team member, they will get access to all monitors.", - "email": "Email", - "selectRole": "Select role", - "inviteLink": "Invite link", - "cancel": "Cancel", - "noMembers": "There are no team members with this role", - "getToken": "Get token", - "emailToken": "E-mail token", - "table": { - "name": "Name", - "email": "Email", - "role": "Role", - "created": "Created" - }, - "addTeamMember": { - "addMemberMenu": "Add Team Member", - "title": "Register new team member", - "description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.", - "addButton": "Add Member" - }, - "register": "Register a team member", - "registerToast": { - "success": "User created, share credentials with the member securely.", - "dbUserExists": "User already exists.", - "unknownError": "Unknown error occurred." - }, - "registerTeamMember": { - "title": "Register team member", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "Please enter a name", - "pattern": "Name must contain only letters, spaces, apostrophes, or hyphens" - } - }, - "lastName": { - "errors": { - "empty": "Please enter a surname", - "pattern": "Surname must contain only letters, spaces, apostrophes, or hyphens" - } - }, - "email": { - "errors": { - "empty": "To continue, please enter an email address", - "invalid": "Please recheck validity of entered email address" - } - }, - "role": { - "errors": { - "empty": "Role is required" - } - } - } - } - } - }, - "role": "Role", - "changeTeamPassword": { - "changePasswordMenu": "Reset Password", - "title": "Reset team member password", - "description": "Create a new password for this team member. You will need to share the password with them securely.", - "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way." - } - }, - "monitorState": { - "paused": "Paused", - "resumed": "Resumed", - "active": "Active" - }, - "menu": { - "uptime": "Uptime", - "pagespeed": "Pagespeed", - "infrastructure": "Infrastructure", - "incidents": "Incidents", - "statusPages": "Status pages", - "maintenance": "Maintenance", - "integrations": "Integrations", - "settings": "Settings", - "support": "Support", - "discussions": "Discussions", - "docs": "Docs", - "changelog": "Changelog", - "profile": "Profile", - "password": "Password", - "team": "Team", - "logOut": "Log out", - "notifications": "Notifications", - "logs": "Logs" - }, - "settingsEmailUser": "Email user - Username for authentication, overrides email address if specified", - "state": "State", - "statusBreadCrumbsStatusPages": "Status Pages", - "statusBreadCrumbsDetails": "Details", - "commonSaving": "Saving...", - "navControls": "Controls", - "incidentsPageTitle": "Incidents", - "passwordPanel": { - "passwordChangedSuccess": "Your password was changed successfully.", - "passwordInputIncorrect": "Your password input was incorrect.", - "currentPassword": "Current password", - "enterCurrentPassword": "Enter your current password", - "newPassword": "New password", - "enterNewPassword": "Enter your new password", - "confirmNewPassword": "Confirm new password", - "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.", - "saving": "Saving..." - }, - "emailSent": "Email sent successfully", - "failedToSendEmail": "Failed to send email", - "settingsTestEmailSuccess": "Test email sent successfully", - "settingsTestEmailFailed": "Failed to send test email", - "settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}", - "settingsTestEmailUnknownError": "Unknown error", - "statusMsg": { - "paused": "Monitoring is paused.", - "up": "Your site is up.", - "down": "Your site is down.", - "pending": "Pending..." - }, - "uptimeGeneralInstructions": { - "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", - "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", - "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).", - "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", - "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", - "https": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard." - }, - "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "Toggles light & dark" - }, - "toasts": { - "networkError": "Network error", - "checkConnection": "Please check your connection", - "unknownError": "Unknown error" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "Continue", - "back": "Back" - }, - "inputs": { - "email": { - "label": "Email", - "placeholder": "jordan.ellis@domain.com", - "errors": { - "empty": "To continue, please enter your email address", - "invalid": "Please recheck validity of entered email address" - } - }, - "password": { - "label": "Password", - "rules": { - "length": { - "beginning": "Must be at least", - "highlighted": "8 characters long" - }, - "special": { - "beginning": "Must contain at least", - "highlighted": "one special character" - }, - "number": { - "beginning": "Must contain at least", - "highlighted": "one number" - }, - "uppercase": { - "beginning": "Must contain at least", - "highlighted": "one upper character" - }, - "lowercase": { - "beginning": "Must contain at least", - "highlighted": "one lower character" - }, - "match": { - "beginning": "Passwords", - "highlighted": "must match" - } - }, - "errors": { - "empty": "Please enter your password", - "length": "Password must be at least 8 characters long", - "uppercase": "Password must contain at least 1 uppercase letter", - "lowercase": "Password must contain at least 1 lowercase letter", - "number": "Password must contain at least 1 number", - "special": "Password must contain at least 1 special character", - "incorrect": "The password you provided does not match our records" - } - }, - "passwordConfirm": { - "label": "Confirm password", - "placeholder": "Re-enter password to confirm", - "errors": { - "empty": "Please enter your password again for confirmation (helps with typos)", - "different": "Entered passwords don't match, so one of them is probably mistyped" - } - }, - "firstName": { - "label": "Name", - "placeholder": "Jordan", - "errors": { - "empty": "Please enter your name", - "length": "Name must be less than 50 characters", - "pattern": "Name must contain only letters, spaces, apostrophes, or hyphens" - } - }, - "lastName": { - "label": "Surname", - "placeholder": "Ellis", - "errors": { - "empty": "Please enter your surname", - "length": "Surname must be less than 50 characters", - "pattern": "Surname must contain only letters, spaces, apostrophes, or hyphens" - } - } - }, - "errors": { - "validation": "Error validating data." - }, - "fields": { - "password": { - "errors": { - "incorrect": "The password you provided does not match our records" - } - }, - "role": { - "errors": { - "min": "At least one role is required" - } - } - } - }, - "login": { - "heading": "Log in to continue", - "subheadings": { - "stepOne": "Enter your email", - "stepTwo": "Enter your password" - }, - "links": { - "forgotPassword": "Forgot password?", - "register": "Do not have an account?", - "forgotPasswordLink": "Reset password", - "registerLink": "Register here" - }, - "toasts": { - "success": "Welcome back! You're successfully logged in.", - "incorrectPassword": "Incorrect password" - }, - "errors": { - "password": { - "incorrect": "The password you provided does not match our records" - } - }, - "welcome": "Welcome back to Checkmate!" - }, - "registration": { - "heading": { - "superAdmin": "Create super admin", - "user": "Sign Up" - }, - "subheadings": { - "stepOne": "Enter your personal details", - "stepTwo": "Enter your email", - "stepThree": "Create your password" - }, - "description": { - "superAdmin": "Create your super admin account to get started", - "user": "Sign up as a user and ask super admin for access to your monitors" - }, - "gettingStartedButton": { - "superAdmin": "Create super admin account", - "user": "Sign up regular user" - }, - "termsAndPolicies": "By creating an account, you agree to our Terms of Service and Privacy Policy.", - "links": { - "login": "Already have an account? Log In" - }, - "toasts": { - "success": "Welcome! Your account was created successfully." - }, - "welcome": "Welcome to Checkmate!" - }, - "forgotPassword": { - "heading": "Forgot password?", - "subheadings": { - "stepOne": "No worries, we'll send you reset instructions.", - "stepTwo": "We sent a password reset link to ", - "stepThree": "Your new password must be different from previously used passwords.", - "stepFour": "Your password has been successfully reset. Click below to log in magically." - }, - "buttons": { - "openEmail": "Open email app", - "resetPassword": "Reset password" - }, - "imageAlts": { - "passwordKey": "Password key icon", - "email": "Email icon", - "lock": "Lock icon", - "passwordConfirm": "Password confirm icon" - }, - "links": { - "login": "Go back to Log In", - "resend": "Didn't receive the email? Click to resend" - }, - "toasts": { - "sent": "Instructions sent to .", - "emailNotFound": "Email not found.", - "redirect": "Redirecting in ...", - "success": "Your password was reset successfully.", - "error": "Unable to reset password. Please try again later or contact support." - } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "Successfully reconnected to the server.", - "stillUnreachable": "Server is still unreachable. Please try again later." - }, - "alertBox": "Server Connection Error", - "description": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.", - "retryButton": { - "default": "Retry connection", - "processing": "Connecting..." - } - } - }, - "createNotifications": { - "title": "Create notification channel", - "nameSettings": { - "title": "Name", - "description": "A descriptive name for your integration.", - "nameLabel": "Name", - "namePlaceholder": "e.g. Slack notifications" - }, - "typeSettings": { - "title": "Type", - "description": "Select the type of notification channel you want to create.", - "typeLabel": "Type" - }, - "emailSettings": { - "title": "Email", - "description": "Destination email addresses.", - "emailLabel": "Email address", - "emailPlaceholder": "e.g. john@example.com" - }, - "slackSettings": { - "title": "Slack", - "description": "Configure your Slack webhook here", - "webhookLabel": "Slack webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "Configure your PagerDuty integration here", - "integrationKeyLabel": "Integration key", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discord", - "description": "Configure your Discord webhook here", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "webhookSettings": { - "title": "Webhook", - "description": "Configure your webhook here", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "testNotification": "Test notification", - "dialogDeleteTitle": "Are you sure you want to delete this notification?", - "dialogDeleteConfirm": "Delete" - }, - "notificationConfig": { - "title": "Notifications", - "description": "Select the notifications channels you want to use" - }, - "monitorStatus": { - "checkingEvery": "Checking every {{interval}}", - "withCaptureAgent": "with Capture agent {{version}}", - "up": "up", - "down": "down", - "paused": "paused" - }, - "advancedMatching": "Advanced matching", - "sendTestNotifications": "Send test notifications", - "selectAll": "Select all", - "showAdminLoginLink": "Show \"Administrator? Login Here\" link on the status page", - "logsPage": { - "title": "Logs", - "description": "System logs - last 1000 lines", - "tabs": { - "queue": "Job queue", - "logs": "Server logs", - "diagnostics": "Diagnostics" - }, - "toast": { - "fetchLogsSuccess": "Logs fetched successfully" - }, - "logLevelSelect": { - "title": "Log level", - "values": { - "all": "All", - "info": "Info", - "warn": "Warn", - "error": "Error", - "debug": "Debug" - } - } - }, - "queuePage": { - "title": "Queue", - "refreshButton": "Refresh", - "flushButton": "Flush queue", - "jobTable": { - "title": "Jobs currently in queue", - "idHeader": "Monitor ID", - "urlHeader": "URL", - "typeHeader": "Type", - "activeHeader": "Active", - "lockedAtHeader": "Locked at", - "runCountHeader": "Run count", - "failCountHeader": "Fail count", - "lastRunHeader": "Last run at", - "lastFinishedAtHeader": "Last finished at", - "lastRunTookHeader": "Last run took", - "intervalHeader": "Interval" - }, - "metricsTable": { - "title": "Queue metrics", - "metricHeader": "Metric", - "valueHeader": "Value" - }, - "failedJobTable": { - "title": "Failed jobs", - "monitorIdHeader": "Monitor ID", - "monitorUrlHeader": "Monitor URL", - "failCountHeader": "Fail count", - "failedAtHeader": "Last failed at", - "failReasonHeader": "Fail reason" - } - }, - "export": { - "title": "Export Monitors", - "success": "Monitors exported successfully!", - "failed": "Failed to export monitors" - }, - "monitorActions": { - "title": "Export/Import", - "import": "Import Monitors", - "export": "Export Monitors", - "deleteSuccess": "Monitor deleted successfully", - "deleteFailed": "Failed to delete monitor", - "details": "Details" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "Developed by Bluewave Labs", - "labelVersion": "Version", - "title": "About" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "Add demo monitors", - "description": "Add sample monitors for demonstration purposes.", - "title": "Demo monitors" - }, - "emailSettings": { - "buttonSendTestEmail": "Send test e-mail", - "description": "Configure the email settings for your system. This is used to send notifications and alerts.", - "descriptionTransport": "This builds an SMTP transport for NodeMailer", - "labelAddress": "Email address - Used for authentication", - "labelConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting", - "labelHost": "Email host - Hostname or IP address to connect to", - "labelIgnoreTLS": "Disable STARTTLS: Don't use TLS even if the server supports it", - "labelPassword": "Email password - Password for authentication", - "labelPasswordSet": "Password is set. Click Reset to change it.", - "labelPool": "Enable connection pooling: Reuse existing connections to improve performance", - "labelPort": "Email port - Port to connect to", - "labelRejectUnauthorized": "Reject invalid certificates: Reject connections with self-signed or untrusted certificates", - "labelRequireTLS": "Force STARTTLS: Require TLS upgrade, fail if not supported", - "labelSecure": "Use SSL (recommended): Encrypt the connection using SSL/TLS", - "labelTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP", - "labelUser": "Email user - Username for authentication, overrides email address if specified", - "linkTransport": "See specifications here", - "placeholderUser": "Leave empty if not required", - "title": "Email", - "toastEmailRequiredFieldsError": "Email address, host, port and password are required" - }, - "pageSpeedSettings": { - "description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.", - "labelApiKeySet": "API key is set. Click Reset to change it.", - "labelApiKey": "PageSpeed API key", - "title": "Google PageSpeed API key" - }, - "saveButtonLabel": "Save", - "statsSettings": { - "clearAllStatsButton": "Clear all stats", - "clearAllStatsDescription": "Clear all stats. This is irreversible.", - "clearAllStatsDialogConfirm": "Yes, clear all stats", - "clearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.", - "clearAllStatsDialogTitle": "Do you want to clear all stats?", - "description": "Define how long you want to retain historical data. You can also clear all existing data.", - "labelTTL": "The days you want to keep monitoring history.", - "labelTTLOptional": "0 for infinite", - "title": "Monitor history" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "Remove all monitors", - "description": "Remove all monitors from your system.", - "dialogConfirm": "Yes, remove all monitors", - "dialogDescription": "Once removed, the monitors cannot be retrieved.", - "dialogTitle": "Do you want to remove all monitors?", - "title": "System reset" - }, - "timezoneSettings": { - "description": "Select the timezone used to display dates and times throughout the application.", - "label": "Display timezone", - "title": "Display timezone" - }, - "title": "Settings", - "uiSettings": { - "description": "Switch between light and dark mode, or change user interface language.", - "labelLanguage": "Language", - "labelTheme": "Theme mode", - "title": "Appearance" - }, - "urlSettings": { - "description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.", - "label": "Display IP/URL on status page", - "selectDisabled": "Disabled", - "selectEnabled": "Enabled", - "title": "Monitor IP/URL on Status Page" - }, - "globalThresholds": { - "title": "Global Thresholds", - "description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring." - } - }, - "statusPageCreate": { - "buttonSave": "Save" - }, - "incidentsOptionsHeaderFilterResolved": "Resolved", - "settingsSave": "Save", - "statusPageCreateAppearanceTitle": "Appearance", - "confirmPassword": "Confirm password", - "monitorHooks": { - "failureAddDemoMonitors": "Failed to add demo monitors", - "successAddDemoMonitors": "Successfully added demo monitors" - }, - "settingsAppearance": "Appearance", - "settingsDisplayTimezone": "Display timezone", - "settingsGeneralSettings": "General settings", - "incidentsOptionsHeaderTotalIncidents": "Total Incidents", - "statusPage": { - "deleteSuccess": "Status page deleted successfully", - "deleteFailed": "Failed to delete status page", - "createSuccess": "Status page created successfully", - "updateSuccess": "Status page updated successfully", - "generalSettings": "General settings", - "contents": "Contents", - "fallback": { - "checks": [ - "Monitor and display the health of your services in real time", - "Track multiple services and share their status", - "Keep users informed about outages and performance" - ], - "title": "A status page is used to:", - "actionButton": "Let's create your first status page!" - } - }, - "testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button", - "incidentsTableResolvedAt": "Resolved at", - "incidentsTableActionResolve": "Resolve", - "checkHooks": { - "failureResolveOne": "Failed to resolve incident.", - "failureResolveAll": "Failed to resolve all incidents.", - "failureResolveMonitor": "Failed to resolve monitor incidents." - }, - "checkFormError": "Please check the form for errors.", - "diagnosticsPage": { - "diagnosticDescription": "System diagnostics", - "statsDescription": "System statistics", - "gauges": { - "heapAllocationTitle": "Heap allocation", - "heapAllocationSubtitle": "% of available memory", - "heapUsageTitle": "Heap usage", - "heapUsageSubtitle": "% of available memory", - "heapUtilizationTitle": "Heap utilization", - "heapUtilizationSubtitle": "% of allocated", - "instantCpuUsageTitle": "Instant CPU usage", - "instantCpuUsageSubtitle": "% of 1s used by CPU" - }, - "stats": { - "eventLoopDelayTitle": "Event loop delay", - "uptimeTitle": "Uptime", - "usedHeapSizeTitle": "Used heap size", - "totalHeapSizeTitle": "Total heap size", - "osMemoryLimitTitle": "OS Memory Limit" - } - }, - "pageSpeedLighthouseAPI": "Use the Lighthouse PageSpeed API to monitor your website", - "time": { - "threeMinutes": "3 minutes", - "fiveMinutes": "5 minutes", - "tenMinutes": "10 minutes", - "twentyMinutes": "20 minutes", - "oneHour": "1 hour", - "oneDay": "1 day", - "oneWeek": "1 week", - "fourMinutes": "4 minutes", - "oneMinute": "1 minute", - "twoMinutes": "2 minutes", - "fifteenSeconds": "15 seconds", - "thirtySeconds": "30 seconds" - }, - "general": { - "noOptionsFound": "No {{unit}} found" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [ - "Track the performance of your servers", - "Identify bottlenecks and optimize usage", - "Ensure reliability with real-time monitoring" - ], - "title": "An infrastructure monitor is used to:", - "actionButton": "Let's create your first infrastructure monitor!" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [ - "Mark your maintenance periods", - "Eliminate any misunderstandings", - "Stop sending alerts in maintenance windows" - ], - "title": "A maintenance window is used to:", - "actionButton": "Let's create your first maintenance window!" - } - }, - "pageSpeed": { - "fallback": { - "checks": [ - "Report on the user experience of a page", - "Help analyze webpage speed", - "Give suggestions on how the page can be improved" - ], - "title": "A PageSpeed monitor is used to:", - "actionButton": "Let's create your first PageSpeed monitor!" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [ - "Check if websites or servers are online & responsive", - "Alert teams about downtime or performance issues", - "Monitor HTTP endpoints, pings, containers & ports", - "Track historical uptime and reliability trends" - ], - "title": "An uptime monitor is used to:", - "actionButton": "Let's create your first uptime monitor!" - } - }, - "editUserPage": { - "form": { - "email": "Email", - "firstName": "First name", - "lastName": "Last name", - "role": "Roles", - "save": "Save" - }, - "table": { - "actionHeader": "Action", - "roleHeader": "Role" - }, - "title": "Edit user", - "toast": { - "successUserUpdate": "User updated successfully", - "validationErrors": "Validation errors" - } - }, - "incidentsPageActionResolveMonitor": "Resolve monitor incidents", - "incidentsPageActionResolveAll": "Resolve all incidents", - "matchMethodOptions": { - "equal": "Equal", - "equalPlaceholder": "success", - "include": "Include", - "includePlaceholder": "ok", - "regex": "Regex", - "regexPlaceholder": "^(success|ok)$", - "text": "Match Method" - }, - "monitorType": { - "docker": { - "label": "Container Name/ID", - "namePlaceholder": "My Container", - "placeholder": "my-app or abcd1234" - }, - "http": { - "label": "URL to monitor", - "namePlaceholder": "Google", - "placeholder": "google.com" - }, - "ping": { - "label": "IP address to monitor", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" - }, - "port": { - "label": "URL to monitor", - "namePlaceholder": "Localhost:5173", - "placeholder": "localhost" - }, - "game": { - "label": "URL to monitor", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "JSON Path" - }, - "bytesPerSecond": "Bytes per second", - "bytesReceived": "Bytes Received", - "bytesSent": "Bytes Sent", - "chooseGame": "Choose game", - "createMonitorPage": { - "incidentConfigDescription": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.", - "incidentConfigStatusWindowLabel": "How many checks should be in the sliding window?", - "incidentConfigStatusWindowThresholdLabel": "What percentage of checks in the sliding window fail/succeed before monitor status changes?", - "incidentConfigTitle": "Incidents", - "incidentConfigDescriptionV2": "Number of consecutive checks needed to change monitor status. Maximum 25", - "incidentConfigStatusCheckNumber": "How many checks before status change?", - "intervalTitle": "Check interval", - "intervalDescription": "How often the monitor should be checked." - }, - "dataRate": "Data Rate", - "dataReceived": "Data Received", - "dataSent": "Data Sent", - "details": "Details", - "drops": "Drops", - "errors": "Errors", - "errorsIn": "Errors In", - "errorsOut": "Errors Out", - "gameServerMonitoring": "Game Server Monitoring", - "gameServerMonitoringDescription": "Check whether your game server is running or not", - "network": "Network", - "networkDrops": "Network Drops", - "networkErrors": "Network Errors", - "networkInterface": "Network Interface", - "noNetworkStatsAvailable": "No network stats available.", - "packetsPerSecond": "Packets per second", - "packetsReceived": "Packets Received", - "packetsReceivedRate": "Packets Received Rate", - "packetsSent": "Packets Sent", - "rate": "Rate", - "selectInterface": "Select Interface" + "aboutus": "About Us", + "access": "Access", + "actions": "Actions", + "add": "Add", + "addMonitors": "Add monitors", + "administrator": "Administrator?", + "advancedMatching": "Advanced matching", + "ago": "ago", + "area": "Area", + "auth": { + "common": { + "errors": { + "validation": "Error validating data." + }, + "fields": { + "password": { + "errors": { + "incorrect": "The password you provided does not match our records" + } + }, + "role": { + "errors": { + "min": "At least one role is required" + } + } + }, + "inputs": { + "email": { + "errors": { + "empty": "To continue, please enter your email address", + "invalid": "Please recheck validity of entered email address" + }, + "label": "Email", + "placeholder": "jordan.ellis@domain.com" + }, + "firstName": { + "errors": { + "empty": "Please enter your name", + "length": "Name must be less than 50 characters", + "pattern": "Name must contain only letters, spaces, apostrophes, or hyphens" + }, + "label": "Name", + "placeholder": "Jordan" + }, + "lastName": { + "errors": { + "empty": "Please enter your surname", + "length": "Surname must be less than 50 characters", + "pattern": "Surname must contain only letters, spaces, apostrophes, or hyphens" + }, + "label": "Surname", + "placeholder": "Ellis" + }, + "password": { + "errors": { + "empty": "Please enter your password", + "incorrect": "The password you provided does not match our records", + "length": "Password must be at least 8 characters long", + "lowercase": "Password must contain at least 1 lowercase letter", + "number": "Password must contain at least 1 number", + "special": "Password must contain at least 1 special character", + "uppercase": "Password must contain at least 1 uppercase letter" + }, + "label": "Password", + "rules": { + "length": { + "beginning": "Must be at least", + "highlighted": "8 characters long" + }, + "lowercase": { + "beginning": "Must contain at least", + "highlighted": "one lower character" + }, + "match": { + "beginning": "Passwords", + "highlighted": "must match" + }, + "number": { + "beginning": "Must contain at least", + "highlighted": "one number" + }, + "special": { + "beginning": "Must contain at least", + "highlighted": "one special character" + }, + "uppercase": { + "beginning": "Must contain at least", + "highlighted": "one upper character" + } + } + }, + "passwordConfirm": { + "errors": { + "different": "Entered passwords don't match, so one of them is probably mistyped", + "empty": "Please enter your password again for confirmation (helps with typos)" + }, + "label": "Confirm password", + "placeholder": "Re-enter password to confirm" + } + }, + "navigation": { + "back": "Back", + "continue": "Continue" + } + }, + "forgotPassword": { + "buttons": { + "openEmail": "Open email app", + "resetPassword": "Reset password" + }, + "heading": "Forgot password?", + "imageAlts": { + "email": "Email icon", + "lock": "Lock icon", + "passwordConfirm": "Password confirm icon", + "passwordKey": "Password key icon" + }, + "links": { + "login": "Go back to Log In", + "resend": "Didn't receive the email? Click to resend" + }, + "subheadings": { + "stepFour": "Your password has been successfully reset. Click below to log in magically.", + "stepOne": "No worries, we'll send you reset instructions.", + "stepThree": "Your new password must be different from previously used passwords.", + "stepTwo": "We sent a password reset link to " + }, + "toasts": { + "emailNotFound": "Email not found.", + "error": "Unable to reset password. Please try again later or contact support.", + "redirect": "Redirecting in ...", + "sent": "Instructions sent to .", + "success": "Your password was reset successfully." + } + }, + "login": { + "errors": { + "password": { + "incorrect": "The password you provided does not match our records" + } + }, + "heading": "Log in to continue", + "links": { + "forgotPassword": "Forgot password?", + "forgotPasswordLink": "Reset password", + "register": "Do not have an account?", + "registerLink": "Register here" + }, + "subheadings": { + "stepOne": "Enter your email", + "stepTwo": "Enter your password" + }, + "toasts": { + "incorrectPassword": "Incorrect password", + "success": "Welcome back! You're successfully logged in." + }, + "welcome": "Welcome back to Checkmate!" + }, + "registration": { + "description": { + "superAdmin": "Create your super admin account to get started", + "user": "Sign up as a user and ask super admin for access to your monitors" + }, + "gettingStartedButton": { + "superAdmin": "Create super admin account", + "user": "Sign up regular user" + }, + "heading": { + "superAdmin": "Create super admin", + "user": "Sign Up" + }, + "links": { + "login": "Already have an account? Log In" + }, + "subheadings": { + "stepOne": "Enter your personal details", + "stepThree": "Create your password", + "stepTwo": "Enter your email" + }, + "termsAndPolicies": "By creating an account, you agree to our Terms of Service and Privacy Policy.", + "toasts": { + "success": "Welcome! Your account was created successfully." + }, + "welcome": "Welcome to Checkmate!" + } + }, + "avgCpuTemperature": "Average CPU Temperature", + "bar": "Bar", + "basicInformation": "Basic Information", + "bulkImport": { + "fallbackPage": "Import a file to upload a list of servers in bulk", + "invalidFileType": "Invalid file type", + "noFileSelected": "No file selected", + "parsingFailed": "Parsing failed", + "selectFile": "Select File", + "selectFileDescription": "You can download our or sample", + "selectFileTips": "Select CSV file to upload", + "title": "Bulk Import", + "uploadFailed": "Upload failed", + "uploadSuccess": "Monitors created successfully!", + "validationFailed": "Validation failed" + }, + "bytesPerSecond": "Bytes per second", + "bytesReceived": "Bytes Received", + "bytesSent": "Bytes Sent", + "cancel": "Cancel", + "checkFormError": "Please check the form for errors.", + "checkFrequency": "Check frequency", + "checkHooks": { + "failureResolveAll": "Failed to resolve all incidents.", + "failureResolveMonitor": "Failed to resolve monitor incidents.", + "failureResolveOne": "Failed to resolve incident." + }, + "checkingEvery": "Checking every", + "chooseGame": "Choose game", + "city": "CITY", + "ClickUpload": "Click to upload", + "common": { + "appName": "Checkmate", + "buttons": { + "toggleTheme": "Toggles light & dark" + }, + "monitoringAgentName": "Capture", + "toasts": { + "checkConnection": "Please check your connection", + "networkError": "Network error", + "unknownError": "Unknown error" + } + }, + "commonSave": "Save", + "commonSaving": "Saving...", + "companyName": "Company name", + "configure": "Configure", + "confirmPassword": "Confirm password", + "cores": "Cores", + "country": "COUNTRY", + "cpu": "CPU", + "cpuFrequency": "CPU Frequency", + "cpuLogical": "CPU (Logical)", + "cpuPhysical": "CPU (Physical)", + "cpuTemperature": "CPU Temperature", + "cpuUsage": "CPU usage", + "createA": "Create a", + "createMaintenance": "Create maintenance", + "createMaintenanceWindow": "Create maintenance window", + "createMaintenanceWindowPage": { + "generalSettingsTitle": "General settings", + "generalSettingsDescription": "General maintenance window settings", + "generalSettingsName": "Name", + "generalSettingsNamePlaceholder": "Maintenance at __ : __ for ___ minutes", + "generalSettingsRepeat": "Repeat period", + "timingSettingsTitle": "Start & end times", + "timingSettingsDescription": "Set the start time (UTC) and end time (UTC) of the maintenance window", + "durationTitle": "Duration", + "durationDescription": "Set the duration of the maintenance window", + "monitorsTitle": "Monitors", + "monitorsDescription": "Select the monitors you want to apply this maintenance window to" + }, + "createMonitor": "Create monitor", + "createMonitorPage": { + "incidentConfigDescription": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.", + "incidentConfigDescriptionV2": "Number of consecutive checks needed to change monitor status. Maximum 25", + "incidentConfigStatusCheckNumber": "How many checks before status change?", + "incidentConfigStatusWindowLabel": "How many checks should be in the sliding window?", + "incidentConfigStatusWindowThresholdLabel": "What percentage of checks in the sliding window fail/succeed before monitor status changes?", + "incidentConfigTitle": "Incidents", + "intervalDescription": "How often do you want to check this service?", + "intervalTitle": "Check interval" + }, + "createNew": "Create new", + "createNotifications": { + "dialogDeleteConfirm": "Delete", + "dialogDeleteContent": "This action cannot be undone.", + "dialogDeleteTitle": "Are you sure you want to delete this notification?", + "discordSettings": { + "description": "Configure your Discord webhook here", + "title": "Discord", + "webhookLabel": "Discord Webhook URL", + "webhookPlaceholder": "https://your-server.com/webhook" + }, + "emailSettings": { + "description": "Destination email address", + "emailLabel": "Email address", + "emailPlaceholder": "e.g. john@example.com", + "title": "Email" + }, + "nameSettings": { + "description": "A descriptive name for your integration.", + "nameLabel": "Name", + "namePlaceholder": "e.g. Slack notifications", + "title": "Name" + }, + "pagerdutySettings": { + "description": "Configure your PagerDuty integration here", + "integrationKeyLabel": "Integration key", + "integrationKeyPlaceholder": "1234567890", + "title": "PagerDuty" + }, + "slackSettings": { + "description": "Configure your Slack webhook here", + "title": "Slack", + "webhookLabel": "Slack webhook URL", + "webhookPlaceholder": "https://hooks.slack.com/services/..." + }, + "testNotification": "Test notification", + "title": "Create notification channel", + "typeSettings": { + "description": "Select the type of notification channel you want to create.", + "title": "Type", + "typeLabel": "Type" + }, + "webhookSettings": { + "description": "Configure your webhook here", + "title": "Webhook", + "webhookLabel": "Webhook URL", + "webhookPlaceholder": "https://your-server.com/webhook" + } + }, + "createTeamsPage": { + "basicInfoDescription": "Team description", + "basicInfoName": "Team name", + "basicInfoSubtitle": "Set team name and description", + "basicInfoTitle": "Basic info" + }, + "createYour": "Create your", + "dataRate": "Data Rate", + "dataReceived": "Data Received", + "dataSent": "Data Sent", + "date&Time": "Date & Time", + "delete": "Delete", + "DeleteAccountButton": "Remove account", + "DeleteAccountTitle": "Remove account", + "DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.", + "DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.", + "deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.", + "deleteDialogTitle": "Do you really want to delete this monitor?", + "deleteStatusPage": "Do you want to delete this status page?", + "deleteStatusPageConfirm": "Yes, delete status page", + "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", + "DeleteWarningTitle": "Really remove this account?", + "details": "Details", + "diagnosticsPage": { + "diagnosticDescription": "System diagnostics", + "statBoxJobsTitle": "Jobs", + "statBoxActiveJobsTitle": "Active jobs", + "statBoxFailingJobsTitle": "Failing jobs", + "statBoxTotalRunsTitle": "Total runs", + "statBoxTotalFailuresTitle": "Total failures", + "tableHeaderId": "Monitor ID", + "tableHeaderUrl": "URL", + "tableHeaderInterval": "Interval", + "tableHeaderType": "Type", + "tableHeaderActive": "Active", + "tableHeaderRunCount": "Runs", + "tableHeaderLastRun": "Last run", + "tableHeaderLockedAt": "Running", + "tableHeaderLastFinishedAt": "Finished", + "tableHeaderLastRunTook": "Duration", + + "gauges": { + "heapAllocationSubtitle": "% of available memory", + "heapAllocationTitle": "Heap allocation", + "heapUsageSubtitle": "% of available memory", + "heapUsageTitle": "Heap usage", + "heapUtilizationSubtitle": "% of allocated", + "heapUtilizationTitle": "Heap utilization", + "instantCpuUsageSubtitle": "% of 1s used by CPU", + "instantCpuUsageTitle": "Instant CPU usage" + }, + "stats": { + "eventLoopDelayTitle": "Event loop delay", + "osMemoryLimitTitle": "OS Memory Limit", + "totalHeapSizeTitle": "Total heap size", + "uptimeTitle": "Uptime", + "usedHeapSizeTitle": "Used heap size" + }, + "statsDescription": "System statistics" + }, + "disk": "Disk", + "diskUsage": "Disk Usage", + "displayName": "Display name", + "distributedRightCategoryTitle": "Monitor", + "distributedStatusHeaderText": "Real-time, real-device coverage", + "distributedStatusServerMonitors": "Server Monitors", + "distributedStatusServerMonitorsDescription": "Monitor status of related servers", + "distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city", + "distributedUptimeCreateAdvancedSettings": "Advanced settings", + "distributedUptimeCreateChecks": "Checks to perform", + "distributedUptimeCreateChecksDescription": "You can always add or remove checks after adding your site.", + "distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.", + "distributedUptimeCreateIncidentNotification": "Incident notifications", + "distributedUptimeCreateSelectURL": "Here you can select the URL of the host, together with the type of monitor.", + "distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.", + "distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated", + "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", + "distributedUptimeStatus30Days": "30 days", + "distributedUptimeStatus60Days": "60 days", + "distributedUptimeStatus90Days": "90 days", + "distributedUptimeStatusBasicInfoDescription": "Define company name and the subdomain that your status page points to.", + "distributedUptimeStatusBasicInfoHeader": "Basic Information", + "distributedUptimeStatusCompanyNameLabel": "Company name", + "distributedUptimeStatusContactAdmin": "Please contact your administrator", + "distributedUptimeStatusCreateStatusPage": "status page", + "distributedUptimeStatusCreateStatusPageAccess": "Access", + "distributedUptimeStatusCreateStatusPageReady": "If your status page is ready, you can mark it as published.", + "distributedUptimeStatusCreateYour": "Create your", + "distributedUptimeStatusDevices": "Devices", + "distributedUptimeStatusEditYour": "Edit your", + "distributedUptimeStatusLogoDescription": "Upload a logo for your status page", + "distributedUptimeStatusLogoHeader": "Logo", + "distributedUptimeStatusLogoUploadButton": "Upload logo", + "distributedUptimeStatusPageAddressLabel": "Your status page address", + "distributedUptimeStatusPageDeleteConfirm": "Yes, delete status page", + "distributedUptimeStatusPageDeleteDescription": "Once deleted, your status page cannot be retrieved.", + "distributedUptimeStatusPageDeleteDialog": "Do you want to delete this status page?", + "distributedUptimeStatusPageNotPublic": "This status page is not public.", + "distributedUptimeStatusPageNotSetUp": "A status page is not set up.", + "distributedUptimeStatusPublishedLabel": "Published and visible to the public", + "distributedUptimeStatusStandardMonitorsDescription": "Attach standard monitors to your status page.", + "distributedUptimeStatusStandardMonitorsHeader": "Standard Monitors", + "distributedUptimeStatusUpt": "UPT", + "distributedUptimeStatusUptBurned": "UPT Burned", + "distributedUptimeStatusUptLogo": "Upt Logo", + "dockerContainerMonitoring": "Docker container monitoring", + "dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.", + "DragandDrop": "drag and drop", + "drops": "Drops", + "duration": "Duration", + "edit": "Edit", + "editing": "Editing...", + "editMaintenance": "Edit maintenance", + "editUserPage": { + "form": { + "email": "Email", + "firstName": "First name", + "lastName": "Last name", + "role": "Roles", + "save": "Save" + }, + "table": { + "actionHeader": "Action", + "roleHeader": "Role" + }, + "title": "Edit user", + "toast": { + "successUserUpdate": "User updated successfully", + "validationErrors": "Validation errors" + } + }, + "EmailDescriptionText": "This is your current email address — it cannot be changed.", + "emailSent": "Email sent successfully", + "errorInvalidFieldId": "Invalid field ID provided", + "errorInvalidTypeId": "Invalid notification type provided", + "errorPages": { + "serverUnreachable": { + "alertBox": "Server Connection Error", + "description": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.", + "retryButton": { + "default": "Retry connection", + "processing": "Connecting..." + }, + "toasts": { + "reconnected": "Successfully reconnected to the server.", + "stillUnreachable": "Server is still unreachable. Please try again later." + } + } + }, + "errors": "Errors", + "errorsIn": "Errors In", + "errorsOut": "Errors Out", + "expectedValue": "Expected value", + "export": { + "failed": "Failed to export monitors", + "success": "Monitors exported successfully!", + "title": "Export Monitors" + }, + "failedToSendEmail": "Failed to send email", + "features": "Features", + "FirstName": "First name", + "frequency": "Frequency", + "friendlyNameInput": "Friendly name", + "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", + "gameServerMonitoring": "Game Server Monitoring", + "gameServerMonitoringDescription": "Check whether your game server is running or not", + "gb": "GB", + "general": { + "noOptionsFound": "No {{unit}} found" + }, + "greeting": { + "append": "The afternoon is your playground—let's make it epic!", + "overview": "Here's an overview of your {{type}} monitors.", + "prepend": "Hey there" + }, + "high": "high", + "host": "Host", + "http": "HTTP", + "https": "HTTPS", + "ignoreTLSError": "Ignore TLS/SSL error", + "ignoreTLSErrorDescription": "Ignore TLS/SSL errors and continue checking the website's availability", + "incidentsOptionsHeader": "Incidents for:", + "incidentsOptionsHeaderFilterAll": "All", + "incidentsOptionsHeaderFilterBy": "Filter by:", + "incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve", + "incidentsOptionsHeaderFilterDown": "Down", + "incidentsOptionsHeaderFilterResolved": "Resolved", + "incidentsOptionsHeaderLastDay": "Last day", + "incidentsOptionsHeaderLastHour": "Last hour", + "incidentsOptionsHeaderLastWeek": "Last week", + "incidentsOptionsHeaderShow": "Show:", + "incidentsOptionsHeaderTotalIncidents": "Total Incidents", + "incidentsOptionsPlaceholderAllServers": "All servers", + "incidentsPageActionResolveAll": "Resolve all incidents", + "incidentsPageActionResolveMonitor": "Resolve monitor incidents", + "incidentsPageTitle": "Incidents", + "incidentsTableActionResolve": "Resolve", + "incidentsTableDateTime": "Date & Time", + "incidentsTableMessage": "Message", + "incidentsTableMonitorName": "Monitor Name", + "incidentsTableNoIncidents": "No incidents recorded", + "incidentsTablePaginationLabel": "incidents", + "incidentsTableResolvedAt": "Resolved at", + "incidentsTableStatus": "Status", + "incidentsTableStatusCode": "Status Code", + "infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.", + "infrastructureAuthorizationSecretLabel": "Authorization secret", + "infrastructureCreateGeneralSettingsDescription": "Here you can select the URL of the host, together with the friendly name and authorization secret to connect to the server agent.", + "infrastructureCreateMonitor": "Create Infrastructure Monitor", + "infrastructureCreateYour": "Create your", + "infrastructureCustomizeAlerts": "Customize alerts", + "infrastructureDisplayNameLabel": "Display name", + "infrastructureEditMonitor": "Save Infrastructure Monitor", + "infrastructureEditYour": "Edit your", + "infrastructureMonitor": { + "fallback": { + "actionButton": "Let's create your first infrastructure monitor!", + "checks": [ + "Track the performance of your servers", + "Identify bottlenecks and optimize usage", + "Ensure reliability with real-time monitoring" + ], + "title": "An infrastructure monitor is used to:" + } + }, + "infrastructureMonitorCreated": "Infrastructure monitor created successfully!", + "infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!", + "infrastructureProtocol": "Protocol", + "infrastructureServerRequirement": "The server you are monitoring must be running the", + "infrastructureServerUrlLabel": "Server URL", + "integrations": "Integrations", + "integrationsDiscord": "Discord", + "integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel", + "integrationsPrism": "Connect Prism to your favorite service.", + "integrationsSlack": "Slack", + "integrationsSlackInfo": "Connect with Slack and see incidents in a channel", + "integrationsZapier": "Zapier", + "integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere", + "invalidFileFormat": "Unsupported file format!", + "invalidFileSize": "File size is too large!", + "inviteNoTokenFound": "No invite token found", + "LastName": "Last name", + "loginHere": "Login here", + "logsPage": { + "description": "System logs - last 1000 lines", + "logLevelSelect": { + "title": "Log level", + "values": { + "all": "All", + "debug": "Debug", + "error": "Error", + "info": "Info", + "warn": "Warn" + } + }, + "tabs": { + "diagnostics": "Diagnostics", + "logs": "Server logs", + "queue": "Job queue" + }, + "title": "Logs", + "toast": { + "fetchLogsSuccess": "Logs fetched successfully" + } + }, + "low": "low", + "maintenance": "maintenance", + "maintenanceRepeat": "Maintenance Repeat", + "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", + "maintenanceWindow": { + "fallback": { + "actionButton": "Let's create your first maintenance window!", + "checks": [ + "Mark your maintenance periods", + "Eliminate any misunderstandings", + "Stop sending alerts in maintenance windows" + ], + "title": "A maintenance window is used to:" + } + }, + "maintenanceWindowDescription": "During maintenance windows, all monitoring is suspended for selected monitors. No network checks will be performed, preventing any status updates or notifications from being triggered. Your monitors will appear frozen at their last known status, and status pages will display a maintenance indicator. Once the maintenance window ends, monitoring automatically resumes, and alerts will trigger if issues are detected. Maintenance periods do not count against uptime calculations.", + "maintenanceWindowName": "Maintenance Window Name", + "maskedPageSpeedKeyPlaceholder": "*************************************", + "matchMethod": "Match Method", + "matchMethodOptions": { + "equal": "Equal", + "equalPlaceholder": "success", + "include": "Include", + "includePlaceholder": "ok", + "regex": "Regex", + "regexPlaceholder": "^(success|ok)$", + "text": "Match Method" + }, + "MaxSize": "Maximum Size", + "mb": "MB", + "mem": "Mem", + "memory": "Memory", + "memoryUsage": "Memory usage", + "menu": { + "changelog": "Changelog", + "discussions": "Discussions", + "docs": "Docs", + "incidents": "Incidents", + "infrastructure": "Infrastructure", + "integrations": "Integrations", + "logOut": "Log out", + "diagnostics": "Diagnostics", + "maintenance": "Maintenance", + "notifications": "Notifications", + "pagespeed": "Pagespeed", + "password": "Password", + "profile": "Profile", + "settings": "Settings", + "statusPages": "Status pages", + "support": "Support", + "team": "Team", + "uptime": "Uptime" + }, + "message": "Message", + "monitor": "monitor", + "monitorActions": { + "deleteFailed": "Failed to delete monitor", + "deleteSuccess": "Monitor deleted successfully", + "details": "Details", + "export": "Export Monitors", + "import": "Import Monitors", + "title": "Export/Import" + }, + "monitorDisplayName": "Monitor display name", + "monitorHooks": { + "failureAddDemoMonitors": "Failed to add demo monitors", + "successAddDemoMonitors": "Successfully added demo monitors" + }, + "monitors": "monitors", + "monitorState": { + "active": "Active", + "paused": "Paused", + "resumed": "Resumed" + }, + "monitorStatus": { + "checkingEvery": "Checking every {{interval}}", + "down": "down", + "paused": "paused", + "up": "up", + "withCaptureAgent": "with Capture agent {{version}}" + }, + "monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding", + "monitorStatusUp": "Monitor {name} ({url}) is now UP and responding", + "monitorsToApply": "Monitors to apply maintenance window to", + "monitorType": { + "docker": { + "label": "Container Name/ID", + "namePlaceholder": "My Container", + "placeholder": "my-app or abcd1234" + }, + "game": { + "label": "URL to monitor", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" + }, + "http": { + "label": "URL to monitor", + "namePlaceholder": "Google", + "placeholder": "google.com" + }, + "ping": { + "label": "IP address to monitor", + "namePlaceholder": "Google", + "placeholder": "1.1.1.1" + }, + "port": { + "label": "URL to monitor", + "namePlaceholder": "Localhost:5173", + "placeholder": "localhost" + } + }, + "ms": "ms", + "navControls": "Controls", + "network": "Network", + "networkDrops": "Network Drops", + "networkErrors": "Network Errors", + "networkInterface": "Network Interface", + "nextWindow": "Next window", + "noNetworkStatsAvailable": "No network stats available.", + "notFoundButton": "Go to the main dashboard", + "notificationConfig": { + "description": "Select the notifications channels you want to use", + "title": "Notifications" + }, + "notifications": { + "addOrEditNotifications": "Add or edit notifications", + "create": { + "failed": "Failed to create notification", + "success": "Notification created successfully" + }, + "createButton": "Create notification channel", + "createTitle": "Notification channel", + "delete": { + "failed": "Failed to delete notification", + "success": "Notification deleted successfully" + }, + "discord": { + "description": "To send data to a Discord channel from Checkmate via Discord notifications using webhooks, you can use Discord's incoming Webhooks feature.", + "label": "Discord", + "webhookLabel": "Discord Webhook URL", + "webhookPlaceholder": "https://discord.com/api/webhooks/...", + "webhookRequired": "Discord webhook URL is required" + }, + "edit": { + "failed": "Failed to update notification", + "success": "Notification updated successfully" + }, + "enableNotifications": "Enable {{platform}} notifications", + "fallback": { + "actionButton": "Let's create your first notification channel!", + "checks": [ + "Alert teams about downtime or performance issues", + "Let engineers know when incidents happen", + "Keep administrators informed of system changes" + ], + "title": "A notification channel is used to:" + }, + "fetch": { + "failed": "Failed to fetch notifications", + "success": "Notifications fetched successfully" + }, + "integrationButton": "Notification Integration", + "networkError": "Network error occurred", + "slack": { + "description": "To enable Slack notifications, create a Slack app and enable incoming webhooks. After that, simply provide the webhook URL here.", + "label": "Slack", + "webhookLabel": "Webhook URL", + "webhookPlaceholder": "https://hooks.slack.com/services/...", + "webhookRequired": "Slack webhook URL is required" + }, + "telegram": { + "chatIdLabel": "Your Chat ID", + "chatIdPlaceholder": "-1001234567890", + "description": "To enable Telegram notifications, create a Telegram bot using BotFather, an official bot for creating and managing Telegram bots. Then, get the API token and chat ID and write them down here.", + "fieldsRequired": "Telegram token and chat ID are required", + "label": "Telegram", + "tokenLabel": "Your bot token", + "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" + }, + "test": { + "failed": "Failed to send test notification", + "success": "Test notification sent successfully" + }, + "testFailed": "Failed to send test notification", + "testNotification": "Test notification", + "testNotificationDevelop": "Test notification 2", + "testSuccess": "Test notification sent successfully!", + "unsupportedType": "Unsupported notification type", + "webhook": { + "description": "You can set up a custom webhook to receive notifications when incidents occur.", + "label": "Webhooks", + "urlLabel": "Webhook URL", + "urlPlaceholder": "https://your-server.com/webhook", + "urlRequired": "Webhook URL is required" + } + }, + "notifyEmails": "Also notify via email to multiple addresses (coming soon)", + "notifySMS": "Notify via SMS (coming soon)", + "now": "Now", + "os": "OS", + "packetsPerSecond": "Packets per second", + "packetsReceived": "Packets Received", + "packetsReceivedRate": "Packets Received Rate", + "packetsSent": "Packets Sent", + "pageSpeedMonitor": { + "fallback": { + "actionButton": "Let's create your first PageSpeed monitor!", + "checks": [ + "Report on the user experience of a page", + "Help analyze webpage speed", + "Give suggestions on how the page can be improved" + ], + "title": "A PageSpeed monitor is used to:" + } + }, + "pageSpeedAddApiKey": "to add your API key.", + "pageSpeedConfigureSettingsDescription": "Enter the URL of the website you want to carry out PageSpeed tests on", + "pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.", + "pageSpeedDetailsPerformanceReportCalculator": "See calculator", + "pageSpeedLearnMoreLink": "Click here", + "pageSpeedLighthouseAPI": "Use the Lighthouse PageSpeed API to monitor your website", + "pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.", + "passwordPanel": { + "confirmNewPassword": "Confirm new password", + "currentPassword": "Current password", + "enterCurrentPassword": "Enter your current password", + "enterNewPassword": "Enter your new password", + "newPassword": "New password", + "passwordChangedSuccess": "Your password was changed successfully.", + "passwordInputIncorrect": "Your password input was incorrect.", + "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.", + "saving": "Saving..." + }, + "pause": "Pause", + "PhotoDescriptionText": "This photo will be displayed in your profile page.", + "pingMonitoring": "Ping monitoring", + "pingMonitoringDescription": "Check whether your server is available or not.", + "portMonitoring": "Port monitoring", + "portMonitoringDescription": "Check whether your port is open or not.", + "portToMonitor": "Port to monitor", + "publicLink": "Public link", + "publicURL": "Public URL", + "queuePage": { + "failedJobTable": { + "failCountHeader": "Fail count", + "failedAtHeader": "Last failed at", + "failReasonHeader": "Fail reason", + "monitorIdHeader": "Monitor ID", + "monitorUrlHeader": "Monitor URL", + "title": "Failed jobs" + }, + "flushButton": "Flush queue", + "jobTable": { + "activeHeader": "Active", + "failCountHeader": "Fail count", + "idHeader": "Monitor ID", + "intervalHeader": "Interval", + "lastFinishedAtHeader": "Last finished at", + "lastRunHeader": "Last run at", + "lastRunTookHeader": "Last run took", + "lockedAtHeader": "Locked at", + "runCountHeader": "Run count", + "title": "Jobs currently in queue", + "typeHeader": "Type", + "urlHeader": "URL" + }, + "metricsTable": { + "metricHeader": "Metric", + "title": "Queue metrics", + "valueHeader": "Value" + }, + "refreshButton": "Refresh", + "title": "Queue" + }, + "rate": "Rate", + "remove": "Remove", + "removeLogo": "Remove Logo", + "repeat": "Repeat", + "reset": "Reset", + "response": "RESPONSE", + "responseTime": "Response time", + "resume": "Resume", + "roles": { + "admin": "Admin", + "demoUser": "Demo user", + "superAdmin": "Super admin", + "teamMember": "Team member" + }, + "save": "Save", + "selectAll": "Select all", + "selectInterface": "Select Interface", + "sendTestNotifications": "Send test notifications", + "seperateEmails": "You can separate multiple emails with a comma", + "settingsAppearance": "Appearance", + "settingsDisabled": "Disabled", + "settingsDisplayTimezone": "Display timezone", + "settingsEmailUser": "Email user - Username for authentication, overrides email address if specified", + "settingsFailedToClearStats": "Failed to clear stats", + "settingsFailedToDeleteMonitors": "Failed to delete all monitors", + "settingsFailedToSave": "Failed to save settings", + "settingsGeneralSettings": "General settings", + "settingsMonitorsDeleted": "Successfully deleted all monitors", + "settingsPage": { + "aboutSettings": { + "labelDevelopedBy": "Developed by Bluewave Labs", + "labelVersion": "Version", + "title": "About" + }, + "demoMonitorsSettings": { + "buttonAddMonitors": "Add demo monitors", + "description": "Add sample monitors for demonstration purposes.", + "title": "Demo monitors" + }, + "emailSettings": { + "buttonSendTestEmail": "Send test e-mail", + "description": "Configure the email settings for your system. This is used to send notifications and alerts.", + "descriptionTransport": "This builds an SMTP transport for NodeMailer", + "labelAddress": "Email address - Used for authentication", + "labelConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting", + "labelHost": "Email host - Hostname or IP address to connect to", + "labelIgnoreTLS": "Disable STARTTLS: Don't use TLS even if the server supports it", + "labelPassword": "Email password - Password for authentication", + "labelPasswordSet": "Password is set. Click Reset to change it.", + "labelPool": "Enable connection pooling: Reuse existing connections to improve performance", + "labelPort": "Email port - Port to connect to", + "labelRejectUnauthorized": "Reject invalid certificates: Reject connections with self-signed or untrusted certificates", + "labelRequireTLS": "Force STARTTLS: Require TLS upgrade, fail if not supported", + "labelSecure": "Use SSL (recommended): Encrypt the connection using SSL/TLS", + "labelTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP", + "labelUser": "Email user - Username for authentication, overrides email address if specified", + "linkTransport": "See specifications here", + "placeholderUser": "Leave empty if not required", + "title": "Email", + "toastEmailRequiredFieldsError": "Email address, host, port and password are required" + }, + "globalThresholds": { + "description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring.", + "title": "Global Thresholds" + }, + "pageSpeedSettings": { + "description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.", + "labelApiKey": "PageSpeed API key", + "labelApiKeySet": "API key is set. Click Reset to change it.", + "title": "Google PageSpeed API key" + }, + "saveButtonLabel": "Save", + "statsSettings": { + "clearAllStatsButton": "Clear all stats", + "clearAllStatsDescription": "Clear all stats. This is irreversible.", + "clearAllStatsDialogConfirm": "Yes, clear all stats", + "clearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.", + "clearAllStatsDialogTitle": "Do you want to clear all stats?", + "description": "Define how long you want to retain historical data. You can also clear all existing data.", + "labelTTL": "The days you want to keep monitoring history.", + "labelTTLOptional": "0 for infinite", + "title": "Monitor history" + }, + "systemResetSettings": { + "buttonRemoveAllMonitors": "Remove all monitors", + "description": "Remove all monitors from your system.", + "dialogConfirm": "Yes, remove all monitors", + "dialogDescription": "Once removed, the monitors cannot be retrieved.", + "dialogTitle": "Do you want to remove all monitors?", + "title": "System reset" + }, + "timezoneSettings": { + "description": "Select the timezone used to display dates and times throughout the application.", + "label": "Display timezone", + "title": "Display timezone" + }, + "title": "Settings", + "uiSettings": { + "description": "Switch between light and dark mode, or change user interface language.", + "labelLanguage": "Language", + "labelTheme": "Theme mode", + "title": "Appearance" + }, + "urlSettings": { + "description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.", + "label": "Display IP/URL on status page", + "selectDisabled": "Disabled", + "selectEnabled": "Enabled", + "title": "Monitor IP/URL on Status Page" + } + }, + "settingsSave": "Save", + "settingsStatsCleared": "Stats cleared successfully", + "settingsSuccessSaved": "Settings saved successfully", + "settingsTestEmailFailed": "Failed to send test email", + "settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}", + "settingsTestEmailSuccess": "Test email sent successfully", + "settingsTestEmailUnknownError": "Unknown error", + "showAdminLoginLink": "Show \"Administrator? Login Here\" link on the status page", + "showCharts": "Show charts", + "shown": "Shown", + "showUptimePercentage": "Show uptime percentage", + "starPromptDescription": "See the latest releases and help grow the community on GitHub", + "starPromptTitle": "Star Checkmate", + "startTime": "Start time", + "state": "State", + "status": "Status", + "statusBreadCrumbsDetails": "Details", + "statusBreadCrumbsStatusPages": "Status Pages", + "statusCode": "Status code", + "statusMsg": { + "down": "Your site is down.", + "paused": "Monitoring is paused.", + "pending": "Pending...", + "up": "Your site is up." + }, + "statusPage": { + "contents": "Contents", + "createSuccess": "Status page created successfully", + "deleteFailed": "Failed to delete status page", + "deleteSuccess": "Status page deleted successfully", + "fallback": { + "actionButton": "Let's create your first status page!", + "checks": [ + "Monitor and display the health of your services in real time", + "Track multiple services and share their status", + "Keep users informed about outages and performance" + ], + "title": "A status page is used to:" + }, + "generalSettings": "General settings", + "updateSuccess": "Status page updated successfully" + }, + "statusPageCreate": { + "buttonSave": "Save" + }, + "createStatusPage": { + "basicConfigTitle": "Basic information", + "basicConfigDescription": "Add a name and optional description", + "nameConfigLabel": "Name", + "descriptionConfigLabel": "Description", + "accessConfigTitle": "Access", + "accessConfigDescription": "Choose whether your status page is published and available to the public", + "publishedConfigLabel": "Published", + "urlConfigTitle": "URL", + "urlConfigDescription": "The URL of your status page", + "dialogDeleteTitle": "Do you want to delete this status page?", + "dialogDeleteContent": "Once deleted, your status page cannot be retrieved." + }, + "statusPageCreateAppearanceDescription": "Define the default look and feel of your public status page.", + "statusPageCreateAppearanceTitle": "Appearance", + "statusPageCreateBasicInfoDescription": "Define company name and the subdomain that your status page points to.", + "statusPageCreateBasicInfoStatusPageAddress": "Your status page address", + "statusPageCreateSelectTimeZoneDescription": "Select the timezone that your status page will be displayed in.", + "statusPageCreateSettings": "If your status page is ready, you can mark it as published.", + "statusPageCreateSettingsCheckboxLabel": "Published and visible to the public", + "statusPageCreateTabsContent": "Status page servers", + "statusPageCreateTabsContentDescription": "You can add any number of servers that you monitor to your status page. You can also reorder them for the best viewing experience.", + "statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page", + "statusPageName": "Status page name", + "statusPageStatus": "A public status page is not set up.", + "statusPageStatusContactAdmin": "Please contact to your administrator", + "statusPageStatusNoPage": "There's no status page here.", + "statusPageStatusNotPublic": "This status page is not public.", + "statusPageStatusServiceStatus": "Service status", + "submit": "Submit", + "SupportedFormats": "Supported formats", + "teamPanel": { + "addTeamMember": { + "addButton": "Add Member", + "addMemberMenu": "Add Team Member", + "description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.", + "title": "Register new team member" + }, + "cancel": "Cancel", + "changeTeamPassword": { + "changePasswordMenu": "Reset Password", + "description": "Create a new password for this team member. You will need to share the password with them securely.", + "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way.", + "title": "Reset team member password" + }, + "email": "Email", + "emailToken": "E-mail token", + "filter": { + "all": "All", + "member": "Member" + }, + "getToken": "Get token", + "inviteDescription": "When you add a new team member, they will get access to all monitors.", + "inviteLink": "Invite link", + "inviteNewTeamMember": "Invite new team member", + "inviteTeamMember": "Invite a team member", + "noMembers": "There are no team members with this role", + "register": "Register a team member", + "registerTeamMember": { + "auth": { + "common": { + "inputs": { + "email": { + "errors": { + "empty": "To continue, please enter an email address", + "invalid": "Please recheck validity of entered email address" + } + }, + "firstName": { + "errors": { + "empty": "Please enter a name", + "pattern": "Name must contain only letters, spaces, apostrophes, or hyphens" + } + }, + "lastName": { + "errors": { + "empty": "Please enter a surname", + "pattern": "Surname must contain only letters, spaces, apostrophes, or hyphens" + } + }, + "role": { + "errors": { + "empty": "Role is required" + } + } + } + } + }, + "title": "Register team member" + }, + "registerToast": { + "dbUserExists": "User already exists.", + "success": "User created, share credentials with the member securely.", + "unknownError": "Unknown error occurred." + }, + "role": "Role", + "selectRole": "Select role", + "table": { + "created": "Created", + "email": "Email", + "name": "Name", + "role": "Role" + }, + "teamMembers": "Team members" + }, + "testLocale": "testLocale", + "testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button", + "time": { + "fifteenSeconds": "15 seconds", + "fiveMinutes": "5 minutes", + "fourMinutes": "4 minutes", + "oneDay": "1 day", + "oneHour": "1 hour", + "oneMinute": "1 minute", + "oneWeek": "1 week", + "tenMinutes": "10 minutes", + "thirtySeconds": "30 seconds", + "threeMinutes": "3 minutes", + "twentyMinutes": "20 minutes", + "twoMinutes": "2 minutes" + }, + "timezone": "Timezone", + "timeZoneInfo": "All dates and times are in GMT+0 time zone.", + "title": "Title", + "tlsErrorIgnored": "TLS/SSL errors ignored", + "total": "Total", + "type": "Type", + "update": "Update", + "uptime": "Uptime", + "uptimeAdvancedMatching": { + "jsonPath": "JSON Path" + }, + "uptimeCreate": "The expected value is used to match against response result, and the match determines the status.", + "uptimeCreateJsonPath": "This expression will be evaluated against the reponse JSON data and the result will be used to match against the expected value. See", + "uptimeCreateJsonPathQuery": "for query language documentation.", + "uptimeGeneralInstructions": { + "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).", + "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", + "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", + "https": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", + "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", + "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard." + }, + "uptimeMonitor": { + "fallback": { + "actionButton": "Let's create your first uptime monitor!", + "checks": [ + "Check if websites or servers are online & responsive", + "Alert teams about downtime or performance issues", + "Monitor HTTP endpoints, pings, containers & ports", + "Track historical uptime and reliability trends" + ], + "title": "An uptime monitor is used to:" + } + }, + "url": "URL", + "urlMonitor": "URL to monitor", + "used": "Used", + "webhookSendError": "Error sending webhook notification to {platform}", + "webhookSendSuccess": "Webhook notification sent successfully", + "webhookUnsupportedPlatform": "Unsupported platform: {platform}", + "websiteMonitoring": "Website monitoring", + "websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.", + "whenNewIncident": "When there is a new incident,", + "window": "window", + "YourPhoto": "Profile photo" } diff --git a/client/src/locales/es.json b/client/src/locales/es.json index ed6a98c5d..440d40027 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -129,32 +129,32 @@ "distributedUptimeStatusPublishedLabel": "", "distributedUptimeStatusCompanyNameLabel": "", "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "30 dias", - "distributedUptimeStatus60Days": "60 días", - "distributedUptimeStatus90Days": "90 dias", + "distributedUptimeStatus30Days": "", + "distributedUptimeStatus60Days": "", + "distributedUptimeStatus90Days": "", "distributedUptimeStatusPageNotSetUp": "", "distributedUptimeStatusContactAdmin": "", "distributedUptimeStatusPageNotPublic": "", "distributedUptimeStatusPageDeleteDialog": "", "distributedUptimeStatusPageDeleteConfirm": "", "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "Dispositivos", + "distributedUptimeStatusDevices": "", "distributedUptimeStatusUpt": "", "distributedUptimeStatusUptBurned": "", "distributedUptimeStatusUptLogo": "", "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "Incidentes", + "incidentsTablePaginationLabel": "", "incidentsTableMonitorName": "", - "incidentsTableStatus": "Estado", + "incidentsTableStatus": "", "incidentsTableDateTime": "", "incidentsTableStatusCode": "", - "incidentsTableMessage": "Mensaje", + "incidentsTableMessage": "", "incidentsOptionsHeader": "", "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "Todo", + "incidentsOptionsHeaderFilterAll": "", "incidentsOptionsHeaderFilterDown": "", "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "Mostrar:", + "incidentsOptionsHeaderShow": "", "incidentsOptionsHeaderLastHour": "", "incidentsOptionsHeaderLastDay": "", "incidentsOptionsHeaderLastWeek": "", @@ -165,22 +165,22 @@ "infrastructureCustomizeAlerts": "", "infrastructureAlertNotificationDescription": "", "infrastructureCreateMonitor": "", - "infrastructureProtocol": "Protocolo", + "infrastructureProtocol": "", "infrastructureServerUrlLabel": "", "infrastructureDisplayNameLabel": "", "infrastructureAuthorizationSecretLabel": "", - "gb": "GB", - "mb": "MB", - "mem": "Mem", + "gb": "", + "mb": "", + "mem": "", "memoryUsage": "", - "cpu": "CPU", + "cpu": "", "cpuUsage": "", "cpuTemperature": "", "diskUsage": "", "used": "", - "total": "Total", - "cores": "Nucleos", - "frequency": "Frecuencia", + "total": "", + "cores": "", + "frequency": "", "status": "", "cpuPhysical": "", "cpuLogical": "", @@ -189,25 +189,25 @@ "memory": "", "disk": "", "uptime": "", - "os": "SO", + "os": "", "host": "", "actions": "", "integrations": "", "integrationsPrism": "", - "integrationsSlack": "Slack", + "integrationsSlack": "", "integrationsSlackInfo": "", - "integrationsDiscord": "Discord", + "integrationsDiscord": "", "integrationsDiscordInfo": "", - "integrationsZapier": "Zapier", + "integrationsZapier": "", "integrationsZapierInfo": "", - "commonSave": "Guardar", + "commonSave": "", "createYour": "", "createMonitor": "", - "pause": "Pausar", - "resume": "Resumir", - "editing": "Editando...", - "url": "URL", - "access": "Acceso", + "pause": "", + "resume": "", + "editing": "", + "url": "", + "access": "", "timezone": "", "features": "", "administrator": "", @@ -231,13 +231,13 @@ "friendlyNamePlaceholder": "", "maintenanceRepeat": "", "maintenance": "", - "duration": "Duración", + "duration": "", "addMonitors": "", - "window": "ventana", - "cancel": "Cancelar", - "message": "Mensaje", - "low": "bajo", - "high": "alto", + "window": "", + "cancel": "", + "message": "", + "low": "", + "high": "", "statusCode": "", "date&Time": "", "type": "", @@ -421,13 +421,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "", @@ -489,8 +483,7 @@ "ping": "", "docker": "", "port": "", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "", @@ -1078,11 +1071,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json index 4949817c7..d98ece35b 100644 --- a/client/src/locales/fi.json +++ b/client/src/locales/fi.json @@ -421,13 +421,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "Tauota", @@ -489,8 +483,7 @@ "ping": "", "docker": "", "port": "", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "", @@ -1078,11 +1071,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index 1ebc8bad4..bfce12843 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -425,13 +425,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "Pause", @@ -493,8 +487,7 @@ "ping": "Entrez l'adresse IP ou le nom d'hôte à tester (par exemple, 192.168.1.100 ou exemple.fr) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.", "docker": "Entrer l'ID Docker du container. Les identifiants Docker doivent être les 64 caractères de l'ID Docker. Vous pouvez utiliser la commande docker inspect pour avoir l'ID complet.", "port": "Entrez l'URL ou l'adresse IP du serveur, le numéro de port et un nom d'affichage familier qui apparaîtra sur le tableau de bord.", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "Checkmate", @@ -1103,11 +1096,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json index f458b8ed0..6107c19d0 100644 --- a/client/src/locales/ja.json +++ b/client/src/locales/ja.json @@ -425,13 +425,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "一時停止", @@ -493,8 +487,7 @@ "ping": "pingするIPアドレスまたはホスト名を入力(例: 192.168.1.100 または example.com)し、ダッシュボードに表示される明確な表示名を追加。", "docker": "コンテナのDocker IDを入力。Docker IDは64文字のフルDocker IDである必要があります。docker inspect を実行してフルコンテナIDを取得できます。", "port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "Checkmate", @@ -1082,11 +1075,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json index 1a6d9a62f..9e1fa6bc0 100644 --- a/client/src/locales/pt-BR.json +++ b/client/src/locales/pt-BR.json @@ -425,13 +425,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "Pausado", @@ -493,8 +487,7 @@ "ping": "Insira o endereço IP ou nome de domínio para ping (ex. 192.168.1.100 ou exemplo.com.br) e adicione uma descrição que aparecerá na dashboard.", "docker": "Insira o Docker Id do seu container. Docker Ids devem ser todos os 64 caracteres. Você pode executar docker inspect para descobrir o Id completo.", "port": "Insira a URL ou o IP do servidor, aporta e uma descrição que aparecerá na dashboard.", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "Checkmate", @@ -1103,11 +1096,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index cdb4d235d..aa54de393 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -10,7 +10,7 @@ "settingsFailedToClearStats": "Не удалось очистить статистику", "settingsMonitorsDeleted": "Успешно удалены все мониторы", "settingsFailedToDeleteMonitors": "Не удалось удалить все мониторы", - "starPromptTitle": "Запустить Checkmate.", + "starPromptTitle": "", "starPromptDescription": "Ознакомьтесь с последними релизами и помогите развить сообщество на GitHub", "https": "HTTPS", "http": "HTTP", @@ -84,35 +84,31 @@ "unsupportedType": "Неподдерживаемый тип уведомления", "networkError": "Произошла сетевая ошибка", "fallback": { - "title": "Канал уведомлений используется для:", - "checks": [ - "Дайте знать команде, если сервер ложится или что-то виснет", - "Сообщите инженерам, когда появляется какая-то проблема", - "Уведомляйте админов, если в системе что-то поменялось" - ], - "actionButton": "Давайте создадим ваш первый канал уведомлений!" + "title": "", + "checks": [""], + "actionButton": "" }, - "createButton": "Создать канал уведомлений", - "createTitle": "Канал уведомлений", + "createButton": "", + "createTitle": "", "create": { - "success": "Уведомление успешно создано", - "failed": "Не удалось создать уведомление." + "success": "", + "failed": "" }, "fetch": { - "success": "Уведомления успешно получены", - "failed": "Не удалось получить уведомления." + "success": "", + "failed": "" }, "delete": { - "success": "Уведомление успешно удалено", - "failed": "Не удалось удалить уведомление." + "success": "", + "failed": "" }, "edit": { - "success": "Уведомление успешно обновлено", - "failed": "Не удалось обновить уведомление." + "success": "", + "failed": "" }, "test": { - "success": "Тестовое уведомление успешно отправлено", - "failed": "Не удалось отправить тестовое уведомление." + "success": "", + "failed": "" } }, "testLocale": "testLocale", @@ -336,8 +332,8 @@ "validationFailed": "Не удалось выполнить проверку", "noFileSelected": "Файл не выбран", "fallbackPage": "Импортируйте файл для массовой загрузки списка серверов", - "invalidFileType": "Неверный тип файла", - "uploadFailed": "Загрузка не удалась" + "invalidFileType": "", + "uploadFailed": "" }, "DeleteAccountTitle": "Удалить аккаунт", "DeleteAccountButton": "Удалить аккаунт", @@ -382,56 +378,50 @@ "created": "Создано" }, "addTeamMember": { - "addMemberMenu": "Добавить члена команды", - "title": "Зарегистрируйте нового члена команды", - "description": "Создайте нового пользователя и передайте ему учётные данные. Этот метод даёт участнику мгновенный доступ ко всем мониторам.", - "addButton": "Добавить участника" + "addMemberMenu": "", + "title": "", + "description": "", + "addButton": "" }, - "register": "Зарегистрируйте члена команды", + "register": "", "registerToast": { - "success": "Созданный пользователем, безопасный обмен учетными данными с участником.", - "dbUserExists": "Пользователь уже существует.", - "unknownError": "Произошла неизвестная ошибка." + "success": "", + "dbUserExists": "", + "unknownError": "" }, "registerTeamMember": { - "title": "Зарегистрировать члена команды", + "title": "", "auth": { "common": { "inputs": { "firstName": { "errors": { - "empty": "Пожалуйста, введите имя", - "pattern": "Имя должно содержать только буквы, пробелы, апострофы или дефисы." + "empty": "", + "pattern": "" } }, "lastName": { "errors": { - "empty": "Пожалуйста, введите фамилию", - "pattern": "Фамилия должна содержать только буквы, пробелы, апострофы или дефисы." + "empty": "", + "pattern": "" } }, "email": { "errors": { - "empty": "Чтобы продолжить, введите адрес электронной почты.", - "invalid": "Пожалуйста, перепроверьте правильность введенного адреса электронной почты." + "empty": "", + "invalid": "" } }, "role": { "errors": { - "empty": "Требуется роль" + "empty": "" } } } } } }, - "role": "Роль", - "changeTeamPassword": { - "changePasswordMenu": "Сбросить пароль", - "title": "Сбросить пароль члена команды", - "description": "Создайте новый пароль для этого члена команды. Вам необходимо будет сообщить ему пароль по секретному адресу.", - "success": "Пароль успешно сброшен. Убедитесь, что вы предоставили учётные данные участнику безопасным способом." - } + "role": "" }, "monitorState": { "paused": "Остановлено", @@ -455,8 +445,8 @@ "password": "Пароль", "team": "Команда", "logOut": "Выйти", - "notifications": "Уведомления", - "logs": "Логи" + "notifications": "", + "logs": "" }, "settingsEmailUser": "Email пользователь", "state": "Состояние", @@ -484,8 +474,8 @@ "settingsTestEmailUnknownError": "Неизвестная ошибка", "statusMsg": { "paused": "Мониторинг приостановлен", - "up": "Ваш сайт запущен.", - "down": "Сайт оффлайн.", + "up": "", + "down": "", "pending": "Ожидание..." }, "uptimeGeneralInstructions": { @@ -493,14 +483,13 @@ "ping": "Ведите IP-адрес или имя хоста для пинга (например, 192.168.1.100 или example.com) и добавьте понятное имя, которое будет отображаться на панели инструментов.", "docker": "Введите Docker ID вашего контейнера. Docker ID должен быть полным 64-символьным Docker ID. Вы можете запустить docker inspect , чтобы получить полный ID контейнера.", "port": "Введите URL-адрес или IP-адрес сервера, номер порта и понятное имя, которое будет отображаться на панели инструментов.", - "game": "Введите IP-адрес или имя хоста и номер порта для пинга (например, 192.168.1.100 или example.com) и выберите тип игры.", - "https": "Введите URL-адрес или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное отображаемое имя, которое будет отображаться на панели управления." + "game": "" }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Захватить", + "appName": "", + "monitoringAgentName": "", "buttons": { - "toggleTheme": "Переключить тему" + "toggleTheme": "" }, "toasts": { "networkError": "Ошибка сети", @@ -517,7 +506,7 @@ "inputs": { "email": { "label": "Электронная почта", - "placeholder": "jordan.ellis@domain.com", + "placeholder": "", "errors": { "empty": "Для продолжения введите свой адрес электронной почты", "invalid": "Пожалуйста, проверьте правильность введённого адреса электронной почты" @@ -571,7 +560,7 @@ }, "firstName": { "label": "Имя", - "placeholder": "Jordan", + "placeholder": "", "errors": { "empty": "Пожалуйста, введите свое имя", "length": "Имя должно быть короче 50 символов", @@ -580,7 +569,7 @@ }, "lastName": { "label": "Фамилия", - "placeholder": "Ellis", + "placeholder": "", "errors": { "empty": "Пожалуйста, введите свою фамилию", "length": "Фамилия должна быть короче 50 символов", @@ -594,12 +583,12 @@ "fields": { "password": { "errors": { - "incorrect": "Введённый вами пароль неверный" + "incorrect": "" } }, "role": { "errors": { - "min": "Требуется хотя бы одна роль" + "min": "" } } } @@ -613,8 +602,8 @@ "links": { "forgotPassword": "Забыли пароль?", "register": "У вас нет учетной записи?", - "forgotPasswordLink": "Сбросить пароль", - "registerLink": "Зарегистрируйтесь здесь" + "forgotPasswordLink": "", + "registerLink": "" }, "toasts": { "success": "С возвращением! Вы успешно вошли в систему.", @@ -622,10 +611,10 @@ }, "errors": { "password": { - "incorrect": "Предоставленный вами пароль не соответствует нашим записям." + "incorrect": "" } }, - "welcome": "Добро пожаловать обратно в Checkmate!" + "welcome": "" }, "registration": { "heading": { @@ -652,7 +641,7 @@ "toasts": { "success": "Добро пожаловать! Ваша учетная запись была создана успешно." }, - "welcome": "Добро пожаловать в Checkmate!" + "welcome": "" }, "forgotPassword": { "heading": "Забыли пароль?", @@ -663,471 +652,446 @@ "stepFour": "Ваш пароль был успешно сброшен. Нажмите ниже, чтобы волшебным образом войти в систему." }, "buttons": { - "openEmail": "Открыть Gmail", + "openEmail": "", "resetPassword": "Сбросить пароль" }, "imageAlts": { - "passwordKey": "Password key icon", - "email": "Email icon", - "lock": "Lock icon", - "passwordConfirm": "Password confirm icon" + "passwordKey": "", + "email": "", + "lock": "", + "passwordConfirm": "" }, "links": { - "login": "Вернуться к Входу", - "resend": "Не получили письмо? Нажмите, чтобы отправить повторно" + "login": "", + "resend": "" }, "toasts": { - "sent": "Инструкция отправлена на .", - "emailNotFound": "Email не найден.", - "redirect": "Перенаправление через ...", - "success": "Ваш пароль успешно сброшен.", - "error": "Не удалось сбросить пароль. Повторите попытку позже или обратитесь в службу поддержки." + "sent": "", + "emailNotFound": "", + "redirect": "", + "success": "", + "error": "" } } }, "errorPages": { "serverUnreachable": { "toasts": { - "reconnected": "Успешное повторное подключение к серверу.", - "stillUnreachable": "Сервер по-прежнему недоступен. Повторите попытку позже." + "reconnected": "", + "stillUnreachable": "" }, - "alertBox": "Ошибка подключения к серверу", - "description": "Не удаётся подключиться к серверу. Проверьте подключение к Интернету или конфигурацию развёртывания, если проблема не устранена.", + "alertBox": "", + "description": "", "retryButton": { - "default": "Повторите попытку подключения", - "processing": "Подключение..." + "default": "", + "processing": "" } } }, "createNotifications": { - "title": "Создать канал уведомлений", + "title": "", "nameSettings": { - "title": "Имя", - "description": "Описательное имя для вашей интеграции.", - "nameLabel": "Имя", - "namePlaceholder": "например, уведомления Slack" + "title": "", + "description": "", + "nameLabel": "", + "namePlaceholder": "" }, "typeSettings": { - "title": "Тип", - "description": "Выберите тип канала уведомлений, который вы хотите создать.", - "typeLabel": "Тип" + "title": "", + "description": "", + "typeLabel": "" }, "emailSettings": { - "title": "Электронная почта", - "description": "Адреса электронной почты получателей.", - "emailLabel": "Адрес электронной почты", - "emailPlaceholder": "например, john@example.com" + "title": "", + "description": "", + "emailLabel": "", + "emailPlaceholder": "" }, "slackSettings": { - "title": "Slack", - "description": "Настройте свой вебхук Slack здесь", - "webhookLabel": "Slack вебхук URL", - "webhookPlaceholder": "https://hooks.slack.com/services/..." + "title": "", + "description": "", + "webhookLabel": "", + "webhookPlaceholder": "" }, "pagerdutySettings": { - "title": "PagerDuty", - "description": "Настройте интеграцию PagerDuty здесь", - "integrationKeyLabel": "Интеграционный ключ", - "integrationKeyPlaceholder": "1234567890" + "title": "", + "description": "", + "integrationKeyLabel": "", + "integrationKeyPlaceholder": "" }, "discordSettings": { - "title": "Discord", - "description": "Настройте вебхук Discord здесь", - "webhookLabel": "Discord вебхук URL", - "webhookPlaceholder": "https://your-server.com/webhook" + "title": "", + "description": "", + "webhookLabel": "", + "webhookPlaceholder": "" }, "webhookSettings": { - "title": "Вебхук", - "description": "Настройте свой вебхук здесь", - "webhookLabel": "URL-адрес веб-перехватчика", - "webhookPlaceholder": "https://your-server.com/webhook" + "title": "", + "description": "", + "webhookLabel": "", + "webhookPlaceholder": "" }, - "testNotification": "Тестовое уведомление", - "dialogDeleteTitle": "Вы уверены, что хотите удалить это уведомление?", - "dialogDeleteConfirm": "Удалить" + "testNotification": "", + "dialogDeleteTitle": "", + "dialogDeleteConfirm": "" }, "notificationConfig": { - "title": "Уведомления", - "description": "Выберите каналы уведомлений, которые вы хотите использовать" + "title": "", + "description": "" }, "monitorStatus": { - "checkingEvery": "Проверка каждый {{interval}}", - "withCaptureAgent": "с агентом захвата {{version}}", - "up": "вверх", - "down": "вниз", - "paused": "приостановлено" + "checkingEvery": "", + "withCaptureAgent": "", + "up": "", + "down": "", + "paused": "" }, - "advancedMatching": "Расширенное сопоставление", - "sendTestNotifications": "Отправить тестовое уведомление", - "selectAll": "Выбрать все", - "showAdminLoginLink": "Показывать ссылку «Администратор? Войти здесь» на странице статуса", + "advancedMatching": "", + "sendTestNotifications": "", + "selectAll": "", + "showAdminLoginLink": "", "logsPage": { - "title": "Логи", - "description": "Системные логи - последние 1000 строк", + "title": "", + "description": "", "tabs": { - "queue": "Очередь заданий", - "logs": "Серверные логи", - "diagnostics": "Диагностика" + "queue": "", + "logs": "", + "diagnostics": "" }, "toast": { - "fetchLogsSuccess": "Логи успешно получены" + "fetchLogsSuccess": "" }, "logLevelSelect": { - "title": "Уровень логов", + "title": "", "values": { - "all": "Все", - "info": "Информация", - "warn": "Предупреждение", - "error": "Ошибка", - "debug": "Дебаг" + "all": "", + "info": "", + "warn": "", + "error": "", + "debug": "" } } }, "queuePage": { - "title": "Очередь", - "refreshButton": "Обновить", - "flushButton": "Очистить очередь", + "title": "", + "refreshButton": "", + "flushButton": "", "jobTable": { - "title": "Задачи в настоящее время в очереди", - "idHeader": "ID монитора", - "urlHeader": "URL", - "typeHeader": "Тип", - "activeHeader": "Активный", - "lockedAtHeader": "Заблокировано на", - "runCountHeader": "Количество запусков", - "failCountHeader": "Количество неудач", - "lastRunHeader": "Последний запуск в", - "lastFinishedAtHeader": "Последний раз закончил в", - "lastRunTookHeader": "Последний забег занял", - "intervalHeader": "Интервал" + "title": "", + "idHeader": "", + "urlHeader": "", + "typeHeader": "", + "activeHeader": "", + "lockedAtHeader": "", + "runCountHeader": "", + "failCountHeader": "", + "lastRunHeader": "", + "lastFinishedAtHeader": "", + "lastRunTookHeader": "", + "intervalHeader": "" }, "metricsTable": { - "title": "Метрики очереди", - "metricHeader": "Метрическая", - "valueHeader": "Ценить" + "title": "", + "metricHeader": "", + "valueHeader": "" }, "failedJobTable": { - "title": "Неудачные задачи", - "monitorIdHeader": "ID монитора", - "monitorUrlHeader": "URL монитора", - "failCountHeader": "Количество падений", - "failedAtHeader": "Последнее падение в", - "failReasonHeader": "Причина падения" + "title": "", + "monitorIdHeader": "", + "monitorUrlHeader": "", + "failCountHeader": "", + "failedAtHeader": "", + "failReasonHeader": "" } }, "export": { - "title": "Экспорт мониторов", - "success": "Мониторы успешно экспортированы!", - "failed": "Не удалось экспортировать мониторы" + "title": "", + "success": "", + "failed": "" }, "monitorActions": { - "title": "Экспорт/Импорт", - "import": "Импортные мониторы", - "export": "Экспорт мониторов", - "deleteSuccess": "Монитор успешно удален", - "deleteFailed": "Не удалось удалить монитор", - "details": "Подробности" + "title": "", + "import": "", + "export": "", + "deleteSuccess": "", + "deleteFailed": "", + "details": "" }, "settingsPage": { "aboutSettings": { - "labelDevelopedBy": "Разработано Bluewave Labs", - "labelVersion": "Версия", - "title": "Об" + "labelDevelopedBy": "", + "labelVersion": "", + "title": "" }, "demoMonitorsSettings": { - "buttonAddMonitors": "Добавить демо мониторы", - "description": "Добавьте образцы мониторов для демонстрации.", - "title": "Демо мониторы" + "buttonAddMonitors": "", + "description": "", + "title": "" }, "emailSettings": { - "buttonSendTestEmail": "Отправить тестовое письмо", - "description": "Настройте параметры электронной почты в вашей системе. Она используется для отправки уведомлений и оповещений.", - "descriptionTransport": "Это создает SMTP-транспорт для NodeMailer.", - "labelAddress": "Адрес электронной почты — используется для аутентификации.", - "labelConnectionHost": "Хост подключения электронной почты — имя хоста для использования в приветствии HELO/EHLO.", - "labelHost": "Хост электронной почты — имя хоста или IP-адрес для подключения.", - "labelIgnoreTLS": "Отключить STARTTLS: не использовать TLS, даже если сервер его поддерживает.", - "labelPassword": "Пароль электронной почты - Пароль для аутентификации", - "labelPasswordSet": "Пароль установлен. Нажмите «Сброс», чтобы изменить его.", - "labelPool": "Включить пул соединений: повторно использовать существующие соединения для повышения производительности.", - "labelPort": "Порт электронной почты — порт для подключения", - "labelRejectUnauthorized": "Отклонять недействительные сертификаты: отклонять соединения с самоподписанными или ненадежными сертификатами.", - "labelRequireTLS": "Принудительный STARTTLS: требуется обновление TLS, выдается ошибка, если не поддерживается", - "labelSecure": "Использовать SSL (рекомендуется): зашифровать соединение с помощью SSL/TLS.", - "labelTLSServername": "Имя сервера TLS — необязательное имя хоста для проверки TLS, если хост — это IP-адрес.", - "labelUser": "Пользователь электронной почты — имя пользователя для аутентификации, переопределяет адрес электронной почты, если указан", - "linkTransport": "Технические характеристики смотрите здесь.", - "placeholderUser": "Оставьте пустым, если не требуется", - "title": "Электронная почта", - "toastEmailRequiredFieldsError": "Требуется адрес электронной почты, хост, порт и пароль" + "buttonSendTestEmail": "", + "description": "", + "descriptionTransport": "", + "labelAddress": "", + "labelConnectionHost": "", + "labelHost": "", + "labelIgnoreTLS": "", + "labelPassword": "", + "labelPasswordSet": "", + "labelPool": "", + "labelPort": "", + "labelRejectUnauthorized": "", + "labelRequireTLS": "", + "labelSecure": "", + "labelTLSServername": "", + "labelUser": "", + "linkTransport": "", + "placeholderUser": "", + "title": "", + "toastEmailRequiredFieldsError": "" }, "pageSpeedSettings": { - "description": "Введите ключ API Google PageSpeed, чтобы включить мониторинг Google PageSpeed. Нажмите «Сбросить», чтобы обновить ключ.", - "labelApiKeySet": "API-ключ установлен. Нажмите «Сброс», чтобы изменить его.", - "labelApiKey": "API-ключ PageSpeed", - "title": "API-ключ Google PageSpeed" + "description": "", + "labelApiKeySet": "", + "labelApiKey": "", + "title": "" }, - "saveButtonLabel": "Сохранить", + "saveButtonLabel": "", "statsSettings": { - "clearAllStatsButton": "Очистить всю статистику", - "clearAllStatsDescription": "Очистить всю статистику. Это необратимо.", - "clearAllStatsDialogConfirm": "Да, очистить всю статистику", - "clearAllStatsDialogDescription": "После удаления история и статистика мониторинга не могут быть восстановлены.", - "clearAllStatsDialogTitle": "Вы хотите очистить всю статистику?", - "description": "Укажите, как долго вы хотите хранить данные. Вы также можете удалить все существующие данные.", - "labelTTL": "Дни, за которые вы хотите посмотреть историю.", - "labelTTLOptional": "0 для бесконечности", - "title": "История монитора" + "clearAllStatsButton": "", + "clearAllStatsDescription": "", + "clearAllStatsDialogConfirm": "", + "clearAllStatsDialogDescription": "", + "clearAllStatsDialogTitle": "", + "description": "", + "labelTTL": "", + "labelTTLOptional": "", + "title": "" }, "systemResetSettings": { - "buttonRemoveAllMonitors": "Удалить все мониторы", - "description": "Удалите все мониторы из вашей системы.", - "dialogConfirm": "Да, удалить все мониторы", - "dialogDescription": "После удаления, мониторы нельзя будет воостановить.", - "dialogTitle": "Вы хотите удалить все мониторы?", - "title": "Сброс системы" + "buttonRemoveAllMonitors": "", + "description": "", + "dialogConfirm": "", + "dialogDescription": "", + "dialogTitle": "", + "title": "" }, "timezoneSettings": { - "description": "Выберите часовой пояс, используемый для отображения даты и времени в приложении.", - "label": "Отображение часового пояса", - "title": "Отображение часового пояса" + "description": "", + "label": "", + "title": "" }, - "title": "Настройки", + "title": "", "uiSettings": { - "description": "Переключайтесь между светлым и темным режимами или меняйте язык пользовательского интерфейса.", - "labelLanguage": "Язык", - "labelTheme": "Режим темы", - "title": "Появление" + "description": "", + "labelLanguage": "", + "labelTheme": "", + "title": "" }, "urlSettings": { - "description": "Отображение IP-адреса или URL-адреса монитора на общедоступной странице состояния. Если эта опция отключена, для защиты конфиденциальной информации будет отображаться только имя монитора.", - "label": "Отображать IP/URL на странице статуса", - "selectDisabled": "Выключено", - "selectEnabled": "Включено", - "title": "Мониторинг IP/URL на странице статуса" + "description": "", + "label": "", + "selectDisabled": "", + "selectEnabled": "", + "title": "" }, "globalThresholds": { - "title": "Глобальные пороги", - "description": "Настройте глобальные пороговые значения для процессора, памяти, диска и температуры. Если значение указано, оно будет автоматически включено для мониторинга." + "title": "", + "description": "" } }, "statusPageCreate": { - "buttonSave": "Сохранить" + "buttonSave": "" }, - "incidentsOptionsHeaderFilterResolved": "Решено", - "settingsSave": "Сохранить", - "statusPageCreateAppearanceTitle": "Внешний вид", - "confirmPassword": "Подтвердите пароль", + "incidentsOptionsHeaderFilterResolved": "", + "settingsSave": "", + "statusPageCreateAppearanceTitle": "", + "confirmPassword": "", "monitorHooks": { - "failureAddDemoMonitors": "Не удалось добавить демонстрационные мониторы.", - "successAddDemoMonitors": "Демонстрационные мониторы успешно добавлены" + "failureAddDemoMonitors": "", + "successAddDemoMonitors": "" }, - "settingsAppearance": "Внешний вид", - "settingsDisplayTimezone": "Отображение часового пояса", - "settingsGeneralSettings": "Общие настройки", - "incidentsOptionsHeaderTotalIncidents": "Всего инцидентов", + "settingsAppearance": "", + "settingsDisplayTimezone": "", + "settingsGeneralSettings": "", + "incidentsOptionsHeaderTotalIncidents": "", "statusPage": { - "deleteSuccess": "Страница статуса успешно удалена", - "deleteFailed": "Не удалось удалить страницу статуса", - "createSuccess": "Страница статуса успешно создана", - "updateSuccess": "Страница статуса успешно обновлена", - "generalSettings": "Общие настройки", - "contents": "Содержание", + "deleteSuccess": "", + "deleteFailed": "", + "createSuccess": "", + "updateSuccess": "", + "generalSettings": "", + "contents": "", "fallback": { - "checks": [ - "Мониторинг и отображение состояния ваших сервисов в режиме реального времени", - "Отслеживайте состояние нескольких сервисов и делитесь их статусом", - "Дайте пользователям информацию о сбоях и производительности" - ], - "title": "Страница статуса используется для:", - "actionButton": "Давайте создадим вашу первую страницу статуса!" + "checks": [""], + "title": "", + "actionButton": "" } }, - "testNotificationsDisabled": "Для этого монитора не настроены уведомления. Вам нужно добавить их, нажав кнопку «Настроить».", - "incidentsTableResolvedAt": "Решено в", - "incidentsTableActionResolve": "Решено", + "testNotificationsDisabled": "", + "incidentsTableResolvedAt": "", + "incidentsTableActionResolve": "", "checkHooks": { - "failureResolveOne": "Не удалось разрешить инцидент.", - "failureResolveAll": "Не удалось разрешить все инциденты.", - "failureResolveMonitor": "Не удалось устранить инциденты с монитором." + "failureResolveOne": "", + "failureResolveAll": "", + "failureResolveMonitor": "" }, - "checkFormError": "Пожалуйста, проверьте форму на наличие ошибок.", + "checkFormError": "", "diagnosticsPage": { - "diagnosticDescription": "Системная диагностика", - "statsDescription": "Системная статистика", + "diagnosticDescription": "", + "statsDescription": "", "gauges": { - "heapAllocationTitle": "Использовано кучи", - "heapAllocationSubtitle": "% доступной памяти", - "heapUsageTitle": "Выделено памяти в куче", - "heapUsageSubtitle": "% доступно памяти", - "heapUtilizationTitle": "Использовано кучи", - "heapUtilizationSubtitle": "% от доступного", - "instantCpuUsageTitle": "Загруженность CPU", - "instantCpuUsageSubtitle": "% загрузка CPU за 1s" + "heapAllocationTitle": "", + "heapAllocationSubtitle": "", + "heapUsageTitle": "", + "heapUsageSubtitle": "", + "heapUtilizationTitle": "", + "heapUtilizationSubtitle": "", + "instantCpuUsageTitle": "", + "instantCpuUsageSubtitle": "" }, "stats": { - "eventLoopDelayTitle": "Задержка цикла событий", - "uptimeTitle": "Аптайм", - "usedHeapSizeTitle": "Размер используемой кучи", - "totalHeapSizeTitle": "Общий размер кучи", - "osMemoryLimitTitle": "Ограничение памяти ОS" + "eventLoopDelayTitle": "", + "uptimeTitle": "", + "usedHeapSizeTitle": "", + "totalHeapSizeTitle": "", + "osMemoryLimitTitle": "" } }, - "pageSpeedLighthouseAPI": "Используйте API Lighthouse PageSpeed для мониторинга вашего сайта", + "pageSpeedLighthouseAPI": "", "time": { - "threeMinutes": "3 минуты", - "fiveMinutes": "5 минут", - "tenMinutes": "10 минут", - "twentyMinutes": "20 минут", - "oneHour": "1 час", - "oneDay": "1 день", - "oneWeek": "1 неделя", - "fourMinutes": "4 минуты", - "oneMinute": "1 минута", - "twoMinutes": "2 минуты", - "fifteenSeconds": "15 секунд", - "thirtySeconds": "30 секунд" + "threeMinutes": "", + "fiveMinutes": "", + "tenMinutes": "", + "twentyMinutes": "", + "oneHour": "", + "oneDay": "", + "oneWeek": "", + "fourMinutes": "", + "oneMinute": "", + "twoMinutes": "", + "fifteenSeconds": "", + "thirtySeconds": "" }, "general": { - "noOptionsFound": "{{unit}} не найдено" + "noOptionsFound": "" }, "infrastructureMonitor": { "fallback": { - "checks": [ - "Отслеживайте производительность серверов", - "Выявляйте узкие места и оптимизируйте использование", - "Обеспечивайте надежность с помощью мониторинга в режиме реального времени" - ], - "title": "Монитор инфраструктуры используется для:", - "actionButton": "Давайте создадим ваш первый монитор инфраструктуры!" + "checks": [""], + "title": "", + "actionButton": "" } }, "maintenanceWindow": { "fallback": { - "checks": [ - "Отметьте периоды технического обслуживания.", - "Устраните любые недопонимания.", - "Прекратите отправку оповещений в периоды технического обслуживания." - ], - "title": "Период технического обслуживания используется для:", - "actionButton": "Давайте создадим ваше первое окно технического обслуживания!" + "checks": [""], + "title": "", + "actionButton": "" } }, "pageSpeed": { "fallback": { - "checks": [ - "Отчет об удобстве использования страницы", - "Помощь в анализе скорости загрузки веб-страницы", - "Предложение о том, как можно улучшить страницу" - ], - "title": "Монитор PageSpeed используется для:", - "actionButton": "Давайте создадим ваш первый монитор PageSpeed!" + "checks": [""], + "title": "", + "actionButton": "" } }, "uptimeMonitor": { "fallback": { - "checks": [ - "Проверьте, доступны ли веб-сайты или серверы и отвечают ли они", - "Оповещайте команды о сбоях или проблемах с производительностью", - "Мониторинг конечных точек HTTP, пингов, контейнеров и портов", - "Отслеживайте динамику времени безотказной работы и надежности" - ], - "title": "Монитор работоспособности используется для:", - "actionButton": "Давайте создадим ваш первый монитор времени безотказной работы!" + "checks": [""], + "title": "", + "actionButton": "" } }, "editUserPage": { "form": { - "email": "Электронная почта", - "firstName": "Имя", - "lastName": "Фамилия", - "role": "Роли", - "save": "Сохранить" + "email": "", + "firstName": "", + "lastName": "", + "role": "", + "save": "" }, "table": { - "actionHeader": "Действие", - "roleHeader": "Роль" + "actionHeader": "", + "roleHeader": "" }, - "title": "Редактировать пользователя", + "title": "", "toast": { - "successUserUpdate": "Пользователь успешно обновлен", - "validationErrors": "Ошибки проверки" + "successUserUpdate": "", + "validationErrors": "" } }, - "incidentsPageActionResolveMonitor": "Разрешение инцидентов мониторинга", - "incidentsPageActionResolveAll": "Решить все инциденты", + "incidentsPageActionResolveMonitor": "", + "incidentsPageActionResolveAll": "", "matchMethodOptions": { - "equal": "Равный", - "equalPlaceholder": "успех", - "include": "Включить", - "includePlaceholder": "хорошо", - "regex": "Regex", - "regexPlaceholder": "^(success|ok)$", - "text": "Метод сопоставления" + "equal": "", + "equalPlaceholder": "", + "include": "", + "includePlaceholder": "", + "regex": "", + "regexPlaceholder": "", + "text": "" }, "monitorType": { "docker": { - "label": "Имя/ID контейнера", - "namePlaceholder": "Мой контейнер", - "placeholder": "my-app или abcd1234" + "label": "", + "namePlaceholder": "", + "placeholder": "" }, "http": { - "label": "URL для мониторинга", - "namePlaceholder": "Google", - "placeholder": "google.com" + "label": "", + "namePlaceholder": "", + "placeholder": "" }, "ping": { - "label": "IP-адрес для мониторинга", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" + "label": "", + "namePlaceholder": "", + "placeholder": "" }, "port": { - "label": "URL для мониторинга", - "namePlaceholder": "Локальный хост:5173", - "placeholder": "локальный хост" + "label": "", + "namePlaceholder": "", + "placeholder": "" }, "game": { - "label": "URL для мониторинга", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" + "label": "", + "namePlaceholder": "", + "placeholder": "" } }, "uptimeAdvancedMatching": { - "jsonPath": "путь к JSON" + "jsonPath": "" }, - "bytesPerSecond": "Байты в секунду", - "bytesReceived": "Получено байтов", - "bytesSent": "Отправлено байтов", - "chooseGame": "Выбрать игру", + "bytesPerSecond": "", + "bytesReceived": "", + "bytesSent": "", + "chooseGame": "", "createMonitorPage": { - "incidentConfigDescription": "Мониторинг использует скользящее окно, чтобы понять, когда ресурс недоступен. Статус изменится, если нужный процент проверок покажет сбой.", - "incidentConfigStatusWindowLabel": "Сколько проверок должно быть в скользящем окне?", - "incidentConfigStatusWindowThresholdLabel": "Какой процент проверок в скользящем окне завершается неудачей/успешностью до изменения статуса монитора?", - "incidentConfigTitle": "Инциденты", - "incidentConfigDescriptionV2": "Количество последовательных проверок, необходимых для изменения статуса монитора. Максимум 25.", - "incidentConfigStatusCheckNumber": "Сколько проверок перед сменой статуса?", - "intervalTitle": "Интервал проверки", - "intervalDescription": "Как часто следует проверять монитор." + "incidentConfigDescription": "", + "incidentConfigStatusWindowLabel": "", + "incidentConfigStatusWindowThresholdLabel": "", + "incidentConfigTitle": "" }, - "dataRate": "Скорость передачи данных", - "dataReceived": "Данные получены", - "dataSent": "Данные отправлены", - "details": "Подробности", - "drops": "Падений", - "errors": "Ошибки", - "errorsIn": "Ошибки в", - "errorsOut": "Ошибки вне", - "gameServerMonitoring": "Мониторинг игрового сервера", - "gameServerMonitoringDescription": "Проверьте, запущен ли ваш игровой сервер.", - "network": "Сеть", - "networkDrops": "Сетевые падения", - "networkErrors": "Ошибки сети", - "networkInterface": "Сетевой интерфейс", - "noNetworkStatsAvailable": "Статистика сети отсутствует.", - "packetsPerSecond": "Пакетов в секунду", - "packetsReceived": "Получено пакетов", - "packetsReceivedRate": "Скорость получения пакетов", - "packetsSent": "Отправлено пакетов", - "rate": "коэффициент", - "selectInterface": "Выберите интерфейс" + "dataRate": "", + "dataReceived": "", + "dataSent": "", + "details": "", + "drops": "", + "errors": "", + "errorsIn": "", + "errorsOut": "", + "gameServerMonitoring": "", + "gameServerMonitoringDescription": "", + "network": "", + "networkDrops": "", + "networkErrors": "", + "networkInterface": "", + "noNetworkStatsAvailable": "", + "packetsPerSecond": "", + "packetsReceived": "", + "packetsReceivedRate": "", + "packetsSent": "", + "rate": "", + "selectInterface": "" } diff --git a/client/src/locales/th.json b/client/src/locales/th.json index c32849afe..c3f571b6b 100644 --- a/client/src/locales/th.json +++ b/client/src/locales/th.json @@ -425,13 +425,7 @@ } } }, - "role": "บทบาท", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "บทบาท" }, "monitorState": { "paused": "หยุดชั่วคราว", @@ -493,8 +487,7 @@ "ping": "กรอกที่อยู่ IP หรือชื่อโฮสต์ที่ต้องการ ping (เช่น 192.168.1.100 หรือ example.com) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", "docker": "กรอกชื่อหรือ ID ของคอนเทนเนอร์ Docker คุณสามารถใช้ชื่อคอนเทนเนอร์ (เช่น my-app) หรือ ID ของคอนเทนเนอร์ (ID เต็ม 64 ตัวอักษร หรือ ID สั้น)", "port": "กรอก URL หรือ IP ของเซิร์ฟเวอร์ หมายเลขพอร์ต และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", - "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม", - "https": "" + "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม" }, "common": { "appName": "Checkmate", @@ -1103,11 +1096,7 @@ "incidentConfigDescription": "การใช้หน้าต่างเลื่อนจะช่วยกำหนดเวลาที่ตัวตรวจสอบล้มเหลว สถานะของตัวตรวจสอบจะเปลี่ยนก็ต่อเมื่อเปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนตรงตามค่าที่กำหนด", "incidentConfigStatusWindowLabel": "ต้องมีการตรวจสอบกี่ครั้งในหน้าต่างเลื่อน?", "incidentConfigStatusWindowThresholdLabel": "ต้องการให้กี่เปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนล้มเหลว/สำเร็จ ก่อนที่จะเปลี่ยนสถานะของตัวตรวจสอบ?", - "incidentConfigTitle": "เหตุการณ์", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "เหตุการณ์" }, "dataRate": "อัตราการส่งข้อมูล", "dataReceived": "ข้อมูลที่รับ", diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 59f294533..0ab45ebe9 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -421,13 +421,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "Durduruldu", @@ -489,8 +483,7 @@ "ping": "", "docker": "", "port": "", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "Checkmate", @@ -1078,11 +1071,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json index 305cefe87..c2d1aa6fc 100644 --- a/client/src/locales/zh-CN.json +++ b/client/src/locales/zh-CN.json @@ -425,13 +425,7 @@ } } }, - "role": "角色", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "角色" }, "monitorState": { "paused": "暂停", @@ -493,8 +487,7 @@ "ping": "输入要 ping 的 IP 地址或主机名(例如.. 192.168.1.100 或 example.com),并添加显示在仪表板上的清晰显示名称。", "docker": "码输入 Docker 容器名称或 ID。您可以使用容器名称(例如.. my-app)或容器 ID(完整的 64 字符 ID 或短 ID)。", "port": "输入服务器的 URL 或 IP、端口号和仪表板上显示的清晰显示名称。", - "game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。", - "https": "" + "game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。" }, "common": { "appName": "Checkmate", @@ -1095,11 +1088,7 @@ "incidentConfigDescription": "滑动窗口用于确定监视器何时发生故障。仅当滑动窗口中的检查百分比满足指定值时,监视器的状态才会改变。", "incidentConfigStatusWindowLabel": "在滑动窗口中应包含多少次检测?", "incidentConfigStatusWindowThresholdLabel": "在监视器状态改变之前,滑动窗口中的检查失败/成功的百分比是多少?", - "incidentConfigTitle": "事件", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "事件" }, "dataRate": "数据速率", "dataReceived": "已接收数据", diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json index e5ee58e58..e57ebd487 100644 --- a/client/src/locales/zh-TW.json +++ b/client/src/locales/zh-TW.json @@ -425,13 +425,7 @@ } } }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } + "role": "" }, "monitorState": { "paused": "", @@ -493,8 +487,7 @@ "ping": "", "docker": "", "port": "", - "game": "", - "https": "" + "game": "" }, "common": { "appName": "Checkmate", @@ -1082,11 +1075,7 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" + "incidentConfigTitle": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/main.jsx b/client/src/main.jsx deleted file mode 100644 index dd42587af..000000000 --- a/client/src/main.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import ReactDOM from "react-dom/client"; -import App from "./App.jsx"; -import "./index.css"; -import { BrowserRouter as Router } from "react-router-dom"; -import { Provider } from "react-redux"; -import { persistor, store } from "./store"; -import { PersistGate } from "redux-persist/integration/react"; -import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx"; -import { networkService } from "./Utils/NetworkService"; -export { networkService }; -import I18nLoader from "./Components/v1/I18nLoader"; - -ReactDOM.createRoot(document.getElementById("root")).render( - - - - - - - - - - -); diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 000000000..2a389de52 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,15 @@ +import { createRoot } from "react-dom/client"; +import App from "./App.tsx"; +import { Provider } from "react-redux"; +import { store, persistor } from "@/store"; +import { PersistGate } from "redux-persist/integration/react"; + +createRoot(document.getElementById("root")!).render( + // + + + + + + // +); diff --git a/client/src/pages/auth/Invite.tsx b/client/src/pages/auth/Invite.tsx new file mode 100644 index 000000000..238c9e805 --- /dev/null +++ b/client/src/pages/auth/Invite.tsx @@ -0,0 +1,222 @@ +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import MenuItem from "@mui/material/MenuItem"; +import Stack from "@mui/material/Stack"; +import { Button, TextInput, Select } from "@/components/inputs"; +import { BasePage, ConfigBox } from "@/components/design-elements"; + +import { useEffect, useState, useMemo } from "react"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { inviteSchema } from "@/validation/zod"; +import { z } from "zod"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useGet, usePost } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +const host = import.meta.env.VITE_APP_HOST; + +type FormValues = z.infer; + +const Invite = () => { + const { t } = useTranslation(); + const theme = useTheme(); + + const [token, setToken] = useState(null); + const { post, loading } = usePost(); + const onSubmit = async (data: FormValues) => { + const res = await post("/invite", data); + if (res) { + setToken(res.data); + } + }; + + const { response } = useGet>("/roles"); + const roles = response?.data; + const orgRoles = useMemo( + () => roles?.filter((role: any) => role.scope === "organization"), + [roles] + ); + const teamRoles = useMemo( + () => roles?.filter((role: any) => role.scope === "team"), + [roles] + ); + + const { response: teamsResponse } = useGet>("/teams"); + const teams = teamsResponse?.data; + + const { + handleSubmit, + control, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(inviteSchema), + defaultValues: { + email: "", + teamId: "", + teamRoleId: "", + orgRoleId: "", + }, + }); + + useEffect(() => { + reset({ + teamId: teams?.[0]?._id || "", + teamRoleId: teamRoles?.[0]?._id || "", + }); + }, [teams, teamRoles, reset]); + + return ( + + + ( + + )} + /> + { + return ( + + + + ); + }} + /> + { + const currentRole = teamRoles?.find( + (role: any) => role._id === field.value + ); + return ( + + + + + {currentRole && `Permissions:`} + + {currentRole?.permissions?.map((perm: string) => ( + + - {perm} + + ))} + + + ); + }} + /> + + } + /> + + + { + const currentOrgRole = orgRoles?.find( + (role: any) => role._id === field.value + ); + return ( + + + + + {currentOrgRole && `Permissions:`} + + {currentOrgRole?.permissions?.map((perm: string) => ( + + - {perm} + + ))} + + + ); + }} + /> + + } + /> + {token && ( + + Invite link: + {`${host}/register/${token}`} + + )} + + + + + ); +}; + +export default Invite; diff --git a/client/src/pages/auth/Login.tsx b/client/src/pages/auth/Login.tsx new file mode 100644 index 000000000..a8653ee8b --- /dev/null +++ b/client/src/pages/auth/Login.tsx @@ -0,0 +1,150 @@ +import { AuthBasePage } from "@/components/auth"; +import { Button, TextInput, TextLink } from "@/components/inputs"; +import Stack from "@mui/material/Stack"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { usePost } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import { useTheme } from "@mui/material/styles"; +import { useAppDispatch } from "@/hooks/AppHooks"; +import { useTranslation } from "react-i18next"; +import { + setAuthenticated, + setUser, + setSelectedTeamId, + logout, +} from "@/features/authSlice"; +import { z } from "zod"; +import { useForm, Controller } from "react-hook-form"; +import type { IUser } from "@/types/user"; + +const schema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), +}); + +type FormData = z.infer; + +const Login = () => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const theme = useTheme(); + const { post, loading } = usePost(); + const navigate = useNavigate(); + + const { + handleSubmit, + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + email: "", + password: "", + }, + }); + + const onSubmit = async (data: FormData) => { + const result = await post("/auth/login", data); + + if (!result) { + dispatch(logout()); + navigate("/login"); + } + + const user = result?.data || null; + if (!user) { + dispatch(logout()); + navigate("/login"); + return; + } + + dispatch(setAuthenticated(true)); + dispatch(setUser(user)); + dispatch(setSelectedTeamId(user.teams[0]?.id || null)); + navigate("/"); + }; + + return ( + + + + ( + + )} + /> + ( + + )} + /> + + + + + + + ); +}; + +export default Login; diff --git a/client/src/pages/auth/Register.tsx b/client/src/pages/auth/Register.tsx new file mode 100644 index 000000000..43564ada9 --- /dev/null +++ b/client/src/pages/auth/Register.tsx @@ -0,0 +1,65 @@ +import { AuthBasePage } from "@/components/auth"; +import Stack from "@mui/material/Stack"; +import { RegisterForm } from "./RegisterForm"; + +import { + setAuthenticated, + setUser, + setSelectedTeamId, + logout, +} from "@/features/authSlice"; +import { useAppDispatch } from "@/hooks/AppHooks"; +import { useTranslation } from "react-i18next"; +import { usePost } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { registerSchema } from "@/validation/zod"; + +type FormData = z.infer; +const Register = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { post, loading, error } = usePost, any>(); + + const dispatch = useAppDispatch(); + + const onSubmit = async (data: FormData) => { + const submit = { + email: data.email, + password: data.password, + firstName: data.firstName, + lastName: data.lastName, + }; + const result = await post("/auth/register", submit); + + if (!result) { + dispatch(logout()); + navigate("/login"); + } + + const user = result?.data || null; + if (!user) { + dispatch(logout()); + navigate("/login"); + return; + } + + dispatch(setAuthenticated(true)); + dispatch(setUser(user)); + dispatch(setSelectedTeamId(user.teams[0]?.id || null)); + navigate("/"); + }; + + return ( + + + + + + ); +}; + +export default Register; diff --git a/client/src/pages/auth/RegisterForm.tsx b/client/src/pages/auth/RegisterForm.tsx new file mode 100644 index 000000000..51f248519 --- /dev/null +++ b/client/src/pages/auth/RegisterForm.tsx @@ -0,0 +1,177 @@ +import Stack from "@mui/material/Stack"; +import { TextInput } from "@/components/inputs"; +import { Button } from "@/components/inputs"; +import Typography from "@mui/material/Typography"; + +import { useForm, Controller } from "react-hook-form"; +import type { SubmitHandler } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useInitForm } from "@/hooks/forms/UseInitRegisterForm"; + +import { registerSchema } from "@/validation/zod"; +type FormData = z.infer; + +export const RegisterForm = ({ + mode = "register", + hasUser = false, + initialData, + onSubmit, + loading, + error, +}: { + mode?: "register" | "invite"; + hasUser?: boolean; + initialData?: Partial; + onSubmit: SubmitHandler; + loading: boolean; + error: string | null; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + + const { + handleSubmit, + control, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + defaultValues: { + email: "", + password: "", + confirmPassword: "", + }, + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + if (hasUser && mode === "invite") { + return ( + + ); + } + + return ( + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + {error && {error}} + + ); +}; diff --git a/client/src/pages/auth/RegisterInvite.tsx b/client/src/pages/auth/RegisterInvite.tsx new file mode 100644 index 000000000..f064d16bf --- /dev/null +++ b/client/src/pages/auth/RegisterInvite.tsx @@ -0,0 +1,89 @@ +import { AuthBasePage } from "@/components/auth"; +import Stack from "@mui/material/Stack"; +import { RegisterForm } from "./RegisterForm"; + +import { + setAuthenticated, + setUser, + setSelectedTeamId, +} from "@/features/authSlice"; +import { useAppDispatch } from "@/hooks/AppHooks"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useTranslation } from "react-i18next"; +import { usePost } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { registerSchema } from "@/validation/zod"; +import { useGet } from "@/hooks/UseApi"; +import { useParams } from "react-router"; +import type { IUser } from "@/types/user"; + +type FormData = z.infer; +const RegisterInvite = () => { + const { id: token } = useParams(); + + const { t } = useTranslation(); + const navigate = useNavigate(); + const { post, loading, error } = usePost, IUser>(); + const { response, error: errorInvite } = useGet>( + `/invite/${token}` + ); + const { user, invite } = response?.data || {}; + const hasUser = Boolean(user); + const dispatch = useAppDispatch(); + + const onSubmit = async (data: FormData) => { + const submit = { + ...(data.firstName && { firstName: data.firstName }), + ...(data.lastName && { lastName: data.lastName }), + ...(data.password && { password: data.password }), + ...(data.confirmPassword && { confirmPassword: data.confirmPassword }), + }; + const result = await post(`/auth/register/invite/${token}`, submit); + + if (!result) { + dispatch(setAuthenticated(false)); + navigate("/login"); + return; + } + + const user: IUser = result.data; + + if (!user) { + dispatch(setAuthenticated(false)); + navigate("/login"); + return; + } + + dispatch(setAuthenticated(true)); + dispatch(setUser(user)); + dispatch(setSelectedTeamId(user.teams[0]?.id || null)); + navigate("/"); + }; + + if (errorInvite) { + return "No invite found"; + } + + return ( + + + + + + ); +}; + +export default RegisterInvite; diff --git a/client/src/pages/diagnostic/DiagnosticLogDetails.tsx b/client/src/pages/diagnostic/DiagnosticLogDetails.tsx new file mode 100644 index 000000000..b39037f20 --- /dev/null +++ b/client/src/pages/diagnostic/DiagnosticLogDetails.tsx @@ -0,0 +1,25 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; + +import { useTheme, alpha } from "@mui/material/styles"; +import type { ILogEntry } from "@/types/log"; + +export const DiagnosticLogDetails = ({ logEntry }: { logEntry: ILogEntry }) => { + const theme = useTheme(); + + return ( + + + {"Stack trace"} + + + {logEntry.stack}{" "} + + + ); +}; diff --git a/client/src/pages/diagnostic/DiagnosticPage.tsx b/client/src/pages/diagnostic/DiagnosticPage.tsx new file mode 100644 index 000000000..35fd6ac4c --- /dev/null +++ b/client/src/pages/diagnostic/DiagnosticPage.tsx @@ -0,0 +1,38 @@ +import { BasePage, Tab, Tabs, InfoBox } from "@/components/design-elements"; +import { DiagnosticsLogTab } from "@/pages/diagnostic/DiagnosticsLogTab"; +import { DiagnosticsQueueTab } from "@/pages/diagnostic/DiagnosticsQueueTab"; +import { FileText, ListTodo } from "lucide-react"; +import { useState } from "react"; + +const DiagnosticPage = () => { + const [tabValue, setTabValue] = useState("logs"); + + const handleTabChange = (_: React.SyntheticEvent, newValue: string) => { + setTabValue(newValue); + }; + + return ( + + + + } + /> + } + /> + + {tabValue === "logs" && } + {tabValue === "jobs" && } + + ); +}; + +export default DiagnosticPage; diff --git a/client/src/pages/diagnostic/DiagnosticsLogTab.tsx b/client/src/pages/diagnostic/DiagnosticsLogTab.tsx new file mode 100644 index 000000000..9412c5c32 --- /dev/null +++ b/client/src/pages/diagnostic/DiagnosticsLogTab.tsx @@ -0,0 +1,103 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Select } from "@/components/inputs"; +import MenuItem from "@mui/material/MenuItem"; +import { Table } from "@/components/design-elements"; +import type { Header } from "@/components/design-elements/Table"; +import { DiagnosticLogDetails } from "@/pages/diagnostic/DiagnosticLogDetails"; + +import { useState } from "react"; +import { useGet } from "@/hooks/UseApi"; +import { useTheme } from "@mui/material/styles"; +import type { ILogEntry } from "@/types/log"; +import type { ApiResponse } from "@/hooks/UseApi"; + +export const DiagnosticsLogTab = () => { + const theme = useTheme(); + const [logLevel, setLogLevel] = useState("all"); + const { response } = useGet>( + `/diagnostic/logs`, + {}, + { refreshInterval: 5000 } + ); + + const logEntries = response?.data || []; + + const levelColors = { + info: theme.palette.success.main, + warn: theme.palette.warning.main, + error: theme.palette.error.main, + debug: theme.palette.accent.main, + }; + + const headers: Header[] = [ + { + id: "timestamp", + content: "Timestamp", + render: (log: ILogEntry) => log.timestamp, + }, + { + id: "level", + content: "Level", + render: (log: ILogEntry) => { + const color = + levelColors[log.level] || theme.palette.primary.contrastText; + return ( + + {log.level} + + ); + }, + }, + { + id: "service", + content: "Service", + render: (log: ILogEntry) => log.service, + }, + { + id: "message", + content: "Message", + render: (log: ILogEntry) => log.message, + }, + ]; + + const filteredLogEntries = logEntries + .filter((log: ILogEntry) => { + if (logLevel === "all") return true; + return log.level === logLevel; + }) + .map((log, idx) => ({ ...log, id: `${log.timestamp}-${idx}` })); + + return ( + + + Log level: + + + + { + if (!row.stack) return null; + return ; + }} + cardsOnSmallScreens={true} + /> + + ); +}; diff --git a/client/src/pages/diagnostic/DiagnosticsQueueTab.tsx b/client/src/pages/diagnostic/DiagnosticsQueueTab.tsx new file mode 100644 index 000000000..99ce2e305 --- /dev/null +++ b/client/src/pages/diagnostic/DiagnosticsQueueTab.tsx @@ -0,0 +1,165 @@ +import Stack from "@mui/material/Stack"; +import { StatBox } from "@/components/design-elements"; +import { Table } from "@/components/design-elements"; + +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { IJob, IJobMetrics } from "@/types/job"; +import type { Header } from "@/components/design-elements/Table"; + +export const DiagnosticsQueueTab = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const { response } = useGet< + ApiResponse<{ jobs: IJob[]; metrics: IJobMetrics }> + >(`/diagnostic/jobs`, {}, { refreshInterval: 5000 }); + + const jobs: IJob[] = response?.data?.jobs || []; + const metrics = response?.data?.metrics; + + if (!metrics) { + return null; + } + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: t("diagnosticsPage.tableHeaderId"), + render: (row) => { + return row?.id || "-"; + }, + }, + { + id: "url", + content: t("diagnosticsPage.tableHeaderUrl"), + render: (row) => { + return row?.data?.url || "-"; + }, + }, + { + id: "interval", + content: t("diagnosticsPage.tableHeaderInterval"), + render: (row) => { + if (!row?.data?.interval) return "-"; + const seconds = row.data.interval / 1000; + return `${seconds}s`; + }, + }, + { + id: "type", + content: t("diagnosticsPage.tableHeaderType"), + render: (row) => { + return row?.data?.type || "-"; + }, + }, + { + id: "active", + content: t("diagnosticsPage.tableHeaderActive"), + render: (row) => { + return row?.data?.isActive ? "true" : "false"; + }, + }, + + { + id: "runCount", + content: t("diagnosticsPage.tableHeaderRunCount"), + render: (row) => { + return row?.runCount || "-"; + }, + }, + { + id: "lastRun", + content: t("diagnosticsPage.tableHeaderLastRun"), + render: (row) => { + if (!row?.lastRunAt) return "-"; + const date = new Date(row.lastRunAt); + return ( + +
{date.toLocaleDateString()}
+
{date.toLocaleTimeString()}
+
+ ); + }, + }, + { + id: "lockedAt", + content: t("diagnosticsPage.tableHeaderLockedAt"), + render: (row) => { + if (!row?.lockedAt) return "-"; + const date = new Date(row.lockedAt); + return ( + +
{date.toLocaleDateString()}
+
{date.toLocaleTimeString()}
+
+ ); + }, + }, + { + id: "lastFinishedAt", + content: t("diagnosticsPage.tableHeaderLastFinishedAt"), + render: (row) => { + if (!row?.lastFinishedAt) return "-"; + const date = new Date(row.lastFinishedAt); + return ( + +
{date.toLocaleDateString()}
+
{date.toLocaleTimeString()}
+
+ ); + }, + }, + { + id: "lastRunTook", + content: t("diagnosticsPage.tableHeaderLastRunTook"), + render: (row) => { + return row?.lastRunTook || "-"; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( + + + + + + + + +
+ + ); +}; diff --git a/client/src/pages/incidents/IncidentTable.tsx b/client/src/pages/incidents/IncidentTable.tsx new file mode 100644 index 000000000..b62acd341 --- /dev/null +++ b/client/src/pages/incidents/IncidentTable.tsx @@ -0,0 +1,165 @@ +import { Table, Pagination, ValueLabel } from "@/components/design-elements"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Button } from "@/components/inputs"; +import type { Header } from "@/components/design-elements/Table"; + +import { usePatch } from "@/hooks/UseApi"; +import { useTranslation } from "react-i18next"; +import { formatDateWithTz } from "@/utils/TimeUtils"; +import { useAppSelector } from "@/hooks/AppHooks"; +import type { IIncident } from "@/types/incident"; + +export const IncidentTable = ({ + incidents, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, + refetch, +}: { + incidents: IIncident[]; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; + refetch: () => void; +}) => { + const { patch, loading: isPatching } = usePatch(); + const { t } = useTranslation(); + const uiTimezone = useAppSelector((state: any) => state.ui.timezone); + + const getHeaders = (t: Function, uiTimezone: string) => { + const headers: Header[] = [ + { + id: "name", + content: t("incidentsTableMonitorName"), + render: (row) => { + return row.monitorId.name || "N/A"; + }, + }, + { + id: "start", + content: "Incident start", + render: (row) => { + return formatDateWithTz( + row.startedAt, + "ddd, MMMM D, YYYY, HH:mm A", + uiTimezone + ); + }, + }, + { + id: "end", + content: "Incident end", + render: (row) => { + return row.endedAt + ? formatDateWithTz( + row.endedAt, + "ddd, MMMM D, YYYY, HH:mm A", + uiTimezone + ) + : "Ongoing"; + }, + }, + { + id: "status", + content: "Current monitor status", + render: (row) => { + return ( + + {row.monitorId.status} + + ); + }, + }, + { + id: "resolved", + content: "Resolved", + render: (row) => { + return ( + + ); + }, + }, + { + id: "resolutionType", + content: "Resolution type", + render: (row) => { + return row.resolutionType || "N/A"; + }, + }, + { + id: "resolvedBy", + content: "Resolved by", + render: (row) => { + return row.resolvedBy?.email || "N/A"; + }, + }, + { + id: "actions", + content: "Actions", + render: (row) => { + return row.resolved ? null : ( + + ); + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(t, uiTimezone); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + return ( + +
{ + alert(JSON.stringify(row, null, 2)); + }} + /> + + + ); +}; diff --git a/client/src/pages/incidents/IncidentsPage.tsx b/client/src/pages/incidents/IncidentsPage.tsx new file mode 100644 index 000000000..5b98e5996 --- /dev/null +++ b/client/src/pages/incidents/IncidentsPage.tsx @@ -0,0 +1,121 @@ +import Stack from "@mui/material/Stack"; +import MenuItem from "@mui/material/MenuItem"; +import { Select } from "@/components/inputs"; +import { BasePage, InfoBox } from "@/components/design-elements"; +import { IncidentTable } from "@/pages/incidents/IncidentTable"; +import { HeaderRange } from "@/components/common/HeaderRange"; + +import { useTheme } from "@mui/material/styles"; +import { useParams } from "react-router"; +import { useState, useEffect } from "react"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { SelectChangeEvent } from "@mui/material/Select"; +import type { IIncident } from "@/types/incident"; +import type { IMonitor } from "@/types/monitor"; + +const IncidentsPage = () => { + const theme = useTheme(); + const [selectedMonitorId, setSelectedMonitorId] = useState("all"); + const { id } = useParams<{ id: string }>(); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [range, setRange] = useState("2h"); + const [resolved, setResolved] = useState(false); + + const { response, isValidating, refetch } = useGet< + ApiResponse<{ count: number; incidents: IIncident[] }> + >( + `/incidents?page=${page}&rowsPerPage=${rowsPerPage}&range=${range}${ + selectedMonitorId && selectedMonitorId !== "all" + ? `&monitorId=${selectedMonitorId}` + : "" + }${resolved !== null ? `&resolved=${resolved}` : ""}`, + {}, + { keepPreviousData: true }, + { useTeamIdAsKey: true } + ); + + const incidents = response?.data?.incidents + ? [...response.data.incidents].reverse() + : []; + const count = response?.data?.count || 0; + + const { response: monitorResponse } = useGet>( + `/monitors`, + {}, + { keepPreviousData: true } + ); + + const monitors: IMonitor[] = monitorResponse?.data || []; + + useEffect(() => { + if (id) { + setSelectedMonitorId(id); + } + }, [id]); + + return ( + + + + + + onChange={(e: SelectChangeEvent) => { + setSelectedMonitorId(e.target.value); + }} + value={selectedMonitorId} + > + All monitors + {monitors.map((monitor) => ( + + {monitor.name} + + ))} + + + onChange={(e: SelectChangeEvent) => { + if (e.target.value === "resolved") { + setResolved(true); + } else if (e.target.value === "unresolved") { + setResolved(false); + } else { + setResolved(null); + } + }} + value={ + resolved === null ? "all" : resolved ? "resolved" : "unresolved" + } + > + All + Resolved + Unresolved + + + + + + + ); +}; + +export default IncidentsPage; diff --git a/client/src/pages/infrastructure/InfraConfig.tsx b/client/src/pages/infrastructure/InfraConfig.tsx new file mode 100644 index 000000000..0e8fc8904 --- /dev/null +++ b/client/src/pages/infrastructure/InfraConfig.tsx @@ -0,0 +1,62 @@ +import { monitorSchemaInfra } from "@/validation/zod"; +import { useGet, usePatch } from "@/hooks/UseApi"; +import { InfraForm } from "@/pages/infrastructure/InfraForm"; + +import { useParams } from "react-router"; +import type { ApiResponse } from "@/hooks/UseApi"; +import humanInterval from "human-interval"; +import { z } from "zod"; +import { useNavigate } from "react-router"; +import type { INotificationChannel } from "@/types/notification-channel"; +import type { IMonitor } from "@/types/monitor"; + +const InfraConfigurePage = () => { + type FormValues = z.infer; + type SubmitValues = Omit & { + interval: number | undefined; + }; + + const { id } = useParams(); + const navigate = useNavigate(); + const { response } = useGet>( + "/notification-channels" + ); + const { response: monitorResponse } = useGet>( + `/monitors/${id}` + ); + const monitor = monitorResponse?.data || null; + const notificationOptions = response?.data ?? []; + + const { patch, loading, error } = usePatch, IMonitor>(); + + const onSubmit = async (data: FormValues) => { + let interval = humanInterval(data.interval); + if (!interval) interval = 180000; + const submitData = { + type: data.type, + secret: data.secret, + name: data.name, + notificationChannels: data.notificationChannels, + interval, + }; + const result = await patch(`/monitors/${id}`, submitData); + if (result) { + navigate(`/infrastructure/${id}`); + } else { + console.error(error); + } + }; + return ( + + ); +}; + +export default InfraConfigurePage; diff --git a/client/src/pages/infrastructure/InfraCreate.tsx b/client/src/pages/infrastructure/InfraCreate.tsx new file mode 100644 index 000000000..90c13eabe --- /dev/null +++ b/client/src/pages/infrastructure/InfraCreate.tsx @@ -0,0 +1,43 @@ +import { InfraForm } from "@/pages/infrastructure/InfraForm"; + +import humanInterval from "human-interval"; +import { z } from "zod"; +import { monitorSchemaInfra } from "@/validation/zod"; +import { useGet, usePost } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import type { INotificationChannel } from "@/types/notification-channel"; +const InfraCreatePage = () => { + type FormValues = z.infer; + type SubmitValues = Omit & { + interval: number | undefined; + }; + const navigate = useNavigate(); + const { response } = useGet>( + "/notification-channels" + ); + const { post, loading, error } = usePost(); + + const onSubmit = async (data: FormValues) => { + let interval = humanInterval(data.interval); + if (!interval) interval = 60000; + const submitData = { ...data, interval }; + const result = await post("/monitors", submitData); + if (result) { + navigate("/infrastructure"); + } else { + console.error(error); + } + }; + + const notificationOptions = response?.data ?? []; + return ( + + ); +}; + +export default InfraCreatePage; diff --git a/client/src/pages/infrastructure/InfraDetails.tsx b/client/src/pages/infrastructure/InfraDetails.tsx new file mode 100644 index 000000000..d72167985 --- /dev/null +++ b/client/src/pages/infrastructure/InfraDetails.tsx @@ -0,0 +1,126 @@ +import { BasePage, StatBox } from "@/components/design-elements"; +import { HeaderControls, HeaderRange } from "@/components/monitors"; +import Stack from "@mui/material/Stack"; +import prettyMilliseconds from "pretty-ms"; +import { getStatusPalette } from "@/utils/MonitorUtils"; +import { InfraDetailsGraphs } from "@/pages/infrastructure/InfraDetailsGraphs"; + +import { useGet, usePatch } from "@/hooks/UseApi"; +import { useState } from "react"; +import { useTheme } from "@mui/material/styles"; +import { useParams } from "react-router"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { IMonitor, IMonitorWithMonitorStats } from "@/types/monitor"; +import type { IInfraCheck } from "@/types/check"; +import { + getFrequency, + getCores, + getAvgTemp, + getGbs, + getDiskTotalGbs, + getOsAndPlatform, +} from "@/pages/infrastructure/InfraUtils"; +import { InfraDetailsGauges } from "./InfraDetailsGauges"; + +const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; + +const InfraDetailsPage = () => { + const theme = useTheme(); + const { id } = useParams(); + const [range, setRange] = useState("2h"); + + const { response, isValidating, refetch } = useGet< + ApiResponse + >( + `/monitors/${id}?embedChecks=true&range=${range}`, + {}, + { refreshInterval: GLOBAL_REFRESH, keepPreviousData: true } + ); + + const { patch, loading: isPatching } = usePatch(); + + const monitor = response?.data?.monitor; + const stats = response?.data?.stats; + const checks = (response?.data?.checks || []) as IInfraCheck[]; + const palette = getStatusPalette(monitor?.status || "initializing"); + + const streakDuration = stats?.currentStreakStartedAt + ? Date.now() - stats?.currentStreakStartedAt + : 0; + + const lastChecked = stats?.lastCheckTimestamp + ? Date.now() - stats?.lastCheckTimestamp + : -1; + + if (!monitor) { + return null; + } + + return ( + + + + + = 0 + ? `${prettyMilliseconds(lastChecked, { + secondsDecimalDigits: 0, + })} ago` + : "N/A" + } + /> + + + + + + + + + + {/* Infrastructure overview */} + + + + + {/* Network + */} + + ); +}; + +export default InfraDetailsPage; diff --git a/client/src/pages/infrastructure/InfraDetailsGauges.tsx b/client/src/pages/infrastructure/InfraDetailsGauges.tsx new file mode 100644 index 000000000..6f1c9f711 --- /dev/null +++ b/client/src/pages/infrastructure/InfraDetailsGauges.tsx @@ -0,0 +1,84 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BaseChart } from "@/components/monitors/Chart"; +import { Gauge } from "@/components/design-elements"; + +import { getGbs, getFrequency } from "./InfraUtils"; +import type { IInfraCheck } from "@/types/check"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +const InfraDetailGauge = ({ + title, + progress, + upperLabel, + upperValue, + lowerLabel, + lowerValue, +}: { + title: string; + progress: number; + upperLabel?: string; + upperValue?: string | number; + lowerLabel?: string; + lowerValue?: string | number; +}) => { + const theme = useTheme(); + return ( + + + + + + {upperLabel} + {upperValue} + + + {lowerLabel} + {lowerValue} + + + ); +}; + +export const InfraDetailsGauges = ({ checks }: { checks: IInfraCheck[] }) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + return ( + + + + {checks[0]?.disk?.map((disk, idx) => { + return ( + + ); + })} + + ); +}; diff --git a/client/src/pages/infrastructure/InfraDetailsGraphs.tsx b/client/src/pages/infrastructure/InfraDetailsGraphs.tsx new file mode 100644 index 000000000..1380fb6e3 --- /dev/null +++ b/client/src/pages/infrastructure/InfraDetailsGraphs.tsx @@ -0,0 +1,86 @@ +import Grid from "@mui/material/Grid"; +import { HistogramInfrastructure } from "@/components/monitors"; + +import type { IInfraCheck } from "@/types/check"; +import { useMemo } from "react"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +const getChartConfigs = (theme: any, checks: IInfraCheck[]) => { + return [ + { + type: "memory", + dataKeys: ["memory.usage_percent"], + strokeColor: theme.palette.accent.main, + gradientStartColor: theme.palette.accent.main, + yDomain: [0, 1], + idx: null, + }, + { + type: "cpu", + dataKeys: ["cpu.used_percent"], + strokeColor: theme.palette.success.main, + gradientStartColor: theme.palette.success.main, + yDomain: [0, 1], + idx: null, + }, + { + type: "temp", + dataKeys: ["avg_temp"], + strokeColor: theme.palette.error.main, + gradientStartColor: theme.palette.error.main, + yDomain: [0, 150], + idx: null, + }, + ...(checks[0]?.disk?.map((_, idx) => ({ + type: "disk", + dataKeys: [`disk[${idx}].usage_percent`], + strokeColor: theme.palette.warning.main, + gradientStartColor: theme.palette.warning.main, + yDomain: [0, 1], + idx, + })) || []), + ]; +}; + +export const InfraDetailsGraphs = ({ + checks, + range, +}: { + checks: IInfraCheck[]; + range: string; +}) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const chartConfigs = useMemo( + () => getChartConfigs(theme, checks), + [theme, checks] + ); + return ( + + {chartConfigs.map((config) => { + return ( + + + + ); + })} + + ); +}; diff --git a/client/src/pages/infrastructure/InfraDetailsNetGraphs.tsx b/client/src/pages/infrastructure/InfraDetailsNetGraphs.tsx new file mode 100644 index 000000000..6fb325180 --- /dev/null +++ b/client/src/pages/infrastructure/InfraDetailsNetGraphs.tsx @@ -0,0 +1,124 @@ +import Stack from "@mui/material/Stack"; +import MenuItem from "@mui/material/MenuItem"; +import Grid from "@mui/material/Grid"; +import { HistogramInfrastructure } from "@/components/monitors"; +import { Select } from "@/components/inputs"; + +import type { IInfraCheck } from "@/types/check"; +import { useMemo, useState, useEffect } from "react"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +const getChartConfigs = ( + theme: any, + checks: IInfraCheck[], + selectedInterface: string +) => { + const idx = checks[0]?.net.findIndex((i) => i.name === selectedInterface); + if (idx === -1) return []; + return [ + { + type: "Bytes received", + dataKeys: [`net[${idx}].bytes_recv`], + strokeColor: theme.palette.accent.main, + gradientStartColor: theme.palette.accent.main, + yDomain: [0, 1], + idx: null, + }, + { + type: "Bytes sent", + dataKeys: [`net[${idx}].bytes_sent`], + strokeColor: theme.palette.success.main, + gradientStartColor: theme.palette.success.main, + yDomain: [0, 1], + idx: null, + }, + { + type: "Packets received", + dataKeys: [`net[${idx}].packets_recv`], + strokeColor: theme.palette.warning.main, + gradientStartColor: theme.palette.warning.main, + yDomain: [0, 1], + idx: null, + }, + { + type: "Packets sent", + dataKeys: [`net[${idx}].packets_sent`], + strokeColor: theme.palette.error.main, + gradientStartColor: theme.palette.error.main, + yDomain: [0, 1], + idx: null, + }, + ]; +}; + +export const InfraDetailsNetGraphs = ({ + checks, + range, +}: { + checks: IInfraCheck[]; + range: string; +}) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const [selectedInterface, setSelectedInterface] = useState(""); + const chartConfigs = useMemo( + () => getChartConfigs(theme, checks, selectedInterface), + [theme, checks, selectedInterface] + ); + + useEffect(() => { + if (checks[0]) { + const firstNonLoopback = checks[0].net.find( + (iface) => iface.name !== "lo" + ); + setSelectedInterface(firstNonLoopback?.name || ""); + } + }, [checks]); + return ( + + + + + + + + {chartConfigs.map((config) => { + return ( + + + + ); + })} + + + ); +}; diff --git a/client/src/pages/infrastructure/InfraForm.tsx b/client/src/pages/infrastructure/InfraForm.tsx new file mode 100644 index 000000000..c888ec3f9 --- /dev/null +++ b/client/src/pages/infrastructure/InfraForm.tsx @@ -0,0 +1,293 @@ +import Stack from "@mui/material/Stack"; +import { + TextInput, + Button, + RadioWithDescription, + AutoComplete, +} from "@/components/inputs"; +import RadioGroup from "@mui/material/RadioGroup"; + +import { ConfigBox, BasePage } from "@/components/design-elements"; +import { Trash2 } from "lucide-react"; +import { Typography } from "@mui/material"; + +import { useEffect, useState, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { monitorSchemaInfra } from "@/validation/zod"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + useForm, + Controller, + useWatch, + type SubmitHandler, +} from "react-hook-form"; +import { useTheme } from "@mui/material/styles"; +import { useInitForm } from "@/hooks/forms/UseInitInfraForm"; + +type FormValues = z.infer; + +export const InfraForm = ({ + mode = "create", + initialData, + onSubmit, + notificationOptions, + loading, +}: { + mode?: "create" | "configure"; + initialData?: Partial; + onSubmit: SubmitHandler; + notificationOptions: any[]; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + const urlInputRef = useRef(null); + const [selectedProtocol, setSelectedProtocol] = useState("https"); + + const { + handleSubmit, + control, + setValue, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(monitorSchemaInfra) as any, + defaultValues: defaults, + mode: "onChange", + }); + + const url = useWatch({ control, name: "url" }); + + useEffect(() => { + reset(defaults); + if (defaults.url.startsWith("http://")) { + setSelectedProtocol("http"); + } + }, [initialData, reset, defaults]); + + useEffect(() => { + if (!url) { + setValue("url", `${selectedProtocol}://`); + } + + const hasProtocol = /^(http|https):\/\//i.test(url); + if (hasProtocol) { + const newUrl = url.replace( + /^(http|https):\/\//i, + `${selectedProtocol}://` + ); + if (newUrl !== url) setValue("url", newUrl); + } else { + setValue("url", `${selectedProtocol}://${url}`); + } + }, [selectedProtocol, setValue, url]); + + useEffect(() => { + const input = urlInputRef.current; + if (input) { + const len = input.value.length; + input.setSelectionRange(len, len); + } + }, [selectedProtocol]); + + const notificationChannels = useWatch({ + control, + name: "notificationChannels", + }); + + return ( + + + { + setSelectedProtocol(e.target.value); + }} + > + + + + ( + { + const input = e.target; + setTimeout(() => { + input.setSelectionRange( + input.value.length, + input.value.length + ); + }, 0); + }} + type="text" + fieldLabel={t("url")} + fullWidth + error={!!errors.url} + helperText={errors.url ? errors.url.message : ""} + /> + )} + /> + + ( + + )} + /> + ( + + )} + /> + + } + /> + ( + { + const target = e.target as HTMLInputElement; + field.onChange(target.valueAsNumber); + }} + /> + )} + /> + } + /> + + ( + option.name} + value={notificationOptions.filter((o: any) => + (field.value || []).includes(o._id) + )} + onChange={(_, newValue) => { + field.onChange(newValue.map((o: any) => o._id)); + }} + /> + )} + /> + + {notificationChannels.map((notificationId) => { + const option = notificationOptions.find( + (o: any) => o._id === notificationId + ); + if (!option) return null; + return ( + + {option.name} + { + const updated = notificationChannels.filter( + (id) => id !== notificationId + ); + setValue("notificationChannels", updated); + }} + style={{ cursor: "pointer" }} + /> + + ); + })} + + + } + /> + ( + + )} + /> + } + /> + + + + + ); +}; diff --git a/client/src/pages/infrastructure/InfraMonitors.tsx b/client/src/pages/infrastructure/InfraMonitors.tsx new file mode 100644 index 000000000..e12d14765 --- /dev/null +++ b/client/src/pages/infrastructure/InfraMonitors.tsx @@ -0,0 +1,102 @@ +import { + MonitorBasePageWithStates, + UpStatusBox, + DownStatusBox, + PausedStatusBox, + InfoBox, +} from "@/components/design-elements"; +import { HeaderCreate } from "@/components/monitors"; +import Stack from "@mui/material/Stack"; +import { Dialog } from "@/components/inputs"; +import { InfraMonitorsTable } from "./InfraMonitorsTable"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { IMonitor, IMonitorWithStats } from "@/types/monitor"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useState } from "react"; +import { useDelete } from "@/hooks/UseApi"; + +const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; + +const PageSpeedMonitorsPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const { deleteFn } = useDelete(); + const { response, loading, isValidating, error, refetch } = useGet< + ApiResponse + >( + `/monitors?embedChecks=true&type=infrastructure&page=${page}&rowsPerPage=${rowsPerPage}`, + {}, + { + refreshInterval: GLOBAL_REFRESH, + keepPreviousData: true, + dedupingInterval: 0, + }, + { + useTeamIdAsKey: true, + } + ); + const monitors: IMonitor[] = response?.data?.monitors ?? ([] as IMonitor[]); + const count = response?.data?.count || 0; + const upCount = response?.data?.upCount || 0; + const downCount = response?.data?.downCount || 0; + const pausedCount = response?.data?.pausedCount || 0; + + const handleConfirm = async () => { + await deleteFn(`/monitors/${selectedMonitor?._id}`); + setSelectedMonitor(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + + return ( + + + + + + + + + + + + ); +}; + +export default PageSpeedMonitorsPage; diff --git a/client/src/pages/infrastructure/InfraMonitorsTable.tsx b/client/src/pages/infrastructure/InfraMonitorsTable.tsx new file mode 100644 index 000000000..094f25ff9 --- /dev/null +++ b/client/src/pages/infrastructure/InfraMonitorsTable.tsx @@ -0,0 +1,202 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Table, Pagination, StatusLabel } from "@/components/design-elements"; +import type { Header } from "@/components/design-elements/Table"; +import { ActionsMenu } from "@/components/actions-menu"; +import { Gauge } from "@/components/design-elements"; + +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { usePatch } from "@/hooks/UseApi"; + +import type { IMonitor } from "@/types/monitor"; +import type { ActionMenuItem } from "@/components/actions-menu"; + +export const InfraMonitorsTable = ({ + monitors, + refetch, + setSelectedMonitor, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: IMonitor[]; + refetch: Function; + setSelectedMonitor: Function; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const navigate = useNavigate(); + const { + patch, + // loading: isPatching, + // error: postError, + } = usePatch(); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + const getActions = (monitor: IMonitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: "Open site", + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: "Details", + action: () => { + navigate(`${monitor._id}`); + }, + }, + { + id: 3, + label: "Incidents", + action: () => { + navigate(`/incidents/${monitor._id}`); + }, + }, + { + id: 4, + label: "Configure", + action: () => { + navigate(`/infrastructure/${monitor._id}/configure`); + }, + }, + // { + // id: 5, + // label: "Clone", + // action: () => { + // console.log("Open clone"); + // }, + // }, + { + id: 6, + label: monitor.isActive ? "Pause" : "Resume", + action: async () => { + await patch(`/monitors/${monitor._id}/active`); + refetch(); + }, + closeMenu: true, + }, + { + id: 7, + label: Remove, + action: () => { + setSelectedMonitor(monitor); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: t("host"), + render: (row) => { + return row.name; + }, + }, + { + id: "status", + content: t("status"), + render: (row) => { + return ; + }, + }, + { + id: "cpu", + content: t("cpu"), + render: (row) => { + const cpuUsage = + (row.latestChecks?.[0]?.system?.cpu?.usage_percent || 0) * 100; + return ; + }, + }, + { + id: "memory", + content: t("memory"), + render: (row) => { + const memoryUsage = + (row.latestChecks?.[0]?.system?.memory?.usage_percent || 0) * 100; + return ; + }, + }, + { + id: "disk", + content: t("disk"), + render: (row) => { + const totalDiskUsage = row.latestChecks?.[0]?.system?.disk.reduce( + (acc, disk) => acc + disk.usage_percent, + 0 + ); + const diskCount = row.latestChecks?.[0]?.system?.disk.length || 1; + const diskUsage = ((totalDiskUsage || 0) / diskCount) * 100; + return ; + }, + }, + + { + id: "actions", + content: t("actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + let headers = getHeaders(); + + if (isSmall) { + headers = headers.filter((h) => h.id !== "histogram"); + } + return ( + +
{ + navigate(`/infrastructure/${row._id}`); + }} + /> + + + ); +}; diff --git a/client/src/pages/infrastructure/InfraUtils.ts b/client/src/pages/infrastructure/InfraUtils.ts new file mode 100644 index 000000000..20f06c43c --- /dev/null +++ b/client/src/pages/infrastructure/InfraUtils.ts @@ -0,0 +1,49 @@ +import type { IDiskInfo, IHostInfo } from "@/types/check"; + +export const getFrequency = (frequency: number): string => { + if (!frequency) return "N/A"; + const ghz = (frequency / 1000).toFixed(2); + return `${ghz} GHz`; +}; + +export const getCores = (cores: number) => { + if (!cores) return "N/A"; + if (cores === 1) return `${cores} core`; + return `${cores} cores`; +}; + +export const getAvgTemp = (temps: number[]): string => { + if (!temps || temps.length === 0) return "N/A"; + const avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length; + return `${avgTemp.toFixed(2)} °C`; +}; + +export const getGbs = (bytes: number): string => { + if (!bytes) { + return "N/A"; + } + if (bytes === 0) { + return "0 GB"; + } + + const GB = bytes / (1024 * 1024 * 1024); + const MB = bytes / (1024 * 1024); + + if (GB >= 1) { + return GB.toFixed(2) + " GB"; + } else { + return MB.toFixed(2) + " MB"; + } +}; + +export const getDiskTotalGbs = (disk: IDiskInfo[]): string => { + const totalBytes = + disk?.reduce((acc, disk) => acc + (disk.total_bytes || 0), 0) || 0; + return getGbs(totalBytes); +}; + +export const getOsAndPlatform = (hostInfo: IHostInfo): string => { + const os = hostInfo?.pretty_name || hostInfo?.os || "N/A"; + const platform = hostInfo?.platform || "N/A"; + return `${os} (${platform})`; +}; diff --git a/client/src/pages/maintenance/Maintenance.tsx b/client/src/pages/maintenance/Maintenance.tsx new file mode 100644 index 000000000..40571a463 --- /dev/null +++ b/client/src/pages/maintenance/Maintenance.tsx @@ -0,0 +1,157 @@ +import { HeaderCreate } from "@/components/notification-channels/HeaderCreate"; +import { BasePageWithStates, InfoBox } from "@/components/design-elements"; +import { ActionsMenu } from "@/components/actions-menu"; +import { Table } from "@/components/design-elements"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { Dialog } from "@/components/inputs"; + +import { useState } from "react"; +import { usePatch, useGet, useDelete } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import type { Header } from "@/components/design-elements/Table"; +import { useTranslation } from "react-i18next"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { IMaintenance } from "@/types/maintenance"; +import type { ActionMenuItem } from "@/components/actions-menu"; + +const MaintenancePage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const [selectedMaintenance, setSelectedMaintenance] = + useState(null); + const open = Boolean(selectedMaintenance); + + const { patch, loading: isPausing } = usePatch<{}, any>(); + const { response, isValidating, refetch } = useGet>( + "/maintenance", + {}, + {} + ); + const { deleteFn, loading: isDeleting } = useDelete(); + + const maintenance = response?.data || []; + const navigate = useNavigate(); + + const handleConfirm = async () => { + if (!selectedMaintenance) return; + const res = await deleteFn(`/maintenance/${selectedMaintenance._id}`); + if (res) { + setSelectedMaintenance(null); + refetch(); + } + }; + + const handleCancel = () => { + setSelectedMaintenance(null); + }; + + const getActions = (maintenance: IMaintenance): ActionMenuItem[] => { + return [ + { + id: 1, + label: "Configure", + action: () => { + navigate(`/maintenance/${maintenance._id}/configure`); + }, + closeMenu: true, + }, + { + id: 2, + label: maintenance.isActive ? "Disable" : "Enable", + action: async () => { + const res = await patch(`/maintenance/${maintenance._id}/active`, {}); + if (res) { + refetch(); + } + }, + closeMenu: true, + }, + { + id: 7, + label: Remove, + action: async () => { + setSelectedMaintenance(maintenance); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: "Name", + render: (row) => { + return {row?.name}; + }, + }, + { + id: "active", + content: "Active", + render: (row) => { + const active = row.isActive ? "Yes" : "No"; + return {active}; + }, + }, + { + id: "repeat", + content: "Repeat", + render: (row) => { + const active = row.repeat ? row.repeat : "No"; + return {active}; + }, + }, + { + id: "actions", + content: t("actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( + + + +
{ + navigate(`/maintenance/${row._id}/configure`); + }} + /> + + + + ); +}; + +export default MaintenancePage; diff --git a/client/src/pages/maintenance/MaintenanceConfig.tsx b/client/src/pages/maintenance/MaintenanceConfig.tsx new file mode 100644 index 000000000..16dd407a6 --- /dev/null +++ b/client/src/pages/maintenance/MaintenanceConfig.tsx @@ -0,0 +1,38 @@ +import { MaintenanceForm } from "@/pages/maintenance/MaintenanceForm"; + +import { useParams } from "react-router"; +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { usePatch, useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { maintenanceSchema } from "@/validation/zod"; +import type { IMonitor } from "@/types/monitor"; + +type FormValues = z.infer; + +const MaintenanceConfigPage = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const { patch, loading: isPatching } = usePatch(); + const { response, loading } = useGet>(`/maintenance/${id}`); + const { response: monitorsResponse, loading: monitorsLoading } = + useGet>("/monitors"); + const maintenance = response?.data; + const monitors = monitorsResponse?.data || []; + const onSubmit = async (data: FormValues) => { + const res = await patch(`/maintenance/${id}`, data); + if (res) { + navigate(-1); + } + }; + return ( + + ); +}; + +export default MaintenanceConfigPage; diff --git a/client/src/pages/maintenance/MaintenanceCreate.tsx b/client/src/pages/maintenance/MaintenanceCreate.tsx new file mode 100644 index 000000000..f1a88a866 --- /dev/null +++ b/client/src/pages/maintenance/MaintenanceCreate.tsx @@ -0,0 +1,37 @@ +import { MaintenanceForm } from "@/pages/maintenance/MaintenanceForm"; + +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { usePost, useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { maintenanceSchema } from "@/validation/zod"; +import type { IMonitor } from "@/types/monitor"; +import type { IMaintenance } from "@/types/maintenance"; + +type FormValues = z.infer; + +const MaintenanceCreatePage = () => { + const navigate = useNavigate(); + const { post, loading } = usePost(); + const { response, loading: monitorsLoading } = + useGet>("/monitors"); + + const monitors = response?.data || []; + console.log(monitors); + + const onSubmit = async (data: FormValues) => { + const res = await post("/maintenance", data); + if (res) { + navigate(-1); + } + }; + return ( + + ); +}; + +export default MaintenanceCreatePage; diff --git a/client/src/pages/maintenance/MaintenanceForm.tsx b/client/src/pages/maintenance/MaintenanceForm.tsx new file mode 100644 index 000000000..5061a051f --- /dev/null +++ b/client/src/pages/maintenance/MaintenanceForm.tsx @@ -0,0 +1,229 @@ +import Stack from "@mui/material/Stack"; +import { Button, DateTimePicker } from "@/components/inputs"; +import { ConfigBox, BasePage, InfoBox } from "@/components/design-elements"; +import { TextInput, Select, AutoComplete } from "@/components/inputs"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; + +import dayjs from "dayjs"; +import type { Dayjs } from "dayjs"; +import { useTheme } from "@mui/material/styles"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { MaintenanceRepeats } from "@/types/maintenance"; +import { maintenanceSchema } from "@/validation/zod"; +import { useForm, Controller, type SubmitHandler } from "react-hook-form"; +import { useEffect } from "react"; +import { useInitForm } from "@/hooks/forms/UseInitMaintenanceForm"; +import type { IMonitor } from "@/types/monitor"; + +type FormValues = z.infer; + +export const MaintenanceForm = ({ + monitorOptions, + initialData, + onSubmit, + loading, +}: { + monitorOptions: IMonitor[]; + initialData?: Partial; + onSubmit: SubmitHandler; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + + const { + handleSubmit, + control, + reset, + getValues, + formState: { errors }, + } = useForm({ + resolver: zodResolver(maintenanceSchema), + defaultValues: defaults, + mode: "onChange", + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + const onError = (errors: any) => { + console.log(errors); + console.log(getValues()); + }; + + return ( + + + + ( + + )} + /> + ( + + )} + /> + + } + /> + + { + const value = field.value ? dayjs(field.value) : null; + return ( + <> + + field.onChange(val?.toDate()) + } + /> + {errors?.startTime?.message && ( + + {errors.startTime.message} + + )} + + ); + }} + /> + { + const value = field.value ? dayjs(field.value) : null; + return ( + <> + + field.onChange(val?.toDate()) + } + /> + {errors?.endTime?.message && ( + + {errors.endTime.message} + + )} + + ); + }} + /> + + } + /> + { + const selectedMonitors = monitorOptions.filter((o: any) => + (field.value || []).includes(o._id) + ); + const count = selectedMonitors.length; + + return ( + option.name} + value={selectedMonitors} + onChange={(_, newValue) => { + field.onChange(newValue.map((o: any) => o._id)); + }} + renderInput={(params) => ( + 0 ? `${count} selected` : "Type to search" + } + /> + )} + /> + ); + }} + /> + } + /> + + + + + + ); +}; diff --git a/client/src/pages/notification-channels/NotificationChannels.tsx b/client/src/pages/notification-channels/NotificationChannels.tsx new file mode 100644 index 000000000..5544323d3 --- /dev/null +++ b/client/src/pages/notification-channels/NotificationChannels.tsx @@ -0,0 +1,163 @@ +import { HeaderCreate } from "@/components/notification-channels/HeaderCreate"; +import Typography from "@mui/material/Typography"; +import { BasePageWithStates, InfoBox } from "@/components/design-elements"; +import { Table } from "@/components/design-elements"; +import { ActionsMenu } from "@/components/actions-menu"; +import { Dialog } from "@/components/inputs"; + +import { useState } from "react"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router"; +import type { ActionMenuItem } from "@/components/actions-menu"; +import type { Header } from "@/components/design-elements/Table"; +import type { INotificationChannel } from "@/types/notification-channel"; +import { useGet, usePatch, useDelete } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useTranslation } from "react-i18next"; + +const NotificationChannelsPage = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const theme = useTheme(); + const [selectedChannel, setSelectedChannel] = + useState(null); + const open = Boolean(selectedChannel); + + const { response, isValidating, error, refetch } = useGet< + ApiResponse + >("/notification-channels", {}, {}); + + const { patch, loading: pausing } = usePatch<{}, INotificationChannel>(); + const { deleteFn, loading: deleting } = + useDelete>(); + + const notificationChannels = response?.data || []; + + const handleConfirm = async () => { + if (!selectedChannel) return; + const res = await deleteFn(`/notification-channels/${selectedChannel._id}`); + if (res) { + setSelectedChannel(null); + refetch(); + } + }; + + const handleCancel = () => { + setSelectedChannel(null); + }; + + const getActions = (channel: INotificationChannel): ActionMenuItem[] => { + return [ + { + id: 1, + label: "Configure", + action: () => { + navigate(`/notification-channels/${channel._id}/configure`); + }, + closeMenu: true, + }, + { + id: 2, + label: channel.isActive ? "Disable" : "Enable", + action: async () => { + const res = await patch( + `/notification-channels/${channel._id}/active`, + {} + ); + if (res) { + refetch(); + } + }, + closeMenu: true, + }, + { + id: 7, + label: Remove, + action: async () => { + setSelectedChannel(channel); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: "Name", + render: (row) => { + return {row?.name}; + }, + }, + { + id: "active", + content: "Active", + render: (row) => { + const active = row.isActive ? "Yes" : "No"; + return {active}; + }, + }, + { + id: "type", + content: "Type", + render: (row) => { + return ( + {row?.type} + ); + }, + }, + { + id: "actions", + content: t("actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( + + + +
{ + navigate(`/notification-channels/${row._id}/configure`); + }} + /> + + + + ); +}; + +export default NotificationChannelsPage; diff --git a/client/src/pages/notification-channels/NotificationChannelsConfig.tsx b/client/src/pages/notification-channels/NotificationChannelsConfig.tsx new file mode 100644 index 000000000..89b436a2d --- /dev/null +++ b/client/src/pages/notification-channels/NotificationChannelsConfig.tsx @@ -0,0 +1,40 @@ +import { NotificationChannelsForm } from "./NotificationChannelsForm"; + +import { useParams } from "react-router"; +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { usePatch, useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { notificationChannelSchema } from "@/validation/zod"; +import type { INotificationChannel } from "@/types/notification-channel"; + +type FormValues = z.infer; + +const NotificationsChannelConfigPage = () => { + const navigate = useNavigate(); + const { id } = useParams<{ id: string }>(); + const { response, loading } = useGet>( + `/notification-channels/${id}` + ); + const { patch, loading: updating } = usePatch< + FormValues, + INotificationChannel + >(); + const notification = response?.data; + + const onSubmit = async (data: FormValues) => { + const res = await patch(`/notification-channels/${id}`, data); + if (res) { + navigate(-1); + } + }; + return ( + + ); +}; + +export default NotificationsChannelConfigPage; diff --git a/client/src/pages/notification-channels/NotificationChannelsCreate.tsx b/client/src/pages/notification-channels/NotificationChannelsCreate.tsx new file mode 100644 index 000000000..0890f410b --- /dev/null +++ b/client/src/pages/notification-channels/NotificationChannelsCreate.tsx @@ -0,0 +1,24 @@ +import { NotificationChannelsForm } from "./NotificationChannelsForm"; + +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { usePost } from "@/hooks/UseApi"; +import { notificationChannelSchema } from "@/validation/zod"; +import type { INotificationChannel } from "@/types/notification-channel"; + +type FormValues = z.infer; + +const NotificationsChannelCreatePage = () => { + const { post, loading } = usePost(); + const navigate = useNavigate(); + + const onSubmit = async (data: FormValues) => { + const res = await post("/notification-channels", data); + if (res) { + navigate(-1); + } + }; + return ; +}; + +export default NotificationsChannelCreatePage; diff --git a/client/src/pages/notification-channels/NotificationChannelsForm.tsx b/client/src/pages/notification-channels/NotificationChannelsForm.tsx new file mode 100644 index 000000000..b439b54a6 --- /dev/null +++ b/client/src/pages/notification-channels/NotificationChannelsForm.tsx @@ -0,0 +1,233 @@ +import Stack from "@mui/material/Stack"; +import { Button } from "@/components/inputs"; +import { ConfigBox, BasePage } from "@/components/design-elements"; +import { TextInput, Select } from "@/components/inputs"; +import MenuItem from "@mui/material/MenuItem"; + +import { usePost } from "@/hooks/UseApi"; +import { useToast } from "@/hooks/UseToast"; +import { ChannelTypes } from "@/types/notification-channel"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { notificationChannelSchema } from "@/validation/zod"; +import { useForm, Controller, type SubmitHandler } from "react-hook-form"; +import { useEffect } from "react"; +import { useInitForm } from "@/hooks/forms/UseInitNotificationsChannelForm"; +import { Typography } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; + +type FormValues = z.infer; + +export const NotificationChannelsForm = ({ + initialData, + onSubmit, + loading, +}: { + mode?: string; + initialData?: Partial; + onSubmit: SubmitHandler; + loading: boolean; +}) => { + const { t } = useTranslation(); + const { defaults } = useInitForm({ initialData: initialData }); + const { post, loading: isPosting } = usePost(); + const theme = useTheme(); + const { toastSuccess } = useToast(); + + const { + handleSubmit, + control, + reset, + watch, + getValues, + trigger, + formState: { errors }, + } = useForm({ + resolver: zodResolver(notificationChannelSchema), + defaultValues: defaults, + mode: "onChange", + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + const type = watch("type"); + + const onError = (errors: any) => { + console.log(errors); + console.log(getValues()); + }; + + const handleTest = async () => { + const isValid = await trigger(); + if (isValid) { + const res = await post("/notification-channels/test", getValues()); + if (res) { + toastSuccess("Success"); + } + } + }; + + return ( + + ( + + )} + /> + } + /> + { + return ( + + ); + }} + /> + } + /> + { + return ( + + ); + }} + /> + ) : ( + { + const getPlaceholder = () => { + switch (type) { + case "slack": + return "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"; + case "discord": + return "https://discord.com/api/webhooks/YOUR/WEBHOOK/URL"; + case "webhook": + return "https://your-server.com/webhook"; + default: + return "https://example.com/webhook"; + } + }; + + return ( + + ); + }} + /> + ) + } + /> + + + + + + ); +}; diff --git a/client/src/pages/pagespeed/PageSpeedConfig.tsx b/client/src/pages/pagespeed/PageSpeedConfig.tsx new file mode 100644 index 000000000..be958281f --- /dev/null +++ b/client/src/pages/pagespeed/PageSpeedConfig.tsx @@ -0,0 +1,62 @@ +import { monitorSchemaPageSpeed } from "@/validation/zod"; +import { useGet, usePatch } from "@/hooks/UseApi"; +import { PageSpeedForm } from "@/pages/pagespeed/PageSpeedForm"; + +import { useParams } from "react-router"; +import type { ApiResponse } from "@/hooks/UseApi"; +import humanInterval from "human-interval"; +import { z } from "zod"; +import { useNavigate } from "react-router"; +import type { INotificationChannel } from "@/types/notification-channel"; +import type { IMonitor } from "@/types/monitor"; + +const PageSpeedConfigurePage = () => { + type FormValues = z.infer; + type SubmitValues = Omit & { + interval: number | undefined; + }; + + const { id } = useParams(); + const navigate = useNavigate(); + const { response } = useGet>( + "/notification-channels" + ); + const { response: monitorResponse } = useGet>( + `/monitors/${id}` + ); + const monitor = monitorResponse?.data; + const notificationOptions = response?.data ?? []; + + const { patch, loading, error } = usePatch, IMonitor>(); + + const onSubmit = async (data: FormValues) => { + let interval = humanInterval(data.interval); + if (!interval) interval = 180000; + const submitData = { + type: data.type, + name: data.name, + notificationChannels: data.notificationChannels, + interval, + }; + console.log(submitData); + const result = await patch(`/monitors/${id}`, submitData); + if (result) { + navigate(`/pagespeed/${id}`); + } else { + console.error(error); + } + }; + return ( + + ); +}; + +export default PageSpeedConfigurePage; diff --git a/client/src/pages/pagespeed/PageSpeedCreate.tsx b/client/src/pages/pagespeed/PageSpeedCreate.tsx new file mode 100644 index 000000000..5bd6dee7c --- /dev/null +++ b/client/src/pages/pagespeed/PageSpeedCreate.tsx @@ -0,0 +1,43 @@ +import { PageSpeedForm } from "@/pages/pagespeed/PageSpeedForm"; + +import humanInterval from "human-interval"; +import { z } from "zod"; +import { monitorSchema } from "@/validation/zod"; +import { useGet, usePost } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import type { INotificationChannel } from "@/types/notification-channel"; +const UptimeCreatePage = () => { + type FormValues = z.infer; + type SubmitValues = Omit & { + interval: number | undefined; + }; + const navigate = useNavigate(); + const { response } = useGet>( + "/notification-channels" + ); + const { post, loading, error } = usePost(); + + const onSubmit = async (data: FormValues) => { + let interval = humanInterval(data.interval); + if (!interval) interval = 60000; + const submitData = { ...data, interval }; + const result = await post("/monitors", submitData); + if (result) { + navigate("/pagespeed"); + } else { + console.error(error); + } + }; + + const notificationOptions = response?.data ?? []; + return ( + + ); +}; + +export default UptimeCreatePage; diff --git a/client/src/pages/pagespeed/PageSpeedDetails.tsx b/client/src/pages/pagespeed/PageSpeedDetails.tsx new file mode 100644 index 000000000..1bd152750 --- /dev/null +++ b/client/src/pages/pagespeed/PageSpeedDetails.tsx @@ -0,0 +1,97 @@ +import Stack from "@mui/material/Stack"; +import { BasePage, StatBox } from "@/components/design-elements"; +import { HeaderControls, HeaderRange } from "@/components/monitors"; +import { HistogramPageSpeedScores } from "@/components/monitors/HistogramPageSpeedScores"; +import { ChartPageSpeedReport } from "@/components/monitors/ChartPageSpeedReport"; +import { ChartPageSpeedReportLegend } from "@/components/monitors/ChartPageSpeedReportLegend"; + +import { useState } from "react"; +import { useParams } from "react-router"; +import { useTheme } from "@mui/material/styles"; +import { useGet, usePatch } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { IMonitor, IMonitorWithMonitorStats } from "@/types/monitor"; +import { getStatusPalette } from "@/utils/MonitorUtils"; +import prettyMilliseconds from "pretty-ms"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; +const PageSpeedDetailsPage = () => { + const theme = useTheme(); + const { id } = useParams(); + const [range, setRange] = useState("2h"); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + const { patch, loading: isPatching } = usePatch(); + + const { response, isValidating, refetch } = useGet< + ApiResponse + >( + `/monitors/${id}?embedChecks=true&range=${range}`, + {}, + { refreshInterval: GLOBAL_REFRESH, keepPreviousData: true } + ); + + const monitor = response?.data?.monitor; + const stats = response?.data?.stats; + const checks = response?.data?.checks || []; + console.log(checks); + + const streakDuration = stats?.currentStreakStartedAt + ? Date.now() - stats?.currentStreakStartedAt + : 0; + + const lastChecked = stats?.lastCheckTimestamp + ? Date.now() - stats?.lastCheckTimestamp + : -1; + + if (!monitor) { + return null; + } + + const palette = getStatusPalette(monitor?.status); + return ( + + + + + = 0 + ? `${prettyMilliseconds(lastChecked, { + secondsDecimalDigits: 0, + })} ago` + : "N/A" + } + /> + + + + + + + + + + ); +}; + +export default PageSpeedDetailsPage; diff --git a/client/src/pages/pagespeed/PageSpeedForm.tsx b/client/src/pages/pagespeed/PageSpeedForm.tsx new file mode 100644 index 000000000..1b7b768aa --- /dev/null +++ b/client/src/pages/pagespeed/PageSpeedForm.tsx @@ -0,0 +1,279 @@ +import Stack from "@mui/material/Stack"; +import { + TextInput, + Button, + RadioWithDescription, + AutoComplete, +} from "@/components/inputs"; +import RadioGroup from "@mui/material/RadioGroup"; + +import { ConfigBox, BasePage } from "@/components/design-elements"; +import { Trash2 } from "lucide-react"; +import { Typography } from "@mui/material"; + +import { useEffect, useState, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { monitorSchemaPageSpeed } from "@/validation/zod"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + useForm, + Controller, + useWatch, + type SubmitHandler, +} from "react-hook-form"; +import { useTheme } from "@mui/material/styles"; +import { useInitForm } from "@/hooks/forms/UseInitPageSpeedForm"; + +type FormValues = z.infer; + +export const PageSpeedForm = ({ + mode = "create", + initialData, + onSubmit, + notificationOptions, + loading, +}: { + mode?: "create" | "configure"; + initialData?: Partial; + onSubmit: SubmitHandler; + notificationOptions: any[]; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + const urlInputRef = useRef(null); + const [selectedProtocol, setSelectedProtocol] = useState("https"); + + const { + handleSubmit, + control, + setValue, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(monitorSchemaPageSpeed) as any, + defaultValues: defaults, + mode: "onChange", + }); + + const url = useWatch({ control, name: "url" }); + + useEffect(() => { + reset(defaults); + if (defaults.url.startsWith("http://")) { + setSelectedProtocol("http"); + } + }, [initialData, reset, defaults]); + + useEffect(() => { + if (!url) { + setValue("url", `${selectedProtocol}://`); + } + + const hasProtocol = /^(http|https):\/\//i.test(url); + if (hasProtocol) { + const newUrl = url.replace( + /^(http|https):\/\//i, + `${selectedProtocol}://` + ); + if (newUrl !== url) setValue("url", newUrl); + } else { + setValue("url", `${selectedProtocol}://${url}`); + } + }, [selectedProtocol, setValue, url]); + + useEffect(() => { + const input = urlInputRef.current; + if (input) { + const len = input.value.length; + input.setSelectionRange(len, len); + } + }, [selectedProtocol]); + + const notificationChannels = useWatch({ + control, + name: "notificationChannels", + }); + + return ( + + + { + setSelectedProtocol(e.target.value); + }} + > + + + + ( + { + const input = e.target; + setTimeout(() => { + input.setSelectionRange( + input.value.length, + input.value.length + ); + }, 0); + }} + type="text" + fieldLabel={t("url")} + fullWidth + error={!!errors.url} + helperText={errors.url ? errors.url.message : ""} + /> + )} + /> + + ( + + )} + /> + + } + /> + ( + { + const target = e.target as HTMLInputElement; + field.onChange(target.valueAsNumber); + }} + /> + )} + /> + } + /> + + ( + option.name} + value={notificationOptions.filter((o: any) => + (field.value || []).includes(o._id) + )} + onChange={(_, newValue) => { + field.onChange(newValue.map((o: any) => o._id)); + }} + /> + )} + /> + + {notificationChannels.map((notificationId) => { + const option = notificationOptions.find( + (o: any) => o._id === notificationId + ); + if (!option) return null; + return ( + + {option.name} + { + const updated = notificationChannels.filter( + (id) => id !== notificationId + ); + setValue("notificationChannels", updated); + }} + style={{ cursor: "pointer" }} + /> + + ); + })} + + + } + /> + ( + + )} + /> + } + /> + + + + + ); +}; diff --git a/client/src/pages/pagespeed/PageSpeedMonitorTable.tsx b/client/src/pages/pagespeed/PageSpeedMonitorTable.tsx new file mode 100644 index 000000000..728435c77 --- /dev/null +++ b/client/src/pages/pagespeed/PageSpeedMonitorTable.tsx @@ -0,0 +1,186 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Table, Pagination, StatusLabel } from "@/components/design-elements"; +import { HistogramPageSpeed } from "@/components/monitors"; +import type { Header } from "@/components/design-elements/Table"; +import { ActionsMenu } from "@/components/actions-menu"; + +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { usePatch } from "@/hooks/UseApi"; + +import type { IMonitor } from "@/types/monitor"; +import type { ActionMenuItem } from "@/components/actions-menu"; + +export const PageSpeedMonitorTable = ({ + monitors, + refetch, + setSelectedMonitor, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: IMonitor[]; + refetch: Function; + setSelectedMonitor: Function; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const navigate = useNavigate(); + const { + patch, + // loading: isPatching, + // error: postError, + } = usePatch(); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + const getActions = (monitor: IMonitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: "Open site", + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: "Details", + action: () => { + navigate(`${monitor._id}`); + }, + }, + { + id: 3, + label: "Incidents", + action: () => { + navigate(`/incidents/${monitor._id}`); + }, + }, + { + id: 4, + label: "Configure", + action: () => { + navigate(`/pagespeed/${monitor._id}/configure`); + }, + }, + // { + // id: 5, + // label: "Clone", + // action: () => { + // console.log("Open clone"); + // }, + // }, + { + id: 6, + label: monitor.isActive ? "Pause" : "Resume", + action: async () => { + await patch(`/monitors/${monitor._id}/active`); + refetch(); + }, + closeMenu: true, + }, + { + id: 7, + label: Remove, + action: () => { + setSelectedMonitor(monitor); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: t("host"), + render: (row) => { + return row.name; + }, + }, + { + id: "status", + content: t("status"), + render: (row) => { + return ; + }, + }, + { + id: "histogram", + content: "PageSpeed score", + render: (row) => { + return ( + + + + ); + }, + }, + + { + id: "actions", + content: t("actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + let headers = getHeaders(); + + if (isSmall) { + headers = headers.filter((h) => h.id !== "histogram"); + } + return ( + +
{ + navigate(`/pagespeed/${row._id}`); + }} + /> + + + ); +}; diff --git a/client/src/pages/pagespeed/PageSpeedMonitors.tsx b/client/src/pages/pagespeed/PageSpeedMonitors.tsx new file mode 100644 index 000000000..0e3aefd8c --- /dev/null +++ b/client/src/pages/pagespeed/PageSpeedMonitors.tsx @@ -0,0 +1,102 @@ +import { + MonitorBasePageWithStates, + UpStatusBox, + DownStatusBox, + PausedStatusBox, + InfoBox, +} from "@/components/design-elements"; +import { HeaderCreate } from "@/components/monitors"; +import Stack from "@mui/material/Stack"; +import { Dialog } from "@/components/inputs"; +import { PageSpeedMonitorTable } from "./PageSpeedMonitorTable"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { IMonitor, IMonitorWithStats } from "@/types/monitor"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useState } from "react"; +import { useDelete } from "@/hooks/UseApi"; + +const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; + +const PageSpeedMonitorsPage = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const { deleteFn } = useDelete(); + const { response, loading, isValidating, error, refetch } = useGet< + ApiResponse + >( + `/monitors?embedChecks=true&type=pagespeed&page=${page}&rowsPerPage=${rowsPerPage}`, + {}, + { + refreshInterval: GLOBAL_REFRESH, + keepPreviousData: true, + dedupingInterval: 0, + }, + { + useTeamIdAsKey: true, + } + ); + const monitors: IMonitor[] = response?.data?.monitors ?? ([] as IMonitor[]); + const count = response?.data?.count || 0; + const upCount = response?.data?.upCount || 0; + const downCount = response?.data?.downCount || 0; + const pausedCount = response?.data?.pausedCount || 0; + + const handleConfirm = async () => { + await deleteFn(`/monitors/${selectedMonitor?._id}`); + setSelectedMonitor(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + + return ( + + + + + + + + + + + + ); +}; + +export default PageSpeedMonitorsPage; diff --git a/client/src/pages/profile/Profile.tsx b/client/src/pages/profile/Profile.tsx new file mode 100644 index 000000000..944c115ed --- /dev/null +++ b/client/src/pages/profile/Profile.tsx @@ -0,0 +1,48 @@ +import { BasePage, InfoBox } from "@/components/design-elements"; +import { ProfileForm } from "@/pages/profile/ProfileForm"; + +import { useState, useEffect } from "react"; +import type { IUser } from "@/types/user"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useGet, usePatch } from "@/hooks/UseApi"; +import { z } from "zod"; +import { profileSchema } from "@/validation/zod"; +type FormValues = z.infer; + +const Profile = () => { + const [user, setUser] = useState | null>(null); + const { response, loading } = useGet>(`/profile`, {}, {}); + useEffect(() => { + if (response?.data) setUser(response.data); + }, [response]); + + const { patch, loading: isPatching } = usePatch>(); + + const onSubmit = async (data: FormValues) => { + const res = await patch(`/profile`, data); + if (res) { + setUser(res.data); + } + }; + const initialData = { + firstName: user?.firstName || "", + lastName: user?.lastName || "", + }; + + return ( + + + + + ); +}; + +export default Profile; diff --git a/client/src/pages/profile/ProfileForm.tsx b/client/src/pages/profile/ProfileForm.tsx new file mode 100644 index 000000000..c52a84843 --- /dev/null +++ b/client/src/pages/profile/ProfileForm.tsx @@ -0,0 +1,151 @@ +import Stack from "@mui/material/Stack"; +import { TextInput } from "@/components/inputs"; +import { Button } from "@/components/inputs"; + +import { useForm, Controller } from "react-hook-form"; +import type { Resolver, SubmitHandler } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useInitForm } from "@/hooks/forms/UseInitProfileForm"; +import type { IUser } from "@/types/user"; + +import { profileSchema } from "@/validation/zod"; +type FormData = z.infer; + +export const ProfileForm = ({ + user, + initialData, + onSubmit, + loading, +}: { + user: Partial | null; + hasUser?: boolean; + initialData?: Partial; + onSubmit: SubmitHandler; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + const resolver = zodResolver(profileSchema) as unknown as Resolver; + + const { + handleSubmit, + control, + reset, + formState: { errors }, + } = useForm({ + resolver, + defaultValues: { + firstName: "", + lastName: "", + password: undefined, + confirmPassword: undefined, + }, + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + return ( + + {/* Can add control later */} + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + + ); +}; diff --git a/client/src/pages/recovery/Recovery.tsx b/client/src/pages/recovery/Recovery.tsx new file mode 100644 index 000000000..f2ebbe772 --- /dev/null +++ b/client/src/pages/recovery/Recovery.tsx @@ -0,0 +1,27 @@ +import { AuthBasePage } from "@/components/auth"; + +import { RecoveryForm } from "./RecoveryForm"; +import { recoverySchema } from "@/validation/zod"; +import { z } from "zod"; +import { usePost } from "@/hooks/UseApi"; +import { useToast } from "@/hooks/UseToast"; + +type FormData = z.infer; + +const Recovery = () => { + const { post, loading } = usePost(); + const { showToast } = useToast(); + + const onSubmit = async (data: FormData) => { + await post("/recovery", data); + showToast("If the email exists, a recovery link has been sent"); + }; + + return ( + + + + ); +}; + +export default Recovery; diff --git a/client/src/pages/recovery/RecoveryForm.tsx b/client/src/pages/recovery/RecoveryForm.tsx new file mode 100644 index 000000000..15d30d380 --- /dev/null +++ b/client/src/pages/recovery/RecoveryForm.tsx @@ -0,0 +1,89 @@ +import Stack from "@mui/material/Stack"; +import { TextInput } from "@/components/inputs"; +import { Button } from "@/components/inputs"; + +import { useForm, Controller } from "react-hook-form"; +import type { SubmitHandler } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useInitForm } from "@/hooks/forms/UseInitRegisterForm"; + +import { recoverySchema } from "@/validation/zod"; +type FormData = z.infer; + +export const RecoveryForm = ({ + initialData, + onSubmit, + loading, +}: { + initialData?: Partial; + onSubmit: SubmitHandler; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + + const { + handleSubmit, + control, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(recoverySchema), + defaultValues: { + email: "", + }, + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + return ( + + ( + + )} + /> + + + + ); +}; diff --git a/client/src/pages/recovery/Reset.tsx b/client/src/pages/recovery/Reset.tsx new file mode 100644 index 000000000..8ed11c602 --- /dev/null +++ b/client/src/pages/recovery/Reset.tsx @@ -0,0 +1,54 @@ +import { AuthBasePage } from "@/components/auth"; + +import { useParams } from "react-router"; +import { ResetForm } from "@/pages/recovery/ResetForm"; +import { resetSchema } from "@/validation/zod"; +import { z } from "zod"; +import { usePost } from "@/hooks/UseApi"; +import { useToast } from "@/hooks/UseToast"; +import { useNavigate } from "react-router"; +import { useAppDispatch } from "@/hooks/AppHooks"; +import { + setAuthenticated, + setUser, + setSelectedTeamId, + logout, +} from "@/features/authSlice"; + +type FormData = z.infer; + +const Reset = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const { post, loading } = usePost<{ password: string; token: string }>(); + const { toastSuccess } = useToast(); + const { id: token } = useParams<{ id: string }>(); + + const onSubmit = async (data: FormData) => { + const result = await post(`/recovery/reset`, { + password: data.password, + token: token || "", + }); + + const user = result?.data || null; + if (!user) { + dispatch(logout()); + navigate("/login"); + return; + } + + toastSuccess("Password reset successful!"); + dispatch(setAuthenticated(true)); + dispatch(setUser(user)); + dispatch(setSelectedTeamId(user.teams[0]?.id || null)); + navigate("/"); + }; + + return ( + + + + ); +}; + +export default Reset; diff --git a/client/src/pages/recovery/ResetForm.tsx b/client/src/pages/recovery/ResetForm.tsx new file mode 100644 index 000000000..366766c9f --- /dev/null +++ b/client/src/pages/recovery/ResetForm.tsx @@ -0,0 +1,109 @@ +import Stack from "@mui/material/Stack"; +import { TextInput } from "@/components/inputs"; +import { Button } from "@/components/inputs"; + +import { useForm, Controller } from "react-hook-form"; +import type { SubmitHandler } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect } from "react"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useInitForm } from "@/hooks/forms/UseInitResetForm"; + +import { resetSchema } from "@/validation/zod"; +type FormData = z.infer; + +export const ResetForm = ({ + initialData, + onSubmit, + loading, +}: { + hasUser?: boolean; + initialData?: Partial; + onSubmit: SubmitHandler; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + + const { + handleSubmit, + control, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(resetSchema), + defaultValues: { + password: "", + confirmPassword: "", + }, + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + return ( + + ( + + )} + /> + ( + + )} + /> + + + ); +}; diff --git a/client/src/pages/settings/Settings.tsx b/client/src/pages/settings/Settings.tsx new file mode 100644 index 000000000..6e89e28d0 --- /dev/null +++ b/client/src/pages/settings/Settings.tsx @@ -0,0 +1,69 @@ +import Stack from "@mui/material/Stack"; +import { BasePage, ConfigBox } from "@/components/design-elements"; +import { AutoComplete, Select, LanguageSelector } from "@/components/inputs"; +import MenuItem from "@mui/material/MenuItem"; +import Typography from "@mui/material/Typography"; + +import { useTheme } from "@mui/material/styles"; +import { useEffect, useState } from "react"; +import { setTimezone, setMode } from "@/features/uiSlice"; +import { useAppSelector, useAppDispatch } from "@/hooks/AppHooks"; +import timezones from "@/utils/timezones.json"; +import type { ITimeZone } from "@/types/timezone"; +import type { SelectChangeEvent } from "@mui/material/Select"; + +const SettingsPage = () => { + const theme = useTheme(); + const dispatch = useAppDispatch(); + const timezone = useAppSelector((state) => state.ui.timezone); + const mode = useAppSelector((state) => state.ui.mode); + const [selectedTimezone, setSelectedTimezone] = useState( + null + ); + useEffect(() => { + const tz = timezones.find((tz) => tz._id === timezone); + setSelectedTimezone(tz || null); + }, [timezone]); + return ( + + + + Time zone + + option.name} + onChange={(_, timezone: ITimeZone) => { + dispatch(setTimezone(timezone._id)); + }} + /> + + + UI Mode + + + + Language + + + + } + /> + + ); +}; + +export default SettingsPage; diff --git a/client/src/pages/status-page/StatusPageConfig.tsx b/client/src/pages/status-page/StatusPageConfig.tsx new file mode 100644 index 000000000..172ebd6f9 --- /dev/null +++ b/client/src/pages/status-page/StatusPageConfig.tsx @@ -0,0 +1,46 @@ +import { StatusPageForm } from "@/pages/status-page/StatusPageForm"; + +import { useParams } from "react-router"; +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { usePatch, useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { statusPageSchema } from "@/validation/zod"; +import type { IMonitor } from "@/types/monitor"; +import type { IStatusPageWithMonitors } from "@/types/status-page"; + +type FormValues = z.infer; + +const StatusPageConfigPage = () => { + const navigate = useNavigate(); + const { id } = useParams(); + + const { patch, loading } = usePatch(); + const { response, isValidating } = + useGet>("/monitors"); + const { response: statusPageResponse } = useGet>( + `/status-pages/${id}` + ); + + const monitors = response?.data || []; + const initialData = statusPageResponse?.data; + initialData.monitors = + initialData?.monitors?.map((monitor: IMonitor) => monitor?._id) || []; + + const onSubmit = async (data: FormValues) => { + const res = await patch(`/status-pages/${id}`, data); + if (res) { + navigate(-1); + } + }; + return ( + + ); +}; + +export default StatusPageConfigPage; diff --git a/client/src/pages/status-page/StatusPageCreate.tsx b/client/src/pages/status-page/StatusPageCreate.tsx new file mode 100644 index 000000000..28bb9902f --- /dev/null +++ b/client/src/pages/status-page/StatusPageCreate.tsx @@ -0,0 +1,36 @@ +import { StatusPageForm } from "@/pages/status-page/StatusPageForm"; + +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { usePost, useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { statusPageSchema } from "@/validation/zod"; +import type { IMonitor } from "@/types/monitor"; + +type FormValues = z.infer; + +const StatusPageCreatePage = () => { + const navigate = useNavigate(); + + const { post, loading } = usePost(); + const { response, isValidating } = + useGet>("/monitors"); + + const monitors = response?.data || []; + + const onSubmit = async (data: FormValues) => { + const res = await post("/status-pages", data); + if (res) { + navigate(-1); + } + }; + return ( + + ); +}; + +export default StatusPageCreatePage; diff --git a/client/src/pages/status-page/StatusPageDetails.tsx b/client/src/pages/status-page/StatusPageDetails.tsx new file mode 100644 index 000000000..133395fa4 --- /dev/null +++ b/client/src/pages/status-page/StatusPageDetails.tsx @@ -0,0 +1,42 @@ +import Stack from "@mui/material/Stack"; +import { StatusHeader } from "@/components/status-pages/StatusHeader"; +import { StatusPageRow } from "@/components/status-pages/StatusPageRow"; +import { BasePage } from "@/components/design-elements"; + +import { useTheme } from "@mui/material/styles"; +import type { IMonitor } from "@/types/monitor"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useParams } from "react-router"; +import type { IStatusPageWithMonitors } from "@/types/status-page"; +import { NameHeader } from "@/components/status-pages/NameHeader"; +const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; +const StatusPages = () => { + const theme = useTheme(); + const { id } = useParams(); + const { response } = useGet>( + `/status-pages/${id}`, + {}, + { refreshInterval: GLOBAL_REFRESH, keepPreviousData: true } + ); + const statusPage = response?.data; + const monitors: IMonitor[] = statusPage?.monitors || []; + + if (!statusPage) { + return null; + } + + return ( + + + + + {monitors?.map((monitor: IMonitor) => { + return ; + })} + + + ); +}; + +export default StatusPages; diff --git a/client/src/pages/status-page/StatusPageForm.tsx b/client/src/pages/status-page/StatusPageForm.tsx new file mode 100644 index 000000000..f2d58167a --- /dev/null +++ b/client/src/pages/status-page/StatusPageForm.tsx @@ -0,0 +1,213 @@ +import Stack from "@mui/material/Stack"; +import { Button, FormControlLabel } from "@/components/inputs"; +import { ConfigBox, BasePage } from "@/components/design-elements"; +import { TextInput, AutoComplete, Checkbox } from "@/components/inputs"; +import Typography from "@mui/material/Typography"; +import { Trash2 } from "lucide-react"; + +import { useTheme } from "@mui/material/styles"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { statusPageSchema } from "@/validation/zod"; +import { + useForm, + Controller, + useWatch, + type SubmitHandler, +} from "react-hook-form"; +import { useEffect } from "react"; +import { useInitForm } from "@/hooks/forms/UseInitStatusPageForm"; +import type { IMonitor } from "@/types/monitor"; + +type FormValues = z.infer; + +export const StatusPageForm = ({ + initialData, + monitorOptions, + onSubmit, + loading, +}: { + mode?: string; + monitorOptions: IMonitor[]; + initialData?: Partial; + onSubmit: SubmitHandler; + loading: boolean; +}) => { + const { t } = useTranslation(); + const { defaults } = useInitForm({ initialData: initialData }); + const theme = useTheme(); + + const { + handleSubmit, + control, + reset, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(statusPageSchema), + defaultValues: defaults, + mode: "onChange", + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + const onError = (errors: any) => { + console.log(errors); + }; + + const monitors = useWatch({ + control, + name: "monitors", + }); + + return ( + + + ( + + )} + /> + ( + + )} + /> + + } + /> + ( + + )} + /> + } + /> + ( + field.onChange(e.target.checked)} + /> + } + /> + )} + /> + } + /> + + ( + option.name} + value={monitorOptions.filter((o: any) => + (field.value || []).includes(o._id) + )} + onChange={(_, newValue) => { + field.onChange(newValue.map((o: any) => o._id)); + }} + /> + )} + /> + + {monitors.map((monitorId) => { + const option = monitorOptions.find((o) => o._id === monitorId); + if (!option) return null; + return ( + + {option.name} + { + const updated = monitors.filter( + (id) => id !== monitorId + ); + setValue("monitors", updated); + }} + style={{ cursor: "pointer" }} + /> + + ); + })} + + + } + /> + + + + + ); +}; diff --git a/client/src/pages/status-page/StatusPagePublic.tsx b/client/src/pages/status-page/StatusPagePublic.tsx new file mode 100644 index 000000000..7d3f06c7e --- /dev/null +++ b/client/src/pages/status-page/StatusPagePublic.tsx @@ -0,0 +1,42 @@ +import Stack from "@mui/material/Stack"; +import { StatusHeader } from "@/components/status-pages/StatusHeader"; +import { StatusPageRow } from "@/components/status-pages/StatusPageRow"; +import { BasePage } from "@/components/design-elements"; + +import { useTheme } from "@mui/material/styles"; +import type { IMonitor } from "@/types/monitor"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useParams } from "react-router"; +import type { IStatusPageWithMonitors } from "@/types/status-page"; +import { NameHeader } from "@/components/status-pages/NameHeader"; +const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; +const StatusPages = () => { + const theme = useTheme(); + const { url } = useParams(); + const { response } = useGet>( + `/status-pages/public/${url}`, + {}, + { refreshInterval: GLOBAL_REFRESH, keepPreviousData: true } + ); + const statusPage = response?.data; + const monitors: IMonitor[] = statusPage?.monitors || []; + + if (!statusPage) { + return null; + } + + return ( + + + + + {monitors?.map((monitor: IMonitor) => { + return ; + })} + + + ); +}; + +export default StatusPages; diff --git a/client/src/Components/v1/Avatar/index.css b/client/src/pages/status-page/StatusPageTable.tsx similarity index 100% rename from client/src/Components/v1/Avatar/index.css rename to client/src/pages/status-page/StatusPageTable.tsx diff --git a/client/src/pages/status-page/StatusPages.tsx b/client/src/pages/status-page/StatusPages.tsx new file mode 100644 index 000000000..03fc5ed5b --- /dev/null +++ b/client/src/pages/status-page/StatusPages.tsx @@ -0,0 +1,147 @@ +import { HeaderCreate } from "@/components/notification-channels/HeaderCreate"; +import Typography from "@mui/material/Typography"; +import { BasePageWithStates, InfoBox } from "@/components/design-elements"; +import { Table } from "@/components/design-elements"; +import { ActionsMenu } from "@/components/actions-menu"; +import { Dialog } from "@/components/inputs"; + +import { useState } from "react"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router"; +import type { ActionMenuItem } from "@/components/actions-menu"; +import type { Header } from "@/components/design-elements/Table"; +import type { IStatusPage } from "@/types/status-page"; +import { useGet, useDelete } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useTranslation } from "react-i18next"; + +const StatusPages = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const theme = useTheme(); + const [selectedStatusPage, setSelectedStatusPage] = + useState(null); + const open = Boolean(selectedStatusPage); + + const { response, isValidating, error, refetch } = useGet< + ApiResponse + >("/status-pages", {}, {}); + + const statusPages = response?.data || []; + + const { deleteFn, loading: deleting } = useDelete(); + + const getActions = (statusPage: IStatusPage): ActionMenuItem[] => { + return [ + { + id: 1, + label: "Configure", + action: () => { + navigate(`/status-pages/${statusPage._id}/configure`); + }, + closeMenu: true, + }, + { + id: 7, + label: Remove, + action: async () => { + setSelectedStatusPage(statusPage); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: "Name", + render: (row) => { + return {row?.name}; + }, + }, + { + id: "published", + content: "Published", + render: (row) => { + const published = row.isPublished ? "Yes" : "No"; + return {published}; + }, + }, + { + id: "url", + content: "Public URL", + render: (row) => { + return ( + {row?.url} + ); + }, + }, + { + id: "actions", + content: t("actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + const handleConfirm = async () => { + if (!selectedStatusPage) return; + const res = await deleteFn(`/status-pages/${selectedStatusPage._id}`); + if (res) { + setSelectedStatusPage(null); + refetch(); + } + }; + + const handleCancel = () => { + setSelectedStatusPage(null); + }; + + return ( + + + +
{ + navigate(`/status-pages/${row._id}`); + }} + /> + + + + ); +}; + +export default StatusPages; diff --git a/client/src/pages/teams/TeamDetails.tsx b/client/src/pages/teams/TeamDetails.tsx new file mode 100644 index 000000000..24ca96ab9 --- /dev/null +++ b/client/src/pages/teams/TeamDetails.tsx @@ -0,0 +1,95 @@ +import { BasePage } from "@/components/design-elements"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import { Table } from "@/components/design-elements"; +import { HeaderCreate } from "@/components/teams/HeaderCreate"; +import type { Header } from "@/components/design-elements/Table"; + +import { useNavigate } from "react-router"; +import { useTheme } from "@mui/material/styles"; +import { useParams } from "react-router"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { ITeamMember } from "@/types/team-member"; + +const TeamDetailsPage = () => { + const theme = useTheme(); + const navigate = useNavigate(); + const { id } = useParams(); + const { response } = useGet>(`/teams/${id}`); + const { response: teamMembersResponse } = useGet>( + `/team-members?teamId=${id}` + ); + const team = response?.data; + + // Get all team members + const teamMembers = teamMembersResponse?.data; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: "Name", + render: (row) => { + return row?.userId?.firstName + " " + row?.userId?.lastName; + }, + }, + + { + id: "role", + content: "Role", + render: (row) => { + return row?.roleId?.name; + }, + }, + { + id: "permissions", + content: "Permissions", + render: (row) => { + return ( + + {row?.roleId?.permissions.map((permission) => ( + {permission} + ))} + + ); + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( + + + + {"Name:"} + {team?.name} + + + {"Description:"} + {team?.description} + + + + + Team Members + +
{ + navigate(`/teams/${id}/member/${row._id}/configure`); + }} + /> + + ); +}; + +export default TeamDetailsPage; diff --git a/client/src/pages/teams/TeamMemberConfig.tsx b/client/src/pages/teams/TeamMemberConfig.tsx new file mode 100644 index 000000000..82b231f0c --- /dev/null +++ b/client/src/pages/teams/TeamMemberConfig.tsx @@ -0,0 +1,84 @@ +import { TeamMemberForm } from "./TeamMemberForm"; +import { Button } from "@/components/inputs"; +import { Dialog } from "@/components/inputs"; + +import { useState } from "react"; +import { useGet, usePatch } from "@/hooks/UseApi"; +import { useParams } from "react-router"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import { useDelete } from "@/hooks/UseApi"; +import { useTranslation } from "react-i18next"; +import { mutate } from "swr"; + +const TeamMemberConfig = () => { + const [dialogOpen, setDialogOpen] = useState(false); + const { deleteFn, loading: deleting } = useDelete(); + const navigate = useNavigate(); + const { t } = useTranslation(); + const { id, memberId } = useParams(); + const { patch, loading } = usePatch(); + const { response } = useGet>("/team-members"); + const { response: rolesResponse } = + useGet>("/roles?type=team"); + const teamMembers = response?.data || []; + const roles = rolesResponse?.data || []; + + const user = teamMembers?.find((tm: any) => tm._id === memberId); + + const onSubmit = async (data: any) => { + const roleId = data.roleId; + const res = await patch(`/team-members/${memberId}`, { roleId }); + if (res) { + navigate(-1); + } + }; + + const handleConfirm = async () => { + await deleteFn(`/team-members/${id}`); + mutate("/teams/joined"); + navigate(-1); + setDialogOpen(false); + }; + + const handleCancel = () => { + setDialogOpen(false); + }; + + return ( + <> + setDialogOpen(true)} + disabled={deleting} + > + {t("delete")} + + } + breadcrumbOverride={["teams", id || "", "configure team member"]} + /> + + + ); +}; + +export default TeamMemberConfig; diff --git a/client/src/pages/teams/TeamMemberCreate.tsx b/client/src/pages/teams/TeamMemberCreate.tsx new file mode 100644 index 000000000..3c85d730d --- /dev/null +++ b/client/src/pages/teams/TeamMemberCreate.tsx @@ -0,0 +1,42 @@ +import { TeamMemberForm } from "./TeamMemberForm"; + +import { useParams } from "react-router"; +import { useGet, usePost } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import { mutate } from "swr"; +const TeamMemberCreate = () => { + const navigate = useNavigate(); + const { id } = useParams(); + const { response } = useGet>("/team-members?type=org"); + const { response: rolesResponse } = + useGet>("/roles?type=team"); + const { post } = usePost>(); + const teamMembers = response?.data || []; + const roles = rolesResponse?.data || []; + + const onSubmit = async (data: any) => { + const res = await post("/team-members", { ...data }); + if (res) { + mutate("/teams/joined"); + navigate(-1); + } + }; + + return ( + + ); +}; + +export default TeamMemberCreate; diff --git a/client/src/pages/teams/TeamMemberForm.tsx b/client/src/pages/teams/TeamMemberForm.tsx new file mode 100644 index 000000000..b498d8b0b --- /dev/null +++ b/client/src/pages/teams/TeamMemberForm.tsx @@ -0,0 +1,148 @@ +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import { Button } from "@/components/inputs"; +import { ConfigBox, BasePage } from "@/components/design-elements"; +import { Select } from "@/components/inputs"; +import MenuItem from "@mui/material/MenuItem"; + +import { useTheme } from "@mui/material/styles"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { teamMemberSchema } from "@/validation/zod"; +import { useForm, Controller, type SubmitHandler } from "react-hook-form"; +import { useEffect } from "react"; +import { useInitForm } from "@/hooks/forms/UseInitTeamMemberForm"; + +type FormValues = z.infer; + +export const TeamMemberForm = ({ + mode = "create", + initialData, + teamMembers = [], + roles = [], + onSubmit, + loading, + deleteButton, + breadcrumbOverride, +}: { + mode?: string; + initialData?: Partial; + teamMembers: any[]; + roles: any[]; + onSubmit: SubmitHandler; + loading: boolean; + deleteButton?: React.ReactNode; + breadcrumbOverride?: string[]; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + + const { + handleSubmit, + control, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(teamMemberSchema) as any, + defaultValues: defaults, + mode: "onChange", + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + return ( + + { + return ( + + + + ); + }} + /> + } + /> + { + const selectedRole = roles.find( + (role) => role._id === field.value + ); + return ( + + + + + Included permissions: + + {selectedRole?.permissions?.map((perm: string) => ( + - {perm} + ))} + + + ); + }} + /> + } + /> + + + {deleteButton} + + + ); +}; diff --git a/client/src/pages/teams/Teams.tsx b/client/src/pages/teams/Teams.tsx new file mode 100644 index 000000000..80c75afcd --- /dev/null +++ b/client/src/pages/teams/Teams.tsx @@ -0,0 +1,115 @@ +import { BasePage, InfoBox } from "@/components/design-elements"; +import { Table } from "@/components/design-elements"; +import { HeaderCreate } from "@/components/teams/HeaderCreate"; +import { ActionsMenu } from "@/components/actions-menu"; +import { Dialog } from "@/components/inputs"; + +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import type { ActionMenuItem } from "@/components/actions-menu"; +import type { Header } from "@/components/design-elements/Table"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useGet, useDelete } from "@/hooks/UseApi"; +import type { ITeam } from "@/types/team"; +import { useState } from "react"; +import { mutate } from "swr"; + +const TeamsPage = () => { + const navigate = useNavigate(); + const [selectedTeam, setSelectedTeam] = useState(null); + const isDialogOpen = Boolean(selectedTeam); + const { t } = useTranslation(); + const { response, loading, refetch } = useGet>("/teams"); + const { deleteFn } = useDelete(); + const teams = response?.data || []; + + const handleConfirm = async () => { + await deleteFn(`/teams/${selectedTeam?._id}`); + setSelectedTeam(null); + refetch(); + mutate("/teams/joined"); + }; + const handleCancel = () => { + setSelectedTeam(null); + }; + + const getActions = (team: ITeam): ActionMenuItem[] => { + return [ + { + id: 1, + label: "Configure", + action: () => { + navigate(`/teams/${team._id}/configure`); + }, + closeMenu: true, + }, + { + id: 2, + label: "Delete", + action: () => { + setSelectedTeam(team); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: "Team name", + render: (row) => { + return row.name; + }, + }, + { + id: "description", + content: "Team description", + render: (row) => { + return row.description; + }, + }, + { + id: "actions", + content: "Team description", + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( + + + +
{ + navigate(`/teams/${row._id}`); + }} + /> + + + ); +}; + +export default TeamsPage; diff --git a/client/src/pages/teams/TeamsConfig.tsx b/client/src/pages/teams/TeamsConfig.tsx new file mode 100644 index 000000000..2aac5e783 --- /dev/null +++ b/client/src/pages/teams/TeamsConfig.tsx @@ -0,0 +1,40 @@ +import { TeamsForm } from "@/pages/teams/TeamsForm"; + +import { useNavigate, useParams } from "react-router"; +import { z } from "zod"; +import { teamSchema } from "@/validation/zod"; +import { useGet, usePatch } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; + +const TeamsConfigPage = () => { + type FormValues = Omit, "roleId">; + const navigate = useNavigate(); + const { response } = useGet>("/roles?type=team"); + const { patch, loading } = usePatch>(); + + const { id } = useParams(); + const { response: teamResponse } = useGet>(`/teams/${id}`); + const team = teamResponse?.data || null; + const roles = response?.data || []; + + const onSubmit = async (data: FormValues) => { + const toSubmit = { + name: data.name, + description: data.description, + }; + await patch(`/teams/${id}`, toSubmit); + navigate("/teams"); + }; + + return ( + + ); +}; + +export default TeamsConfigPage; diff --git a/client/src/pages/teams/TeamsCreate.tsx b/client/src/pages/teams/TeamsCreate.tsx new file mode 100644 index 000000000..89cc72403 --- /dev/null +++ b/client/src/pages/teams/TeamsCreate.tsx @@ -0,0 +1,31 @@ +import { TeamsForm } from "@/pages/teams/TeamsForm"; + +import { useNavigate } from "react-router"; +import { z } from "zod"; +import { teamSchema } from "@/validation/zod"; +import { useGet, usePost } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { mutate } from "swr"; + +const TeamsCreatePage = () => { + type FormValues = z.infer; + const { response } = useGet>("/roles?type=team"); + const { post, loading } = usePost(); + const navigate = useNavigate(); + + const onSubmit = async (data: FormValues) => { + await post("/teams", data); + mutate("/teams/joined"); + navigate("/teams"); + }; + + return ( + + ); +}; + +export default TeamsCreatePage; diff --git a/client/src/pages/teams/TeamsForm.tsx b/client/src/pages/teams/TeamsForm.tsx new file mode 100644 index 000000000..09c70b7e0 --- /dev/null +++ b/client/src/pages/teams/TeamsForm.tsx @@ -0,0 +1,154 @@ +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import { Button } from "@/components/inputs"; +import { ConfigBox, BasePage } from "@/components/design-elements"; +import { TextInput, Select } from "@/components/inputs"; +import MenuItem from "@mui/material/MenuItem"; + +import { useTheme } from "@mui/material/styles"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { teamSchema } from "@/validation/zod"; +import { useForm, Controller, type SubmitHandler } from "react-hook-form"; +import { useEffect } from "react"; +import { useInitForm } from "@/hooks/forms/UseInitTeamsForm"; + +type FormValues = z.infer; + +export const TeamsForm = ({ + mode = "create", + initialData, + roles = [], + onSubmit, + loading, +}: { + mode?: string; + initialData?: Partial; + roles: any[]; + onSubmit: SubmitHandler; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + + const { + handleSubmit, + control, + setValue, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(teamSchema) as any, + defaultValues: defaults, + mode: "onChange", + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + useEffect(() => { + setValue("roleId", roles[0]?._id || ""); + }, [roles, setValue]); + + return ( + + + ( + + )} + /> + ( + + )} + /> + + } + /> + {mode === "create" && ( + { + const selectedRole = roles.find( + (role) => role._id === field.value + ); + return ( + + + + + Included permissions: + + {selectedRole?.permissions?.map((perm: string) => ( + - {perm} + ))} + + + ); + }} + /> + } + /> + )} + + + + + ); +}; diff --git a/client/src/pages/uptime/CheckTable.tsx b/client/src/pages/uptime/CheckTable.tsx new file mode 100644 index 000000000..87eebc774 --- /dev/null +++ b/client/src/pages/uptime/CheckTable.tsx @@ -0,0 +1,94 @@ +import { Table, Pagination, StatusLabel } from "@/components/design-elements"; +import Box from "@mui/material/Box"; + +import type { Header } from "@/components/design-elements/Table"; +import type { ICheck, IChecksWithCount } from "@/types/check"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { MonitorStatus } from "@/types/monitor"; + +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useGet } from "@/hooks/UseApi"; +import { formatDateWithTz } from "@/utils/TimeUtils"; +import { useAppSelector } from "@/hooks/AppHooks"; + +const getHeaders = (t: Function, uiTimezone: string) => { + const headers: Header[] = [ + { + id: "status", + content: t("status"), + render: (row) => { + return ; + }, + }, + { + id: "date", + content: t("date&Time"), + render: (row) => { + return formatDateWithTz( + row.createdAt, + "ddd, MMMM D, YYYY, HH:mm A", + uiTimezone + ); + }, + }, + { + id: "statusCode", + content: t("statusCode"), + render: (row) => { + return row.httpStatusCode || "N/A"; + }, + }, + ]; + return headers; +}; + +export const CheckTable = ({ monitorId }: { monitorId: string }) => { + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const { t } = useTranslation(); + const uiTimezone = useAppSelector((state: any) => state.ui.timezone); + const headers = getHeaders(t, uiTimezone); + + const { response, error } = useGet>( + `/monitors/${monitorId}/checks?page=${page}&rowsPerPage=${rowsPerPage}`, + {}, + { keepPreviousData: true } + ); + + const checks = response?.data?.checks || []; + const count = response?.data?.count || 0; + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + if (error) { + console.error(error); + } + + return ( + +
+ + + ); +}; diff --git a/client/src/pages/uptime/MonitorTable.tsx b/client/src/pages/uptime/MonitorTable.tsx new file mode 100644 index 000000000..1d5368ee2 --- /dev/null +++ b/client/src/pages/uptime/MonitorTable.tsx @@ -0,0 +1,184 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Table, Pagination, StatusLabel } from "@/components/design-elements"; +import { HistogramResponseTime } from "@/components/monitors"; +import type { Header } from "@/components/design-elements/Table"; +import { ActionsMenu } from "@/components/actions-menu"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { usePatch } from "@/hooks/UseApi"; + +import type { IMonitor } from "@/types/monitor"; +import type { ActionMenuItem } from "@/components/actions-menu"; + +export const MonitorTable = ({ + monitors, + refetch, + setSelectedMonitor, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: IMonitor[]; + refetch: Function; + setSelectedMonitor: Function; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + const { + patch, + // loading: isPatching, + // error: postError, + } = usePatch(); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + const getActions = (monitor: IMonitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: "Open site", + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: "Details", + action: () => { + navigate(`${monitor._id}`); + }, + }, + { + id: 3, + label: "Incidents", + action: () => { + navigate(`/incidents/${monitor._id}`); + }, + }, + { + id: 4, + label: "Configure", + action: () => { + navigate(`/uptime/${monitor._id}/configure`); + }, + }, + // { + // id: 5, + // label: "Clone", + // action: () => { + // console.log("Open clone"); + // }, + // }, + { + id: 6, + label: monitor.isActive ? "Pause" : "Resume", + action: async () => { + await patch(`/monitors/${monitor._id}/active`); + refetch(); + }, + closeMenu: true, + }, + { + id: 7, + label: Remove, + action: () => { + setSelectedMonitor(monitor); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const headers: Header[] = [ + { + id: "name", + content: t("host"), + render: (row) => { + return row.name; + }, + }, + { + id: "status", + content: t("status"), + render: (row) => { + return ; + }, + }, + { + id: "histogram", + content: t("responseTime"), + render: (row) => { + return ( + + + + ); + }, + }, + { + id: "type", + content: t("type"), + render: (row) => { + return row.type; + }, + }, + { + id: "actions", + content: t("actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(); + + return ( + +
{ + navigate(`/uptime/${row._id}`); + }} + /> + + + ); +}; diff --git a/client/src/pages/uptime/UptimeConfigure.tsx b/client/src/pages/uptime/UptimeConfigure.tsx new file mode 100644 index 000000000..c738ab8e0 --- /dev/null +++ b/client/src/pages/uptime/UptimeConfigure.tsx @@ -0,0 +1,62 @@ +import { monitorSchema } from "@/validation/zod"; +import { useGet, usePatch } from "@/hooks/UseApi"; +import { UptimeForm } from "@/pages/uptime/UptimeForm"; + +import { useParams } from "react-router"; +import type { ApiResponse } from "@/hooks/UseApi"; +import humanInterval from "human-interval"; +import { z } from "zod"; +import { useNavigate } from "react-router"; +import type { INotificationChannel } from "@/types/notification-channel"; +import type { IMonitor } from "@/types/monitor"; + +const UptimeConfigurePage = () => { + type FormValues = z.infer; + type SubmitValues = Omit & { + interval: number | undefined; + }; + + const { id } = useParams(); + const navigate = useNavigate(); + const { response } = useGet>( + "/notification-channels" + ); + const { response: monitorResponse } = useGet>( + `/monitors/${id}` + ); + const monitor = monitorResponse?.data || {}; + const notificationOptions = response?.data ?? []; + + const { patch, loading, error } = usePatch, IMonitor>(); + + const onSubmit = async (data: FormValues) => { + let interval = humanInterval(data.interval); + if (!interval) interval = 60000; + const submitData = { + type: data.type, + name: data.name, + n: data.n, + notificationChannels: data.notificationChannels, + interval, + }; + const result = await patch(`/monitors/${id}`, submitData); + if (result) { + navigate(`/uptime/${id}`); + } else { + console.error(error); + } + }; + return ( + + ); +}; + +export default UptimeConfigurePage; diff --git a/client/src/pages/uptime/UptimeCreate.tsx b/client/src/pages/uptime/UptimeCreate.tsx new file mode 100644 index 000000000..eb3941e15 --- /dev/null +++ b/client/src/pages/uptime/UptimeCreate.tsx @@ -0,0 +1,43 @@ +import { UptimeForm } from "./UptimeForm"; + +import humanInterval from "human-interval"; +import { z } from "zod"; +import { monitorSchema } from "@/validation/zod"; +import { useGet, usePost } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import { useNavigate } from "react-router"; +import type { INotificationChannel } from "@/types/notification-channel"; +const UptimeCreatePage = () => { + type FormValues = z.infer; + type SubmitValues = Omit & { + interval: number | undefined; + }; + const navigate = useNavigate(); + const { response } = useGet>( + "/notification-channels" + ); + const { post, loading, error } = usePost(); + + const onSubmit = async (data: FormValues) => { + let interval = humanInterval(data.interval); + if (!interval) interval = 60000; + const submitData = { ...data, interval }; + const result = await post("/monitors", submitData); + if (result) { + navigate("/uptime"); + } else { + console.error(error); + } + }; + + const notificationOptions = response?.data ?? []; + return ( + + ); +}; + +export default UptimeCreatePage; diff --git a/client/src/pages/uptime/UptimeDetails.tsx b/client/src/pages/uptime/UptimeDetails.tsx new file mode 100644 index 000000000..79d6a9b70 --- /dev/null +++ b/client/src/pages/uptime/UptimeDetails.tsx @@ -0,0 +1,162 @@ +import { BasePage, StatBox } from "@/components/design-elements"; +import { + HeaderControls, + HeaderRange, + HistogramStatus, + ChartAvgResponse, + ChartResponseTime, +} from "@/components/monitors"; +import Stack from "@mui/material/Stack"; +import { CheckTable } from "@/pages/uptime/CheckTable"; + +import type { IMonitor, IMonitorWithMonitorStats } from "@/types/monitor"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; +import { useParams } from "react-router"; +import { useGet, usePatch, type ApiResponse } from "@/hooks/UseApi"; +import { useState } from "react"; +import { getStatusPalette } from "@/utils/MonitorUtils"; +import prettyMilliseconds from "pretty-ms"; +const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; + +const UptimeDetailsPage = () => { + const { id } = useParams(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + // Local state + const [range, setRange] = useState("2h"); + + const { response, isValidating, error, refetch } = useGet< + ApiResponse + >( + `/monitors/${id}?embedChecks=true&range=${range}`, + + {}, + { refreshInterval: GLOBAL_REFRESH, keepPreviousData: true } + ); + + const { + response: upResponse, + isValidating: upIsValidating, + error: upError, + } = useGet>( + `/monitors/${id}?embedChecks=true&range=${range}&status=up`, + {}, + { keepPreviousData: true } + ); + + const { + response: downResponse, + error: downError, + isValidating: downIsValidating, + } = useGet>( + `/monitors/${id}?embedChecks=true&range=${range}&status=down`, + {}, + { keepPreviousData: true } + ); + + const { + patch, + loading: isPatching, + error: postError, + } = usePatch(); + + const monitor = response?.data?.monitor; + + if (!monitor) { + return null; + } + + const stats = response?.data?.stats || null; + const avgResponseTime = stats?.avgResponseTime || 0; + const maxResponseTime = stats?.maxResponseTime || 0; + + const streakDuration = stats?.currentStreakStartedAt + ? Date.now() - stats?.currentStreakStartedAt + : 0; + + const lastChecked = stats?.lastCheckTimestamp + ? Date.now() - stats?.lastCheckTimestamp + : -1; + + const checks = response?.data?.checks || []; + const upChecks = upResponse?.data?.checks + ? [...upResponse.data.checks].reverse() + : []; + const downChecks = downResponse?.data?.checks + ? [...downResponse.data.checks].reverse() + : []; + + const palette = getStatusPalette(monitor?.status); + + if (error || upError || downError || postError) { + console.error("Error fetching monitor data:", { + error, + upError, + downError, + postError, + }); + } + + return ( + + + + + = 0 + ? `${prettyMilliseconds(lastChecked, { + secondsDecimalDigits: 0, + })} ago` + : "N/A" + } + /> + + + + + + + + + + + + ); +}; + +export default UptimeDetailsPage; diff --git a/client/src/pages/uptime/UptimeForm.tsx b/client/src/pages/uptime/UptimeForm.tsx new file mode 100644 index 000000000..88a96b599 --- /dev/null +++ b/client/src/pages/uptime/UptimeForm.tsx @@ -0,0 +1,306 @@ +import Stack from "@mui/material/Stack"; +import { + TextInput, + Button, + AutoComplete, + RadioWithDescription, +} from "@/components/inputs"; +import { ConfigBox, BasePage } from "@/components/design-elements"; +import RadioGroup from "@mui/material/RadioGroup"; +import FormControl from "@mui/material/FormControl"; +import { Trash2 } from "lucide-react"; +import { Typography } from "@mui/material"; + +import { useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { monitorSchema } from "@/validation/zod"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + useForm, + Controller, + useWatch, + type SubmitHandler, +} from "react-hook-form"; +import { useTheme } from "@mui/material/styles"; +import { useInitForm } from "@/hooks/forms/UseInitMonitorFrom"; + +type FormValues = z.infer; + +export const UptimeForm = ({ + mode = "create", + initialData, + onSubmit, + notificationOptions, + loading, +}: { + mode?: "create" | "configure"; + initialData?: Partial; + onSubmit: SubmitHandler; + notificationOptions: any[]; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + const urlInputRef = useRef(null); + + const { + handleSubmit, + control, + setValue, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(monitorSchema) as any, + defaultValues: defaults, + mode: "onChange", + }); + + useEffect(() => { + reset(defaults); + }, [initialData, reset, defaults]); + + const selectedType = useWatch({ + control, + name: "type", + }); + const url = useWatch({ control, name: "url" }); + + const notificationChannels = useWatch({ + control, + name: "notificationChannels", + }); + + useEffect(() => { + if (!selectedType) return; + + if (selectedType !== "http" && selectedType !== "https") { + setValue("url", ""); + return; + } + + if (selectedType !== "http" && selectedType !== "https") return; + + if (!url) { + setValue("url", `${selectedType}://`); + return; + } + + const hasProtocol = /^(http|https):\/\//i.test(url); + + if (hasProtocol) { + const newUrl = url.replace(/^(http|https):\/\//i, `${selectedType}://`); + if (newUrl !== url) setValue("url", newUrl); + } else { + setValue("url", `${selectedType}://${url}`); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedType, setValue]); + + useEffect(() => { + const input = urlInputRef.current; + if (input) { + const len = input.value.length; + input.setSelectionRange(len, len); + } + }, [selectedType]); + + return ( + + {mode === "create" && ( + ( + + + + + + + + )} + /> + } + /> + )} + + ( + { + const input = e.target; + setTimeout(() => { + input.setSelectionRange( + input.value.length, + input.value.length + ); + }, 0); + }} + type="text" + fieldLabel={t("url")} + fullWidth + error={!!errors.url} + helperText={errors.url ? errors.url.message : ""} + /> + )} + /> + ( + + )} + /> + + } + /> + ( + { + const target = e.target as HTMLInputElement; + field.onChange(target.valueAsNumber); + }} + /> + )} + /> + } + /> + + ( + option.name} + value={notificationOptions.filter((o: any) => + (field.value || []).includes(o._id) + )} + onChange={(_, newValue) => { + field.onChange(newValue.map((o: any) => o._id)); + }} + /> + )} + /> + + {notificationChannels.map((notificationId) => { + const option = notificationOptions.find( + (o: any) => o._id === notificationId + ); + if (!option) return null; + return ( + + {option.name} + { + const updated = notificationChannels.filter( + (id) => id !== notificationId + ); + setValue("notificationChannels", updated); + }} + style={{ cursor: "pointer" }} + /> + + ); + })} + + + } + /> + ( + + )} + /> + } + /> + + + + + ); +}; diff --git a/client/src/pages/uptime/UptimeMonitors.tsx b/client/src/pages/uptime/UptimeMonitors.tsx new file mode 100644 index 000000000..241506c3a --- /dev/null +++ b/client/src/pages/uptime/UptimeMonitors.tsx @@ -0,0 +1,104 @@ +import { + MonitorBasePageWithStates, + UpStatusBox, + DownStatusBox, + PausedStatusBox, + InfoBox, +} from "@/components/design-elements"; +import { HeaderCreate } from "@/components/monitors"; +import Stack from "@mui/material/Stack"; +import { Dialog } from "@/components/inputs"; +import { MonitorTable } from "@/pages/uptime/MonitorTable"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; +import type { IMonitor, IMonitorWithStats } from "@/types/monitor"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useState } from "react"; +import { useDelete } from "@/hooks/UseApi"; + +const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; + +const UptimeMonitors = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + + const { deleteFn } = useDelete(); + const { response, loading, isValidating, error, refetch } = useGet< + ApiResponse + >( + `/monitors?embedChecks=true&type=http&type=https&type=ping&page=${page}&rowsPerPage=${rowsPerPage}`, + {}, + { + refreshInterval: GLOBAL_REFRESH, + keepPreviousData: true, + dedupingInterval: 0, + }, + { + useTeamIdAsKey: true, + } + ); + + const monitors: IMonitor[] = response?.data?.monitors ?? ([] as IMonitor[]); + const count = response?.data?.count || 0; + const upCount = response?.data?.upCount || 0; + const downCount = response?.data?.downCount || 0; + const pausedCount = response?.data?.pausedCount || 0; + + const handleConfirm = async () => { + await deleteFn(`/monitors/${selectedMonitor?._id}`); + setSelectedMonitor(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + + return ( + + + + + + + + + + + + ); +}; + +export default UptimeMonitors; diff --git a/client/src/routes/Router.tsx b/client/src/routes/Router.tsx new file mode 100644 index 000000000..196ce6c1c --- /dev/null +++ b/client/src/routes/Router.tsx @@ -0,0 +1,140 @@ +import { Routes, Route } from "react-router"; +import AuthLogin from "@/pages/auth/Login"; +import AuthRegister from "@/pages/auth/Register"; +import AuthRegisterInvite from "@/pages/auth/RegisterInvite"; +import UptimeMonitorsPage from "@/pages/uptime/UptimeMonitors"; +import UptimeCreatePage from "@/pages/uptime/UptimeCreate"; +import UptimeDetailsPage from "@/pages/uptime/UptimeDetails"; +import UptimeConfigurePage from "@/pages/uptime/UptimeConfigure"; +import SettingsPage from "@/pages/settings/Settings"; +import TeamsPage from "@/pages/teams/Teams"; +import TeamsCreatePage from "@/pages/teams/TeamsCreate"; +import TeamsConfigPage from "@/pages/teams/TeamsConfig"; +import TeamDetailsPage from "@/pages/teams/TeamDetails"; +import TeamMemberConfigPage from "@/pages/teams/TeamMemberConfig"; +import TeamMemberCreatePage from "@/pages/teams/TeamMemberCreate"; +import InvitePage from "@/pages/auth/Invite"; +import NotificationChannelsPage from "@/pages/notification-channels/NotificationChannels"; +import NotificationChannelsCreatePage from "@/pages/notification-channels/NotificationChannelsCreate"; +import NotificationChannelsConfigPage from "@/pages/notification-channels/NotificationChannelsConfig"; +import IncidentsPage from "@/pages/incidents/IncidentsPage"; +import PageSpeedMonitorsPage from "@/pages/pagespeed/PageSpeedMonitors"; +import PageSpeedDetailsPage from "@/pages/pagespeed/PageSpeedDetails"; +import PageSpeedConfigurePage from "@/pages/pagespeed/PageSpeedConfig"; +import PageSpeedCreatePage from "@/pages/pagespeed/PageSpeedCreate"; + +import InfraMonitorsPage from "@/pages/infrastructure/InfraMonitors"; +import InfraDetailsPage from "@/pages/infrastructure/InfraDetails"; +import InfraConfigurePage from "@/pages/infrastructure/InfraConfig"; +import InfraCreatePage from "@/pages/infrastructure/InfraCreate"; + +import MaintenancePage from "@/pages/maintenance/Maintenance"; +import MaintenanceConfigPage from "@/pages/maintenance/MaintenanceConfig"; +import MaintenanceCreatePage from "@/pages/maintenance/MaintenanceCreate"; + +import StatusPages from "@/pages/status-page/StatusPages"; +import StatusDetailsPages from "@/pages/status-page/StatusPageDetails"; +import StatusPublicPage from "@/pages/status-page/StatusPagePublic"; +import StatusCreatePage from "@/pages/status-page/StatusPageCreate"; +import StatusConfigPage from "@/pages/status-page/StatusPageConfig"; + +import DiagnosticPage from "@/pages/diagnostic/DiagnosticPage"; + +import RecoveryPage from "@/pages/recovery/Recovery"; +import ResetPage from "@/pages/recovery/Reset"; + +import ProfilePage from "@/pages/profile/Profile"; + +import RootLayout from "@/components/layouts/RootLayout"; +import { ProtectedRoute } from "@/components/protected-route"; + +const Router = () => { + return ( + + } /> + } /> + } /> + } /> + } /> + } /> + + + + + } + > + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + } + /> + } /> + } /> + } /> + + } /> + } + /> + } + /> + } + /> + } /> + } /> + } /> + } + /> + } /> + } /> + } /> + } + /> + } /> + } /> + + } /> + } /> + } + /> + } /> + } /> + } /> + } + /> + + } /> + + } /> + + + ); +}; + +export default Router; diff --git a/client/src/store.js b/client/src/store.js deleted file mode 100644 index 565d68519..000000000 --- a/client/src/store.js +++ /dev/null @@ -1,46 +0,0 @@ -import { configureStore, combineReducers } from "@reduxjs/toolkit"; - -import authReducer from "./Features/Auth/authSlice"; -import v2AuthReducer from "./Features/Auth/v2AuthSlice"; -import uiReducer from "./Features/UI/uiSlice"; -import storage from "redux-persist/lib/storage"; -import { persistReducer, persistStore, createTransform } from "redux-persist"; - -const authTransform = createTransform( - (inboundState) => { - const { profileImage, ...rest } = inboundState; - return rest; - }, - // No transformation on rehydration - null, - // Only applies to auth - { whitelist: ["auth"] } -); - -const persistConfig = { - key: "root", - storage, - whitelist: ["auth", "v2Auth", "ui"], - transforms: [authTransform], -}; - -const rootReducer = combineReducers({ - auth: authReducer, - v2Auth: v2AuthReducer, - ui: uiReducer, -}); - -const persistedReducer = persistReducer(persistConfig, rootReducer); - -export const store = configureStore({ - reducer: persistedReducer, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ - serializableCheck: { - ignoredActions: ["persist/PERSIST", "persist/REHYDRATE", "persist/REGISTER"], - }, - }), -}); - -export const persistor = persistStore(store); -export default store; diff --git a/client/src/store.ts b/client/src/store.ts new file mode 100644 index 000000000..755855ce7 --- /dev/null +++ b/client/src/store.ts @@ -0,0 +1,46 @@ +import { configureStore, combineReducers } from "@reduxjs/toolkit"; +import authReducer from "@/features/authSlice"; +import uiReducer from "@/features/uiSlice"; + +import { + persistStore, + persistReducer, + FLUSH, + REHYDRATE, + PAUSE, + PERSIST, + PURGE, + REGISTER, +} from "redux-persist"; +import storage from "redux-persist/lib/storage"; // defaults to localStorage for web + +const rootReducer = combineReducers({ + auth: authReducer, + ui: uiReducer, +}); + +const persistConfig = { + key: "root", + storage, + whitelist: ["auth", "ui"], +}; + +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + reducer: persistedReducer, + devTools: process.env.NODE_ENV === "development", + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }), +}); + +export const persistor = persistStore(store); + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} +export type AppDispatch = typeof store.dispatch; diff --git a/client/src/theme/palette.ts b/client/src/theme/palette.ts new file mode 100644 index 000000000..9bb62e4af --- /dev/null +++ b/client/src/theme/palette.ts @@ -0,0 +1,131 @@ +import { lighten, darken } from "@mui/material/styles"; + +const typographyBase = 13; + +export const typographyLevels = { + base: typographyBase, + xs: `${(typographyBase - 4) / 16}rem`, + s: `${(typographyBase - 2) / 16}rem`, + m: `${typographyBase / 16}rem`, + l: `${(typographyBase + 2) / 16}rem`, + xl: `${(typographyBase + 10) / 16}rem`, +}; + +export const colors = { + offWhite: "#FEFEFE", + offBlack: "#131315", + gray0: "#FDFDFD", + gray10: "#F4F4FF", + gray50: "#F9F9F9", + gray100: "#F3F3F3", + gray200: "#EFEFEF", + gray250: "#DADADA", + gray500: "#A2A3A3", + gray700: "#313131", + gray900: "#1c1c1c", + blueGray50: "#E8F0FE", + blueGray400: "#8b9dc3", + blueGray450: "#7C8BA1", + blueGray500: "#475467", + blueGray600: "#344054", + blueGray800: "#1C2130", + blueGray900: "#515151", + blueBlueWave: "#1570EF", + lightBlueWave: "#CDE2FF", + green100: "#67cd78", + green200: "#4B9B77", + green400: "#079455", + green700: "#026513", + orange100: "#FD8F22", + orange200: "#D69A5D", + orange600: "#9B734B", + orange700: "#884605", + red100: "#F27C7C", + red400: "#D92020", + red600: "#9B4B4B", + red700: "#980303", +}; + +export const lightPalette = { + accent: { + main: colors.blueBlueWave, + light: lighten(colors.blueBlueWave, 0.2), + dark: darken(colors.blueBlueWave, 0.2), + contrastText: colors.offWhite, + }, + primary: { + main: colors.offWhite, + contrastText: colors.blueGray800, + contrastTextSecondary: colors.blueGray600, + contrastTextTertiary: colors.blueGray500, + lowContrast: colors.gray250, + veryLowContrast: lighten(colors.gray250, 0.5), + }, + secondary: { + main: colors.gray200, + light: colors.lightBlueWave, + dark: lighten(colors.gray200, 0.1), + contrastText: colors.blueGray600, + }, + tertiary: { + main: colors.gray100, + contrastText: colors.blueGray800, + }, + success: { + main: colors.green700, + contrastText: colors.offWhite, + lowContrast: colors.green400, + }, + warning: { + main: colors.orange700, + contrastText: colors.offWhite, + lowContrast: colors.orange100, + }, + error: { + main: colors.red700, + contrastText: colors.offWhite, + lowContrast: colors.red400, + }, +}; + +export const darkPalette = { + accent: { + main: colors.blueBlueWave, + light: lighten(colors.blueBlueWave, 0.2), + dark: darken(colors.blueBlueWave, 0.2), + contrastText: colors.offWhite, + }, + primary: { + main: colors.offBlack, + contrastText: colors.blueGray50, + contrastTextSecondary: colors.gray200, + contrastTextTertiary: colors.gray500, + lowContrast: colors.blueGray600, + veryLowContrast: darken(colors.blueGray600, 0.5), + }, + secondary: { + main: colors.gray700, + light: colors.lightBlueWave, + dark: lighten(colors.gray700, 0.1), + contrastText: colors.gray200, + }, + tertiary: { + main: colors.blueGray800, + contrastText: colors.gray100, + }, + success: { + main: colors.green100, + contrastText: colors.offBlack, + lowContrast: colors.green200, + }, + warning: { + main: colors.orange200, + contrastText: colors.offBlack, + lowContrast: colors.orange600, + }, + error: { + main: colors.red100, + contrastText: colors.offBlack, + lowContrast: colors.red600, + }, +}; diff --git a/client/src/theme/theme.ts b/client/src/theme/theme.ts new file mode 100644 index 000000000..e11f4ac39 --- /dev/null +++ b/client/src/theme/theme.ts @@ -0,0 +1,119 @@ +import { createTheme } from "@mui/material"; +import { lightPalette, darkPalette, typographyLevels } from "./palette"; + +import type { Theme } from "@mui/material/styles"; + +export type PaletteKey = { + [K in keyof Theme["palette"]]: Theme["palette"][K] extends { main: any } + ? K + : never; +}[keyof Theme["palette"]]; + +const fontFamilyPrimary = 'system-ui, sans-serif'; +const shadow = + "0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03)"; + +export const theme = (mode: string, palette: any) => + createTheme({ + breakpoints: { + values: { + xs: 300, + sm: 600, + md: 900, + lg: 1200, + xl: 1536, + }, + }, + spacing: 2, + palette: { + mode: mode, + ...palette, + }, + typography: { + fontFamily: fontFamilyPrimary, + fontSize: typographyLevels.base, + h1: { + fontSize: typographyLevels.xl, + color: palette.primary.contrastText, + fontWeight: 500, + }, + h2: { + fontSize: typographyLevels.l, + color: palette.primary.contrastTextSecondary, + fontWeight: 400, + }, + + body1: { + fontSize: typographyLevels.m, + color: palette.primary.contrastTextTertiary, + fontWeight: 400, + }, + body2: { + fontSize: typographyLevels.s, + color: palette.primary.contrastTextTertiary, + fontWeight: 400, + }, + }, + + components: { + MuiButtonBase: { + defaultProps: { + disableRipple: true, + }, + }, + MuiFormLabel: { + styleOverrides: { + root: ({ theme }) => ({ + fontSize: typographyLevels.base, + "&.Mui-focused": { + color: theme.palette.secondary.contrastText, + }, + }), + }, + }, + MuiInputLabel: { + styleOverrides: { + root: ({ theme }) => ({ + top: `-${theme.spacing(4)}`, + "&.MuiInputLabel-shrink": { + top: 0, + }, + }), + }, + }, + + MuiOutlinedInput: { + styleOverrides: { + root: { + "&.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderColor: palette.accent.main, + }, + }, + }, + }, + + MuiPaper: { + styleOverrides: { + root: ({ theme }) => { + return { + marginTop: 4, + padding: 0, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.primary.lowContrast, + borderRadius: 4, + boxShadow: shadow, + backgroundColor: theme.palette.primary.main, + backgroundImage: "none", + }; + }, + }, + }, + }, + shape: { + borderRadius: 2, + }, + }); + +export const lightTheme = createTheme(theme("light", lightPalette)); +export const darkTheme = createTheme(theme("dark", darkPalette)); diff --git a/client/src/types/check.ts b/client/src/types/check.ts new file mode 100644 index 000000000..60effdbda --- /dev/null +++ b/client/src/types/check.ts @@ -0,0 +1,173 @@ +export interface CheckTimingPhases { + wait: number; + dns: number; + tcp: number; + tls: number; + request: number; + firstByte: number; + download: number; + total: number; +} + +export interface CheckTimings { + start: string; + socket: string; + lookup: string; + connect: string; + secureConnect: string; + response: string; + end: string; + phases: CheckTimingPhases; +} + +export interface ILighthouseAudit { + id?: string; + title?: string; + score?: number | null; + displayValue?: string; + numericValue?: number; + numericUnit?: string; +} + +export interface ICheckLighthouseFields { + accessibility: number; + bestPractices: number; + seo: number; + performance: number; + audits: { + cls: ILighthouseAudit; + si: ILighthouseAudit; + fcp: ILighthouseAudit; + lcp: ILighthouseAudit; + tbt: ILighthouseAudit; + }; +} + +export interface ISystemInfo { + cpu: ICpuInfo; + memory: IMemoryInfo; + disk: IDiskInfo[]; + host: IHostInfo; + net: INetInfo[]; +} +export interface ICpuInfo { + physical_core: number; + logical_core: number; + frequency: number; + current_frequency: number; + temperature: number[]; // per-core temps + free_percent: number; + usage_percent: number; +} + +export interface IMemoryInfo { + total_bytes: number; + available_bytes: number; + used_bytes: number; + usage_percent: number; +} + +export interface IHostInfo { + os?: string; + platform?: string; + kernel_version?: string; + pretty_name?: string; +} +export interface IDiskInfo { + device: string; + total_bytes: number; + free_bytes: number; + used_bytes: number; + usage_percent: number; + total_inodes?: number; + free_inodes?: number; + used_inodes?: number; + inodes_usage_percent?: number; + read_bytes?: number; + write_bytes?: number; + read_time?: number; + write_time?: number; +} +export interface INetInfo { + name: string; + bytes_sent: number; + bytes_recv: number; + packets_sent: number; + packets_recv: number; + err_in: number; + err_out: number; + drop_in: number; + drop_out: number; + fifo_in: number; + fifo_out: number; +} +export interface ICheck { + _id: string; + metadata: { + monitorId: string; + type: string; + teamId: string; + }; + status: string; + message: string; + responseTime: number; + normalResponseTime?: number; + httpStatusCode: number; + ack: boolean; + expiry: string; + createdAt: string; + updatedAt: string; + timings: CheckTimings; + lighthouse?: ICheckLighthouseFields; + system?: ISystemInfo; +} +export interface CheckWithMonitor { + _id: string; + metadata: { + monitorId: { + _id: string; + name: string; + }; + type: string; + teamId: string; + }; + status: string; + message: string; + responseTime: number; + normalResponseTime?: number; + httpStatusCode: number; + ack: boolean; + expiry: string; + createdAt: string; + updatedAt: string; + timings: CheckTimings; +} + +export interface GroupedCheck { + _id: string; + avgResponseTime: number; + count: number; +} + +export interface LatestCheck { + status: string; + responseTime: number; + checkedAt: string; + _id: string; +} + +export interface IInfraCheck { + _id: string; + avgResponseTime: number; + count: 9; + cpu: ICpuInfo; + memory: IMemoryInfo; + disk: IDiskInfo[]; + host: IHostInfo; + net: INetInfo[]; +} + +export interface IChecksWithCount { + checks: ICheck[]; + count: number; +} diff --git a/client/src/types/env.d.ts b/client/src/types/env.d.ts new file mode 100644 index 000000000..f711139e8 --- /dev/null +++ b/client/src/types/env.d.ts @@ -0,0 +1,17 @@ +/// + +interface ImportMetaEnv { + readonly VITE_APP_API_V2_BASE_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} + +declare module "*.svg?react" { + import * as React from "react"; + const ReactComponent: React.FunctionComponent< + React.SVGProps & { title?: string } + >; + export default ReactComponent; +} diff --git a/client/src/types/incident.ts b/client/src/types/incident.ts new file mode 100644 index 000000000..4c77510b1 --- /dev/null +++ b/client/src/types/incident.ts @@ -0,0 +1,16 @@ +import type { IMongoUser } from "@/types/user"; +import type { IMonitor } from "@/types/monitor"; + +export interface IIncident { + _id: string; + monitorId: IMonitor; + teamId: string; + startedAt: string; + startCheck: string; + resolved: boolean; + endedAt?: string; + endCheck?: string; + resolvedBy?: IMongoUser; + resolutionType?: string; + resolutionNote?: string; +} diff --git a/client/src/types/job.ts b/client/src/types/job.ts new file mode 100644 index 000000000..61814fa0d --- /dev/null +++ b/client/src/types/job.ts @@ -0,0 +1,29 @@ +import type { IMonitor } from "./monitor"; +export interface IJob { + active: boolean; + id: string; + data: IMonitor; + lastFinishedAt: number; + lastRunAt: number; + lastRunTook: number; + lockedAt: number | null; + repeat: number; + runCount: number; + template: string; +} + +export interface IJobMetrics { + jobs: number; + activeJobs: number; + failingJobs: number; + jobsWithFailures: Array<{ + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + failedAt: number | null; + failCount: number | null; + failReason: string | null; + }>; + totalRuns: number; + totalFailures: number; +} diff --git a/client/src/types/log.ts b/client/src/types/log.ts new file mode 100644 index 000000000..d77229a6b --- /dev/null +++ b/client/src/types/log.ts @@ -0,0 +1,7 @@ +export interface ILogEntry { + level: "debug" | "info" | "warn" | "error"; + message: string; + service: string; + stack: string; + timestamp: string; +} diff --git a/client/src/types/maintenance.ts b/client/src/types/maintenance.ts new file mode 100644 index 000000000..8216ba9c3 --- /dev/null +++ b/client/src/types/maintenance.ts @@ -0,0 +1,18 @@ +export const MaintenanceRepeats = ["no repeat", "daily", "weekly"] as const; +export type MaintenanceRepeat = (typeof MaintenanceRepeats)[number]; + +export interface IMaintenance extends Document { + _id: string; + orgId: string; + teamId: string; + name: string; + isActive: boolean; + repeat: MaintenanceRepeat; + monitors: string[]; + startTime: Date; + endTime: Date; + createdBy: string; + updatedBy: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/client/src/types/monitor.ts b/client/src/types/monitor.ts new file mode 100644 index 000000000..0980c1ffc --- /dev/null +++ b/client/src/types/monitor.ts @@ -0,0 +1,37 @@ +import type { GroupedCheck, ICheck } from "@/types/check"; +import type { IMonitorStats } from "./monitorStats"; + +export type MonitorStatus = "up" | "down" | "initializing"; + +export interface IMonitor { + checks: ICheck[]; + createdAt: string; + createdBy: string; + interval: number; + isActive: boolean; + latestChecks: ICheck[]; + n: number; + name: string; + notificationChannels: string[]; + status: MonitorStatus; + type: string; + updatedAt: string; + updatedBy: string; + url: string; + __v: number; + _id: string; +} + +export interface IMonitorWithStats { + count: number; + downCount: number; + upCount: number; + pausedCount: number; + monitors: IMonitor[]; +} + +export interface IMonitorWithMonitorStats { + checks: GroupedCheck[]; + monitor: IMonitor; + stats: IMonitorStats; +} diff --git a/client/src/types/monitorStats.ts b/client/src/types/monitorStats.ts new file mode 100644 index 000000000..14032a84c --- /dev/null +++ b/client/src/types/monitorStats.ts @@ -0,0 +1,19 @@ +import type { MonitorStatus } from "@/types//monitor"; + +export interface IMonitorStats { + monitorId: string; + avgResponseTime: number; + maxResponseTime: number; + totalChecks: number; + totalUpChecks: number; + totalDownChecks: number; + uptimePercentage: number; + lastCheckTimestamp: number; + lastResponseTime: number; + timeOfLastFailure: number; + currentStreak: number; + currentStreakStatus: MonitorStatus; + currentStreakStartedAt: number; + createdAt: Date; + updatedAt: Date; +} diff --git a/client/src/types/mui.d.ts b/client/src/types/mui.d.ts new file mode 100644 index 000000000..6bc500b93 --- /dev/null +++ b/client/src/types/mui.d.ts @@ -0,0 +1,25 @@ +import "@mui/material/Button"; + +declare module "@mui/material/styles" { + interface Palette { + accent: Palette["primary"]; + tertiary: Palette["primary"]; + } + interface PaletteOptions { + accent?: PaletteOptions["primary"]; + tertiary?: PaletteOptions["primary"]; + } + + interface PaletteColor { + contrastTextSecondary?: string; + contrastTextTertiary?: string; + lowContrast?: string; + veryLowContrast?: string; + } +} + +declare module "@mui/material/Button" { + interface ButtonPropsColorOverrides { + accent: true; + } +} diff --git a/client/src/types/notification-channel.ts b/client/src/types/notification-channel.ts new file mode 100644 index 000000000..e31a3ed9b --- /dev/null +++ b/client/src/types/notification-channel.ts @@ -0,0 +1,21 @@ +export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const; +export type ChannelType = (typeof ChannelTypes)[number]; + +export interface INotificationChannelConfig { + url?: string; // For webhook, slack, discord + emailAddress?: string; // For email +} + +export interface INotificationChannel { + _id: string; + orgId: string; + teamId: string; + name: string; + type: ChannelType; + config: INotificationChannelConfig; + isActive: boolean; + createdBy: string; + updatedBy: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/client/src/types/status-page.ts b/client/src/types/status-page.ts new file mode 100644 index 000000000..460c13a68 --- /dev/null +++ b/client/src/types/status-page.ts @@ -0,0 +1,19 @@ +import type { IMonitor } from "@/types/monitor"; + +export interface IStatusPage { + _id: string; + name: string; + description: string; + url: string; + isPublished: boolean; + monitors: string[]; +} + +export interface IStatusPageWithMonitors { + _id: string; + name: string; + description: string; + url: string; + isPublished: boolean; + monitors: IMonitor[]; +} diff --git a/client/src/types/team-member.ts b/client/src/types/team-member.ts new file mode 100644 index 000000000..12cd83cd3 --- /dev/null +++ b/client/src/types/team-member.ts @@ -0,0 +1,17 @@ +export interface ITeamMember { + _id: string; + orgId: string; + teamId: string; + userId: { + _id: string; + firstName: string; + lastName: string; + email: string; + }; + roleId: { + _id: string; + name: string; + scope: string; + permissions: string[]; + }; +} diff --git a/client/src/types/team.ts b/client/src/types/team.ts new file mode 100644 index 000000000..f8c2fa2e7 --- /dev/null +++ b/client/src/types/team.ts @@ -0,0 +1,5 @@ +export interface ITeam { + _id: string; + name: string; + description: string; +} diff --git a/client/src/types/timezone.ts b/client/src/types/timezone.ts new file mode 100644 index 000000000..383af3ec0 --- /dev/null +++ b/client/src/types/timezone.ts @@ -0,0 +1,4 @@ +export interface ITimeZone { + _id: string; + name: string; +} diff --git a/client/src/types/user.ts b/client/src/types/user.ts new file mode 100644 index 000000000..81e362786 --- /dev/null +++ b/client/src/types/user.ts @@ -0,0 +1,21 @@ +export interface IUser { + id: string; + email: string; + firstName: string; + lastName: string; + org: { + name: string; + permissions: string[]; + }; + teams: { + id: string; + name: string; + permissions: string; + }[]; +} +export interface IMongoUser { + _id: string; + email: string; + firstName: string; + lastName: string; +} diff --git a/client/src/utils/ApiClient.ts b/client/src/utils/ApiClient.ts new file mode 100644 index 000000000..eca9554d6 --- /dev/null +++ b/client/src/utils/ApiClient.ts @@ -0,0 +1,43 @@ +import axios from "axios"; +import type { AxiosError } from "axios"; +import type { AxiosRequestConfig, AxiosResponse } from "axios"; +const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; + +const api = axios.create({ + baseURL: BASE_URL || "http://localhost:55555/api/v2", + withCredentials: true, +}); + +const onSuccess = (response: AxiosResponse) => response; +const onError = (error: AxiosError) => { + if (error.response?.status === 401) { + window.location.href = "/login"; + } + return Promise.reject(error); +}; + +api.interceptors.response.use(onSuccess, onError); + +export const get = ( + url: string, + config: AxiosRequestConfig = {} +): Promise> => api.get(url, config); + +export const post = ( + url: string, + data: any, + config: AxiosRequestConfig = {} +): Promise> => api.post(url, data, config); + +export const patch = ( + url: string, + data: any, + config: AxiosRequestConfig = {} +): Promise> => api.patch(url, data, config); + +export const deleteOp = ( + url: string, + config: AxiosRequestConfig = {} +): Promise> => api.delete(url, config); + +export default api; diff --git a/client/src/utils/DataUtils.ts b/client/src/utils/DataUtils.ts new file mode 100644 index 000000000..4a86b3a9b --- /dev/null +++ b/client/src/utils/DataUtils.ts @@ -0,0 +1,41 @@ +const MIN_OUT = 10; +const MAX_OUT = 100; + +export const normalizeResponseTimes = < + T extends Record, + K extends keyof T +>( + checks: T[], + key: K +): (T & { normalResponseTime: number })[] => { + if (!Array.isArray(checks) || checks.length === 0) + return checks as (T & { + normalResponseTime: number; + })[]; + + if (checks.length === 1) { + return [ + { + ...checks[0], + normalResponseTime: 50, + }, + ]; + } + + const { min, max } = checks.reduce( + (acc, check) => { + if (check[key] > acc.max) acc.max = check[key]; + if (check[key] < acc.min) acc.min = check[key]; + return acc; + }, + { max: -Infinity, min: Infinity } + ); + + const range = max - min || 1; + + return checks.map((check) => ({ + ...check, + normalResponseTime: + MIN_OUT + ((check[key] - min) * (MAX_OUT - MIN_OUT)) / range, + })); +}; diff --git a/client/src/utils/MonitorUtils.ts b/client/src/utils/MonitorUtils.ts new file mode 100644 index 000000000..62d4283fe --- /dev/null +++ b/client/src/utils/MonitorUtils.ts @@ -0,0 +1,88 @@ +import type { MonitorStatus } from "@/types/monitor"; +import type { IMonitor } from "@/types/monitor"; +import type { PaletteKey } from "@/theme/theme"; +import type { ValueType } from "@/components/design-elements/StatusLabel"; +export const getStatusPalette = (status: MonitorStatus): PaletteKey => { + const paletteMap: Record = { + up: "success", + down: "error", + initializing: "warning", + }; + return paletteMap[status]; +}; + +export const getValuePalette = (value: ValueType): PaletteKey => { + const paletteMap: Record = { + positive: "success", + negative: "error", + neutral: "warning", + }; + return paletteMap[value]; +}; + +export const getStatusColor = (status: MonitorStatus, theme: any): string => { + const statusColors: Record = { + up: theme.palette.success.lowContrast, + down: theme.palette.error.lowContrast, + initializing: theme.palette.warning.lowContrast, + }; + return statusColors[status]; +}; + +export const getResponseTimeColor = (responseTime: number): PaletteKey => { + if (responseTime < 200) { + return "success"; + } else if (responseTime < 300) { + return "warning"; + } else { + return "error"; + } +}; + +export const getInfraGaugeColor = (val: number, theme: any) => { + if (val < 50) { + return theme.palette.success.main; + } else if (val < 80) { + return theme.palette.warning.lowContrast; + } else { + return theme.palette.error.lowContrast; + } +}; + +export const getPageSpeedPalette = (score: number): PaletteKey => { + if (score >= 90) return "success"; + else if (score >= 50) return "warning"; + else return "error"; +}; + +export const formatUrl = (url: string, maxLength: number = 55) => { + if (!url) return ""; + + const strippedUrl = url.replace(/^https?:\/\//, ""); + return strippedUrl.length > maxLength + ? `${strippedUrl.slice(0, maxLength)}…` + : strippedUrl; +}; + +export interface IStatusPageHeaderConfig { + paletteKey: PaletteKey; + message: string; +} +export const getStatusPageHeaderConfig = ( + monitors: IMonitor[] +): IStatusPageHeaderConfig => { + if (!monitors || monitors.length === 0) { + return { paletteKey: "error", message: "No monitors available" }; + } + + const allUp = monitors.every((monitor) => monitor.status === "up"); + const anyDown = monitors.some((monitor) => monitor.status === "down"); + const allDown = monitors.every((monitor) => monitor.status === "down"); + + if (allUp) + return { paletteKey: "success", message: "All systems operational" }; + if (allDown) return { paletteKey: "error", message: "All systems down" }; + if (anyDown) + return { paletteKey: "warning", message: "Partial system outage" }; + return { paletteKey: "warning", message: "Partial system outage" }; +}; diff --git a/client/src/utils/TimeUtils.ts b/client/src/utils/TimeUtils.ts new file mode 100644 index 000000000..cf635cb3d --- /dev/null +++ b/client/src/utils/TimeUtils.ts @@ -0,0 +1,56 @@ +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; +import customParseFormat from "dayjs/plugin/customParseFormat"; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(customParseFormat); +dayjs.extend(duration); + +export const MS_PER_SECOND = 1000; +export const MS_PER_MINUTE = 60 * MS_PER_SECOND; +export const MS_PER_HOUR = 60 * MS_PER_MINUTE; +export const MS_PER_DAY = 24 * MS_PER_HOUR; +export const MS_PER_WEEK = MS_PER_DAY * 7; + +export const formatDateWithTz = ( + timestamp: string, + format: string, + timezone: string +) => { + if (!timestamp) { + return "Unknown time"; + } + const formattedDate = dayjs(timestamp).tz(timezone).format(format); + return formattedDate; +}; + +export const tickDateFormatLookup = (range: string) => { + const tickFormatLookup: Record = { + "2h": "h:mm A", + "24h": "h:mm A", + "7d": "MM/D, h:mm A", + "30d": "ddd. M/D", + }; + const format = tickFormatLookup[range]; + if (format === undefined) { + return ""; + } + return format; +}; + +export const tooltipDateFormatLookup = (range: string) => { + const dateFormatLookup: Record = { + "2h": "ddd. MMMM D, YYYY, hh:mm A", + "24h": "ddd. MMMM D, YYYY, hh:mm A", + "7d": "ddd. MMMM D, YYYY, hh:mm A", + "30d": "ddd. MMMM D, YYYY", + }; + const format = dateFormatLookup[range]; + if (format === undefined) { + return ""; + } + return format; +}; diff --git a/client/src/utils/i18n.ts b/client/src/utils/i18n.ts new file mode 100644 index 000000000..91ba90088 --- /dev/null +++ b/client/src/utils/i18n.ts @@ -0,0 +1,42 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; + +type TranslationModule = { + default?: Record; + [key: string]: any; +}; + +const primaryLanguage = "en"; + +// Load all translation files eagerly +const translations = import.meta.glob("../locales/*.json", { + eager: true, +}); + +const resources: Record = {}; +Object.keys(translations).forEach((path) => { + const match = path.match(/\/([^/]+)\.json$/); + if (!match) { + console.warn(`Skipping invalid translation file path: ${path}`); + return; + } + const langCode = match[1]; + resources[langCode] = { + translation: translations[path].default || translations[path], + }; +}); + +i18n.use(initReactI18next).init({ + resources, + lng: primaryLanguage, + fallbackLng: primaryLanguage, + debug: import.meta.env.MODE === "development", + ns: ["translation"], + defaultNS: "translation", + interpolation: { + escapeValue: false, + }, + returnEmptyString: false, +}); + +export default i18n; diff --git a/client/src/utils/timezones.json b/client/src/utils/timezones.json new file mode 100644 index 000000000..fe00fd59b --- /dev/null +++ b/client/src/utils/timezones.json @@ -0,0 +1,1702 @@ +[ + { + "_id": "ETC/UTC", + "name": "ETC/UTC" + }, + { + "_id": "Africa/Abidjan", + "name": "Africa/Abidjan" + }, + { + "_id": "Africa/Accra", + "name": "Africa/Accra" + }, + { + "_id": "Africa/Addis_Ababa", + "name": "Africa/Addis_Ababa" + }, + { + "_id": "Africa/Algiers", + "name": "Africa/Algiers" + }, + { + "_id": "Africa/Asmara", + "name": "Africa/Asmara" + }, + { + "_id": "Africa/Bamako", + "name": "Africa/Bamako" + }, + { + "_id": "Africa/Bangui", + "name": "Africa/Bangui" + }, + { + "_id": "Africa/Banjul", + "name": "Africa/Banjul" + }, + { + "_id": "Africa/Bissau", + "name": "Africa/Bissau" + }, + { + "_id": "Africa/Blantyre", + "name": "Africa/Blantyre" + }, + { + "_id": "Africa/Brazzaville", + "name": "Africa/Brazzaville" + }, + { + "_id": "Africa/Bujumbura", + "name": "Africa/Bujumbura" + }, + { + "_id": "Africa/Cairo", + "name": "Africa/Cairo" + }, + { + "_id": "Africa/Casablanca", + "name": "Africa/Casablanca" + }, + { + "_id": "Africa/Ceuta", + "name": "Africa/Ceuta" + }, + { + "_id": "Africa/Conakry", + "name": "Africa/Conakry" + }, + { + "_id": "Africa/Dakar", + "name": "Africa/Dakar" + }, + { + "_id": "Africa/Dar_es_Salaam", + "name": "Africa/Dar_es_Salaam" + }, + { + "_id": "Africa/Djibouti", + "name": "Africa/Djibouti" + }, + { + "_id": "Africa/Douala", + "name": "Africa/Douala" + }, + { + "_id": "Africa/El_Aaiun", + "name": "Africa/El_Aaiun" + }, + { + "_id": "Africa/Freetown", + "name": "Africa/Freetown" + }, + { + "_id": "Africa/Gaborone", + "name": "Africa/Gaborone" + }, + { + "_id": "Africa/Harare", + "name": "Africa/Harare" + }, + { + "_id": "Africa/Johannesburg", + "name": "Africa/Johannesburg" + }, + { + "_id": "Africa/Juba", + "name": "Africa/Juba" + }, + { + "_id": "Africa/Kampala", + "name": "Africa/Kampala" + }, + { + "_id": "Africa/Khartoum", + "name": "Africa/Khartoum" + }, + { + "_id": "Africa/Kigali", + "name": "Africa/Kigali" + }, + { + "_id": "Africa/Kinshasa", + "name": "Africa/Kinshasa" + }, + { + "_id": "Africa/Lagos", + "name": "Africa/Lagos" + }, + { + "_id": "Africa/Libreville", + "name": "Africa/Libreville" + }, + { + "_id": "Africa/Lome", + "name": "Africa/Lome" + }, + { + "_id": "Africa/Luanda", + "name": "Africa/Luanda" + }, + { + "_id": "Africa/Lubumbashi", + "name": "Africa/Lubumbashi" + }, + { + "_id": "Africa/Lusaka", + "name": "Africa/Lusaka" + }, + { + "_id": "Africa/Malabo", + "name": "Africa/Malabo" + }, + { + "_id": "Africa/Maputo", + "name": "Africa/Maputo" + }, + { + "_id": "Africa/Maseru", + "name": "Africa/Maseru" + }, + { + "_id": "Africa/Mbabane", + "name": "Africa/Mbabane" + }, + { + "_id": "Africa/Mogadishu", + "name": "Africa/Mogadishu" + }, + { + "_id": "Africa/Monrovia", + "name": "Africa/Monrovia" + }, + { + "_id": "Africa/Nairobi", + "name": "Africa/Nairobi" + }, + { + "_id": "Africa/Ndjamena", + "name": "Africa/Ndjamena" + }, + { + "_id": "Africa/Niamey", + "name": "Africa/Niamey" + }, + { + "_id": "Africa/Nouakchott", + "name": "Africa/Nouakchott" + }, + { + "_id": "Africa/Ouagadougou", + "name": "Africa/Ouagadougou" + }, + { + "_id": "Africa/Porto-Novo", + "name": "Africa/Porto-Novo" + }, + { + "_id": "Africa/Sao_Tome", + "name": "Africa/Sao_Tome" + }, + { + "_id": "Africa/Tripoli", + "name": "Africa/Tripoli" + }, + { + "_id": "Africa/Tunis", + "name": "Africa/Tunis" + }, + { + "_id": "Africa/Windhoek", + "name": "Africa/Windhoek" + }, + { + "_id": "America/Adak", + "name": "America/Adak" + }, + { + "_id": "America/Anchorage", + "name": "America/Anchorage" + }, + { + "_id": "America/Anguilla", + "name": "America/Anguilla" + }, + { + "_id": "America/Antigua", + "name": "America/Antigua" + }, + { + "_id": "America/Araguaina", + "name": "America/Araguaina" + }, + { + "_id": "America/Argentina/Buenos_Aires", + "name": "America/Argentina/Buenos_Aires" + }, + { + "_id": "America/Argentina/Catamarca", + "name": "America/Argentina/Catamarca" + }, + { + "_id": "America/Argentina/Cordoba", + "name": "America/Argentina/Cordoba" + }, + { + "_id": "America/Argentina/Jujuy", + "name": "America/Argentina/Jujuy" + }, + { + "_id": "America/Argentina/La_Rioja", + "name": "America/Argentina/La_Rioja" + }, + { + "_id": "America/Argentina/Mendoza", + "name": "America/Argentina/Mendoza" + }, + { + "_id": "America/Argentina/Rio_Gallegos", + "name": "America/Argentina/Rio_Gallegos" + }, + { + "_id": "America/Argentina/Salta", + "name": "America/Argentina/Salta" + }, + { + "_id": "America/Argentina/San_Juan", + "name": "America/Argentina/San_Juan" + }, + { + "_id": "America/Argentina/San_Luis", + "name": "America/Argentina/San_Luis" + }, + { + "_id": "America/Argentina/Tucuman", + "name": "America/Argentina/Tucuman" + }, + { + "_id": "America/Argentina/Ushuaia", + "name": "America/Argentina/Ushuaia" + }, + { + "_id": "America/Aruba", + "name": "America/Aruba" + }, + { + "_id": "America/Asuncion", + "name": "America/Asuncion" + }, + { + "_id": "America/Atikokan", + "name": "America/Atikokan" + }, + { + "_id": "America/Bahia", + "name": "America/Bahia" + }, + { + "_id": "America/Bahia_Banderas", + "name": "America/Bahia_Banderas" + }, + { + "_id": "America/Barbados", + "name": "America/Barbados" + }, + { + "_id": "America/Belem", + "name": "America/Belem" + }, + { + "_id": "America/Belize", + "name": "America/Belize" + }, + { + "_id": "America/Blanc-Sablon", + "name": "America/Blanc-Sablon" + }, + { + "_id": "America/Boa_Vista", + "name": "America/Boa_Vista" + }, + { + "_id": "America/Bogota", + "name": "America/Bogota" + }, + { + "_id": "America/Boise", + "name": "America/Boise" + }, + { + "_id": "America/Cambridge_Bay", + "name": "America/Cambridge_Bay" + }, + { + "_id": "America/Campo_Grande", + "name": "America/Campo_Grande" + }, + { + "_id": "America/Cancun", + "name": "America/Cancun" + }, + { + "_id": "America/Caracas", + "name": "America/Caracas" + }, + { + "_id": "America/Cayenne", + "name": "America/Cayenne" + }, + { + "_id": "America/Cayman", + "name": "America/Cayman" + }, + { + "_id": "America/Chicago", + "name": "America/Chicago" + }, + { + "_id": "America/Chihuahua", + "name": "America/Chihuahua" + }, + { + "_id": "America/Costa_Rica", + "name": "America/Costa_Rica" + }, + { + "_id": "America/Creston", + "name": "America/Creston" + }, + { + "_id": "America/Cuiaba", + "name": "America/Cuiaba" + }, + { + "_id": "America/Curacao", + "name": "America/Curacao" + }, + { + "_id": "America/Danmarkshavn", + "name": "America/Danmarkshavn" + }, + { + "_id": "America/Dawson", + "name": "America/Dawson" + }, + { + "_id": "America/Dawson_Creek", + "name": "America/Dawson_Creek" + }, + { + "_id": "America/Denver", + "name": "America/Denver" + }, + { + "_id": "America/Detroit", + "name": "America/Detroit" + }, + { + "_id": "America/Dominica", + "name": "America/Dominica" + }, + { + "_id": "America/Edmonton", + "name": "America/Edmonton" + }, + { + "_id": "America/Eirunepe", + "name": "America/Eirunepe" + }, + { + "_id": "America/El_Salvador", + "name": "America/El_Salvador" + }, + { + "_id": "America/Fort_Nelson", + "name": "America/Fort_Nelson" + }, + { + "_id": "America/Fortaleza", + "name": "America/Fortaleza" + }, + { + "_id": "America/Glace_Bay", + "name": "America/Glace_Bay" + }, + { + "_id": "America/Godthab", + "name": "America/Godthab" + }, + { + "_id": "America/Goose_Bay", + "name": "America/Goose_Bay" + }, + { + "_id": "America/Grand_Turk", + "name": "America/Grand_Turk" + }, + { + "_id": "America/Grenada", + "name": "America/Grenada" + }, + { + "_id": "America/Guadeloupe", + "name": "America/Guadeloupe" + }, + { + "_id": "America/Guatemala", + "name": "America/Guatemala" + }, + { + "_id": "America/Guayaquil", + "name": "America/Guayaquil" + }, + { + "_id": "America/Guyana", + "name": "America/Guyana" + }, + { + "_id": "America/Halifax", + "name": "America/Halifax" + }, + { + "_id": "America/Havana", + "name": "America/Havana" + }, + { + "_id": "America/Hermosillo", + "name": "America/Hermosillo" + }, + { + "_id": "America/Indiana/Indianapolis", + "name": "America/Indiana/Indianapolis" + }, + { + "_id": "America/Indiana/Knox", + "name": "America/Indiana/Knox" + }, + { + "_id": "America/Indiana/Marengo", + "name": "America/Indiana/Marengo" + }, + { + "_id": "America/Indiana/Petersburg", + "name": "America/Indiana/Petersburg" + }, + { + "_id": "America/Indiana/Tell_City", + "name": "America/Indiana/Tell_City" + }, + { + "_id": "America/Indiana/Vevay", + "name": "America/Indiana/Vevay" + }, + { + "_id": "America/Indiana/Vincennes", + "name": "America/Indiana/Vincennes" + }, + { + "_id": "America/Indiana/Winamac", + "name": "America/Indiana/Winamac" + }, + { + "_id": "America/Inuvik", + "name": "America/Inuvik" + }, + { + "_id": "America/Iqaluit", + "name": "America/Iqaluit" + }, + { + "_id": "America/Jamaica", + "name": "America/Jamaica" + }, + { + "_id": "America/Juneau", + "name": "America/Juneau" + }, + { + "_id": "America/Kentucky/Louisville", + "name": "America/Kentucky/Louisville" + }, + { + "_id": "America/Kentucky/Monticello", + "name": "America/Kentucky/Monticello" + }, + { + "_id": "America/Kralendijk", + "name": "America/Kralendijk" + }, + { + "_id": "America/La_Paz", + "name": "America/La_Paz" + }, + { + "_id": "America/Lima", + "name": "America/Lima" + }, + { + "_id": "America/Los_Angeles", + "name": "America/Los_Angeles" + }, + { + "_id": "America/Lower_Princes", + "name": "America/Lower_Princes" + }, + { + "_id": "America/Maceio", + "name": "America/Maceio" + }, + { + "_id": "America/Managua", + "name": "America/Managua" + }, + { + "_id": "America/Manaus", + "name": "America/Manaus" + }, + { + "_id": "America/Marigot", + "name": "America/Marigot" + }, + { + "_id": "America/Martinique", + "name": "America/Martinique" + }, + { + "_id": "America/Matamoros", + "name": "America/Matamoros" + }, + { + "_id": "America/Mazatlan", + "name": "America/Mazatlan" + }, + { + "_id": "America/Menominee", + "name": "America/Menominee" + }, + { + "_id": "America/Merida", + "name": "America/Merida" + }, + { + "_id": "America/Metlakatla", + "name": "America/Metlakatla" + }, + { + "_id": "America/Mexico_City", + "name": "America/Mexico_City" + }, + { + "_id": "America/Miquelon", + "name": "America/Miquelon" + }, + { + "_id": "America/Moncton", + "name": "America/Moncton" + }, + { + "_id": "America/Monterrey", + "name": "America/Monterrey" + }, + { + "_id": "America/Montevideo", + "name": "America/Montevideo" + }, + { + "_id": "America/Montserrat", + "name": "America/Montserrat" + }, + { + "_id": "America/Nassau", + "name": "America/Nassau" + }, + { + "_id": "America/New_York", + "name": "America/New_York" + }, + { + "_id": "America/Nipigon", + "name": "America/Nipigon" + }, + { + "_id": "America/Nome", + "name": "America/Nome" + }, + { + "_id": "America/Noronha", + "name": "America/Noronha" + }, + { + "_id": "America/North_Dakota/Beulah", + "name": "America/North_Dakota/Beulah" + }, + { + "_id": "America/North_Dakota/Center", + "name": "America/North_Dakota/Center" + }, + { + "_id": "America/North_Dakota/New_Salem", + "name": "America/North_Dakota/New_Salem" + }, + { + "_id": "America/Ojinaga", + "name": "America/Ojinaga" + }, + { + "_id": "America/Panama", + "name": "America/Panama" + }, + { + "_id": "America/Pangnirtung", + "name": "America/Pangnirtung" + }, + { + "_id": "America/Paramaribo", + "name": "America/Paramaribo" + }, + { + "_id": "America/Phoenix", + "name": "America/Phoenix" + }, + { + "_id": "America/Port_of_Spain", + "name": "America/Port_of_Spain" + }, + { + "_id": "America/Port-au-Prince", + "name": "America/Port-au-Prince" + }, + { + "_id": "America/Porto_Velho", + "name": "America/Porto_Velho" + }, + { + "_id": "America/Puerto_Rico", + "name": "America/Puerto_Rico" + }, + { + "_id": "America/Punta_Arenas", + "name": "America/Punta_Arenas" + }, + { + "_id": "America/Rainy_River", + "name": "America/Rainy_River" + }, + { + "_id": "America/Rankin_Inlet", + "name": "America/Rankin_Inlet" + }, + { + "_id": "America/Recife", + "name": "America/Recife" + }, + { + "_id": "America/Regina", + "name": "America/Regina" + }, + { + "_id": "America/Resolute", + "name": "America/Resolute" + }, + { + "_id": "America/Rio_Branco", + "name": "America/Rio_Branco" + }, + { + "_id": "America/Santarem", + "name": "America/Santarem" + }, + { + "_id": "America/Santiago", + "name": "America/Santiago" + }, + { + "_id": "America/Santo_Domingo", + "name": "America/Santo_Domingo" + }, + { + "_id": "America/Sao_Paulo", + "name": "America/Sao_Paulo" + }, + { + "_id": "America/Scoresbysund", + "name": "America/Scoresbysund" + }, + { + "_id": "America/Sitka", + "name": "America/Sitka" + }, + { + "_id": "America/St_Barthelemy", + "name": "America/St_Barthelemy" + }, + { + "_id": "America/St_Johns", + "name": "America/St_Johns" + }, + { + "_id": "America/St_Kitts", + "name": "America/St_Kitts" + }, + { + "_id": "America/St_Lucia", + "name": "America/St_Lucia" + }, + { + "_id": "America/St_Thomas", + "name": "America/St_Thomas" + }, + { + "_id": "America/St_Vincent", + "name": "America/St_Vincent" + }, + { + "_id": "America/Swift_Current", + "name": "America/Swift_Current" + }, + { + "_id": "America/Tegucigalpa", + "name": "America/Tegucigalpa" + }, + { + "_id": "America/Thule", + "name": "America/Thule" + }, + { + "_id": "America/Thunder_Bay", + "name": "America/Thunder_Bay" + }, + { + "_id": "America/Tijuana", + "name": "America/Tijuana" + }, + { + "_id": "America/Toronto", + "name": "America/Toronto" + }, + { + "_id": "America/Tortola", + "name": "America/Tortola" + }, + { + "_id": "America/Vancouver", + "name": "America/Vancouver" + }, + { + "_id": "America/Whitehorse", + "name": "America/Whitehorse" + }, + { + "_id": "America/Winnipeg", + "name": "America/Winnipeg" + }, + { + "_id": "America/Yakutat", + "name": "America/Yakutat" + }, + { + "_id": "America/Yellowknife", + "name": "America/Yellowknife" + }, + { + "_id": "Antarctica/Casey", + "name": "Antarctica/Casey" + }, + { + "_id": "Antarctica/Davis", + "name": "Antarctica/Davis" + }, + { + "_id": "Antarctica/DumontDUrville", + "name": "Antarctica/DumontDUrville" + }, + { + "_id": "Antarctica/Macquarie", + "name": "Antarctica/Macquarie" + }, + { + "_id": "Antarctica/Mawson", + "name": "Antarctica/Mawson" + }, + { + "_id": "Antarctica/McMurdo", + "name": "Antarctica/McMurdo" + }, + { + "_id": "Antarctica/Palmer", + "name": "Antarctica/Palmer" + }, + { + "_id": "Antarctica/Rothera", + "name": "Antarctica/Rothera" + }, + { + "_id": "Antarctica/Syowa", + "name": "Antarctica/Syowa" + }, + { + "_id": "Antarctica/Troll", + "name": "Antarctica/Troll" + }, + { + "_id": "Antarctica/Vostok", + "name": "Antarctica/Vostok" + }, + { + "_id": "Arctic/Longyearbyen", + "name": "Arctic/Longyearbyen" + }, + { + "_id": "Asia/Aden", + "name": "Asia/Aden" + }, + { + "_id": "Asia/Almaty", + "name": "Asia/Almaty" + }, + { + "_id": "Asia/Amman", + "name": "Asia/Amman" + }, + { + "_id": "Asia/Anadyr", + "name": "Asia/Anadyr" + }, + { + "_id": "Asia/Aqtau", + "name": "Asia/Aqtau" + }, + { + "_id": "Asia/Aqtobe", + "name": "Asia/Aqtobe" + }, + { + "_id": "Asia/Ashgabat", + "name": "Asia/Ashgabat" + }, + { + "_id": "Asia/Atyrau", + "name": "Asia/Atyrau" + }, + { + "_id": "Asia/Baghdad", + "name": "Asia/Baghdad" + }, + { + "_id": "Asia/Bahrain", + "name": "Asia/Bahrain" + }, + { + "_id": "Asia/Baku", + "name": "Asia/Baku" + }, + { + "_id": "Asia/Bangkok", + "name": "Asia/Bangkok" + }, + { + "_id": "Asia/Barnaul", + "name": "Asia/Barnaul" + }, + { + "_id": "Asia/Beirut", + "name": "Asia/Beirut" + }, + { + "_id": "Asia/Bishkek", + "name": "Asia/Bishkek" + }, + { + "_id": "Asia/Brunei", + "name": "Asia/Brunei" + }, + { + "_id": "Asia/Chita", + "name": "Asia/Chita" + }, + { + "_id": "Asia/Choibalsan", + "name": "Asia/Choibalsan" + }, + { + "_id": "Asia/Colombo", + "name": "Asia/Colombo" + }, + { + "_id": "Asia/Damascus", + "name": "Asia/Damascus" + }, + { + "_id": "Asia/Dhaka", + "name": "Asia/Dhaka" + }, + { + "_id": "Asia/Dili", + "name": "Asia/Dili" + }, + { + "_id": "Asia/Dubai", + "name": "Asia/Dubai" + }, + { + "_id": "Asia/Dushanbe", + "name": "Asia/Dushanbe" + }, + { + "_id": "Asia/Famagusta", + "name": "Asia/Famagusta" + }, + { + "_id": "Asia/Gaza", + "name": "Asia/Gaza" + }, + { + "_id": "Asia/Hebron", + "name": "Asia/Hebron" + }, + { + "_id": "Asia/Ho_Chi_Minh", + "name": "Asia/Ho_Chi_Minh" + }, + { + "_id": "Asia/Hong_Kong", + "name": "Asia/Hong_Kong" + }, + { + "_id": "Asia/Hovd", + "name": "Asia/Hovd" + }, + { + "_id": "Asia/Irkutsk", + "name": "Asia/Irkutsk" + }, + { + "_id": "Asia/Jakarta", + "name": "Asia/Jakarta" + }, + { + "_id": "Asia/Jayapura", + "name": "Asia/Jayapura" + }, + { + "_id": "Asia/Jerusalem", + "name": "Asia/Jerusalem" + }, + { + "_id": "Asia/Kabul", + "name": "Asia/Kabul" + }, + { + "_id": "Asia/Kamchatka", + "name": "Asia/Kamchatka" + }, + { + "_id": "Asia/Karachi", + "name": "Asia/Karachi" + }, + { + "_id": "Asia/Kathmandu", + "name": "Asia/Kathmandu" + }, + { + "_id": "Asia/Khandyga", + "name": "Asia/Khandyga" + }, + { + "_id": "Asia/Kolkata", + "name": "Asia/Kolkata" + }, + { + "_id": "Asia/Krasnoyarsk", + "name": "Asia/Krasnoyarsk" + }, + { + "_id": "Asia/Kuala_Lumpur", + "name": "Asia/Kuala_Lumpur" + }, + { + "_id": "Asia/Kuching", + "name": "Asia/Kuching" + }, + { + "_id": "Asia/Kuwait", + "name": "Asia/Kuwait" + }, + { + "_id": "Asia/Macau", + "name": "Asia/Macau" + }, + { + "_id": "Asia/Magadan", + "name": "Asia/Magadan" + }, + { + "_id": "Asia/Makassar", + "name": "Asia/Makassar" + }, + { + "_id": "Asia/Manila", + "name": "Asia/Manila" + }, + { + "_id": "Asia/Muscat", + "name": "Asia/Muscat" + }, + { + "_id": "Asia/Nicosia", + "name": "Asia/Nicosia" + }, + { + "_id": "Asia/Novokuznetsk", + "name": "Asia/Novokuznetsk" + }, + { + "_id": "Asia/Novosibirsk", + "name": "Asia/Novosibirsk" + }, + { + "_id": "Asia/Omsk", + "name": "Asia/Omsk" + }, + { + "_id": "Asia/Oral", + "name": "Asia/Oral" + }, + { + "_id": "Asia/Phnom_Penh", + "name": "Asia/Phnom_Penh" + }, + { + "_id": "Asia/Pontianak", + "name": "Asia/Pontianak" + }, + { + "_id": "Asia/Pyongyang", + "name": "Asia/Pyongyang" + }, + { + "_id": "Asia/Qatar", + "name": "Asia/Qatar" + }, + { + "_id": "Asia/Qostanay", + "name": "Asia/Qostanay" + }, + { + "_id": "Asia/Qyzylorda", + "name": "Asia/Qyzylorda" + }, + { + "_id": "Asia/Riyadh", + "name": "Asia/Riyadh" + }, + { + "_id": "Asia/Sakhalin", + "name": "Asia/Sakhalin" + }, + { + "_id": "Asia/Samarkand", + "name": "Asia/Samarkand" + }, + { + "_id": "Asia/Seoul", + "name": "Asia/Seoul" + }, + { + "_id": "Asia/Shanghai", + "name": "Asia/Shanghai" + }, + { + "_id": "Asia/Singapore", + "name": "Asia/Singapore" + }, + { + "_id": "Asia/Srednekolymsk", + "name": "Asia/Srednekolymsk" + }, + { + "_id": "Asia/Taipei", + "name": "Asia/Taipei" + }, + { + "_id": "Asia/Tashkent", + "name": "Asia/Tashkent" + }, + { + "_id": "Asia/Tbilisi", + "name": "Asia/Tbilisi" + }, + { + "_id": "Asia/Tehran", + "name": "Asia/Tehran" + }, + { + "_id": "Asia/Thimphu", + "name": "Asia/Thimphu" + }, + { + "_id": "Asia/Tokyo", + "name": "Asia/Tokyo" + }, + { + "_id": "Asia/Tomsk", + "name": "Asia/Tomsk" + }, + { + "_id": "Asia/Ulaanbaatar", + "name": "Asia/Ulaanbaatar" + }, + { + "_id": "Asia/Urumqi", + "name": "Asia/Urumqi" + }, + { + "_id": "Asia/Ust-Nera", + "name": "Asia/Ust-Nera" + }, + { + "_id": "Asia/Vientiane", + "name": "Asia/Vientiane" + }, + { + "_id": "Asia/Vladivostok", + "name": "Asia/Vladivostok" + }, + { + "_id": "Asia/Yakutsk", + "name": "Asia/Yakutsk" + }, + { + "_id": "Asia/Yangon", + "name": "Asia/Yangon" + }, + { + "_id": "Asia/Yekaterinburg", + "name": "Asia/Yekaterinburg" + }, + { + "_id": "Asia/Yerevan", + "name": "Asia/Yerevan" + }, + { + "_id": "Atlantic/Azores", + "name": "Atlantic/Azores" + }, + { + "_id": "Atlantic/Bermuda", + "name": "Atlantic/Bermuda" + }, + { + "_id": "Atlantic/Canary", + "name": "Atlantic/Canary" + }, + { + "_id": "Atlantic/Cape_Verde", + "name": "Atlantic/Cape_Verde" + }, + { + "_id": "Atlantic/Faroe", + "name": "Atlantic/Faroe" + }, + { + "_id": "Atlantic/Madeira", + "name": "Atlantic/Madeira" + }, + { + "_id": "Atlantic/Reykjavik", + "name": "Atlantic/Reykjavik" + }, + { + "_id": "Atlantic/South_Georgia", + "name": "Atlantic/South_Georgia" + }, + { + "_id": "Atlantic/St_Helena", + "name": "Atlantic/St_Helena" + }, + { + "_id": "Atlantic/Stanley", + "name": "Atlantic/Stanley" + }, + { + "_id": "Australia/Adelaide", + "name": "Australia/Adelaide" + }, + { + "_id": "Australia/Brisbane", + "name": "Australia/Brisbane" + }, + { + "_id": "Australia/Broken_Hill", + "name": "Australia/Broken_Hill" + }, + { + "_id": "Australia/Currie", + "name": "Australia/Currie" + }, + { + "_id": "Australia/Darwin", + "name": "Australia/Darwin" + }, + { + "_id": "Australia/Eucla", + "name": "Australia/Eucla" + }, + { + "_id": "Australia/Hobart", + "name": "Australia/Hobart" + }, + { + "_id": "Australia/Lindeman", + "name": "Australia/Lindeman" + }, + { + "_id": "Australia/Lord_Howe", + "name": "Australia/Lord_Howe" + }, + { + "_id": "Australia/Melbourne", + "name": "Australia/Melbourne" + }, + { + "_id": "Australia/Perth", + "name": "Australia/Perth" + }, + { + "_id": "Australia/Sydney", + "name": "Australia/Sydney" + }, + { + "_id": "Europe/Amsterdam", + "name": "Europe/Amsterdam" + }, + { + "_id": "Europe/Andorra", + "name": "Europe/Andorra" + }, + { + "_id": "Europe/Astrakhan", + "name": "Europe/Astrakhan" + }, + { + "_id": "Europe/Athens", + "name": "Europe/Athens" + }, + { + "_id": "Europe/Belgrade", + "name": "Europe/Belgrade" + }, + { + "_id": "Europe/Berlin", + "name": "Europe/Berlin" + }, + { + "_id": "Europe/Bratislava", + "name": "Europe/Bratislava" + }, + { + "_id": "Europe/Brussels", + "name": "Europe/Brussels" + }, + { + "_id": "Europe/Bucharest", + "name": "Europe/Bucharest" + }, + { + "_id": "Europe/Budapest", + "name": "Europe/Budapest" + }, + { + "_id": "Europe/Chisinau", + "name": "Europe/Chisinau" + }, + { + "_id": "Europe/Copenhagen", + "name": "Europe/Copenhagen" + }, + { + "_id": "Europe/Dublin", + "name": "Europe/Dublin" + }, + { + "_id": "Europe/Gibraltar", + "name": "Europe/Gibraltar" + }, + { + "_id": "Europe/Guernsey", + "name": "Europe/Guernsey" + }, + { + "_id": "Europe/Helsinki", + "name": "Europe/Helsinki" + }, + { + "_id": "Europe/Isle_of_Man", + "name": "Europe/Isle_of_Man" + }, + { + "_id": "Europe/Istanbul", + "name": "Europe/Istanbul" + }, + { + "_id": "Europe/Jersey", + "name": "Europe/Jersey" + }, + { + "_id": "Europe/Kaliningrad", + "name": "Europe/Kaliningrad" + }, + { + "_id": "Europe/Kirov", + "name": "Europe/Kirov" + }, + { + "_id": "Europe/Kyiv", + "name": "Europe/Kyiv" + }, + { + "_id": "Europe/Lisbon", + "name": "Europe/Lisbon" + }, + { + "_id": "Europe/Ljubljana", + "name": "Europe/Ljubljana" + }, + { + "_id": "Europe/London", + "name": "Europe/London" + }, + { + "_id": "Europe/Luxembourg", + "name": "Europe/Luxembourg" + }, + { + "_id": "Europe/Madrid", + "name": "Europe/Madrid" + }, + { + "_id": "Europe/Malta", + "name": "Europe/Malta" + }, + { + "_id": "Europe/Mariehamn", + "name": "Europe/Mariehamn" + }, + { + "_id": "Europe/Minsk", + "name": "Europe/Minsk" + }, + { + "_id": "Europe/Monaco", + "name": "Europe/Monaco" + }, + { + "_id": "Europe/Moscow", + "name": "Europe/Moscow" + }, + { + "_id": "Europe/Oslo", + "name": "Europe/Oslo" + }, + { + "_id": "Europe/Paris", + "name": "Europe/Paris" + }, + { + "_id": "Europe/Podgorica", + "name": "Europe/Podgorica" + }, + { + "_id": "Europe/Prague", + "name": "Europe/Prague" + }, + { + "_id": "Europe/Riga", + "name": "Europe/Riga" + }, + { + "_id": "Europe/Rome", + "name": "Europe/Rome" + }, + { + "_id": "Europe/Samara", + "name": "Europe/Samara" + }, + { + "_id": "Europe/San_Marino", + "name": "Europe/San_Marino" + }, + { + "_id": "Europe/Sarajevo", + "name": "Europe/Sarajevo" + }, + { + "_id": "Europe/Saratov", + "name": "Europe/Saratov" + }, + { + "_id": "Europe/Simferopol", + "name": "Europe/Simferopol" + }, + { + "_id": "Europe/Skopje", + "name": "Europe/Skopje" + }, + { + "_id": "Europe/Sofia", + "name": "Europe/Sofia" + }, + { + "_id": "Europe/Stockholm", + "name": "Europe/Stockholm" + }, + { + "_id": "Europe/Tallinn", + "name": "Europe/Tallinn" + }, + { + "_id": "Europe/Tirane", + "name": "Europe/Tirane" + }, + { + "_id": "Europe/Ulyanovsk", + "name": "Europe/Ulyanovsk" + }, + { + "_id": "Europe/Uzhgorod", + "name": "Europe/Uzhgorod" + }, + { + "_id": "Europe/Vaduz", + "name": "Europe/Vaduz" + }, + { + "_id": "Europe/Vatican", + "name": "Europe/Vatican" + }, + { + "_id": "Europe/Vienna", + "name": "Europe/Vienna" + }, + { + "_id": "Europe/Vilnius", + "name": "Europe/Vilnius" + }, + { + "_id": "Europe/Volgograd", + "name": "Europe/Volgograd" + }, + { + "_id": "Europe/Warsaw", + "name": "Europe/Warsaw" + }, + { + "_id": "Europe/Zagreb", + "name": "Europe/Zagreb" + }, + { + "_id": "Europe/Zaporizhzhia", + "name": "Europe/Zaporizhzhia" + }, + { + "_id": "Europe/Zurich", + "name": "Europe/Zurich" + }, + { + "_id": "Indian/Antananarivo", + "name": "Indian/Antananarivo" + }, + { + "_id": "Indian/Chagos", + "name": "Indian/Chagos" + }, + { + "_id": "Indian/Christmas", + "name": "Indian/Christmas" + }, + { + "_id": "Indian/Cocos", + "name": "Indian/Cocos" + }, + { + "_id": "Indian/Comoro", + "name": "Indian/Comoro" + }, + { + "_id": "Indian/Kerguelen", + "name": "Indian/Kerguelen" + }, + { + "_id": "Indian/Mahe", + "name": "Indian/Mahe" + }, + { + "_id": "Indian/Maldives", + "name": "Indian/Maldives" + }, + { + "_id": "Indian/Mauritius", + "name": "Indian/Mauritius" + }, + { + "_id": "Indian/Mayotte", + "name": "Indian/Mayotte" + }, + { + "_id": "Indian/Reunion", + "name": "Indian/Reunion" + }, + { + "_id": "Pacific/Apia", + "name": "Pacific/Apia" + }, + { + "_id": "Pacific/Auckland", + "name": "Pacific/Auckland" + }, + { + "_id": "Pacific/Bougainville", + "name": "Pacific/Bougainville" + }, + { + "_id": "Pacific/Chatham", + "name": "Pacific/Chatham" + }, + { + "_id": "Pacific/Chuuk", + "name": "Pacific/Chuuk" + }, + { + "_id": "Pacific/Easter", + "name": "Pacific/Easter" + }, + { + "_id": "Pacific/Efate", + "name": "Pacific/Efate" + }, + { + "_id": "Pacific/Enderbury", + "name": "Pacific/Enderbury" + }, + { + "_id": "Pacific/Fakaofo", + "name": "Pacific/Fakaofo" + }, + { + "_id": "Pacific/Fiji", + "name": "Pacific/Fiji" + }, + { + "_id": "Pacific/Funafuti", + "name": "Pacific/Funafuti" + }, + { + "_id": "Pacific/Galapagos", + "name": "Pacific/Galapagos" + }, + { + "_id": "Pacific/Gambier", + "name": "Pacific/Gambier" + }, + { + "_id": "Pacific/Guadalcanal", + "name": "Pacific/Guadalcanal" + }, + { + "_id": "Pacific/Guam", + "name": "Pacific/Guam" + }, + { + "_id": "Pacific/Honolulu", + "name": "Pacific/Honolulu" + }, + { + "_id": "Pacific/Kiritimati", + "name": "Pacific/Kiritimati" + }, + { + "_id": "Pacific/Kosrae", + "name": "Pacific/Kosrae" + }, + { + "_id": "Pacific/Kwajalein", + "name": "Pacific/Kwajalein" + }, + { + "_id": "Pacific/Majuro", + "name": "Pacific/Majuro" + }, + { + "_id": "Pacific/Marquesas", + "name": "Pacific/Marquesas" + }, + { + "_id": "Pacific/Midway", + "name": "Pacific/Midway" + }, + { + "_id": "Pacific/Nauru", + "name": "Pacific/Nauru" + }, + { + "_id": "Pacific/Niue", + "name": "Pacific/Niue" + }, + { + "_id": "Pacific/Norfolk", + "name": "Pacific/Norfolk" + }, + { + "_id": "Pacific/Noumea", + "name": "Pacific/Noumea" + }, + { + "_id": "Pacific/Pago_Pago", + "name": "Pacific/Pago_Pago" + }, + { + "_id": "Pacific/Palau", + "name": "Pacific/Palau" + }, + { + "_id": "Pacific/Pitcairn", + "name": "Pacific/Pitcairn" + }, + { + "_id": "Pacific/Pohnpei", + "name": "Pacific/Pohnpei" + }, + { + "_id": "Pacific/Port_Moresby", + "name": "Pacific/Port_Moresby" + }, + { + "_id": "Pacific/Rarotonga", + "name": "Pacific/Rarotonga" + }, + { + "_id": "Pacific/Saipan", + "name": "Pacific/Saipan" + }, + { + "_id": "Pacific/Tahiti", + "name": "Pacific/Tahiti" + }, + { + "_id": "Pacific/Tarawa", + "name": "Pacific/Tarawa" + }, + { + "_id": "Pacific/Tongatapu", + "name": "Pacific/Tongatapu" + }, + { + "_id": "Pacific/Wake", + "name": "Pacific/Wake" + }, + { + "_id": "Pacific/Wallis", + "name": "Pacific/Wallis" + } +] diff --git a/client/src/validation/zod.ts b/client/src/validation/zod.ts new file mode 100644 index 000000000..6981eb498 --- /dev/null +++ b/client/src/validation/zod.ts @@ -0,0 +1,274 @@ +import { z } from "zod"; +import humanInterval from "human-interval"; +import { ChannelTypes } from "@/types/notification-channel"; +import { MaintenanceRepeats } from "@/types/maintenance"; + +export const registerSchema = z + .object({ + email: z.email({ message: "Invalid email address" }), + firstName: z.string().min(1, { message: "First Name is required" }), + lastName: z.string().min(1, { message: "Last Name is required" }), + password: z + .string() + .min(6, { message: "Password must be at least 6 characters" }), + confirmPassword: z + .string() + .min(6, { message: "Confirm Password must be at least 6 characters" }), + }) + .refine((data) => data.password === data.confirmPassword, { + path: ["confirmPassword"], + message: "Passwords must match", + }); + +const urlRegex = + /^(https?:\/\/)?(localhost|[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?$/; + +const durationSchema = z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val || val.trim() === "") return; + const ms = humanInterval(val); + + if (!ms || isNaN(ms)) { + ctx.addIssue({ + code: "custom", + message: "Invalid duration format", + }); + } else if (ms < 10000) { + ctx.addIssue({ + code: "custom", + message: "Minimum duration is 10 seconds", + }); + } + }); +const durationSchemaPageSpeed = z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val || val.trim() === "") return; + const ms = humanInterval(val); + + if (!ms || isNaN(ms)) { + ctx.addIssue({ + code: "custom", + message: "Invalid duration format", + }); + } else if (ms < 180000) { + ctx.addIssue({ + code: "custom", + message: "Minimum duration is 3 minutes", + }); + } + }); +const durationSchemaInfra = z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val || val.trim() === "") return; + const ms = humanInterval(val); + + if (!ms || isNaN(ms)) { + ctx.addIssue({ + code: "custom", + message: "Invalid duration format", + }); + } else if (ms < 60000) { + ctx.addIssue({ + code: "custom", + message: "Minimum duration is 1 minute", + }); + } + }); + +export const monitorSchema = z.object({ + type: z.string().min(1, "You must select an option"), + url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"), + n: z.coerce + .number({ message: "Number required" }) + .min(1, "Minimum value is 1") + .max(25, "Maximum value is 25"), + notificationChannels: z.array(z.string()).optional().default([]), + name: z.string().min(1, "Display name is required"), + interval: durationSchema, +}); + +export const monitorSchemaPageSpeed = z.object({ + type: z.string().min(1, "You must select an option"), + url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"), + n: z.coerce + .number({ message: "Number required" }) + .min(1, "Minimum value is 1") + .max(25, "Maximum value is 25"), + notificationChannels: z.array(z.string()).optional().default([]), + name: z.string().min(1, "Display name is required"), + interval: durationSchemaPageSpeed, +}); +export const monitorSchemaInfra = z.object({ + type: z.string().min(1, "You must select an option"), + url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"), + secret: z.string().min(1, "Secret is required"), + n: z.coerce + .number({ message: "Number required" }) + .min(1, "Minimum value is 1") + .max(25, "Maximum value is 25"), + notificationChannels: z.array(z.string()).optional().default([]), + name: z.string().min(1, "Display name is required"), + interval: durationSchemaInfra, +}); + +export const teamSchema = z.object({ + name: z.string().min(1, "Team name is required"), + roleId: z.string().min(1, "Role is required"), + description: z.string().optional(), +}); + +export const teamMemberSchema = z.object({ + userId: z.string().min(1, "User is required"), + roleId: z.string().min(1, "Role is required"), + teamId: z.string().min(1, "Team is required"), +}); + +export const inviteSchema = z.object({ + email: z.email("Invalid email address"), + teamId: z.string().min(1, "Team is required"), + teamRoleId: z.string().min(1, "Role is required"), + orgRoleId: z.string().optional(), +}); + +export const notificationChannelSchema = z + .object({ + name: z.string().min(1, "Channel name is required"), + type: z.enum(ChannelTypes, { error: "Invalid channel type" }), + config: z.object({ + url: z + .string() + .regex(urlRegex, "Invalid URL") + .or(z.literal("")) + .optional(), + emailAddress: z + .email("Invalid email address") + .or(z.literal("")) + .optional(), + }), + }) + .superRefine((data, ctx) => { + const { type, config } = data; + if (!config.url && !config.emailAddress) { + ctx.addIssue({ + code: "custom", + message: "Either a URL or an email address must be provided.", + path: ["config"], + }); + } + + if (type === "email" && !config.emailAddress) { + ctx.addIssue({ + code: "custom", + message: "Email address is required for email-type channels.", + path: ["config", "emailAddress"], + }); + } else if (type !== "email" && !config.url) { + ctx.addIssue({ + code: "custom", + message: "URL is required for non-email-type channels.", + path: ["config", "url"], + }); + } + }); + +export const maintenanceSchema = z + .object({ + name: z.string().min(1, "Maintenance name is required"), + repeat: z.enum(MaintenanceRepeats), + startTime: z.date(), + endTime: z.date(), + monitors: z.array(z.string()), + }) + .superRefine((data, ctx) => { + if (data.endTime <= data.startTime) { + ctx.addIssue({ + code: "custom", + path: ["endTime"], + message: "End time must be after start time", + }); + } + + if (data.startTime === data.endTime) { + ctx.addIssue({ + code: "custom", + path: ["endTime"], + message: "Start time and end time cannot be the same", + }); + } + }); + +const statusPageUrlRegex = /^[A-Za-z0-9]+$/; + +export const statusPageSchema = z.object({ + name: z.string().min(1, "Status page name is required"), + description: z.string().optional(), + url: z + .string() + .min(1, "URL is required") + .regex(statusPageUrlRegex, "Invalid URL"), + isPublished: z.boolean().optional(), + monitors: z.array(z.string()), +}); + +export const recoverySchema = z.object({ + email: z.email({ message: "Invalid email address" }).trim().toLowerCase(), +}); + +export const resetSchema = z + .object({ + password: z + .string() + .min(6, { message: "Password must be at least 6 characters" }), + confirmPassword: z + .string() + .min(6, { message: "Confirm Password must be at least 6 characters" }), + }) + .refine((data) => data.password === data.confirmPassword, { + path: ["confirmPassword"], + message: "Passwords must match", + }); + +export const profileSchema = z + .object({ + firstName: z + .string() + .min(1, { message: "First Name must be at least one char" }) + .optional(), + lastName: z + .string() + .min(1, { message: "Last Name must be at least one char" }) + .optional(), + password: z.preprocess( + (val) => (val === "" ? undefined : val), + z + .string() + .min(6, { message: "Password must be at least 6 characters" }) + .optional() + ), + confirmPassword: z.preprocess( + (val) => (val === "" ? undefined : val), + z + .string() + .min(6, { message: "Confirm Password must be at least 6 characters" }) + .optional() + ), + }) + .refine( + (data) => { + if (!data.password && !data.confirmPassword) return true; + + if (!data.password || !data.confirmPassword) return false; + + return data.password === data.confirmPassword; + }, + { + message: "Passwords must match", + path: ["confirmPassword"], + } + ); diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index f44839e9f..c2e46765e 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -1,32 +1,30 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2022", - "useDefineForClassFields": true, - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", + /* 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, - "paths": { - "@/*": ["./src/*"] - }, - "allowJs": true, - "checkJs": false - }, - "include": ["src"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] } diff --git a/client/tsconfig.json b/client/tsconfig.json index 3b77ae840..6fcabc111 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,15 +1,16 @@ { - "files": [], - "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], - "compilerOptions": { - "types": ["node"], - "module": "ESNext", - "moduleResolution": "Bundler", - "skipLibCheck": true, - "paths": { - "@/*": ["./src/*"] - }, - "allowJs": true, - "checkJs": false - } + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "types": ["node"], + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "paths": { + "@/*": ["./src/*"] + } + } } diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json index bbd18d2b2..e5dd44903 100644 --- a/client/tsconfig.node.json +++ b/client/tsconfig.node.json @@ -1,27 +1,27 @@ { - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "ES2023", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": 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, - "allowJs": true, - "checkJs": false - }, - "include": ["vite.config.ts"] + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "allowJs": true, + "checkJs": false + }, + "include": ["vite.config.ts"] } diff --git a/client/tsconfig.tsbuildinfo b/client/tsconfig.tsbuildinfo deleted file mode 100644 index 685a9569a..000000000 --- a/client/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./vite.config.ts","./dist/assets/index-BtHE-Ph5.js","./src/App.jsx","./src/main.jsx","./src/store.js","./src/Components/LanguageSelector.jsx","./src/Components/ActionsMenu/index.jsx","./src/Components/Alert/index.jsx","./src/Components/Animated/PulseDot.jsx","./src/Components/ArrowLeft/index.jsx","./src/Components/ArrowRight/index.jsx","./src/Components/Avatar/index.jsx","./src/Components/Breadcrumbs/index.jsx","./src/Components/Buttons/RoundGradientButton.jsx","./src/Components/Charts/AreaChart/index.jsx","./src/Components/Charts/BarChart/index.jsx","./src/Components/Charts/ChartBox/EmptyView.jsx","./src/Components/Charts/ChartBox/index.jsx","./src/Components/Charts/CustomGauge/index.jsx","./src/Components/Charts/DePINStatusPageBarChart/index.jsx","./src/Components/Charts/LegendBox/index.jsx","./src/Components/Charts/MonitorDetailsAreaChart/index.jsx","./src/Components/Charts/StatusPageBarChart/index.jsx","./src/Components/Charts/Utils/chartUtilFunctions.js","./src/Components/Charts/Utils/chartUtils.jsx","./src/Components/Charts/Utils/gradientUtils.jsx","./src/Components/Check/Check.jsx","./src/Components/CircularCount/index.jsx","./src/Components/Common/AppBar.jsx","./src/Components/Common/Footer.jsx","./src/Components/ConfigBox/index.jsx","./src/Components/ConfigRow/index.jsx","./src/Components/Dialog/genericDialog.jsx","./src/Components/Dialog/index.jsx","./src/Components/Dot/index.jsx","./src/Components/Fallback/FallBackActionButtons.jsx","./src/Components/Fallback/FallbackBackground.jsx","./src/Components/Fallback/FallbackCheckList.jsx","./src/Components/Fallback/FallbackContainer.jsx","./src/Components/Fallback/FallbackPageSpeedWarning.jsx","./src/Components/Fallback/FallbackTitle.jsx","./src/Components/Fallback/index.jsx","./src/Components/FilterHeader/index.jsx","./src/Components/GenericFallback/NetworkError.jsx","./src/Components/GenericFallback/index.jsx","./src/Components/HOC/withAdminCheck.jsx","./src/Components/Heading/index.jsx","./src/Components/Host/index.jsx","./src/Components/HttpStatusLabel/index.jsx","./src/Components/I18nLoader/index.jsx","./src/Components/IconBox/index.jsx","./src/Components/Image/index.jsx","./src/Components/InfoBox/index.jsx","./src/Components/Inputs/Checkbox/index.jsx","./src/Components/Inputs/ColorPicker/index.jsx","./src/Components/Inputs/FieldWrapper/index.jsx","./src/Components/Inputs/ImageUpload/index.jsx","./src/Components/Inputs/Radio/index.jsx","./src/Components/Inputs/Search/index.jsx","./src/Components/Inputs/Select/index.jsx","./src/Components/Inputs/TextInput/index.jsx","./src/Components/Inputs/TextInput/indexV2.tsx","./src/Components/Inputs/TextInput/Adornments/index.jsx","./src/Components/Label/index.jsx","./src/Components/Layouts/AppLayout/index.jsx","./src/Components/Layouts/HomeLayout/index.jsx","./src/Components/Link/index.jsx","./src/Components/MonitorActions/index.jsx","./src/Components/MonitorCountHeader/index.jsx","./src/Components/MonitorCountHeader/skeleton.jsx","./src/Components/MonitorCreateHeader/index.jsx","./src/Components/MonitorCreateHeader/skeleton.jsx","./src/Components/MonitorDetailsControlHeader/index.jsx","./src/Components/MonitorDetailsControlHeader/skeleton.jsx","./src/Components/MonitorDetailsControlHeader/status.jsx","./src/Components/MonitorTimeFrameHeader/index.jsx","./src/Components/MonitorTimeFrameHeader/skeleton.jsx","./src/Components/NotificationConfig/index.jsx","./src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx","./src/Components/NotificationIntegrationModal/Components/TabComponent.jsx","./src/Components/NotificationIntegrationModal/Components/TabPanel.jsx","./src/Components/NotificationIntegrationModal/Hooks/useNotification.js","./src/Components/PageStateWrapper/index.jsx","./src/Components/ProgressBars/index.jsx","./src/Components/ProgressStepper/index.jsx","./src/Components/ProtectedRoute/index.jsx","./src/Components/RoleProtectedRoute/index.jsx","./src/Components/ShareComponent/index.jsx","./src/Components/Sidebar/index.jsx","./src/Components/Sidebar/components/authFooter.jsx","./src/Components/Sidebar/components/collapseButton.jsx","./src/Components/Sidebar/components/logo.jsx","./src/Components/Sidebar/components/navItem.jsx","./src/Components/Skeletons/FullPage/index.jsx","./src/Components/StandardContainer/index.jsx","./src/Components/StarPrompt/index.jsx","./src/Components/StatBox/index.jsx","./src/Components/StatusBoxes/index.jsx","./src/Components/StatusBoxes/skeleton.jsx","./src/Components/Subheader/index.jsx","./src/Components/Tab/index.jsx","./src/Components/Table/TableUtils.js","./src/Components/Table/index.jsx","./src/Components/Table/skeleton.jsx","./src/Components/Table/TablePagination/index.jsx","./src/Components/Table/TablePagination/Actions/index.jsx","./src/Components/TextLink/index.jsx","./src/Components/ThemeSwitch/SunAndMoonIcon.jsx","./src/Components/ThemeSwitch/index.jsx","./src/Components/Toast/body.jsx","./src/Components/Toast/index.jsx","./src/Components/WalletProvider/index.jsx","./src/Features/Auth/authSlice.js","./src/Features/UI/uiSlice.js","./src/Hooks/v1/checkHooks.js","./src/Hooks/v1/inviteHooks.js","./src/Hooks/v1/logHooks.js","./src/Hooks/v1/monitorHooks.js","./src/Hooks/v1/settingsHooks.js","./src/Hooks/v1/useIsAdmin.js","./src/Hooks/v1/useMonitorUtils.js","./src/Hooks/v1/useNotifications.js","./src/Hooks/v1/useSendTestEmail.js","./src/Hooks/v1/userHooks.js","./src/Hooks/v2/UseApi.tsx","./src/Pages/v1/ServerUnreachable.jsx","./src/Pages/v1/About/index.jsx","./src/Pages/v1/Account/index.jsx","./src/Pages/v1/Account/EditUser/index.jsx","./src/Pages/v1/Account/EditUser/hooks/editUser.js","./src/Pages/v1/Account/components/PasswordPanel.jsx","./src/Pages/v1/Account/components/ProfilePanel.jsx","./src/Pages/v1/Account/components/TeamPanel.jsx","./src/Pages/v1/Account/components/AddMemberMenu/index.jsx","./src/Pages/v1/Account/components/AddTeamMember/index.jsx","./src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx","./src/Pages/v1/Account/components/RoleTable/index.jsx","./src/Pages/v1/Auth/CheckEmail.jsx","./src/Pages/v1/Auth/ForgotPassword.jsx","./src/Pages/v1/Auth/NewPasswordConfirmed.jsx","./src/Pages/v1/Auth/SetNewPassword.jsx","./src/Pages/v1/Auth/Login/index.jsx","./src/Pages/v1/Auth/Login/hooks/useLoadingSubmit.jsx","./src/Pages/v1/Auth/Login/hooks/useLoginForm.jsx","./src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx","./src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx","./src/Pages/v1/Auth/Register/index.jsx","./src/Pages/v1/Auth/components/AuthHeader.jsx","./src/Pages/v1/Auth/components/AuthPageWrapper.jsx","./src/Pages/v1/Auth/components/PasswordTooltip.jsx","./src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx","./src/Pages/v1/Auth/hooks/useValidatePassword.jsx","./src/Pages/v1/Incidents/index.jsx","./src/Pages/v1/Incidents/Components/IncidentTable/index.jsx","./src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx","./src/Pages/v1/Incidents/Components/OptionsHeader/skeleton.jsx","./src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx","./src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx","./src/Pages/v1/Incidents/Components/StatusBoxes/skeleton.jsx","./src/Pages/v1/Infrastructure/Create/index.jsx","./src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx","./src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx","./src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx","./src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx","./src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx","./src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx","./src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx","./src/Pages/v1/Infrastructure/Details/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx","./src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx","./src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx","./src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx","./src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx","./src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx","./src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx","./src/Pages/v1/Infrastructure/Details/Components/NetworkStats/skeleton.jsx","./src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx","./src/Pages/v1/Infrastructure/Details/Hooks/useHardwareUtils.jsx","./src/Pages/v1/Infrastructure/Monitors/index.jsx","./src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx","./src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx","./src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx","./src/Pages/v1/Integrations/index.jsx","./src/Pages/v1/Logs/index.jsx","./src/Pages/v1/Logs/Diagnostics/index.jsx","./src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx","./src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx","./src/Pages/v1/Logs/Diagnostics/utils/utils.js","./src/Pages/v1/Logs/Logs/index.jsx","./src/Pages/v1/Logs/Queue/index.jsx","./src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx","./src/Pages/v1/Logs/Queue/components/JobTable/index.jsx","./src/Pages/v1/Logs/Queue/components/Metrics/index.jsx","./src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx","./src/Pages/v1/Maintenance/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx","./src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx","./src/Pages/v1/Maintenance/MaintenanceTable/index.jsx","./src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx","./src/Pages/v1/NotFound/index.jsx","./src/Pages/v1/Notifications/index.jsx","./src/Pages/v1/Notifications/utils.js","./src/Pages/v1/Notifications/components/ActionMenu.jsx","./src/Pages/v1/Notifications/create/index.jsx","./src/Pages/v1/PageSpeed/Create/index.jsx","./src/Pages/v1/PageSpeed/Create/skeleton.jsx","./src/Pages/v1/PageSpeed/Details/index.jsx","./src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx","./src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx","./src/Pages/v1/PageSpeed/Details/Components/Charts/PieChart.jsx","./src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx","./src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx","./src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx","./src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx","./src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx","./src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx","./src/Pages/v1/PageSpeed/Monitors/index.jsx","./src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx","./src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx","./src/Pages/v1/Settings/SettingsAbout.jsx","./src/Pages/v1/Settings/SettingsDemoMonitors.jsx","./src/Pages/v1/Settings/SettingsEmail.jsx","./src/Pages/v1/Settings/SettingsGlobalThresholds.jsx","./src/Pages/v1/Settings/SettingsPagespeed.jsx","./src/Pages/v1/Settings/SettingsStats.jsx","./src/Pages/v1/Settings/SettingsTimeZone.jsx","./src/Pages/v1/Settings/SettingsUI.jsx","./src/Pages/v1/Settings/SettingsURL.jsx","./src/Pages/v1/Settings/index.jsx","./src/Pages/v1/StatusPage/Create/index.jsx","./src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx","./src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx","./src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx","./src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx","./src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx","./src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx","./src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx","./src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx","./src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx","./src/Pages/v1/StatusPage/Status/index.jsx","./src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx","./src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx","./src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx","./src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx","./src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx","./src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx","./src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx","./src/Pages/v1/StatusPage/StatusPages/index.jsx","./src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx","./src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx","./src/Pages/v1/Uptime/BulkImport/Upload.jsx","./src/Pages/v1/Uptime/BulkImport/index.jsx","./src/Pages/v1/Uptime/Create/index.jsx","./src/Pages/v1/Uptime/Create/skeleton.jsx","./src/Pages/v1/Uptime/Details/index.jsx","./src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx","./src/Pages/v1/Uptime/Details/Components/ChartBoxes/skeleton.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx","./src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx","./src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx","./src/Pages/v1/Uptime/Details/Components/ResponseTable/skeleton.jsx","./src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx","./src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx","./src/Pages/v1/Uptime/Monitors/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/LoadingSpinner/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/Skeleton/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx","./src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx","./src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx","./src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx","./src/Pages/v1/Uptime/Monitors/Hooks/useDebounce.jsx","./src/Pages/v2/Auth/Login.tsx","./src/Routes/index.jsx","./src/Utils/ApiClient.ts","./src/Utils/Logger.js","./src/Utils/NetworkService.js","./src/Utils/NetworkServiceProvider.jsx","./src/Utils/debounce.jsx","./src/Utils/fileUtils.js","./src/Utils/greeting.jsx","./src/Utils/i18n.js","./src/Utils/monitorUtils.js","./src/Utils/roleUtils.js","./src/Utils/stringUtils.js","./src/Utils/timeUtils.js","./src/Utils/toastUtils.jsx","./src/Utils/utils.js","./src/Utils/Theme/constants.js","./src/Utils/Theme/darkTheme.js","./src/Utils/Theme/extractColorObject.js","./src/Utils/Theme/globalTheme.js","./src/Utils/Theme/lightTheme.js","./src/Utils/Theme/v2/palette.ts","./src/Utils/Theme/v2/theme.ts","./src/Validation/error.js","./src/Validation/validation.js","./src/types/env.d.ts","./src/types/mui.d.ts"],"errors":true,"version":"5.9.2"} \ No newline at end of file diff --git a/client/vite.config.ts b/client/vite.config.ts index 5a6b428c7..9bac9d0e3 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -6,22 +6,17 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -export default defineConfig(({}) => { - let version = "3.2.1"; - - return { - base: "/", - plugins: [svgr(), react()], - resolve: { - alias: { - "@": path.resolve(__dirname, "src"), - }, - }, - optimizeDeps: { - include: ["@mui/material/Tooltip", "@emotion/styled"], - }, - define: { - __APP_VERSION__: JSON.stringify(version), - }, - }; +export default defineConfig(() => { + return { + base: "/", + plugins: [svgr(), react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + optimizeDeps: { + include: ["@mui/material/Tooltip", "@emotion/styled"], + }, + }; }); diff --git a/server/.dockerignore b/server/.dockerignore deleted file mode 100755 index d88f9054e..000000000 --- a/server/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -./docker \ No newline at end of file diff --git a/server/.github/pull_request_template.md b/server/.github/pull_request_template.md deleted file mode 100755 index 4f4da5d80..000000000 --- a/server/.github/pull_request_template.md +++ /dev/null @@ -1,25 +0,0 @@ -**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)** - -## Describe your changes - -Briefly describe the changes you made and their purpose. - -## Write your issue number after "Fixes " - -Fixes #123 - -## Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks. - -- [ ] (Do not skip this or your PR will be closed) I deployed the application locally. -- [ ] (Do not skip this or your PR will be closed) I have performed a self-review and testing of my code. -- [ ] I have included the issue # in the PR. -- [ ] I have added i18n support to visible strings (instead of `
Add
`, use): - -```Javascript -const { t } = useTranslation(); -
{t('add')}
-``` - -- [ ] The issue I am working on is assigned to me. -- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). -- [ ] My PR is granular and targeted to one specific feature. diff --git a/server/.github/workflows/staging-deploy.yml b/server/.github/workflows/staging-deploy.yml deleted file mode 100755 index 8fadf12d4..000000000 --- a/server/.github/workflows/staging-deploy.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Staging deploy - -on: - push: - branches: ["develop"] - -jobs: - docker-build-and-push: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-backend:staging \ - -f ./docker/staging/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate-backend \ - . - - - name: Push Server Docker image - run: docker push ghcr.io/bluewave-labs/checkmate-backend:staging - - - name: Build Mongo Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-mongo:staging \ - -f ./docker/staging/mongoDB.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate-backend \ - . - - - name: Push MongoDB Docker image - run: docker push ghcr.io/bluewave-labs/checkmate-mongo:staging - - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-redis:staging \ - -f ./docker/staging/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate-backend \ - . - - - name: Push Redis Docker image - run: docker push ghcr.io/bluewave-labs/checkmate-redis:staging - - deploy-to-staging: - needs: docker-build-and-push - runs-on: ubuntu-latest - steps: - - name: SSH into server and restart container using Docker Compose - uses: appleboy/ssh-action@v1.0.0 - with: - host: ${{ secrets.STAGING_SERVER_HOST }} - username: ${{ secrets.STAGING_SERVER_USER }} - key: ${{ secrets.STAGING_SERVER_SSH_KEY }} - script: | - cd checkmate/server/docker/staging - docker compose down - docker compose pull - docker compose up -d diff --git a/server/.gitignore b/server/.gitignore old mode 100755 new mode 100644 index 9ae4e58ed..c82404bbe --- a/server/.gitignore +++ b/server/.gitignore @@ -1,12 +1,15 @@ -node_modules +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnp/ +.pnp.js .env -*.log -*.sh -!scripts/inject-vars.sh -.nyc_output -coverage -.clinic -node_modules -.vscode/* -public -dist \ No newline at end of file +.DS_Store +/logs/ + +# TypeScript +/dist/ +/build/ +*.tsbuildinfo \ No newline at end of file diff --git a/server/.mocharc.cjs b/server/.mocharc.cjs deleted file mode 100755 index e4c8f0dd7..000000000 --- a/server/.mocharc.cjs +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - require: ["esm", "chai/register-expect.js"], // Include Chai's "expect" interface globally - spec: "tests/**/*.test.js", // Specify test files - timeout: 5000, // Set test-case timeout in milliseconds - recursive: true, // Include subdirectories - reporter: "spec", // Use the "spec" reporter - exit: true, // Force Mocha to quit after tests complete -}; diff --git a/server/.nycrc b/server/.nycrc deleted file mode 100755 index 96f20d795..000000000 --- a/server/.nycrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "all": true, - "include": ["controllers/*.js", "utils/*.js", "service/*.js", "db/mongo/modules/*.js"], - "exclude": ["**/*.test.js"], - "reporter": ["html", "text", "lcov"], - "sourceMap": false, - "instrument": true -} diff --git a/server/.prettierrc b/server/.prettierrc deleted file mode 100755 index 681669aa7..000000000 --- a/server/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "printWidth": 150, - "useTabs": true, - "tabWidth": 2, - "singleQuote": false, - "bracketSpacing": true, - "proseWrap": "preserve", - "bracketSameLine": false, - "singleAttributePerLine": true, - "semi": true, - "jsxSingleQuote": false, - "quoteProps": "as-needed", - "arrowParens": "always", - "trailingComma": "es5", - "htmlWhitespaceSensitivity": "css" -} diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 000000000..419998f5b --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,8 @@ +FROM node:20-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build +EXPOSE 52345 +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/server/LICENSE b/server/LICENSE deleted file mode 100755 index 4a43a1939..000000000 --- a/server/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) 2025 BlueWave Labs - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/server/README.md b/server/README.md deleted file mode 100755 index db609cb02..000000000 --- a/server/README.md +++ /dev/null @@ -1,15 +0,0 @@ -

Checkmate backend

- -

The backend service for Checkmate, an open source uptime and infrastructure monitoring application

- -This directory contains the **backend** of Checkmate, which handles data processing, storage, and API services for the Checkmate monitoring tool. The backend is responsible for managing uptime checks, handling real-time alerts, and storing historical monitoring data. - -Checkmate's backend is designed to be lightweight and scalable, ensuring reliable performance even with a high number of active monitors. - -## Installation & documentation - -For setup instructions, configuration details, and deployment guidelines, please visit our official documentation at [docs.checkmate.so](https://docs.checkmate.so). - -## Related repositories - -- [Capture Agent](https://github.com/bluewave-labs/capture) (Optional, provides additional server insights) diff --git a/server/eslint.config.js b/server/eslint.config.js deleted file mode 100755 index 13f1fc038..000000000 --- a/server/eslint.config.js +++ /dev/null @@ -1,30 +0,0 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import mochaPlugin from "eslint-plugin-mocha"; - -/* -Please do not forget to look at the latest eslint configurations and rules. -ESlint v9 configuration is different than v8. -"https://eslint.org/docs/latest/use/configure/" -*/ - -/** @type {import('eslint').Linter.Config[]} */ -export default [ - { - languageOptions: { - globals: { - ...globals.node, // Add Node.js globals - ...globals.chai, // Add Chai globals - }, - ecmaVersion: 2023, - sourceType: "module", - }, - }, - pluginJs.configs.recommended, // Core JS rules - mochaPlugin.configs.flat.recommended, // Mocha rules - { - rules: { - "mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing - }, - }, -]; diff --git a/server/nodemon.json b/server/nodemon.json old mode 100755 new mode 100644 index 389209fa7..687b6d4ed --- a/server/nodemon.json +++ b/server/nodemon.json @@ -1,5 +1,5 @@ { - "ignore": ["src/locales/*", "*.log", "node_modules/*"], - "watch": ["src/**/*.ts", "src/**/*.js", "*.json"], - "ext": "ts,js,json" + "ignore": ["node_modules/*"], + "watch": ["src/**/*.ts", "src/**/*.js", "*.json"], + "ext": "ts,js,json" } diff --git a/server/openapi.json b/server/openapi.json deleted file mode 100644 index 6cc2e7de0..000000000 --- a/server/openapi.json +++ /dev/null @@ -1,3381 +0,0 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "Checkmate API", - "summary": "Checkmate OpenAPI Specifications", - "description": "Checkmate is an open source monitoring tool used to track the operational status and performance of servers and websites. It regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.", - "contact": { - "name": "API Support", - "url": "mailto:support@bluewavelabs.ca", - "email": "support@bluewavelabs.ca" - }, - "license": { - "name": "AGPLv3", - "url": "https://github.com/bluewave-labs/checkmate/tree/HEAD/LICENSE" - }, - "version": "2.3" - }, - "servers": [ - { - "url": "http://localhost:{PORT}/{API_PATH}", - "description": "Local Development Server", - "variables": { - "PORT": { - "description": "API Port", - "enum": ["52345"], - "default": "52345" - }, - "API_PATH": { - "description": "API Base Path", - "enum": ["api/v1"], - "default": "api/v1" - } - } - }, - { - "url": "https://checkmate-demo.bluewavelabs.ca/{API_PATH}", - "description": "Checkmate Demo Server", - "variables": { - "API_PATH": { - "description": "API Base Path", - "enum": ["api/v1"], - "default": "api/v1" - } - } - } - ], - "tags": [ - { - "name": "auth", - "description": "Authentication and user management" - }, - { - "name": "invite", - "description": "Team invitation management" - }, - { - "name": "monitors", - "description": "Monitor management (uptime, page speed, hardware)" - }, - { - "name": "checks", - "description": "Monitor check results and history" - }, - { - "name": "maintenance-window", - "description": "Scheduled maintenance windows" - }, - { - "name": "queue", - "description": "Job queue management" - }, - { - "name": "status-page", - "description": "Public status page management" - }, - { - "name": "settings", - "description": "Application configuration settings" - }, - { - "name": "logs", - "description": "Application logs and diagnostics" - }, - { - "name": "notifications", - "description": "Notification integrations (email, slack, discord, etc.)" - }, - { - "name": "diagnostic", - "description": "System health and performance diagnostics" - } - ], - "paths": { - "/auth/register": { - "post": { - "tags": ["auth"], - "summary": "Register new user", - "description": "Register a new user account with profile information", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/RegisterRequest" - } - } - } - }, - "responses": { - "200": { - "description": "User registered successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthResponse" - } - } - } - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/auth/login": { - "post": { - "tags": ["auth"], - "summary": "User login", - "description": "Authenticate user with email and password", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Login successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthResponse" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/auth/refresh": { - "post": { - "tags": ["auth"], - "summary": "Refresh access token", - "description": "Generate new access token using refresh token", - "parameters": [ - { - "name": "x-refresh-token", - "in": "header", - "description": "Refresh token", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "authorization", - "in": "header", - "description": "Bearer token (old access token)", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Token refreshed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthResponse" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/auth/user/{userId}": { - "put": { - "tags": ["auth"], - "summary": "Update user profile", - "description": "Update user information including profile image", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/UserUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["auth"], - "summary": "Delete user account", - "description": "Permanently delete user account and associated data", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/auth/users": { - "get": { - "tags": ["auth"], - "summary": "Get all users", - "description": "Retrieve all users (admin/superadmin only)", - "responses": { - "200": { - "description": "Users retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/User" - } - } - } - } - ] - } - } - } - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/auth/users/superadmin": { - "get": { - "tags": ["auth"], - "summary": "Check superadmin exists", - "description": "Check if a superadmin account exists in the system", - "responses": { - "200": { - "description": "Check completed", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "exists": { - "type": "boolean" - } - } - } - } - } - ] - } - } - } - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/auth/recovery/request": { - "post": { - "tags": ["auth"], - "summary": "Request password reset", - "description": "Send password reset email to user", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["email"], - "properties": { - "email": { - "type": "string", - "format": "email", - "description": "User email address" - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "description": "User not found" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/auth/recovery/validate": { - "post": { - "tags": ["auth"], - "summary": "Validate recovery token", - "description": "Validate password reset token", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["recoveryToken"], - "properties": { - "recoveryToken": { - "type": "string", - "description": "Password reset token" - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "400": { - "description": "Invalid or expired token" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/auth/recovery/reset": { - "post": { - "tags": ["auth"], - "summary": "Reset password", - "description": "Reset user password with recovery token", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["recoveryToken", "password"], - "properties": { - "recoveryToken": { - "type": "string", - "description": "Password reset token" - }, - "password": { - "type": "string", - "format": "password", - "description": "New password" - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "400": { - "description": "Invalid token or password requirements not met" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/monitors": { - "get": { - "tags": ["monitors"], - "summary": "Get all monitors", - "description": "Retrieve all monitors for the authenticated user", - "responses": { - "200": { - "description": "Monitors retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Monitor" - } - } - } - } - ] - } - } - } - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "post": { - "tags": ["monitors"], - "summary": "Create monitor", - "description": "Create a new monitoring endpoint", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateMonitorRequest" - } - } - } - }, - "responses": { - "201": { - "description": "Monitor created successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Monitor" - } - } - } - ] - } - } - } - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["monitors"], - "summary": "Delete all monitors", - "description": "Delete all monitors (superadmin only)", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/team": { - "get": { - "tags": ["monitors"], - "summary": "Get monitors by team", - "description": "Get monitors filtered by team with optional parameters", - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Filter by monitor status (up/down)", - "schema": { - "type": "boolean" - } - }, - { - "name": "type", - "in": "query", - "description": "Filter by monitor type", - "schema": { - "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] - } - }, - { - "name": "page", - "in": "query", - "description": "Page number for pagination", - "schema": { - "type": "integer", - "minimum": 1, - "default": 1 - } - }, - { - "name": "rowsPerPage", - "in": "query", - "description": "Number of monitors per page", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 10 - } - }, - { - "name": "filter", - "in": "query", - "description": "Search filter value", - "schema": { - "type": "string" - } - }, - { - "name": "field", - "in": "query", - "description": "Field to filter on", - "schema": { - "type": "string", - "enum": ["name", "url", "description"] - } - } - ], - "responses": { - "200": { - "description": "Team monitors retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "monitors": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Monitor" - } - }, - "totalCount": { - "type": "integer" - } - } - } - } - } - ] - } - } - } - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/team/with-checks": { - "get": { - "tags": ["monitors"], - "summary": "Get monitors with recent checks", - "description": "Get team monitors with their most recent check results", - "parameters": [ - { - "name": "limit", - "in": "query", - "description": "Number of recent checks to include per monitor", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 50, - "default": 10 - } - } - ], - "responses": { - "200": { - "description": "Monitors with checks retrieved successfully" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/team/summary": { - "get": { - "tags": ["monitors"], - "summary": "Get monitors summary", - "description": "Get team monitors with summary statistics", - "parameters": [ - { - "name": "type", - "in": "query", - "description": "Filter by monitor type", - "schema": { - "type": "array", - "items": { - "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] - } - } - } - ], - "responses": { - "200": { - "description": "Monitor summary retrieved successfully" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/{monitorId}": { - "get": { - "tags": ["monitors"], - "summary": "Get monitor by ID", - "description": "Retrieve a specific monitor by its ID", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "description": "Monitor ID", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Monitor retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/Monitor" - } - } - } - ] - } - } - } - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "put": { - "tags": ["monitors"], - "summary": "Update monitor", - "description": "Update an existing monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateMonitorRequest" - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["monitors"], - "summary": "Delete monitor", - "description": "Delete a specific monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/uptime/details/{monitorId}": { - "get": { - "tags": ["monitors"], - "summary": "Get uptime details", - "description": "Get detailed uptime statistics for a monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Uptime details retrieved successfully" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/hardware/details/{monitorId}": { - "get": { - "tags": ["monitors"], - "summary": "Get hardware monitoring details", - "description": "Get hardware performance metrics for a monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Hardware details retrieved successfully" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/pause/{monitorId}": { - "post": { - "tags": ["monitors"], - "summary": "Pause/unpause monitor", - "description": "Toggle monitor active status", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/stats/{monitorId}": { - "get": { - "tags": ["monitors"], - "summary": "Get monitor statistics", - "description": "Get comprehensive statistics for a monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Monitor statistics retrieved successfully" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/certificate/{monitorId}": { - "get": { - "tags": ["monitors"], - "summary": "Get SSL certificate info", - "description": "Get SSL certificate information for a monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Certificate information retrieved successfully" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/demo": { - "post": { - "tags": ["monitors"], - "summary": "Add demo monitors", - "description": "Add preconfigured demo monitors for testing", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/export": { - "get": { - "tags": ["monitors"], - "summary": "Export monitors to CSV", - "description": "Export all monitors to CSV format", - "responses": { - "200": { - "description": "CSV file generated successfully", - "content": { - "text/csv": { - "schema": { - "type": "string" - } - } - } - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/bulk": { - "post": { - "tags": ["monitors"], - "summary": "Bulk import monitors", - "description": "Import monitors from CSV file", - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "csvFile": { - "type": "string", - "format": "binary", - "description": "CSV file containing monitor data" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Monitors imported successfully" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/test-email": { - "post": { - "tags": ["monitors"], - "summary": "Send test email", - "description": "Send a test email notification", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/settings": { - "get": { - "tags": ["settings"], - "summary": "Get application settings", - "description": "Retrieve current application configuration", - "responses": { - "200": { - "description": "Settings retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/AppSettings" - } - } - } - ] - } - } - } - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "put": { - "tags": ["settings"], - "summary": "Update application settings", - "description": "Update application configuration (admin/superadmin only)", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AppSettings" - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/settings/test-email": { - "post": { - "tags": ["settings"], - "summary": "Send test email", - "description": "Send test email to verify email configuration", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/notifications": { - "get": { - "tags": ["notifications"], - "summary": "Get team notifications", - "description": "Get all notification configurations for the team", - "responses": { - "200": { - "description": "Notifications retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Notification" - } - } - } - } - ] - } - } - } - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "post": { - "tags": ["notifications"], - "summary": "Create notification", - "description": "Create a new notification integration", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateNotificationRequest" - } - } - } - }, - "responses": { - "201": { - "description": "Notification created successfully" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/notifications/{id}": { - "get": { - "tags": ["notifications"], - "summary": "Get notification by ID", - "description": "Retrieve a specific notification configuration", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Notification retrieved successfully" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "put": { - "tags": ["notifications"], - "summary": "Update notification", - "description": "Update an existing notification configuration", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateNotificationRequest" - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["notifications"], - "summary": "Delete notification", - "description": "Delete a notification configuration", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/notifications/test": { - "post": { - "tags": ["notifications"], - "summary": "Test notification", - "description": "Send a test notification to verify configuration", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/notifications/test/all": { - "post": { - "tags": ["notifications"], - "summary": "Test all notifications", - "description": "Send test notifications to all configured integrations", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/logs": { - "get": { - "tags": ["logs"], - "summary": "Get application logs", - "description": "Retrieve application logs (admin/superadmin only)", - "parameters": [ - { - "name": "level", - "in": "query", - "description": "Log level filter", - "schema": { - "type": "string", - "enum": ["error", "warn", "info", "debug"] - } - }, - { - "name": "limit", - "in": "query", - "description": "Number of log entries to return", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 1000, - "default": 100 - } - } - ], - "responses": { - "200": { - "description": "Logs retrieved successfully" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/diagnostic/system": { - "get": { - "tags": ["diagnostic"], - "summary": "Get system diagnostics", - "description": "Get system health and performance metrics (admin/superadmin only)", - "responses": { - "200": { - "description": "System diagnostics retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/SystemDiagnostics" - } - } - } - ] - } - } - } - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/auth/user": { - "put": { - "tags": ["auth"], - "summary": "Update current user profile", - "description": "Update authenticated user's profile information", - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/UserUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["auth"], - "summary": "Delete current user account", - "description": "Delete authenticated user's account and associated data", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/auth/users/{userId}": { - "get": { - "tags": ["auth"], - "summary": "Get user by ID", - "description": "Get a specific user by ID (superadmin only)", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "User retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/components/schemas/User" - } - } - } - ] - } - } - } - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "put": { - "tags": ["auth"], - "summary": "Update user by ID", - "description": "Update a specific user by ID (superadmin only)", - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserUpdateRequest" - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/invite": { - "post": { - "tags": ["invite"], - "summary": "Create invite token", - "description": "Create a new invitation token", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["email", "role"], - "properties": { - "email": { - "type": "string", - "format": "email" - }, - "role": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "admin"] - } - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Invite token created successfully" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/invite/send": { - "post": { - "tags": ["invite"], - "summary": "Send invitation email", - "description": "Send invitation email to user", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["email"], - "properties": { - "email": { - "type": "string", - "format": "email" - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/invite/verify": { - "post": { - "tags": ["invite"], - "summary": "Verify invitation token", - "description": "Verify an invitation token", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["token"], - "properties": { - "token": { - "type": "string" - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "400": { - "description": "Invalid or expired token" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, - "/checks/{monitorId}": { - "get": { - "tags": ["checks"], - "summary": "Get checks by monitor", - "description": "Get all checks for a specific monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Checks retrieved successfully" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["checks"], - "summary": "Delete checks by monitor", - "description": "Delete all checks for a specific monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/checks/team": { - "get": { - "tags": ["checks"], - "summary": "Get checks by team", - "description": "Get all checks for team monitors", - "responses": { - "200": { - "description": "Team checks retrieved successfully" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["checks"], - "summary": "Delete team checks", - "description": "Delete all checks for team (admin/superadmin only)", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/checks/team/summary": { - "get": { - "tags": ["checks"], - "summary": "Get team checks summary", - "description": "Get summary statistics for team checks", - "responses": { - "200": { - "description": "Team checks summary retrieved successfully" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/checks/team/ttl": { - "put": { - "tags": ["checks"], - "summary": "Update checks TTL", - "description": "Update time-to-live for checks (admin/superadmin only)", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["ttl"], - "properties": { - "ttl": { - "type": "integer", - "minimum": 1, - "description": "Time to live in days" - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/checks/check/{checkId}": { - "put": { - "tags": ["checks"], - "summary": "Acknowledge check", - "description": "Acknowledge a specific check", - "parameters": [ - { - "name": "checkId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/maintenance-window": { - "post": { - "tags": ["maintenance-window"], - "summary": "Create maintenance window", - "description": "Create a new maintenance window", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["name", "startTime", "endTime"], - "properties": { - "name": { - "type": "string", - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 500 - }, - "startTime": { - "type": "string", - "format": "date-time" - }, - "endTime": { - "type": "string", - "format": "date-time" - }, - "monitors": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Maintenance window created successfully" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/maintenance-window/team": { - "get": { - "tags": ["maintenance-window"], - "summary": "Get team maintenance windows", - "description": "Get all maintenance windows for the team", - "responses": { - "200": { - "description": "Maintenance windows retrieved successfully" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/maintenance-window/monitor/{monitorId}": { - "get": { - "tags": ["maintenance-window"], - "summary": "Get maintenance windows by monitor", - "description": "Get all maintenance windows for a specific monitor", - "parameters": [ - { - "name": "monitorId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Monitor maintenance windows retrieved successfully" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/maintenance-window/{id}": { - "get": { - "tags": ["maintenance-window"], - "summary": "Get maintenance window by ID", - "description": "Get a specific maintenance window", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Maintenance window retrieved successfully" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "put": { - "tags": ["maintenance-window"], - "summary": "Update maintenance window", - "description": "Update an existing maintenance window", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string", - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 500 - }, - "startTime": { - "type": "string", - "format": "date-time" - }, - "endTime": { - "type": "string", - "format": "date-time" - }, - "monitors": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "delete": { - "tags": ["maintenance-window"], - "summary": "Delete maintenance window", - "description": "Delete a specific maintenance window", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/queue/health": { - "get": { - "tags": ["queue"], - "summary": "Check queue health", - "description": "Check the health status of the job queue (admin/superadmin only)", - "responses": { - "200": { - "description": "Queue health status retrieved successfully" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/queue/all-metrics": { - "get": { - "tags": ["queue"], - "summary": "Get all queue metrics", - "description": "Get comprehensive queue metrics (admin/superadmin only)", - "responses": { - "200": { - "description": "All queue metrics retrieved successfully" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/queue/flush": { - "post": { - "tags": ["queue"], - "summary": "Flush queue", - "description": "Clear all jobs from the queue (admin/superadmin only)", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "403": { - "$ref": "#/components/responses/Forbidden" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/monitors/games": { - "get": { - "tags": ["monitors"], - "summary": "Get game server list", - "description": "Get available game servers for monitoring", - "responses": { - "200": { - "description": "Game servers retrieved successfully", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - } - } - } - } - } - ] - } - } - } - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/status-page": { - "get": { - "tags": ["status-page"], - "summary": "Get status page", - "description": "Get default status page information", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - }, - "post": { - "tags": ["status-page"], - "summary": "Create status page", - "description": "Create a new status page with optional logo upload", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "logo": { - "type": "string", - "format": "binary", - "description": "Logo file for the status page" - }, - "title": { - "type": "string", - "description": "Status page title" - }, - "description": { - "type": "string", - "description": "Status page description" - }, - "url": { - "type": "string", - "description": "Custom URL for the status page" - } - } - } - } - } - }, - "responses": { - "201": { - "$ref": "#/components/responses/Success" - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - }, - "put": { - "tags": ["status-page"], - "summary": "Update status page", - "description": "Update an existing status page with optional logo upload", - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "properties": { - "logo": { - "type": "string", - "format": "binary", - "description": "Logo file for the status page" - }, - "title": { - "type": "string", - "description": "Status page title" - }, - "description": { - "type": "string", - "description": "Status page description" - }, - "url": { - "type": "string", - "description": "Custom URL for the status page" - } - } - } - } - } - }, - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "400": { - "$ref": "#/components/responses/BadRequest" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/status-page/team": { - "get": { - "tags": ["status-page"], - "summary": "Get status pages by team", - "description": "Get all status pages for the authenticated user's team", - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - }, - "/status-page/{url}": { - "get": { - "tags": ["status-page"], - "summary": "Get status page by URL", - "description": "Get a specific status page by its custom URL", - "parameters": [ - { - "name": "url", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "Custom URL of the status page" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - }, - "delete": { - "tags": ["status-page"], - "summary": "Delete status page", - "description": "Delete a status page by its custom URL", - "parameters": [ - { - "name": "url", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "description": "Custom URL of the status page (supports wildcards)" - } - ], - "responses": { - "200": { - "$ref": "#/components/responses/Success" - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "404": { - "$ref": "#/components/responses/NotFound" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] - } - } - }, - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - "description": "JWT token obtained from login endpoint" - } - }, - "responses": { - "Success": { - "description": "Operation successful", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SuccessResponse" - } - } - } - }, - "NotFound": { - "description": "Resource not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "ValidationError": { - "description": "Validation failed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "Unauthorized": { - "description": "Authentication required or token invalid", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "Forbidden": { - "description": "Insufficient permissions", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - }, - "InternalServerError": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorResponse" - } - } - } - } - }, - "schemas": { - "SuccessResponse": { - "type": "object", - "required": ["success", "msg"], - "properties": { - "success": { - "type": "boolean", - "example": true - }, - "msg": { - "type": "string", - "example": "Operation completed successfully" - }, - "data": { - "type": "object", - "description": "Response payload (varies by endpoint)" - } - } - }, - "ErrorResponse": { - "type": "object", - "required": ["success", "msg"], - "properties": { - "success": { - "type": "boolean", - "example": false - }, - "msg": { - "type": "string", - "example": "An error occurred" - }, - "details": { - "type": "object", - "description": "Additional error details" - } - } - }, - "RegisterRequest": { - "type": "object", - "required": ["firstName", "lastName", "email", "password"], - "properties": { - "firstName": { - "type": "string", - "minLength": 1, - "maxLength": 50, - "example": "John" - }, - "lastName": { - "type": "string", - "minLength": 1, - "maxLength": 50, - "example": "Doe" - }, - "email": { - "type": "string", - "format": "email", - "example": "john@example.com" - }, - "password": { - "type": "string", - "minLength": 8, - "format": "password", - "example": "SecurePass123!" - }, - "profileImage": { - "type": "string", - "format": "binary", - "description": "Optional profile image file" - }, - "role": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "admin", "superadmin", "Demo"] - }, - "default": ["user"] - } - } - }, - "LoginRequest": { - "type": "object", - "required": ["email", "password"], - "properties": { - "email": { - "type": "string", - "format": "email", - "example": "john@example.com" - }, - "password": { - "type": "string", - "format": "password", - "example": "SecurePass123!" - } - } - }, - "AuthResponse": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "data": { - "type": "object", - "properties": { - "token": { - "type": "string", - "description": "JWT access token" - }, - "user": { - "$ref": "#/components/schemas/User" - } - } - } - } - } - ] - }, - "UserUpdateRequest": { - "type": "object", - "properties": { - "firstName": { - "type": "string", - "minLength": 1, - "maxLength": 50 - }, - "lastName": { - "type": "string", - "minLength": 1, - "maxLength": 50 - }, - "password": { - "type": "string", - "format": "password", - "description": "Current password for verification" - }, - "newPassword": { - "type": "string", - "minLength": 8, - "format": "password", - "description": "New password (if changing)" - }, - "profileImage": { - "type": "string", - "format": "binary", - "description": "New profile image file" - }, - "deleteProfileImage": { - "type": "boolean", - "description": "Flag to delete current profile image" - } - } - }, - "User": { - "type": "object", - "properties": { - "_id": { - "type": "string", - "example": "64f123a456b789012c345def" - }, - "firstName": { - "type": "string", - "example": "John" - }, - "lastName": { - "type": "string", - "example": "Doe" - }, - "email": { - "type": "string", - "format": "email", - "example": "john@example.com" - }, - "role": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "admin", "superadmin", "Demo"] - } - }, - "profileImage": { - "type": "string", - "description": "URL or path to profile image" - }, - "isActive": { - "type": "boolean" - }, - "teamId": { - "type": "string" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "CreateMonitorRequest": { - "type": "object", - "required": ["name", "description", "type", "url", "interval"], - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "example": "My Website Monitor" - }, - "description": { - "type": "string", - "maxLength": 500, - "example": "Monitors the main website homepage" - }, - "type": { - "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"], - "example": "http" - }, - "url": { - "type": "string", - "format": "uri", - "example": "https://example.com" - }, - "interval": { - "type": "integer", - "minimum": 30, - "maximum": 3600, - "example": 300, - "description": "Check interval in seconds" - }, - "isActive": { - "type": "boolean", - "default": true - }, - "notifications": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Array of notification IDs to associate with this monitor" - }, - "httpOptions": { - "type": "object", - "properties": { - "method": { - "type": "string", - "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"], - "default": "GET" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "string", - "description": "Request body for POST/PUT requests" - }, - "timeout": { - "type": "integer", - "minimum": 1000, - "maximum": 30000, - "default": 5000, - "description": "Request timeout in milliseconds" - } - } - }, - "assertions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["status-code", "response-time", "body-contains", "header-contains"] - }, - "comparison": { - "type": "string", - "enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"] - }, - "value": { - "type": "string" - } - } - } - } - } - }, - "UpdateMonitorRequest": { - "type": "object", - "properties": { - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100 - }, - "description": { - "type": "string", - "maxLength": 500 - }, - "interval": { - "type": "integer", - "minimum": 30, - "maximum": 3600 - }, - "isActive": { - "type": "boolean" - }, - "notifications": { - "type": "array", - "items": { - "type": "string" - } - }, - "httpOptions": { - "type": "object", - "properties": { - "method": { - "type": "string", - "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"] - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "string" - }, - "timeout": { - "type": "integer", - "minimum": 1000, - "maximum": 30000 - } - } - }, - "assertions": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["status-code", "response-time", "body-contains", "header-contains"] - }, - "comparison": { - "type": "string", - "enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"] - }, - "value": { - "type": "string" - } - } - } - } - } - }, - "Monitor": { - "type": "object", - "properties": { - "_id": { - "type": "string", - "example": "64f123a456b789012c345def" - }, - "userId": { - "type": "string" - }, - "teamId": { - "type": "string" - }, - "name": { - "type": "string", - "example": "My Website Monitor" - }, - "description": { - "type": "string", - "example": "Monitors the main website homepage" - }, - "type": { - "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] - }, - "url": { - "type": "string", - "format": "uri", - "example": "https://example.com" - }, - "interval": { - "type": "integer", - "example": 300 - }, - "isActive": { - "type": "boolean", - "example": true - }, - "status": { - "type": "boolean", - "description": "Current monitor status (up/down)" - }, - "lastChecked": { - "type": "string", - "format": "date-time", - "description": "Timestamp of last check" - }, - "notifications": { - "type": "array", - "items": { - "type": "string" - } - }, - "httpOptions": { - "type": "object", - "properties": { - "method": { - "type": "string" - }, - "headers": { - "type": "object" - }, - "body": { - "type": "string" - }, - "timeout": { - "type": "integer" - } - } - }, - "assertions": { - "type": "array", - "items": { - "type": "object" - } - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "CreateNotificationRequest": { - "type": "object", - "required": ["type", "name"], - "properties": { - "type": { - "type": "string", - "enum": ["email", "webhook", "slack", "discord", "telegram", "zapier"], - "example": "email" - }, - "name": { - "type": "string", - "minLength": 1, - "maxLength": 100, - "example": "Admin Email Notifications" - }, - "config": { - "type": "object", - "description": "Configuration specific to notification type", - "oneOf": [ - { - "title": "Email Configuration", - "properties": { - "to": { - "type": "array", - "items": { - "type": "string", - "format": "email" - } - } - } - }, - { - "title": "Webhook Configuration", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - }, - { - "title": "Slack Configuration", - "properties": { - "webhookUrl": { - "type": "string", - "format": "uri" - }, - "channel": { - "type": "string" - } - } - }, - { - "title": "Discord Configuration", - "properties": { - "webhookUrl": { - "type": "string", - "format": "uri" - } - } - } - ] - }, - "isActive": { - "type": "boolean", - "default": true - } - } - }, - "Notification": { - "type": "object", - "properties": { - "_id": { - "type": "string" - }, - "userId": { - "type": "string" - }, - "teamId": { - "type": "string" - }, - "type": { - "type": "string", - "enum": ["email", "webhook", "slack", "discord", "telegram", "zapier"] - }, - "name": { - "type": "string" - }, - "config": { - "type": "object", - "description": "Type-specific configuration" - }, - "isActive": { - "type": "boolean" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - } - } - }, - "AppSettings": { - "type": "object", - "properties": { - "appName": { - "type": "string", - "example": "Checkmate" - }, - "appUrl": { - "type": "string", - "format": "uri", - "example": "https://checkmate.example.com" - }, - "logLevel": { - "type": "string", - "enum": ["error", "warn", "info", "debug"], - "default": "info" - }, - "emailConfig": { - "type": "object", - "properties": { - "host": { - "type": "string" - }, - "port": { - "type": "integer" - }, - "secure": { - "type": "boolean" - }, - "user": { - "type": "string" - }, - "pass": { - "type": "string", - "format": "password" - }, - "from": { - "type": "string", - "format": "email" - } - } - }, - "webhookRetries": { - "type": "integer", - "minimum": 0, - "maximum": 10, - "default": 3 - }, - "checksRetention": { - "type": "integer", - "minimum": 1, - "maximum": 365, - "default": 90, - "description": "Days to retain check results" - } - } - }, - "SystemDiagnostics": { - "type": "object", - "properties": { - "uptime": { - "type": "number", - "description": "System uptime in seconds" - }, - "memory": { - "type": "object", - "properties": { - "total": { - "type": "number", - "description": "Total memory in bytes" - }, - "used": { - "type": "number", - "description": "Used memory in bytes" - }, - "free": { - "type": "number", - "description": "Free memory in bytes" - } - } - }, - "cpu": { - "type": "object", - "properties": { - "usage": { - "type": "number", - "description": "CPU usage percentage" - }, - "cores": { - "type": "integer", - "description": "Number of CPU cores" - } - } - }, - "database": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["connected", "disconnected", "error"] - }, - "responseTime": { - "type": "number", - "description": "Database response time in milliseconds" - } - } - }, - "queue": { - "type": "object", - "properties": { - "active": { - "type": "integer" - }, - "waiting": { - "type": "integer" - }, - "completed": { - "type": "integer" - }, - "failed": { - "type": "integer" - } - } - } - } - } - } - } -} diff --git a/server/package-lock.json b/server/package-lock.json index 8de5cfc0c..653d86167 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,11509 +1,4606 @@ { - "name": "server", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "server", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "@pulsecron/pulse": "1.6.8", - "axios": "^1.7.2", - "bcryptjs": "3.0.2", - "bullmq": "5.41.2", - "compression": "1.8.1", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "dockerode": "4.0.6", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-rate-limit": "8.0.1", - "gamedig": "^5.3.1", - "got": "14.4.7", - "handlebars": "^4.7.8", - "helmet": "^8.0.0", - "ioredis": "^5.4.2", - "isomorphic-dompurify": "^2.26.0", - "jmespath": "^0.16.0", - "joi": "^17.13.1", - "jsdom": "^26.1.0", - "jsonwebtoken": "9.0.2", - "mailersend": "^2.2.0", - "mjml": "^5.0.0-alpha.4", - "mongoose": "^8.3.3", - "multer": "^1.4.5-lts.1", - "nodemailer": "^6.9.14", - "papaparse": "^5.5.2", - "ping": "0.4.4", - "sharp": "0.33.5", - "ssl-checker": "2.0.10", - "super-simple-scheduler": "1.4.4", - "swagger-ui-express": "5.0.1", - "winston": "^3.13.0" - }, - "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/express": "5.0.3", - "@types/jsonwebtoken": "9.0.10", - "@types/nodemailer": "7.0.1", - "@types/ping": "0.4.4", - "c8": "10.1.3", - "chai": "5.2.0", - "eslint": "^9.17.0", - "eslint-plugin-mocha": "^10.5.0", - "esm": "3.2.25", - "globals": "^15.14.0", - "mocha": "11.1.0", - "prettier": "^3.3.3", - "sinon": "19.0.2", - "tsx": "4.20.5", - "typescript": "5.9.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.3.tgz", - "integrity": "sha512-kiGFeY+Hxf5KbPpjRLf+ffWbkos1aGo8MBfd91oxS3O57RgU3XhZrt/6UzoVF9VMpWbC3v87SRc9jxGrc9qHtQ==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.2" - } - }, - "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "license": "MIT" - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sesv2": { - "version": "3.917.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.917.0.tgz", - "integrity": "sha512-Jwz+3rO9fc2H8JyF4ainL5x06tiQ0n4ewKIpdUeGIauKdndMItYP1aeM0Y8wJQPTF1XEpWFC36cTSDgk/eypxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.916.0", - "@aws-sdk/credential-provider-node": "3.917.0", - "@aws-sdk/middleware-host-header": "3.914.0", - "@aws-sdk/middleware-logger": "3.914.0", - "@aws-sdk/middleware-recursion-detection": "3.914.0", - "@aws-sdk/middleware-user-agent": "3.916.0", - "@aws-sdk/region-config-resolver": "3.914.0", - "@aws-sdk/signature-v4-multi-region": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-endpoints": "3.916.0", - "@aws-sdk/util-user-agent-browser": "3.914.0", - "@aws-sdk/util-user-agent-node": "3.916.0", - "@smithy/config-resolver": "^4.4.0", - "@smithy/core": "^3.17.1", - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/hash-node": "^4.2.3", - "@smithy/invalid-dependency": "^4.2.3", - "@smithy/middleware-content-length": "^4.2.3", - "@smithy/middleware-endpoint": "^4.3.5", - "@smithy/middleware-retry": "^4.4.5", - "@smithy/middleware-serde": "^4.2.3", - "@smithy/middleware-stack": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.4", - "@smithy/util-defaults-mode-node": "^4.2.6", - "@smithy/util-endpoints": "^3.2.3", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-retry": "^4.2.3", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.916.0.tgz", - "integrity": "sha512-Eu4PtEUL1MyRvboQnoq5YKg0Z9vAni3ccebykJy615xokVZUdA3di2YxHM/hykDQX7lcUC62q9fVIvh0+UNk/w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.916.0", - "@aws-sdk/middleware-host-header": "3.914.0", - "@aws-sdk/middleware-logger": "3.914.0", - "@aws-sdk/middleware-recursion-detection": "3.914.0", - "@aws-sdk/middleware-user-agent": "3.916.0", - "@aws-sdk/region-config-resolver": "3.914.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-endpoints": "3.916.0", - "@aws-sdk/util-user-agent-browser": "3.914.0", - "@aws-sdk/util-user-agent-node": "3.916.0", - "@smithy/config-resolver": "^4.4.0", - "@smithy/core": "^3.17.1", - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/hash-node": "^4.2.3", - "@smithy/invalid-dependency": "^4.2.3", - "@smithy/middleware-content-length": "^4.2.3", - "@smithy/middleware-endpoint": "^4.3.5", - "@smithy/middleware-retry": "^4.4.5", - "@smithy/middleware-serde": "^4.2.3", - "@smithy/middleware-stack": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.4", - "@smithy/util-defaults-mode-node": "^4.2.6", - "@smithy/util-endpoints": "^3.2.3", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-retry": "^4.2.3", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.916.0.tgz", - "integrity": "sha512-1JHE5s6MD5PKGovmx/F1e01hUbds/1y3X8rD+Gvi/gWVfdg5noO7ZCerpRsWgfzgvCMZC9VicopBqNHCKLykZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.914.0", - "@aws-sdk/xml-builder": "3.914.0", - "@smithy/core": "^3.17.1", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/signature-v4": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.916.0.tgz", - "integrity": "sha512-3gDeqOXcBRXGHScc6xb7358Lyf64NRG2P08g6Bu5mv1Vbg9PKDyCAZvhKLkG7hkdfAM8Yc6UJNhbFxr1ud/tCQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.916.0.tgz", - "integrity": "sha512-NmooA5Z4/kPFJdsyoJgDxuqXC1C6oPMmreJjbOPqcwo6E/h2jxaG8utlQFgXe5F9FeJsMx668dtxVxSYnAAqHQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/util-stream": "^4.5.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.917.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.917.0.tgz", - "integrity": "sha512-rvQ0QamLySRq+Okc0ZqFHZ3Fbvj3tYuWNIlzyEKklNmw5X5PM1idYKlOJflY2dvUGkIqY3lUC9SC2WL+1s7KIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/credential-provider-env": "3.916.0", - "@aws-sdk/credential-provider-http": "3.916.0", - "@aws-sdk/credential-provider-process": "3.916.0", - "@aws-sdk/credential-provider-sso": "3.916.0", - "@aws-sdk/credential-provider-web-identity": "3.917.0", - "@aws-sdk/nested-clients": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/credential-provider-imds": "^4.2.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.917.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.917.0.tgz", - "integrity": "sha512-n7HUJ+TgU9wV/Z46yR1rqD9hUjfG50AKi+b5UXTlaDlVD8bckg40i77ROCllp53h32xQj/7H0yBIYyphwzLtmg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.916.0", - "@aws-sdk/credential-provider-http": "3.916.0", - "@aws-sdk/credential-provider-ini": "3.917.0", - "@aws-sdk/credential-provider-process": "3.916.0", - "@aws-sdk/credential-provider-sso": "3.916.0", - "@aws-sdk/credential-provider-web-identity": "3.917.0", - "@aws-sdk/types": "3.914.0", - "@smithy/credential-provider-imds": "^4.2.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.916.0.tgz", - "integrity": "sha512-SXDyDvpJ1+WbotZDLJW1lqP6gYGaXfZJrgFSXIuZjHb75fKeNRgPkQX/wZDdUvCwdrscvxmtyJorp2sVYkMcvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.916.0.tgz", - "integrity": "sha512-gu9D+c+U/Dp1AKBcVxYHNNoZF9uD4wjAKYCjgSN37j4tDsazwMEylbbZLuRNuxfbXtizbo4/TiaxBXDbWM7AkQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.916.0", - "@aws-sdk/core": "3.916.0", - "@aws-sdk/token-providers": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.917.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.917.0.tgz", - "integrity": "sha512-pZncQhFbwW04pB0jcD5OFv3x2gAddDYCVxyJVixgyhSw7bKCYxqu6ramfq1NxyVpmm+qsw+ijwi/3cCmhUHF/A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/nested-clients": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.914.0.tgz", - "integrity": "sha512-7r9ToySQ15+iIgXMF/h616PcQStByylVkCshmQqcdeynD/lCn2l667ynckxW4+ql0Q+Bo/URljuhJRxVJzydNA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-logger": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.914.0.tgz", - "integrity": "sha512-/gaW2VENS5vKvJbcE1umV4Ag3NuiVzpsANxtrqISxT3ovyro29o1RezW/Avz/6oJqjnmgz8soe9J1t65jJdiNg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.914.0.tgz", - "integrity": "sha512-yiAjQKs5S2JKYc+GrkvGMwkUvhepXDigEXpSJqUseR/IrqHhvGNuOxDxq+8LbDhM4ajEW81wkiBbU+Jl9G82yQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.914.0", - "@aws/lambda-invoke-store": "^0.0.1", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.916.0.tgz", - "integrity": "sha512-pjmzzjkEkpJObzmTthqJPq/P13KoNFuEi/x5PISlzJtHofCNcyXeVAQ90yvY2dQ6UXHf511Rh1/ytiKy2A8M0g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.17.1", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/signature-v4": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-stream": "^4.5.4", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.916.0.tgz", - "integrity": "sha512-mzF5AdrpQXc2SOmAoaQeHpDFsK2GE6EGcEACeNuoESluPI2uYMpuuNMYrUufdnIAIyqgKlis0NVxiahA5jG42w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-endpoints": "3.916.0", - "@smithy/core": "^3.17.1", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.916.0.tgz", - "integrity": "sha512-tgg8e8AnVAer0rcgeWucFJ/uNN67TbTiDHfD+zIOPKep0Z61mrHEoeT/X8WxGIOkEn4W6nMpmS4ii8P42rNtnA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.916.0", - "@aws-sdk/middleware-host-header": "3.914.0", - "@aws-sdk/middleware-logger": "3.914.0", - "@aws-sdk/middleware-recursion-detection": "3.914.0", - "@aws-sdk/middleware-user-agent": "3.916.0", - "@aws-sdk/region-config-resolver": "3.914.0", - "@aws-sdk/types": "3.914.0", - "@aws-sdk/util-endpoints": "3.916.0", - "@aws-sdk/util-user-agent-browser": "3.914.0", - "@aws-sdk/util-user-agent-node": "3.916.0", - "@smithy/config-resolver": "^4.4.0", - "@smithy/core": "^3.17.1", - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/hash-node": "^4.2.3", - "@smithy/invalid-dependency": "^4.2.3", - "@smithy/middleware-content-length": "^4.2.3", - "@smithy/middleware-endpoint": "^4.3.5", - "@smithy/middleware-retry": "^4.4.5", - "@smithy/middleware-serde": "^4.2.3", - "@smithy/middleware-stack": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.4", - "@smithy/util-defaults-mode-node": "^4.2.6", - "@smithy/util-endpoints": "^3.2.3", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-retry": "^4.2.3", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.914.0.tgz", - "integrity": "sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/config-resolver": "^4.4.0", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.916.0.tgz", - "integrity": "sha512-fuzUMo6xU7e0NBzBA6TQ4FUf1gqNbg4woBSvYfxRRsIfKmSMn9/elXXn4sAE5UKvlwVQmYnb6p7dpVRPyFvnQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/protocol-http": "^5.3.3", - "@smithy/signature-v4": "^5.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.916.0.tgz", - "integrity": "sha512-13GGOEgq5etbXulFCmYqhWtpcEQ6WI6U53dvXbheW0guut8fDFJZmEv7tKMTJgiybxh7JHd0rWcL9JQND8DwoQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.916.0", - "@aws-sdk/nested-clients": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", - "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", - "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-endpoints": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.916.0.tgz", - "integrity": "sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-endpoints": "^3.2.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", - "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.914.0.tgz", - "integrity": "sha512-rMQUrM1ECH4kmIwlGl9UB0BtbHy6ZuKdWFrIknu8yGTRI/saAucqNTh5EI1vWBxZ0ElhK5+g7zOnUuhSmVQYUA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.914.0", - "@smithy/types": "^4.8.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.916.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.916.0.tgz", - "integrity": "sha512-CwfWV2ch6UdjuSV75ZU99N03seEUb31FIUrXBnwa6oONqj/xqXwrxtlUMLx6WH3OJEE4zI3zt5PjlTdGcVwf4g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.916.0", - "@aws-sdk/types": "3.914.0", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.914.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.914.0.tgz", - "integrity": "sha512-k75evsBD5TcIjedycYS7QXQ98AmOtbnxRJOPtCo0IwYRmy7UvqgS/gBL5SmrIqeV6FDSYRQMgdBxSMp6MLmdew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", - "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@balena/dockerignore": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", - "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "license": "Apache-2.0" - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", - "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "license": "MIT", - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", - "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "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", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "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", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "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.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", - "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.16.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", - "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "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.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "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.0", - "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", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", - "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", - "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", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "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.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", - "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.16.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz", - "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==", - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.7.15", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", - "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.2.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "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", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "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", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", - "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", - "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", - "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", - "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", - "license": "MIT", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "license": "BSD-3-Clause" - }, - "node_modules/@pulsecron/pulse": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/@pulsecron/pulse/-/pulse-1.6.8.tgz", - "integrity": "sha512-fg9sT/pfpZTlUYr/ktDu6om6XEm2zmulcZ8TP4JZa+Dmol7/35mMKzDwT1H5a0AUcxiFIRfB4KOpkJ+BI2fgmg==", - "license": "MIT", - "dependencies": { - "cron-parser": "^4.9.0", - "date.js": "~0.3.3", - "debug": "~4.3.4", - "human-interval": "~2.0.1", - "moment-timezone": "^0.5.45", - "mongodb": "^6.5.0" - } - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "license": "MIT" - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@sindresorhus/is": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", - "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "type-detect": "^4.1.0" - } - }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true, - "license": "(Unlicense OR Apache-2.0)" - }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.3.tgz", - "integrity": "sha512-xWL9Mf8b7tIFuAlpjKtRPnHrR8XVrwTj5NPYO/QwZPtc0SDLsPxb56V5tzi5yspSMytISHybifez+4jlrx0vkQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.0.tgz", - "integrity": "sha512-Kkmz3Mup2PGp/HNJxhCWkLNdlajJORLSjwkcfrj0E7nu6STAEdcMR1ir5P9/xOmncx8xXfru0fbUYLlZog/cFg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.3", - "@smithy/types": "^4.8.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.3", - "@smithy/util-middleware": "^4.2.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.1.tgz", - "integrity": "sha512-V4Qc2CIb5McABYfaGiIYLTmo/vwNIK7WXI5aGveBd9UcdhbOMwcvIMxIw/DJj1S9QgOMa/7FBkarMdIC0EOTEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/middleware-serde": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-stream": "^4.5.4", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.3.tgz", - "integrity": "sha512-hA1MQ/WAHly4SYltJKitEsIDVsNmXcQfYBRv2e+q04fnqtAX5qXaybxy/fhUeAMCnQIdAjaGDb04fMHQefWRhw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.4.tgz", - "integrity": "sha512-bwigPylvivpRLCm+YK9I5wRIYjFESSVwl8JQ1vVx/XhCw0PtCi558NwTnT2DaVCl5pYlImGuQTSwMsZ+pIavRw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.3", - "@smithy/querystring-builder": "^4.2.3", - "@smithy/types": "^4.8.0", - "@smithy/util-base64": "^4.3.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.3.tgz", - "integrity": "sha512-6+NOdZDbfuU6s1ISp3UOk5Rg953RJ2aBLNLLBEcamLjHAg1Po9Ha7QIB5ZWhdRUVuOUrT8BVFR+O2KIPmw027g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.3.tgz", - "integrity": "sha512-Cc9W5DwDuebXEDMpOpl4iERo8I0KFjTnomK2RMdhhR87GwrSmUmwMxS4P5JdRf+LsjOdIqumcerwRgYMr/tZ9Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.3.tgz", - "integrity": "sha512-/atXLsT88GwKtfp5Jr0Ks1CSa4+lB+IgRnkNrrYP0h1wL4swHNb0YONEvTceNKNdZGJsye+W2HH8W7olbcPUeA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.5.tgz", - "integrity": "sha512-SIzKVTvEudFWJbxAaq7f2GvP3jh2FHDpIFI6/VAf4FOWGFZy0vnYMPSRj8PGYI8Hjt29mvmwSRgKuO3bK4ixDw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.17.1", - "@smithy/middleware-serde": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "@smithy/url-parser": "^4.2.3", - "@smithy/util-middleware": "^4.2.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.5.tgz", - "integrity": "sha512-DCaXbQqcZ4tONMvvdz+zccDE21sLcbwWoNqzPLFlZaxt1lDtOE2tlVpRSwcTOJrjJSUThdgEYn7HrX5oLGlK9A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/service-error-classification": "^4.2.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-retry": "^4.2.3", - "@smithy/uuid": "^1.1.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.3.tgz", - "integrity": "sha512-8g4NuUINpYccxiCXM5s1/V+uLtts8NcX4+sPEbvYQDZk4XoJfDpq5y2FQxfmUL89syoldpzNzA0R9nhzdtdKnQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.3.tgz", - "integrity": "sha512-iGuOJkH71faPNgOj/gWuEGS6xvQashpLwWB1HjHq1lNNiVfbiJLpZVbhddPuDbx9l4Cgl0vPLq5ltRfSaHfspA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.3.tgz", - "integrity": "sha512-NzI1eBpBSViOav8NVy1fqOlSfkLgkUjUTlohUSgAEhHaFWA3XJiLditvavIP7OpvTjDp5u2LhtlBhkBlEisMwA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.3", - "@smithy/shared-ini-file-loader": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.3.tgz", - "integrity": "sha512-MAwltrDB0lZB/H6/2M5PIsISSwdI5yIh6DaBB9r0Flo9nx3y0dzl/qTMJPd7tJvPdsx6Ks/cwVzheGNYzXyNbQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/querystring-builder": "^4.2.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.3.tgz", - "integrity": "sha512-+1EZ+Y+njiefCohjlhyOcy1UNYjT+1PwGFHCxA/gYctjg3DQWAU19WigOXAco/Ql8hZokNehpzLd0/+3uCreqQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.3.tgz", - "integrity": "sha512-Mn7f/1aN2/jecywDcRDvWWWJF4uwg/A0XjFMJtj72DsgHTByfjRltSqcT9NyE9RTdBSN6X1RSXrhn/YWQl8xlw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.3.tgz", - "integrity": "sha512-LOVCGCmwMahYUM/P0YnU/AlDQFjcu+gWbFJooC417QRB/lDJlWSn8qmPSDp+s4YVAHOgtgbNG4sR+SxF/VOcJQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "@smithy/util-uri-escape": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.3.tgz", - "integrity": "sha512-cYlSNHcTAX/wc1rpblli3aUlLMGgKZ/Oqn8hhjFASXMCXjIqeuQBei0cnq2JR8t4RtU9FpG6uyl6PxyArTiwKA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.3.tgz", - "integrity": "sha512-NkxsAxFWwsPsQiwFG2MzJ/T7uIR6AQNh1SzcxSUnmmIqIQMlLRQDKhc17M7IYjiuBXhrQRjQTo3CxX+DobS93g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.3.tgz", - "integrity": "sha512-9f9Ixej0hFhroOK2TxZfUUDR13WVa8tQzhSzPDgXe5jGL3KmaM9s8XN7RQwqtEypI82q9KHnKS71CJ+q/1xLtQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.3.tgz", - "integrity": "sha512-CmSlUy+eEYbIEYN5N3vvQTRfqt0lJlQkaQUIf+oizu7BbDut0pozfDjBGecfcfWf7c62Yis4JIEgqQ/TCfodaA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.3", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.1.tgz", - "integrity": "sha512-Ngb95ryR5A9xqvQFT5mAmYkCwbXvoLavLFwmi7zVg/IowFPCfiqRfkOKnbc/ZRL8ZKJ4f+Tp6kSu6wjDQb8L/g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.17.1", - "@smithy/middleware-endpoint": "^4.3.5", - "@smithy/middleware-stack": "^4.2.3", - "@smithy/protocol-http": "^5.3.3", - "@smithy/types": "^4.8.0", - "@smithy/util-stream": "^4.5.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.0.tgz", - "integrity": "sha512-QpELEHLO8SsQVtqP+MkEgCYTFW0pleGozfs3cZ183ZBj9z3VC1CX1/wtFMK64p+5bhtZo41SeLK1rBRtd25nHQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.3.tgz", - "integrity": "sha512-I066AigYvY3d9VlU3zG9XzZg1yT10aNqvCaBTw9EPgu5GrsEl1aUkcMvhkIXascYH1A8W0LQo3B1Kr1cJNcQEw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.2.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.4.tgz", - "integrity": "sha512-qI5PJSW52rnutos8Bln8nwQZRpyoSRN6k2ajyoUHNMUzmWqHnOJCnDELJuV6m5PML0VkHI+XcXzdB+6awiqYUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.6.tgz", - "integrity": "sha512-c6M/ceBTm31YdcFpgfgQAJaw3KbaLuRKnAz91iMWFLSrgxRpYm03c3bu5cpYojNMfkV9arCUelelKA7XQT36SQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.4.0", - "@smithy/credential-provider-imds": "^4.2.3", - "@smithy/node-config-provider": "^4.3.3", - "@smithy/property-provider": "^4.2.3", - "@smithy/smithy-client": "^4.9.1", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.3.tgz", - "integrity": "sha512-aCfxUOVv0CzBIkU10TubdgKSx5uRvzH064kaiPEWfNIvKOtNpu642P4FP1hgOFkjQIkDObrfIDnKMKkeyrejvQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.3.tgz", - "integrity": "sha512-v5ObKlSe8PWUHCqEiX2fy1gNv6goiw6E5I/PN2aXg3Fb/hse0xeaAnSpXDiWl7x6LamVKq7senB+m5LOYHUAHw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.3.tgz", - "integrity": "sha512-lLPWnakjC0q9z+OtiXk+9RPQiYPNAovt2IXD3CP4LkOnd9NpUsxOjMx1SnoUVB7Orb7fZp67cQMtTBKMFDvOGg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.2.3", - "@smithy/types": "^4.8.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.4.tgz", - "integrity": "sha512-+qDxSkiErejw1BAIXUFBSfM5xh3arbz1MmxlbMCKanDDZtVEQ7PSKW9FQS0Vud1eI/kYn0oCTVKyNzRlq+9MUw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.3.4", - "@smithy/node-http-handler": "^4.4.3", - "@smithy/types": "^4.8.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "license": "MIT", - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" - } - }, - "node_modules/@so-ric/colorspace/node_modules/color": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", - "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", - "license": "MIT", - "dependencies": { - "color-convert": "^3.0.1", - "color-string": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@so-ric/colorspace/node_modules/color-convert": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", - "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/@so-ric/colorspace/node_modules/color-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", - "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/@so-ric/colorspace/node_modules/color-string": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", - "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", - "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", - "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/nodemailer": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz", - "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@aws-sdk/client-sesv2": "^3.839.0", - "@types/node": "*" - } - }, - "node_modules/@types/ping": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz", - "integrity": "sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/relateurl": { - "version": "0.2.33", - "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", - "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==", - "license": "MIT" - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", - "license": "MIT" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "license": "MIT", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "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-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "license": "MIT" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "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.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz", - "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/barse": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz", - "integrity": "sha512-UEpvriJqAn8zuVinYICuKoPttZy3XxXEoqX/V2uYAL4zzJRuNzCK3+20nAu3YUIa2U7G53kf90wfBIp9/A+Odw==", - "license": "MIT", - "dependencies": { - "readable-stream": "~1.0.2" - } - }, - "node_modules/barse/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "node_modules/barse/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", - "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "license": "BSD-3-Clause", - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/bcryptjs": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", - "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", - "license": "BSD-3-Clause", - "bin": { - "bcrypt": "bin/bcrypt" - } - }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC" - }, - "node_modules/bowser": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", - "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, - "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", - "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.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "license": "Apache-2.0", - "engines": { - "node": ">=16.20.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/bullmq": { - "version": "5.41.2", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.2.tgz", - "integrity": "sha512-wqsUIHW2Td86mTKTepqQpKLLUtP4gmX89bUO1YL2fAorxwj3da1GYtroGZMCg/zgB/+zMRsbylL6DHyMUWX7fA==", - "license": "MIT", - "dependencies": { - "cron-parser": "^4.9.0", - "ioredis": "^5.4.1", - "msgpackr": "^1.11.2", - "node-abort-controller": "^3.1.1", - "semver": "^7.5.4", - "tslib": "^2.0.0", - "uuid": "^9.0.0" - } - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/c8": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", - "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.1", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^3.1.1", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.6", - "test-exclude": "^7.0.1", - "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "monocart-coverage-reports": "^2" - }, - "peerDependenciesMeta": { - "monocart-coverage-reports": { - "optional": true - } - } - }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", - "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "^4.0.4", - "get-stream": "^9.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.4", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.1", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "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/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001751", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", - "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", - "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/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "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/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "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/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/concat-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/concat-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/concat-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cpu-features": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", - "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "buildcheck": "~0.0.6", - "nan": "^2.19.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", - "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-declaration-sorter": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", - "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", - "license": "ISC", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", - "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", - "license": "MIT", - "dependencies": { - "cssnano-preset-default": "^7.0.9", - "lilconfig": "^3.1.3" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/cssnano-preset-default": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.9.tgz", - "integrity": "sha512-tCD6AAFgYBOVpMBX41KjbvRh9c2uUjLXRyV7KHSIrwHiq5Z9o0TFfUCoM3TwVrRsRteN3sVXGNvjVNxYzkpTsA==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.1", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.1", - "postcss-calc": "^10.1.1", - "postcss-colormin": "^7.0.4", - "postcss-convert-values": "^7.0.7", - "postcss-discard-comments": "^7.0.4", - "postcss-discard-duplicates": "^7.0.2", - "postcss-discard-empty": "^7.0.1", - "postcss-discard-overridden": "^7.0.1", - "postcss-merge-longhand": "^7.0.5", - "postcss-merge-rules": "^7.0.6", - "postcss-minify-font-values": "^7.0.1", - "postcss-minify-gradients": "^7.0.1", - "postcss-minify-params": "^7.0.4", - "postcss-minify-selectors": "^7.0.5", - "postcss-normalize-charset": "^7.0.1", - "postcss-normalize-display-values": "^7.0.1", - "postcss-normalize-positions": "^7.0.1", - "postcss-normalize-repeat-style": "^7.0.1", - "postcss-normalize-string": "^7.0.1", - "postcss-normalize-timing-functions": "^7.0.1", - "postcss-normalize-unicode": "^7.0.4", - "postcss-normalize-url": "^7.0.1", - "postcss-normalize-whitespace": "^7.0.1", - "postcss-ordered-values": "^7.0.2", - "postcss-reduce-initial": "^7.0.4", - "postcss-reduce-transforms": "^7.0.1", - "postcss-svgo": "^7.1.0", - "postcss-unique-selectors": "^7.0.4" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/cssnano-utils": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz", - "integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==", - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" - }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/date.js": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", - "integrity": "sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==", - "license": "MIT", - "dependencies": { - "debug": "~3.1.0" - } - }, - "node_modules/date.js/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/date.js/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "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/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT" - }, - "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/docker-modem": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", - "integrity": "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.1", - "readable-stream": "^3.5.0", - "split-ca": "^1.0.1", - "ssh2": "^1.15.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dockerode": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.6.tgz", - "integrity": "sha512-FbVf3Z8fY/kALB9s+P9epCpWhfi/r0N2DgYYcYpsAUlaTxPjdsitsFobnltb+lyCgAIvf9C+4PSWlTnHlJMf1w==", - "license": "Apache-2.0", - "dependencies": { - "@balena/dockerignore": "^1.0.2", - "@grpc/grpc-js": "^1.11.1", - "@grpc/proto-loader": "^0.7.13", - "docker-modem": "^5.0.6", - "protobufjs": "^7.3.2", - "tar-fs": "~2.1.2", - "uuid": "^10.0.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dockerode/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/dompurify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", - "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "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/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.240", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", - "integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==", - "license": "ISC" - }, - "node_modules/emitter-component": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz", - "integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/encoding-sniffer/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "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/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", - "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", - "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", - "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.1", - "@eslint/core": "^0.16.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.38.0", - "@eslint/plugin-kit": "^0.4.0", - "@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-mocha": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-10.5.0.tgz", - "integrity": "sha512-F2ALmQVPT1GoP27O1JTZGrV9Pqg8k79OeIuvw63UxMtQKREZtmkK1NFgkZQ2TW7L2JSSFKHFPTtHu5z8R9QNRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-utils": "^3.0.0", - "globals": "^13.24.0", - "rambda": "^7.4.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-mocha/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-plugin-mocha/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "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-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "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/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "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.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/event-to-promise": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz", - "integrity": "sha512-VOBBfyaADfe378ZzG0tgkzmsvzUyeU5arehrFzNRt5yaASUDshgctTwSrPI17ocAwR3+YftsxRClHF+GBKFByQ==", - "deprecated": "Use promise-toolbox/fromEvent instead", - "license": "MIT" - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz", - "integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==", - "license": "MIT", - "dependencies": { - "ip-address": "10.0.1" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "license": "MIT" - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "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": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "license": "MIT" - }, - "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/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", - "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/form-data-encoder": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", - "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", - "license": "MIT", - "engines": { - "node": ">= 18" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "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/gamedig": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.2.tgz", - "integrity": "sha512-R2b1LwjW783PZsHRl9M8R06UkvJwXmJ6PDKk48UukJQ9ktiwrCeAk90MAZx6nF3oA444uf7r5eHjfaYbNoSV+Q==", - "license": "MIT", - "dependencies": { - "fast-xml-parser": "5.2.5", - "gbxremote": "0.2.1", - "got": "13.0.0", - "iconv-lite": "0.7.0", - "long": "5.3.2", - "minimist": "1.2.8", - "seek-bzip": "2.0.0", - "telnet-client": "2.2.6", - "varint": "6.0.0" - }, - "bin": { - "gamedig": "bin/gamedig.js" - }, - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/gamedig/node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/gamedig/node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/gamedig/node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "license": "MIT", - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/gamedig/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gamedig/node_modules/got": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", - "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/gamedig/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/gamedig/node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/gaxios/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gbxremote": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.2.1.tgz", - "integrity": "sha512-SMehu6Y6ndq2Qgp9VxAb8Np3f+UUD+RWoW2SAMaxzGS96rWXyr4T1GGkecO0HHtxeH1m7pEh4FJWB8a/6aM2XQ==", - "dependencies": { - "any-promise": "^1.1.0", - "barse": "~0.4.2", - "event-to-promise": "^0.7.0", - "string-to-stream": "^1.0.1", - "xmlrpc": "^1.3.1" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "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-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/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", - "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/got": { - "version": "14.4.7", - "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", - "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^7.0.1", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^12.0.1", - "decompress-response": "^6.0.0", - "form-data-encoder": "^4.0.2", - "http2-wrapper": "^2.2.1", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^4.0.1", - "responselike": "^3.0.0", - "type-fest": "^4.26.1" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "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/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/helmet": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", - "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/htmlnano": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.5.tgz", - "integrity": "sha512-IXffzXq1beGQN2rsr03aIPK/rVU1jR2uwHymlAIEf97Tl5WdpG50IsQ5nWGvSGQJ+x6U7S6yac9rRiFgAg4/xQ==", - "license": "MIT", - "dependencies": { - "@types/relateurl": "^0.2.33", - "cosmiconfig": "^9.0.0", - "posthtml": "^0.16.5" - }, - "peerDependencies": { - "cssnano": "^7.0.0", - "postcss": "^8.3.11", - "purgecss": "^7.0.2", - "relateurl": "^0.2.7", - "srcset": "5.0.1", - "svgo": "^3.0.2", - "terser": "^5.10.0", - "uncss": "^0.17.3" - }, - "peerDependenciesMeta": { - "cssnano": { - "optional": true - }, - "postcss": { - "optional": true - }, - "purgecss": { - "optional": true - }, - "relateurl": { - "optional": true - }, - "srcset": { - "optional": true - }, - "svgo": { - "optional": true - }, - "terser": { - "optional": true - }, - "uncss": { - "optional": true - } - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/human-interval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz", - "integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==", - "license": "MIT", - "dependencies": { - "numbered": "^1.1.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "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", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ioredis": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", - "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "1.4.0", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "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-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-json": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", - "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", - "license": "ISC" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "license": "MIT" - }, - "node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/isomorphic-dompurify": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.30.1.tgz", - "integrity": "sha512-VJFbthRrns7BE+q3qSUJ5zxGNjuq4FqiaWXKCwnMoJbumnoQJoeOeOzP/oejKLPPtENckLWoDxGQiv5OkEFC+Q==", - "license": "MIT", - "dependencies": { - "dompurify": "^3.3.0", - "jsdom": "^27.0.1" - }, - "engines": { - "node": ">=20.19.5" - } - }, - "node_modules/isomorphic-dompurify/node_modules/@asamuzakjp/css-color": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", - "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "lru-cache": "^11.2.1" - } - }, - "node_modules/isomorphic-dompurify/node_modules/cssstyle": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", - "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^4.0.3", - "@csstools/css-syntax-patches-for-csstree": "^1.0.14", - "css-tree": "^3.1.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/isomorphic-dompurify/node_modules/data-urls": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", - "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/isomorphic-dompurify/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/isomorphic-dompurify/node_modules/jsdom": { - "version": "27.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", - "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", - "license": "MIT", - "dependencies": { - "@asamuzakjp/dom-selector": "^6.7.2", - "cssstyle": "^5.3.1", - "data-urls": "^6.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^15.1.0", - "ws": "^8.18.3", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/isomorphic-dompurify/node_modules/lru-cache": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", - "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/isomorphic-dompurify/node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/isomorphic-dompurify/node_modules/tldts": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", - "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", - "license": "MIT", - "dependencies": { - "tldts-core": "^7.0.17" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/isomorphic-dompurify/node_modules/tldts-core": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", - "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", - "license": "MIT" - }, - "node_modules/isomorphic-dompurify/node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/isomorphic-dompurify/node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/isomorphic-dompurify/node_modules/webidl-conversions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", - "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, - "node_modules/isomorphic-dompurify/node_modules/whatwg-url": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", - "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", - "license": "MIT", - "dependencies": { - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/isomorphic-unfetch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-4.0.2.tgz", - "integrity": "sha512-1Yd+CF/7al18/N2BDbsLBcp6RO3tucSW+jcLq24dqdX5MNbCNTw1z4BsGsp4zNmjr/Izm2cs/cEqZPp4kvWSCA==", - "license": "MIT", - "dependencies": { - "node-fetch": "^3.2.0", - "unfetch": "^5.0.0" - } - }, - "node_modules/isomorphic-unfetch/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", - "license": "MIT", - "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "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", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "license": "MIT", - "dependencies": { - "jws": "^3.2.2", - "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" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/juice": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/juice/-/juice-11.0.3.tgz", - "integrity": "sha512-VYjPg4WylyWyLPnSiUsJ9tnnGhRZF0vn0YD8WWwaI8FhP9+1UdRMyRDbvqPOH/nBotmLKOc+FI+Oma6FwVWfSw==", - "license": "MIT", - "dependencies": { - "cheerio": "1.0.0", - "commander": "^12.1.0", - "entities": "^7.0.0", - "mensch": "^0.3.4", - "slick": "^1.12.2", - "web-resource-inliner": "^7.0.0" - }, - "bin": { - "juice": "bin/juice" - }, - "engines": { - "node": ">=18.17" - } - }, - "node_modules/juice/node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=18.17" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/juice/node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/juice/node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/juice/node_modules/htmlparser2/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "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", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "license": "MIT" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "license": "MIT", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/luxon": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", - "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/mailersend": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mailersend/-/mailersend-2.6.0.tgz", - "integrity": "sha512-YX2Gyc6Wyw4Q4IsJ4np2Reof8nFWQ2OP/yXZTZcGCz4B7B1BOAYs71Kjb1uNRTTfDChP7rzzWBoOaX6iNlvPAg==", - "license": "MIT", - "dependencies": { - "gaxios": "^6.0.0", - "isomorphic-unfetch": "^4.0.0", - "qs": "^6.11.0" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "license": "CC0-1.0" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" - }, - "node_modules/mensch": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz", - "integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==", - "license": "MIT" - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "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/mime-types/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/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mjml": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml/-/mjml-5.0.0-alpha.6.tgz", - "integrity": "sha512-unizId6dKTQSHq1nGnRQqe58kpD7VJu9p+vfMsKO4911/+VCrxkFe0oiwS7Q6XA3rc224MVChO2Mvueyeh16jg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "mjml-cli": "5.0.0-alpha.6", - "mjml-core": "5.0.0-alpha.6", - "mjml-preset-core": "5.0.0-alpha.6", - "mjml-validator": "5.0.0-alpha.6" - }, - "bin": { - "mjml": "bin/mjml" - } - }, - "node_modules/mjml-accordion": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-5.0.0-alpha.6.tgz", - "integrity": "sha512-K7FS8HZQsfUNkvwUWMjCmaEEtHRrqEDddyaVoOE6+ir4H6aWAJUZqp1j/USQsYwMI3YAS3UDpqQEOagG4pRDqg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-body": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-5.0.0-alpha.6.tgz", - "integrity": "sha512-otkINnYsBVbKSYqOz/FmhQruOpYzT10w9+UGcOZdBwv+UqDoKHXAD8QeSFZxr7fDSFhc6qeFAKNSY5uciT1ZuQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-button": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-5.0.0-alpha.6.tgz", - "integrity": "sha512-Z+0J6F2hk7QxKydlABna/vcGtTl7WfMWMLH74x9T1VgNylUAZ2TaWOUAo1AbWICgYBmZfNw1UkDj4EFvb+B1LA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-carousel": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-5.0.0-alpha.6.tgz", - "integrity": "sha512-G2n6D6smGmQEKGByjR52UD14D/0gtVokaEI4wjG7UjisSGYGbDXJW4thfDv2RdwIoozXyGhJD3vRuHsoCPeFnw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-cli": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-5.0.0-alpha.6.tgz", - "integrity": "sha512-mHmw5MCLNImcUUnYwl1yPF3meAd2EApnriMp8DTEN3zCqXsMg1RV8EB1npp4Camwz8HvVMTvEjSoVotwNWxAMg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "chokidar": "^3.0.0", - "glob": "^10.3.10", - "lodash": "^4.17.21", - "minimatch": "^9.0.3", - "mjml-core": "5.0.0-alpha.6", - "mjml-parser-xml": "5.0.0-alpha.6", - "mjml-validator": "5.0.0-alpha.6", - "yargs": "^17.7.2" - }, - "bin": { - "mjml-cli": "bin/mjml" - } - }, - "node_modules/mjml-cli/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mjml-cli/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mjml-column": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-5.0.0-alpha.6.tgz", - "integrity": "sha512-FWpmyCH1kzV3g0P6po1OMVOLTgQXDTbo6z30ew+CylUYnn1w5Vh30T1ZvSPYQDc2jGqjsX1zuWO/JUjOJ5B2RQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-core": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-5.0.0-alpha.6.tgz", - "integrity": "sha512-rrGr+xrOCnJ+3V/+LeqA4BCp7jrXiRq0q37FlmSs+etE86yNqMMFMgEFEbeGYTTQO07WuqXlhPSuq85ucAoivg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "cheerio": "1.0.0-rc.12", - "cssnano": "^7.0.1", - "detect-node": "^2.0.4", - "htmlnano": "^2.1.1", - "juice": "^11.0.0", - "lodash": "^4.17.21", - "mjml-parser-xml": "5.0.0-alpha.6", - "mjml-validator": "5.0.0-alpha.6", - "postcss": "^8.4.33", - "prettier": "^3.2.4" - } - }, - "node_modules/mjml-divider": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-5.0.0-alpha.6.tgz", - "integrity": "sha512-lw0rQNn2Y4LRcF/ad4JUjVRsyXhYXdaos6yOXPFjRN8aK1T4p7XctZyP5RMl7m8YfXTNxxyN//hN3bQI5DwvgQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-group": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-5.0.0-alpha.6.tgz", - "integrity": "sha512-No9EeJC9GSpqmxfo0laCmUU0w0xhHb3HaliFJPh7YlzObyRE3nvawHtPYpfNW3lgYNl0U7oYMlN9AQum/DaW2Q==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-head": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-5.0.0-alpha.6.tgz", - "integrity": "sha512-owBfZUcwHV2Wjow32QkRNZClbVHIAthacskvowAODUlOfTG1Xj0czlAM6iCG73cSqmZFuocOD9hy47y0l00Qeg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-head-attributes": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-5.0.0-alpha.6.tgz", - "integrity": "sha512-CW/E+IEw3MOLTWkTMbN6egPwoeg22Iup+fICAH9rmITD6SBJ6f37/MFiACSmpkDQIlWdFokBFFKpikbPWFJKQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-head-breakpoint": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-5.0.0-alpha.6.tgz", - "integrity": "sha512-3RsHM0l3VU+NhG4MAsXevLH50s/uOJqbZznGVKkgEBsodKDhFK2ndlmUWZ5ywDO6H2g/qDrZse4c1K43uGoyzA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-head-font": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-5.0.0-alpha.6.tgz", - "integrity": "sha512-+HZ/Ppd/hfFgob9M27Tz9qio/vWwyTwjo40PsYSI6zXKmIUHC5icBcHaezUKMDGadgd+11aXlPLVfs6zGb0hVg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-head-html-attributes": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-head-html-attributes/-/mjml-head-html-attributes-5.0.0-alpha.6.tgz", - "integrity": "sha512-INI3irUFHozLfJCW0pi/499DhlwqpsFEoGQ++NdHlFH87hbQ2XTNlrTHWpgjhSuiJkRJinPYHphRuNyM3KLyVw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-head-preview": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-5.0.0-alpha.6.tgz", - "integrity": "sha512-HGLcYmJ7q4aYD5VNTUKIvk6wvpXUawI5j4yl9m+DEhk2ptKYmayxLpjiAvH+LioNuPh0zTCGt7U9f5Ja0948gQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-head-style": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-5.0.0-alpha.6.tgz", - "integrity": "sha512-yQcHIvZGH641irNt21r4hzODp5rEI/qKJRvGVJ6vzyGm7poYbaoxhK7Re9cRu+xZxLpkiTRgf1DYFjyZN45SiQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-head-title": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-5.0.0-alpha.6.tgz", - "integrity": "sha512-m6dGCAItgobZSJ7wlETFPth6rU0+617ZVk1f88Mz5I+YVuOgaAqdNDQWdue5Uj7B19c55NmtdZwsgOKmhHuwow==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-hero": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-5.0.0-alpha.6.tgz", - "integrity": "sha512-OLOKTK/VW4fQOH8yQz1e5kKHfyF0heECghPhmxgnEHigJLbaJy/rmb4x4aq0Lr74Bme8hsMjkR+oktGej1c4iQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-image": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-5.0.0-alpha.6.tgz", - "integrity": "sha512-NENjbEOzobM0iQLlxJUNuqiNlFSP5E1DfiP37bY8smYmpVe4MXi5HC5ajTH5ZBKWsTN4/UVZL7t+ZSqniTnS5w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-navbar": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-5.0.0-alpha.6.tgz", - "integrity": "sha512-bj328dixWJqox9CaA/rvIKsTZ7c5g1mN9B0gL2vuzVMXgX4qnXkzIfN2hw98TdffaKGsKXC6N25iIDVocj1RrQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-parser-xml": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-5.0.0-alpha.6.tgz", - "integrity": "sha512-2gxzFJXBFq6l8f2/HlDq7MBLtWQja0KPSqePxPBOXmNLjqw6bdSflnQu+OhWVptdywuHm907LNIQVIq0wSo2FQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "detect-node": "2.1.0", - "htmlparser2": "^9.1.0", - "lodash": "^4.17.21" - } - }, - "node_modules/mjml-parser-xml/node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, - "node_modules/mjml-preset-core": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-preset-core/-/mjml-preset-core-5.0.0-alpha.6.tgz", - "integrity": "sha512-6K9sPXlxQfe9Vx/8/wAFW2Sy6ImWU1AP37VG08Ge7Mj117cNeKk3S1piagbj6LpyTd7E7AueO8G+xzvmC5YBBw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "mjml-accordion": "5.0.0-alpha.6", - "mjml-body": "5.0.0-alpha.6", - "mjml-button": "5.0.0-alpha.6", - "mjml-carousel": "5.0.0-alpha.6", - "mjml-column": "5.0.0-alpha.6", - "mjml-divider": "5.0.0-alpha.6", - "mjml-group": "5.0.0-alpha.6", - "mjml-head": "5.0.0-alpha.6", - "mjml-head-attributes": "5.0.0-alpha.6", - "mjml-head-breakpoint": "5.0.0-alpha.6", - "mjml-head-font": "5.0.0-alpha.6", - "mjml-head-html-attributes": "5.0.0-alpha.6", - "mjml-head-preview": "5.0.0-alpha.6", - "mjml-head-style": "5.0.0-alpha.6", - "mjml-head-title": "5.0.0-alpha.6", - "mjml-hero": "5.0.0-alpha.6", - "mjml-image": "5.0.0-alpha.6", - "mjml-navbar": "5.0.0-alpha.6", - "mjml-raw": "5.0.0-alpha.6", - "mjml-section": "5.0.0-alpha.6", - "mjml-social": "5.0.0-alpha.6", - "mjml-spacer": "5.0.0-alpha.6", - "mjml-table": "5.0.0-alpha.6", - "mjml-text": "5.0.0-alpha.6", - "mjml-wrapper": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-raw": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-5.0.0-alpha.6.tgz", - "integrity": "sha512-tY2P+g7bcydVHrk0NPPm2sd+rIB9l5TdCeIdtw+NSOXoHN7vXVezcdr7UYK8TePibzeymh8w9YTWEAIN8fWvLg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-section": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-5.0.0-alpha.6.tgz", - "integrity": "sha512-gJL+9t9hiAfnXl3gG481Xum1qzURXs6bZ+rZnU47R/070+kinOJTCHt82hFGctjyDi264r/BeM6H6y+m0FIq/w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-social": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-5.0.0-alpha.6.tgz", - "integrity": "sha512-vPRBYHKeEUwEiVwXLrXgQnWBblizdQTlrZ2V45xEH9+3Jqolw3SlTw+uoZzhcFSBNl7+ytdQgwp7gRC7Bn5IYQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-spacer": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-5.0.0-alpha.6.tgz", - "integrity": "sha512-ZyLcoAElvkWnijny2eW0ulX6RxoDQT8AZwv5pJ8O16s0mnfcPAPMaZGTCQZHxPbACKJcCKsIoZrQllQuShf3XA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-table": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-5.0.0-alpha.6.tgz", - "integrity": "sha512-ze7iNRyT6uX099KxZoV33u1jOlibmfxkUUY/8BXbyfWUIgcwRzb1DfKtIWT/GOqI2WAqh5ZwHd5XZgueimttEA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-text": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-5.0.0-alpha.6.tgz", - "integrity": "sha512-k7/pUgwZo9ZwnoCVvohsGdpOrJuLpScH183ZxjyPYO8+kOruSFpE2yrGhK/jeLaV4UhdXndNk8dz80wbN0fKjw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6" - } - }, - "node_modules/mjml-validator": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-5.0.0-alpha.6.tgz", - "integrity": "sha512-kL5IJGYXdNN7VunXXJEWyAP40oODRiSNu5NIx5moOXREPHvn7lRqi/XS8MXa48/UBLmKMbltDSQ0DcnESJbodQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9" - } - }, - "node_modules/mjml-wrapper": { - "version": "5.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-5.0.0-alpha.6.tgz", - "integrity": "sha512-qJisqqkQrtq4U74BTgRaNoPjpf2PwYff/QxH5yvkFDXK3fYEoNt011K7lrm/+u5wS9mx4msHZN0WA4TI/EeeOw==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.9", - "lodash": "^4.17.21", - "mjml-core": "5.0.0-alpha.6", - "mjml-section": "5.0.0-alpha.6" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/mocha": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", - "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", - "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mongodb": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", - "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.2" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.3.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "license": "Apache-2.0", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.19.2", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.2.tgz", - "integrity": "sha512-ww2T4dBV+suCbOfG5YPwj9pLCfUVyj8FEA1D3Ux1HHqutpLxGyOYEPU06iPRBW4cKr3PJfOSYsIpHWPTkz5zig==", - "license": "MIT", - "dependencies": { - "bson": "^6.10.4", - "kareem": "2.6.3", - "mongodb": "~6.20.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "license": "MIT", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, - "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", - "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", - "license": "MIT", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/nan": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", - "integrity": "sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==", - "license": "MIT", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "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", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/net": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz", - "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==", - "license": "MIT" - }, - "node_modules/nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" - } - }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "license": "MIT" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, - "node_modules/node-releases": { - "version": "2.0.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", - "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", - "license": "MIT" - }, - "node_modules/nodemailer": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", - "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", - "license": "MIT-0", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", - "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/numbered": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz", - "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==", - "license": "MIT" - }, - "node_modules/nwsapi": { - "version": "2.2.22", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", - "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "license": "MIT", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "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-cancelable": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", - "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "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", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/papaparse": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", - "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", - "license": "MIT" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "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/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/ping": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz", - "integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "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/postcss-calc": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", - "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12 || ^20.9 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.38" - } - }, - "node_modules/postcss-colormin": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.4.tgz", - "integrity": "sha512-ziQuVzQZBROpKpfeDwmrG+Vvlr0YWmY/ZAk99XD+mGEBuEojoFekL41NCsdhyNUtZI7DPOoIWIR7vQQK9xwluw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.1", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-convert-values": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.7.tgz", - "integrity": "sha512-HR9DZLN04Xbe6xugRH6lS4ZQH2zm/bFh/ZyRkpedZozhvh+awAfbA0P36InO4fZfDhvYfNJeNvlTf1sjwGbw/A==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-discard-comments": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.4.tgz", - "integrity": "sha512-6tCUoql/ipWwKtVP/xYiFf1U9QgJ0PUvxN7pTcsQ8Ns3Fnwq1pU5D5s1MhT/XySeLq6GXNvn37U46Ded0TckWg==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz", - "integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==", - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-discard-empty": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz", - "integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==", - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz", - "integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==", - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz", - "integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^7.0.5" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-merge-rules": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.6.tgz", - "integrity": "sha512-2jIPT4Tzs8K87tvgCpSukRQ2jjd+hH6Bb8rEEOUDmmhOeTcqDg5fEFK8uKIu+Pvc3//sm3Uu6FRqfyv7YF7+BQ==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.1", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^5.0.1", - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz", - "integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.1.tgz", - "integrity": "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==", - "license": "MIT", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^5.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-minify-params": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.4.tgz", - "integrity": "sha512-3OqqUddfH8c2e7M35W6zIwv7jssM/3miF9cbCSb1iJiWvtguQjlxZGIHK9JRmc8XAKmE2PFGtHSM7g/VcW97sw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.1", - "cssnano-utils": "^5.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.5.tgz", - "integrity": "sha512-x2/IvofHcdIrAm9Q+p06ZD1h6FPcQ32WtCRVodJLDR+WMn8EVHI1kvLxZuGKz/9EY5nAmI6lIQIrpo4tBy5+ug==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", - "integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==", - "license": "MIT", - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz", - "integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz", - "integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz", - "integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-string": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz", - "integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz", - "integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.4.tgz", - "integrity": "sha512-LvIURTi1sQoZqj8mEIE8R15yvM+OhbR1avynMtI9bUzj5gGKR/gfZFd8O7VMj0QgJaIFzxDwxGl/ASMYAkqO8g==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-url": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz", - "integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz", - "integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-ordered-values": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz", - "integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==", - "license": "MIT", - "dependencies": { - "cssnano-utils": "^5.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.4.tgz", - "integrity": "sha512-rdIC9IlMBn7zJo6puim58Xd++0HdbvHeHaPgXsimMfG1ijC5A9ULvNLSE0rUKVJOvNMcwewW4Ga21ngyJjY/+Q==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.1", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz", - "integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.0.tgz", - "integrity": "sha512-KnAlfmhtoLz6IuU3Sij2ycusNs4jPW+QoFE5kuuUOK8awR6tMxZQrs5Ey3BUz7nFCzT3eqyFgqkyrHiaU2xx3w==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^4.0.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz", - "integrity": "sha512-pmlZjsmEAG7cHd7uK3ZiNSW6otSZ13RHuZ/4cDN/bVglS5EpF2r2oxY99SuOHa8m7AWoBCelTS3JPpzsIs8skQ==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/posthtml": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz", - "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==", - "license": "MIT", - "dependencies": { - "posthtml-parser": "^0.11.0", - "posthtml-render": "^3.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/posthtml-parser": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", - "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", - "license": "MIT", - "dependencies": { - "htmlparser2": "^7.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/posthtml-parser/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/posthtml-parser/node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/posthtml-parser/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/posthtml-parser/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/posthtml-parser/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/posthtml-parser/node_modules/htmlparser2": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", - "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.2", - "domutils": "^2.8.0", - "entities": "^3.0.1" - } - }, - "node_modules/posthtml-render": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", - "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", - "license": "MIT", - "dependencies": { - "is-json": "^2.0.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "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/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rambda": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/rambda/-/rambda-7.5.0.tgz", - "integrity": "sha512-y/M9weqWAH4iopRd7EHDEQQvpFPHj1AA3oHozE9tfITHUtTR7Z9PSlIRRG2l1GuW7sefC1cXFfIcF+cgnShdBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, - "node_modules/seek-bzip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz", - "integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==", - "license": "MIT", - "dependencies": { - "commander": "^6.0.0" - }, - "bin": { - "seek-bunzip": "bin/seek-bunzip", - "seek-table": "bin/seek-bzip-table" - } - }, - "node_modules/seek-bzip/node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/sharp": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", - "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.6.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.5", - "@img/sharp-darwin-x64": "0.33.5", - "@img/sharp-libvips-darwin-arm64": "1.0.4", - "@img/sharp-libvips-darwin-x64": "1.0.4", - "@img/sharp-libvips-linux-arm": "1.0.5", - "@img/sharp-libvips-linux-arm64": "1.0.4", - "@img/sharp-libvips-linux-s390x": "1.0.4", - "@img/sharp-libvips-linux-x64": "1.0.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", - "@img/sharp-libvips-linuxmusl-x64": "1.0.4", - "@img/sharp-linux-arm": "0.33.5", - "@img/sharp-linux-arm64": "0.33.5", - "@img/sharp-linux-s390x": "0.33.5", - "@img/sharp-linux-x64": "0.33.5", - "@img/sharp-linuxmusl-arm64": "0.33.5", - "@img/sharp-linuxmusl-x64": "0.33.5", - "@img/sharp-wasm32": "0.33.5", - "@img/sharp-win32-ia32": "0.33.5", - "@img/sharp-win32-x64": "0.33.5" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", - "license": "MIT" - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", - "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", - "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", - "license": "MIT" - }, - "node_modules/sinon": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", - "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.2", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", - "nise": "^6.1.1", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/slick": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", - "integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==", - "license": "MIT (http://mootools.net/license.txt)", - "engines": { - "node": "*" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "license": "MIT", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "node_modules/split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", - "license": "ISC" - }, - "node_modules/ssh2": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", - "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", - "hasInstallScript": true, - "dependencies": { - "asn1": "^0.2.6", - "bcrypt-pbkdf": "^1.0.2" - }, - "engines": { - "node": ">=10.16.0" - }, - "optionalDependencies": { - "cpu-features": "~0.0.10", - "nan": "^2.23.0" - } - }, - "node_modules/ssl-checker": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/ssl-checker/-/ssl-checker-2.0.10.tgz", - "integrity": "sha512-SS6rrZocToJWHM1p6iVNb583ybB3UqT1fymCHSWuEdXDUqKA6O1D5Fb8KJVmhj3XKXE82IEWcr+idJrc4jUzFQ==", - "license": "MIT" - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz", - "integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==", - "license": "MIT", - "dependencies": { - "emitter-component": "^1.1.1" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-to-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz", - "integrity": "sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.1.0" - } - }, - "node_modules/string-to-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/string-to-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/string-to-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/string-to-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/stylehacks": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.6.tgz", - "integrity": "sha512-iitguKivmsueOmTO0wmxURXBP8uqOO+zikLGZ7Mm9e/94R4w5T999Js2taS/KBOnQ/wdC3jN3vNSrkGDrlnqQg==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.1", - "postcss-selector-parser": "^7.1.0" - }, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" - } - }, - "node_modules/super-simple-scheduler": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.4.tgz", - "integrity": "sha512-o99zHFrrFECwj1FFE+ns4su5hKSYif+SUNB3fUJbaRWGkwLLoj19TRuRJYp72TYt9eDiCii68UBuKiLF4tlkMg==", - "license": "MIT", - "dependencies": { - "uuid": "11.1.0", - "winston": "3.17.0" - } - }, - "node_modules/super-simple-scheduler/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/super-simple-scheduler/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, - "node_modules/super-simple-scheduler/node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/svgo": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", - "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", - "license": "MIT", - "dependencies": { - "commander": "^11.1.0", - "css-select": "^5.1.0", - "css-tree": "^3.0.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.1.1", - "sax": "^1.4.1" - }, - "bin": { - "svgo": "bin/svgo.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/swagger-ui-dist": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz", - "integrity": "sha512-BoiDSeT9PCtHfDYMgX5UpB/qTQy44UoSFRmzHqvhGfgxzEVPHxIW78a+HMLUIHnSFu3z63wjtbq6L6+Rto20Rw==", - "license": "Apache-2.0", - "dependencies": { - "@scarf/scarf": "=1.4.0" - } - }, - "node_modules/swagger-ui-express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", - "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", - "license": "MIT", - "dependencies": { - "swagger-ui-dist": ">=5.0.0" - }, - "engines": { - "node": ">= v0.10.32" - }, - "peerDependencies": { - "express": ">=4.0.0 || >=5.0.0-beta" - } - }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "license": "MIT" - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/telnet-client": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.6.tgz", - "integrity": "sha512-ZUYrLsPtQupQww3eSEORDVOb6ztdtKEghya6TVXPo2tg/UQq2pn5rHhvwuUvyYpbnsoqdNY1fyD1GNkXHR8dYA==", - "license": "MIT", - "dependencies": { - "net": "^1.0.2", - "stream": "^0.0.2" - }, - "funding": { - "type": "paypal", - "url": "https://paypal.me/kozjak" - } - }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "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/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "license": "MIT" - }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "license": "MIT" - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.20.5", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", - "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "license": "Unlicense" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "license": "MIT" - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/undici": { - "version": "6.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", - "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "license": "MIT" - }, - "node_modules/unfetch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-5.0.0.tgz", - "integrity": "sha512-3xM2c89siXg0nHvlmYsQ2zkLASvVMBisZm5lF3gFDqfF2xonNStDJyMpvaOBe0a1Edxmqrf2E0HBdmy9QyZaeg==", - "license": "MIT", - "workspaces": [ - "./packages/isomorphic-unfetch" - ] - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", - "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", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/valid-data-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", - "integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/varint": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", - "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "license": "MIT" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/web-resource-inliner": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz", - "integrity": "sha512-NlfnGF8MY9ZUwFjyq3vOUBx7KwF8bmE+ywR781SB0nWB6MoMxN4BA8gtgP1KGTZo/O/AyWJz7HZpR704eaj4mg==", - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.1", - "escape-goat": "^3.0.0", - "htmlparser2": "^5.0.0", - "mime": "^2.4.6", - "valid-data-url": "^3.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/web-resource-inliner/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.0.1" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/htmlparser2": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz", - "integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^3.3.0", - "domutils": "^2.4.2", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/fb55/htmlparser2?sponsor=1" - } - }, - "node_modules/web-resource-inliner/node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/winston": { - "version": "3.18.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", - "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", - "license": "MIT", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "license": "MIT", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" - }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/xmlbuilder": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", - "integrity": "sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "license": "MIT" - }, - "node_modules/xmlrpc": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz", - "integrity": "sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==", - "license": "MIT", - "dependencies": { - "sax": "1.2.x", - "xmlbuilder": "8.2.x" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.0.0" - } - }, - "node_modules/xmlrpc/node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "license": "ISC" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } + "name": "server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "server", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcryptjs": "3.0.2", + "cacheable-lookup": "7.0.0", + "cookie-parser": "1.4.7", + "cors": "2.8.5", + "dotenv": "17.2.3", + "express": "5.1.0", + "got": "14.5.0", + "jsonwebtoken": "9.0.2", + "mongoose": "8.19.1", + "nodemailer": "7.0.9", + "ping": "1.0.0", + "super-simple-scheduler": "1.4.6", + "winston": "3.18.3", + "zod": "4.1.12" + }, + "devDependencies": { + "@types/cookie-parser": "1.4.9", + "@types/cors": "2.8.19", + "@types/jsonwebtoken": "9.0.10", + "@types/node": "24.7.1", + "@types/nodemailer": "7.0.2", + "nodemon": "3.1.10", + "tsc-alias": "1.8.16", + "tsx": "4.20.6", + "typescript": "5.9.3" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.922.0.tgz", + "integrity": "sha512-cowHCdzir4KmT/MoRyp2RV3BAebjcpiyKU1pidu2D1lI87iGXlxNG7KXJ0W8mjQoGpKa2XcihDY/mtqd/6uVlA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-node": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/signature-v4-multi-region": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.922.0.tgz", + "integrity": "sha512-jdHs7uy7cSpiMvrxhYmqHyJxgK7hyqw4plG8OQ4YTBpq0SbfAxdoOuOkwJ1IVUUQho4otR1xYYjiX/8e8J8qwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", + "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", + "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.922.0.tgz", + "integrity": "sha512-bVF+pI5UCLNkvbiZr/t2fgTtv84s8FCdOGAPxQiQcw5qOZywNuuCCY3wIIchmQr6GJr8YFkEp5LgDCac5EC5aQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.922.0.tgz", + "integrity": "sha512-agCwaD6mBihToHkjycL8ObIS2XOnWypWZZWhJSoWyHwFrhEKz1zGvgylK9Dc711oUfU+zU6J8e0JPKNJMNb3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", + "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.922.0.tgz", + "integrity": "sha512-nbD3G3hShTYxLCkKMqLkLPtKwAAfxdY/k9jHtZmVBFXek2T6tQrqZHKxlAu+fd23Ga4/Aik7DLQQx1RA1a5ipg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.922.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/token-providers": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.922.0.tgz", + "integrity": "sha512-wjGIhgMHGGQfQTdFaJphNOKyAL8wZs6znJdHADPVURmgR+EWLyN/0fDO1u7wx8xaLMZpbHIFWBEvf9TritR/cQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", + "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", + "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", + "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.922.0.tgz", + "integrity": "sha512-ygg8lME1oFAbsH42ed2wtGqfHLoT5irgx6VC4X98j79fV1qXEwwwbqMsAiMQ/HJehpjqAFRVsHox3MHLN48Z5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", + "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@smithy/core": "^3.17.2", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.922.0.tgz", + "integrity": "sha512-uYvKCF1TGh/MuJ4TMqmUM0Csuao02HawcseG4LUDyxdUsd/EFuxalWq1Cx4fKZQ2K8F504efZBjctMAMNY+l7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.922.0.tgz", + "integrity": "sha512-44Y/rNNwhngR2KHp6gkx//TOr56/hx6s4l+XLjOqH7EBCHL7XhnrT1y92L+DLiroVr1tCSmO8eHQwBv0Y2+mvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.922.0.tgz", + "integrity": "sha512-mmsgEEL5pE+A7gFYiJMDBCLVciaXq4EFI5iAP7bPpnHvOplnNOYxVy2IreKMllGvrfjVyLnwxzZYlo5zZ65FWg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.922.0.tgz", + "integrity": "sha512-/inmPnjZE0ZBE16zaCowAvouSx05FJ7p6BQYuzlJ8vxEU0sS0Hf8fvhuiRnN9V9eDUPIBY+/5EjbMWygXL4wlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", + "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-endpoints": "^3.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", + "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", + "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "license": "MIT" + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.1.tgz", + "integrity": "sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.4.tgz", + "integrity": "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.2.tgz", + "integrity": "sha512-4Jys0ni2tB2VZzgslbEgszZyMdTkPOFGA8g+So/NjR8oy6Qwaq4eSwsrRI+NMtb0Dq4kqCzGUu/nGUx7OM/xfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.2.tgz", + "integrity": "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.4.tgz", + "integrity": "sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.5.tgz", + "integrity": "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/querystring-builder": "^4.2.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.4.tgz", + "integrity": "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.4.tgz", + "integrity": "sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.4.tgz", + "integrity": "sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.6.tgz", + "integrity": "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.2", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-middleware": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.6.tgz", + "integrity": "sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/service-error-classification": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/uuid": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.4.tgz", + "integrity": "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.4.tgz", + "integrity": "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.4.tgz", + "integrity": "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.4.tgz", + "integrity": "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/querystring-builder": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.4.tgz", + "integrity": "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.4.tgz", + "integrity": "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.4.tgz", + "integrity": "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "@smithy/util-uri-escape": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.4.tgz", + "integrity": "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.4.tgz", + "integrity": "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.4.tgz", + "integrity": "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.4.tgz", + "integrity": "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.2.tgz", + "integrity": "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.2", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.1.tgz", + "integrity": "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.4.tgz", + "integrity": "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.5.tgz", + "integrity": "sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.8.tgz", + "integrity": "sha512-gIoTf9V/nFSIZ0TtgDNLd+Ws59AJvijmMDYrOozoMHPJaG9cMRdqNO50jZTlbM6ydzQYY8L/mQ4tKSw/TB+s6g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.2", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.4.tgz", + "integrity": "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.4.tgz", + "integrity": "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.4.tgz", + "integrity": "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.5.tgz", + "integrity": "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", + "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz", + "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/nodemailer": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.2.tgz", + "integrity": "sha512-Zo6uOA9157WRgBk/ZhMpTQ/iCWLMk7OIs/Q9jvHarMvrzUUP/MDdPHL2U1zpf57HrrWGv4nYQn5uIxna0xY3xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bson": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", + "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.13", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.13.tgz", + "integrity": "sha512-/5/6xDTN/AZHOn93MNaU7HUSzOL/YJWOwa5hV84ib1mPUtQB/ZTg5bn57UGfdNwXGhgQfcPpJDy+eGiNgGJI1w==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", + "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.0.1", + "color-string": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", + "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", + "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "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/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-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/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "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/got": { + "version": "14.5.0", + "resolved": "https://registry.npmjs.org/got/-/got-14.5.0.tgz", + "integrity": "sha512-rZ8+ZMYz9yeTFmFtOiwf/Trcli8gdqgqAz0kmkEOWcT7C3BFG3Z+dLQu0y8je7RogG7Wp1obnu5GerwCaccftw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^6.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/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/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "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" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", + "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/keyv": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.3.tgz", + "integrity": "sha512-h0Un1ieD+HUrzBH6dJXhod3ifSghk5Hw/2Y4/KHBziPlZecrFyE9YOTPU6eOs0V9pYl8gOs86fkr/KN8lUX39A==", + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mongodb": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^6.10.4", + "mongodb-connection-string-url": "^3.0.2" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.3.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", + "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^14.1.0 || ^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", + "integrity": "sha512-oB7hGQJn4f8aebqE7mhE54EReb5cxVgpCxQCQj0K/cK3q4J3Tg08nFP6sM52nJ4Hlm8jsDnhVYpqIITZUAhckQ==", + "license": "MIT", + "dependencies": { + "bson": "^6.10.4", + "kareem": "2.6.3", + "mongodb": "~6.20.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mylas": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz", + "integrity": "sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemailer": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", + "integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/ping": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ping/-/ping-1.0.0.tgz", + "integrity": "sha512-3dxdgGtV+7P/EVo42JhkGSomeO/0GGicSz3mI/yK+AI+VGNAOfakw5XfcbGI4IjyBY+ZZwvuRXdhnNF2uliKew==", + "license": "MIT", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/plimit-lit": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", + "integrity": "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue-lit": "^1.5.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-lit": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz", + "integrity": "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "license": "MIT", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/super-simple-scheduler": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.6.tgz", + "integrity": "sha512-Q2BScoDudGbLF62aHU/WSqomvdfqr4Vv/bCDkPDm+iKxGEltHXXu9q24mKB3XPqzu7eDXthlDNAfJBE49f05CQ==", + "license": "MIT", + "dependencies": { + "uuid": "11.1.0", + "winston": "3.17.0" + } + }, + "node_modules/super-simple-scheduler/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/super-simple-scheduler/node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/tsc-alias": { + "version": "1.8.16", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", + "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.3", + "commander": "^9.0.0", + "get-tsconfig": "^4.10.0", + "globby": "^11.0.4", + "mylas": "^2.1.9", + "normalize-path": "^3.0.0", + "plimit-lit": "^1.2.6" + }, + "bin": { + "tsc-alias": "dist/bin/index.js" + }, + "engines": { + "node": ">=16.20.2" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/winston": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } } diff --git a/server/package.json b/server/package.json old mode 100755 new mode 100644 index 7486e81a2..6e787499b --- a/server/package.json +++ b/server/package.json @@ -1,77 +1,42 @@ { - "name": "server", - "version": "1.0.0", - "description": "", - "main": "index.js", - "type": "module", - "scripts": { - "test": "c8 mocha", - "dev": "tsx watch src/index.js", - "start": "node --watch ./dist/index.js", - "build": "tsc", - "lint": "eslint .", - "lint-fix": "eslint --fix .", - "format": "prettier --write .", - "format-check": "prettier --check ." - }, - "keywords": [], - "author": "", - "license": "ISC", - "engines": { - "node": ">=20" - }, - "dependencies": { - "@pulsecron/pulse": "1.6.8", - "axios": "^1.7.2", - "bcryptjs": "3.0.2", - "bullmq": "5.41.2", - "cacheable-lookup": "7.0.0", - "compression": "1.8.1", - "cookie-parser": "^1.4.7", - "cors": "^2.8.5", - "dockerode": "4.0.6", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-rate-limit": "8.0.1", - "gamedig": "^5.3.1", - "got": "14.4.7", - "handlebars": "^4.7.8", - "helmet": "^8.0.0", - "ioredis": "^5.4.2", - "isomorphic-dompurify": "^2.26.0", - "jmespath": "^0.16.0", - "joi": "^17.13.1", - "jsdom": "^26.1.0", - "jsonwebtoken": "9.0.2", - "mailersend": "^2.2.0", - "mjml": "^5.0.0-alpha.4", - "mongoose": "^8.3.3", - "multer": "^1.4.5-lts.1", - "nodemailer": "^6.9.14", - "papaparse": "^5.5.2", - "ping": "0.4.4", - "sharp": "0.33.5", - "ssl-checker": "2.0.10", - "super-simple-scheduler": "1.4.5", - "swagger-ui-express": "5.0.1", - "winston": "^3.13.0" - }, - "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/express": "5.0.3", - "@types/jsonwebtoken": "9.0.10", - "@types/nodemailer": "7.0.1", - "@types/ping": "0.4.4", - "c8": "10.1.3", - "chai": "5.2.0", - "eslint": "^9.17.0", - "eslint-plugin-mocha": "^10.5.0", - "esm": "3.2.25", - "globals": "^15.14.0", - "mocha": "11.1.0", - "prettier": "^3.3.3", - "sinon": "19.0.2", - "tsx": "4.20.5", - "typescript": "5.9.2" - } + "name": "server", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "dev": "nodemon --exec tsx src/index.js", + "build": "tsc && tsc-alias", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@types/cookie-parser": "1.4.9", + "@types/cors": "2.8.19", + "@types/jsonwebtoken": "9.0.10", + "@types/node": "24.7.1", + "@types/nodemailer": "7.0.2", + "nodemon": "3.1.10", + "tsc-alias": "1.8.16", + "tsx": "4.20.6", + "typescript": "5.9.3" + }, + "dependencies": { + "bcryptjs": "3.0.2", + "cacheable-lookup": "7.0.0", + "cookie-parser": "1.4.7", + "cors": "2.8.5", + "dotenv": "17.2.3", + "express": "5.1.0", + "got": "14.5.0", + "jsonwebtoken": "9.0.2", + "mongoose": "8.19.1", + "nodemailer": "7.0.9", + "ping": "1.0.0", + "super-simple-scheduler": "1.4.6", + "winston": "3.18.3", + "zod": "4.1.12" + } } diff --git a/server/scripts/inject-vars.sh b/server/scripts/inject-vars.sh deleted file mode 100755 index 34365b0d3..000000000 --- a/server/scripts/inject-vars.sh +++ /dev/null @@ -1,7 +0,0 @@ -for i in $(env | grep UPTIME_APP_) -do - key=$(echo $i | cut -d '=' -f 1) - value=$(echo $i | cut -d '=' -f 2-) - echo $key=$value - find ./public/assets -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' + -done \ No newline at end of file diff --git a/server/src/app.js b/server/src/app.js deleted file mode 100644 index 4578de896..000000000 --- a/server/src/app.js +++ /dev/null @@ -1,85 +0,0 @@ -import express from "express"; -import path from "path"; -import { responseHandler } from "./middleware/v1/responseHandler.js"; -import cors from "cors"; -import helmet from "helmet"; -import compression from "compression"; -import cookieParser from "cookie-parser"; -import languageMiddleware from "./middleware/v1/languageMiddleware.js"; -import swaggerUi from "swagger-ui-express"; -import { handleErrors } from "./middleware/v1/handleErrors.js"; -import { setupRoutes } from "./config/routes.js"; -import { generalApiLimiter } from "./middleware/v1/rateLimiter.js"; -import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js"; - -export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { - const allowedOrigin = envSettings.clientHost; - - const app = express(); - app.use(generalApiLimiter); - // Static files - app.use(express.static(frontendPath)); - - // Response handler - app.use(responseHandler); - - app.use( - cors({ - origin: allowedOrigin, - methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", - allowedHeaders: ["Content-Type", "Authorization"], - credentials: true, - }) - ); - app.use(express.json()); - app.use(cookieParser()); - - app.use(sanitizeBody()); - app.use(sanitizeQuery()); - - app.use( - helmet({ - hsts: false, - contentSecurityPolicy: { - useDefaults: true, - directives: { - upgradeInsecureRequests: null, - "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'"], - "object-src": ["'none'"], - "base-uri": ["'self'"], - }, - }, - }) - ); - app.use( - compression({ - level: 6, - threshold: 1024, - filter: (req, res) => { - if (req.headers["x-no-compression"]) { - return false; - } - return compression.filter(req, res); - }, - }) - ); - app.use(languageMiddleware(services.stringService, services.translationService, services.settingsService)); - // Swagger UI - app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); - - app.use("/api/v1/health", (req, res) => { - res.json({ - status: "OK", - }); - }); - - // Main app routes - setupRoutes(app, controllers); - - // FE routes - app.get("*", (req, res) => { - res.sendFile(path.join(frontendPath, "index.html")); - }); - app.use(handleErrors); - return app; -}; diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js deleted file mode 100644 index 927acd1a2..000000000 --- a/server/src/config/controllers.js +++ /dev/null @@ -1,82 +0,0 @@ -import { createCommonDependencies } from "../controllers/v1/baseController.js"; - -// Services - -// V1 Controllers -import MonitorController from "../controllers/v1/monitorController.js"; -import AuthController from "../controllers/v1/authController.js"; -import SettingsController from "../controllers/v1/settingsController.js"; -import CheckController from "../controllers/v1/checkController.js"; -import InviteController from "../controllers/v1/inviteController.js"; -import MaintenanceWindowController from "../controllers/v1/maintenanceWindowController.js"; -import QueueController from "../controllers/v1/queueController.js"; -import LogController from "../controllers/v1/logController.js"; -import StatusPageController from "../controllers/v1/statusPageController.js"; -import NotificationController from "../controllers/v1/notificationController.js"; -import DiagnosticController from "../controllers/v1/diagnosticController.js"; - -// V2 Controllers -import AuthControllerV2 from "../controllers/v2/AuthController.js"; -import InviteControllerV2 from "../controllers/v2/InviteController.js"; -import MaintenanceControllerV2 from "../controllers/v2/MaintenanceController.js"; -import MonitorControllerV2 from "../controllers/v2/MonitorController.js"; -import NotificationChannelControllerV2 from "../controllers/v2/NotificationChannelController.js"; -import QueueControllerV2 from "../controllers/v2/QueueController.js"; -export const initializeControllers = (services) => { - const controllers = {}; - const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService); - - // V1 - controllers.authController = new AuthController(commonDependencies, { - settingsService: services.settingsService, - emailService: services.emailService, - jobQueue: services.jobQueue, - userService: services.userService, - }); - - controllers.monitorController = new MonitorController(commonDependencies, { - settingsService: services.settingsService, - jobQueue: services.jobQueue, - emailService: services.emailService, - monitorService: services.monitorService, - }); - - controllers.settingsController = new SettingsController(commonDependencies, { - settingsService: services.settingsService, - emailService: services.emailService, - }); - controllers.checkController = new CheckController(commonDependencies, { - settingsService: services.settingsService, - checkService: services.checkService, - }); - controllers.inviteController = new InviteController(commonDependencies, { - inviteService: services.inviteService, - }); - - controllers.maintenanceWindowController = new MaintenanceWindowController(commonDependencies, { - settingsService: services.settingsService, - maintenanceWindowService: services.maintenanceWindowService, - }); - controllers.queueController = new QueueController(commonDependencies, { - jobQueue: services.jobQueue, - }); - controllers.logController = new LogController(commonDependencies); - controllers.statusPageController = new StatusPageController(commonDependencies); - controllers.notificationController = new NotificationController(commonDependencies, { - notificationService: services.notificationService, - statusService: services.statusService, - }); - controllers.diagnosticController = new DiagnosticController(commonDependencies, { - diagnosticService: services.diagnosticService, - }); - - //V2 - controllers.authControllerV2 = new AuthControllerV2(services.authServiceV2, services.inviteServiceV2); - controllers.inviteControllerV2 = new InviteControllerV2(services.inviteServiceV2); - controllers.maintenanceControllerV2 = new MaintenanceControllerV2(services.maintenanceServiceV2); - controllers.monitorControllerV2 = new MonitorControllerV2(services.monitorServiceV2, services.checkServiceV2); - controllers.notificationChannelControllerV2 = new NotificationChannelControllerV2(services.notificationChannelServiceV2); - controllers.queueControllerV2 = new QueueControllerV2(services.jobQueueV2); - - return controllers; -}; diff --git a/server/src/config/database.js b/server/src/config/database.js deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/src/config/index.ts b/server/src/config/index.ts index 7ace6f99c..18cb52fca 100644 --- a/server/src/config/index.ts +++ b/server/src/config/index.ts @@ -1,25 +1,29 @@ import dotenv from "dotenv"; -export interface IEevConfig { - NODE_ENV: string; - JWT_SECRET: string; - PORT: number; - PAGESPEED_API_KEY: string; - SMTP_HOST: string; - SMTP_PORT: number; - SMTP_USER: string; - SMTP_PASS: string; +export interface IEnvConfig { + NODE_ENV: string; + LOG_LEVEL: string; + ORIGIN: string; + JWT_SECRET: string; + PORT: number; + PAGESPEED_API_KEY: string; + SMTP_HOST: string; + SMTP_PORT: number; + SMTP_USER: string; + SMTP_PASS: string; } dotenv.config(); -export const config: IEevConfig = { - NODE_ENV: process.env.NODE_ENV || "development", - JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret", - PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, - PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "", - SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com", - SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587, - SMTP_USER: process.env.SMTP_USER || "user@example.com", - SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password", +export const config: IEnvConfig = { + NODE_ENV: process.env.NODE_ENV || "development", + LOG_LEVEL: process.env.LOG_LEVEL || "info", + ORIGIN: process.env.ORIGIN || "http://localhost:5173", + JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret", + PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, + PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "", + SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com", + SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587, + SMTP_USER: process.env.SMTP_USER || "user@example.com", + SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password", }; diff --git a/server/src/config/routes.js b/server/src/config/routes.js deleted file mode 100644 index 13b369262..000000000 --- a/server/src/config/routes.js +++ /dev/null @@ -1,64 +0,0 @@ -import { verifyJWT } from "../middleware/v1/verifyJWT.js"; -import { authApiLimiter } from "../middleware/v1/rateLimiter.js"; - -import AuthRoutes from "../routes/v1/authRoute.js"; -import InviteRoutes from "../routes/v1//inviteRoute.js"; -import MonitorRoutes from "../routes/v1/monitorRoute.js"; -import CheckRoutes from "../routes/v1/checkRoute.js"; -import SettingsRoutes from "../routes/v1/settingsRoute.js"; -import MaintenanceWindowRoutes from "../routes/v1/maintenanceWindowRoute.js"; -import StatusPageRoutes from "../routes/v1/statusPageRoute.js"; -import QueueRoutes from "../routes/v1/queueRoute.js"; -import LogRoutes from "../routes/v1/logRoutes.js"; -import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js"; -import NotificationRoutes from "../routes/v1/notificationRoute.js"; - -//V2 -import AuthRoutesV2 from "../routes/v2/auth.js"; -import InviteRoutesV2 from "../routes/v2/invite.js"; -import MaintenanceRoutesV2 from "../routes/v2/maintenance.js"; -import MonitorRoutesV2 from "../routes/v2/monitors.js"; -import NotificationChannelRoutesV2 from "../routes/v2/notificationChannels.js"; -import QueueRoutesV2 from "../routes/v2/queue.js"; - -export const setupRoutes = (app, controllers) => { - // V1 - const authRoutes = new AuthRoutes(controllers.authController); - const monitorRoutes = new MonitorRoutes(controllers.monitorController); - const settingsRoutes = new SettingsRoutes(controllers.settingsController); - const checkRoutes = new CheckRoutes(controllers.checkController); - const inviteRoutes = new InviteRoutes(controllers.inviteController); - const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController); - const queueRoutes = new QueueRoutes(controllers.queueController); - const logRoutes = new LogRoutes(controllers.logController); - const statusPageRoutes = new StatusPageRoutes(controllers.statusPageController); - const notificationRoutes = new NotificationRoutes(controllers.notificationController); - const diagnosticRoutes = new DiagnosticRoutes(controllers.diagnosticController); - - app.use("/api/v1/auth", authApiLimiter, authRoutes.getRouter()); - app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter()); - app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter()); - app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter()); - app.use("/api/v1/invite", inviteRoutes.getRouter()); - app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter()); - app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter()); - app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter()); - app.use("/api/v1/status-page", statusPageRoutes.getRouter()); - app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); - app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); - - // V2 - const authRoutesV2 = new AuthRoutesV2(controllers.authControllerV2); - const inviteRoutesV2 = new InviteRoutesV2(controllers.inviteControllerV2); - const maintenanceRoutesV2 = new MaintenanceRoutesV2(controllers.maintenanceControllerV2); - const monitorRoutesV2 = new MonitorRoutesV2(controllers.monitorControllerV2); - const notificationChannelRoutesV2 = new NotificationChannelRoutesV2(controllers.notificationChannelControllerV2); - const queueRoutesV2 = new QueueRoutesV2(controllers.queueControllerV2); - - app.use("/api/v2/auth", authApiLimiter, authRoutesV2.getRouter()); - app.use("/api/v2/invite", inviteRoutesV2.getRouter()); - app.use("/api/v2/maintenance", maintenanceRoutesV2.getRouter()); - app.use("/api/v2/monitors", monitorRoutesV2.getRouter()); - app.use("/api/v2/notification-channels", notificationChannelRoutesV2.getRouter()); - app.use("/api/v2/queue", queueRoutesV2.getRouter()); -}; diff --git a/server/src/config/services.js b/server/src/config/services.js deleted file mode 100644 index c1679868d..000000000 --- a/server/src/config/services.js +++ /dev/null @@ -1,306 +0,0 @@ -import ServiceRegistry from "../service/v1/system/serviceRegistry.js"; -import TranslationService from "../service/v1/system/translationService.js"; -import StringService from "../service/v1/system/stringService.js"; -import MongoDB from "../db/v1/MongoDB.js"; -import NetworkService from "../service/v1/infrastructure/networkService.js"; -import EmailService from "../service/v1/infrastructure/emailService.js"; -import BufferService from "../service/v1/infrastructure/bufferService.js"; -import StatusService from "../service/v1/infrastructure/statusService.js"; -import NotificationUtils from "../service/v1/infrastructure/notificationUtils.js"; -import NotificationService from "../service/v1/infrastructure/notificationService.js"; -import ErrorService from "../service/v1/infrastructure/errorService.js"; -import SuperSimpleQueueHelper from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import SuperSimpleQueue from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; -import UserService from "../service/v1/business/userService.js"; -import CheckService from "../service/v1/business/checkService.js"; -import DiagnosticService from "../service/v1/business/diagnosticService.js"; -import InviteService from "../service/v1/business/inviteService.js"; -import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js"; -import MonitorService from "../service/v1/business/monitorService.js"; -import papaparse from "papaparse"; -import axios from "axios"; -import got from "got"; -import ping from "ping"; -import http from "http"; -import https from "https"; -import Docker from "dockerode"; -import net from "net"; -import fs from "fs"; -import path from "path"; -import nodemailer from "nodemailer"; -import pkg from "handlebars"; -const { compile } = pkg; -import mjml2html from "mjml"; -import jwt from "jsonwebtoken"; -import crypto from "crypto"; -import { games } from "gamedig"; -import jmespath from "jmespath"; -import { GameDig } from "gamedig"; - -import { fileURLToPath } from "url"; -import { ObjectId } from "mongodb"; - -// DB Modules -import { NormalizeData, NormalizeDataUptimeDetails } from "../utils/dataUtils.js"; -import { GenerateAvatarImage } from "../utils/imageProcessing.js"; -import { ParseBoolean } from "../utils/utils.js"; - -// Models -import Check from "../db/v1/models/Check.js"; -import Monitor from "../db/v1/models/Monitor.js"; -import User from "../db/v1/models/User.js"; -import InviteToken from "../db/v1/models/InviteToken.js"; -import StatusPage from "../db/v1/models/StatusPage.js"; -import Team from "../db/v1/models/Team.js"; -import MaintenanceWindow from "../db/v1/models/MaintenanceWindow.js"; -import MonitorStats from "../db/v1/models/MonitorStats.js"; -import Notification from "../db/v1/models/Notification.js"; -import RecoveryToken from "../db/v1/models/RecoveryToken.js"; -import AppSettings from "../db/v1/models/AppSettings.js"; - -import InviteModule from "../db/v1/modules/inviteModule.js"; -import CheckModule from "../db/v1/modules/checkModule.js"; -import StatusPageModule from "../db/v1/modules/statusPageModule.js"; -import UserModule from "../db/v1/modules/userModule.js"; -import MaintenanceWindowModule from "../db/v1/modules/maintenanceWindowModule.js"; -import MonitorModule from "../db/v1/modules/monitorModule.js"; -import NotificationModule from "../db/v1/modules/notificationModule.js"; -import RecoveryModule from "../db/v1/modules/recoveryModule.js"; -import SettingsModule from "../db/v1/modules/settingsModule.js"; - -// V2 Business -import AuthServiceV2 from "../service/v2/business/AuthService.js"; -import CheckServiceV2 from "../service/v2/business/CheckService.js"; -import InviteServiceV2 from "../service/v2/business/InviteService.js"; -import MaintenanceServiceV2 from "../service/v2/business/MaintenanceService.js"; -import MonitorServiceV2 from "../service/v2/business/MonitorService.js"; -import MonitorStatsServiceV2 from "../service/v2/business/MonitorStatsService.js"; -import NotificationChannelServiceV2 from "../service/v2/business/NotificationChannelService.js"; -import QueueServiceV2 from "../service/v2/business/QueueService.js"; -import UserServiceV2 from "../service/v2/business/UserService.js"; - -// V2 Infra -import DiscordServiceV2 from "../service/v2/infrastructure/NotificationServices/Discord.js"; -import EmailServiceV2 from "../service/v2/infrastructure/NotificationServices/Email.js"; -import SlackServiceV2 from "../service/v2/infrastructure/NotificationServices/Slack.js"; -import WebhookServiceV2 from "../service/v2/infrastructure/NotificationServices/Webhook.js"; -import JobGeneratorV2 from "../service/v2/infrastructure/JobGenerator.js"; -import JobQueueV2 from "../service/v2/infrastructure/JobQueue.js"; -import NetworkServiceV2 from "../service/v2/infrastructure/NetworkService.js"; -import NotificationServiceV2 from "../service/v2/infrastructure/NotificationService.js"; -import StatusServiceV2 from "../service/v2/infrastructure/StatusService.js"; - -export const initializeServices = async ({ logger, envSettings, settingsService }) => { - const serviceRegistry = new ServiceRegistry({ logger }); - ServiceRegistry.instance = serviceRegistry; - - const translationService = new TranslationService(logger); - await translationService.initialize(); - - const stringService = new StringService(translationService); - - // Create DB - const checkModule = new CheckModule({ logger, Check, Monitor, User }); - const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); - const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); - const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); - const monitorModule = new MonitorModule({ - Monitor, - MonitorStats, - Check, - stringService, - fs, - path, - fileURLToPath, - ObjectId, - NormalizeData, - NormalizeDataUptimeDetails, - }); - const notificationModule = new NotificationModule({ Notification, Monitor }); - const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); - const settingsModule = new SettingsModule({ AppSettings }); - - const db = new MongoDB({ - logger, - envSettings, - checkModule, - inviteModule, - statusPageModule, - userModule, - maintenanceWindowModule, - monitorModule, - notificationModule, - recoveryModule, - settingsModule, - }); - - await db.connect(); - - const networkService = new NetworkService({ - axios, - got, - https, - jmespath, - GameDig, - ping, - logger, - http, - Docker, - net, - stringService, - settingsService, - }); - const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); - const bufferService = new BufferService({ db, logger, envSettings }); - const statusService = new StatusService({ db, logger, buffer: bufferService }); - - const notificationUtils = new NotificationUtils({ - stringService, - emailService, - }); - - const notificationService = new NotificationService({ - emailService, - db, - logger, - networkService, - stringService, - notificationUtils, - }); - - const errorService = new ErrorService(); - - const superSimpleQueueHelper = new SuperSimpleQueueHelper({ - db, - logger, - networkService, - statusService, - notificationService, - }); - - const superSimpleQueue = await SuperSimpleQueue.create({ - envSettings, - db, - logger, - helper: superSimpleQueueHelper, - }); - - // Business services - const userService = new UserService({ - crypto, - db, - emailService, - settingsService, - logger, - stringService, - jwt, - errorService, - jobQueue: superSimpleQueue, - }); - const checkService = new CheckService({ - db, - settingsService, - stringService, - errorService, - }); - const diagnosticService = new DiagnosticService(); - const inviteService = new InviteService({ - db, - settingsService, - emailService, - stringService, - errorService, - }); - const maintenanceWindowService = new MaintenanceWindowService({ - db, - settingsService, - stringService, - errorService, - }); - const monitorService = new MonitorService({ - db, - settingsService, - jobQueue: superSimpleQueue, - stringService, - emailService, - papaparse, - logger, - errorService, - games, - }); - - // V2 Services - const checkServiceV2 = new CheckServiceV2(); - const inviteServiceV2 = new InviteServiceV2(); - const maintenanceServiceV2 = new MaintenanceServiceV2(); - const monitorStatsServiceV2 = new MonitorStatsServiceV2(); - const notificationChannelServiceV2 = new NotificationChannelServiceV2(); - const userServiceV2 = new UserServiceV2(); - const discordServiceV2 = new DiscordServiceV2(); - const emailServiceV2 = new EmailServiceV2(userServiceV2); - const slackServiceV2 = new SlackServiceV2(); - const webhookServiceV2 = new WebhookServiceV2(); - const networkServiceV2 = new NetworkServiceV2(); - const statusServiceV2 = new StatusServiceV2(); - const notificationServiceV2 = new NotificationServiceV2(userServiceV2); - const jobGeneratorV2 = new JobGeneratorV2( - networkServiceV2, - checkServiceV2, - monitorStatsServiceV2, - statusServiceV2, - notificationServiceV2, - maintenanceServiceV2 - ); - const jobQueueV2 = await JobQueueV2.create(jobGeneratorV2); - const authServiceV2 = new AuthServiceV2(jobQueueV2); - const monitorServiceV2 = new MonitorServiceV2(jobQueueV2); - const queueServiceV2 = new QueueServiceV2(jobQueueV2); - - const services = { - //v1 - settingsService, - translationService, - stringService, - db, - networkService, - emailService, - bufferService, - statusService, - notificationService, - jobQueue: superSimpleQueue, - userService, - checkService, - diagnosticService, - inviteService, - maintenanceWindowService, - monitorService, - errorService, - logger, - //v2 - jobQueueV2, - authServiceV2, - checkServiceV2, - inviteServiceV2, - maintenanceServiceV2, - monitorServiceV2, - monitorStatsServiceV2, - notificationChannelServiceV2, - queueServiceV2, - userServiceV2, - discordServiceV2, - emailServiceV2, - slackServiceV2, - webhookServiceV2, - networkServiceV2, - statusServiceV2, - notificationServiceV2, - jobGeneratorV2, - }; - - Object.values(services).forEach((service) => { - ServiceRegistry.register(service.serviceName, service); - }); - - return services; -}; diff --git a/server/src/controllers/AuthController.ts b/server/src/controllers/AuthController.ts new file mode 100644 index 000000000..536c981e2 --- /dev/null +++ b/server/src/controllers/AuthController.ts @@ -0,0 +1,165 @@ +import { Request, Response, NextFunction } from "express"; +import { encode, decode } from "@/utils/JWTUtils.js"; +import AuthService from "@/services/business/AuthService.js"; +import ApiError from "@/utils/ApiError.js"; +import InviteService from "@/services/business/InviteService.js"; +import { registerSchema } from "@/validation/index.js"; +class AuthController { + private authService: AuthService; + private inviteService: InviteService; + constructor(authService: AuthService, inviteService: InviteService) { + this.authService = authService; + this.inviteService = inviteService; + } + + register = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, firstName, lastName, password } = req.body; + + if (!email || !firstName || !lastName || !password) { + throw new Error( + "Email, firstName, lastName, and password are required" + ); + } + + const { tokenizedUser, returnableUser } = await this.authService.register( + { + email, + firstName, + lastName, + password, + } + ); + + if (!returnableUser || !tokenizedUser) { + throw new ApiError("Registration failed"); + } + + const token = encode(tokenizedUser); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(201).json({ + message: "User created successfully", + data: returnableUser, + }); + } catch (error) { + next(error); + } + }; + + registerWithInvite = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const token = req.params.token; + if (!token) { + throw new ApiError("Invite token is required", 400); + } + + const { invite } = await this.inviteService.get(token); + + const { tokenizedUser, returnableUser } = + await this.authService.registerWithInvite(invite, req.body); + + if (!tokenizedUser || !returnableUser) { + throw new Error("Registration failed"); + } + + await this.inviteService.delete(invite._id.toString()); + + const jwt = encode(tokenizedUser); + + res.cookie("token", jwt, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(201).json({ + message: "User registered successfully", + data: returnableUser, + }); + } catch (error) { + next(error); + } + }; + + login = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, password } = req.body; + // Validation + if (!email || !password) { + return res + .status(400) + .json({ message: "Email and password are required" }); + } + const { tokenizedUser, returnableUser } = await this.authService.login({ + email, + password, + }); + + const token = encode(tokenizedUser); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(200).json({ + message: "Login successful", + data: returnableUser, + }); + } catch (error) { + next(error); + } + }; + + logout = (req: Request, res: Response) => { + res.clearCookie("token", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }); + res.status(200).json({ message: "Logout successful" }); + }; + + me = async (req: Request, res: Response, next: NextFunction) => { + const user = req.user; + if (!user) { + throw new ApiError("Unauthorized", 401); + } + const returnableUser = await this.authService.me(user.sub); + res + .status(200) + .json({ message: "User retrieved successfully", data: returnableUser }); + }; + + cleanup = async (req: Request, res: Response) => { + try { + await this.authService.cleanup(); + res.status(200).json({ message: "Cleanup successful" }); + } catch (error) {} + }; + + cleanMonitors = async (req: Request, res: Response) => { + try { + await this.authService.cleanMonitors(); + res.status(200).json({ message: "Monitors cleanup successful" }); + } catch (error) { + res.status(500).json({ message: "Internal server error" }); + } + }; +} + +export default AuthController; diff --git a/server/src/controllers/ChecksController.ts b/server/src/controllers/ChecksController.ts new file mode 100644 index 000000000..ce87bde67 --- /dev/null +++ b/server/src/controllers/ChecksController.ts @@ -0,0 +1,58 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "@/utils/ApiError.js"; +import { CheckService } from "@/services/index.js"; + +export interface IChecksController { + getChecksByStatus: (req: Request, res: Response, next: NextFunction) => void; +} + +class ChecksController implements IChecksController { + private checkService: CheckService; + constructor(checksService: CheckService) { + this.checkService = checksService; + } + + getChecksByStatus = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const { + status, + monitorId, + page = 0, + rowsPerPage = 10, + range, + } = req.validatedQuery; + + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const checks = await this.checkService.getChecksByStatus( + status, + teamId, + monitorId, + page, + rowsPerPage, + range + ); + + return res.status(200).json({ + message: "Checks retrieved successfully", + data: checks, + }); + } catch (error) { + next(error); + } + }; +} + +export default ChecksController; diff --git a/server/src/controllers/DiagnosticController.ts b/server/src/controllers/DiagnosticController.ts new file mode 100644 index 000000000..6c96c9628 --- /dev/null +++ b/server/src/controllers/DiagnosticController.ts @@ -0,0 +1,35 @@ +import { Request, Response, NextFunction } from "express"; +import type { IDiagnosticService } from "@/services/business/DiagnosticService.js"; +import type { IJobData } from "@/services/infrastructure/JobQueue.js"; + +export interface IDiagnosticController { + getLogs: (req: Request, res: Response, next: NextFunction) => void; + getJobs(req: Request, res: Response, next: NextFunction): Promise; +} + +class DiagnosticController implements IDiagnosticController { + private diagnosticService: IDiagnosticService; + constructor(diagnosticService: IDiagnosticService) { + this.diagnosticService = diagnosticService; + } + + getLogs = (req: Request, res: Response, next: NextFunction) => { + try { + const logs = this.diagnosticService.getLogs(); + res.status(200).json({ message: "OK", data: logs }); + } catch (error) { + next(error); + } + }; + + getJobs = async (req: Request, res: Response, next: NextFunction) => { + try { + const jobs = await this.diagnosticService.getJobs(); + res.status(200).json({ message: "OK", data: jobs }); + } catch (error) { + next(error); + } + }; +} + +export default DiagnosticController; diff --git a/server/src/controllers/IncidentController.ts b/server/src/controllers/IncidentController.ts new file mode 100644 index 000000000..acab01f33 --- /dev/null +++ b/server/src/controllers/IncidentController.ts @@ -0,0 +1,112 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "@/utils/ApiError.js"; +import { + IncidentService, + NotificationService, + MonitorService, +} from "@/services/index.js"; +import { getChildLogger } from "@/logger/Logger.js"; +const logger = getChildLogger("IncidentController"); + +export interface IIncidentsController { + getIncidents: (req: Request, res: Response, next: NextFunction) => void; + resolveIncident: (req: Request, res: Response, next: NextFunction) => void; +} + +class IncidentsController implements IIncidentsController { + private incidentService: IncidentService; + private notificationService: NotificationService; + private monitorService: MonitorService; + constructor( + incidentService: IncidentService, + notificationService: NotificationService, + monitorService: MonitorService + ) { + this.incidentService = incidentService; + this.notificationService = notificationService; + this.monitorService = monitorService; + } + + getIncidents = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const { resolved, resolutionType, page, rowsPerPage, range, monitorId } = + req.validatedQuery; + + const incidents = await this.incidentService.getAll( + teamId, + monitorId, + page, + rowsPerPage, + range, + resolved, + resolutionType + ); + + res.status(200).json({ message: "OK", data: incidents }); + } catch (error) { + next(error); + } + }; + + resolveIncident = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const { id } = req.params; + if (!id) { + throw new ApiError("No incident ID", 400); + } + + const { resolutionNote } = req.body; + + const resolved = await this.incidentService.resolve( + teamId, + id, + "manual", + undefined, + userContext.sub, + resolutionNote + ); + + if (!resolved) { + throw new ApiError("Incident not found or could not be resolved", 500); + } + + // This should be best effort only, don't wait and don't fail + this.monitorService + .get(teamId, resolved.monitorId.toString()) + .then((monitor) => { + this.notificationService.handleNotifications(monitor, resolved); + }) + .catch((error) => { + logger.warn(error); + }); + + return res + .status(200) + .json({ message: "Incident resolved", data: resolved }); + } catch (error) { + next(error); + } + }; +} + +export default IncidentsController; diff --git a/server/src/controllers/InviteController.ts b/server/src/controllers/InviteController.ts new file mode 100644 index 000000000..888926b60 --- /dev/null +++ b/server/src/controllers/InviteController.ts @@ -0,0 +1,84 @@ +import { Request, Response, NextFunction } from "express"; +import InviteService from "@/services/business/InviteService.js"; +import ApiError from "@/utils/ApiError.js"; + +class InviteController { + private inviteService: InviteService; + constructor(inviteService: InviteService) { + this.inviteService = inviteService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + + if (!userContext) { + throw new ApiError("Unauthorized", 401); + } + + const userId = userContext.sub; + if (!userId) { + throw new ApiError("No user ID in context", 400); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const { email, orgRoleId, teamId, teamRoleId } = req.body; + + const invite = await this.inviteService.create( + userId, + email, + orgId, + orgRoleId, + teamId, + teamRoleId + ); + res.status(201).json({ message: "OK", data: invite }); + } catch (error: any) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const invites = await this.inviteService.getAll(); + res.status(200).json({ + message: "OK", + data: invites, + }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const token = req.params.id; + if (!token) { + return res.status(400).json({ message: "Token parameter is required" }); + } + const result = await this.inviteService.get(token); + res.status(200).json({ message: "OK", data: result }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + await this.inviteService.delete(id); + res.status(200).json({ message: "OK" }); + } catch (error: any) { + next(error); + } + }; +} + +export default InviteController; diff --git a/server/src/controllers/MaintenanceController.ts b/server/src/controllers/MaintenanceController.ts new file mode 100644 index 000000000..27b872492 --- /dev/null +++ b/server/src/controllers/MaintenanceController.ts @@ -0,0 +1,150 @@ +import { Request, Response, NextFunction } from "express"; +import MaintenanceService from "@/services/business/MaintenanceService.js"; +import ApiError from "@/utils/ApiError.js"; +class MaintenanceController { + private maintenanceService: MaintenanceService; + constructor(maintenanceService: MaintenanceService) { + this.maintenanceService = maintenanceService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const maintenance = await this.maintenanceService.create( + tokenizedUser, + req.body + ); + res.status(201).json({ message: "OK", data: maintenance }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const maintenances = await this.maintenanceService.getAll(teamId); + res.status(200).json({ + message: "OK", + data: maintenances, + }); + } catch (error) { + next(error); + } + }; + + toggleActive = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const maintenance = await this.maintenanceService.toggleActive( + teamId, + tokenizedUser, + id + ); + res.status(200).json({ message: "OK", data: maintenance }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const updatedMaintenance = await this.maintenanceService.update( + teamId, + tokenizedUser, + id, + req.body + ); + res.status(200).json({ message: "OK", data: updatedMaintenance }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const maintenance = await this.maintenanceService.get(teamId, id); + res.status(200).json({ message: "OK", data: maintenance }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + await this.maintenanceService.delete(teamId, id); + res.status(200).json({ message: "OK" }); + } catch (error) { + next(error); + } + }; +} + +export default MaintenanceController; diff --git a/server/src/controllers/MonitorController.ts b/server/src/controllers/MonitorController.ts new file mode 100644 index 000000000..256e81c83 --- /dev/null +++ b/server/src/controllers/MonitorController.ts @@ -0,0 +1,315 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "@/utils/ApiError.js"; +import { MonitorType } from "@/db/models/monitors/Monitor.js"; +import { + MonitorService, + CheckService, + NotificationService, +} from "@/services/index.js"; + +export interface IMonitorController { + create(req: Request, res: Response, next: NextFunction): Promise; + getAll(req: Request, res: Response, next: NextFunction): Promise; + getChecks(req: Request, res: Response, next: NextFunction): Promise; + toggleActive(req: Request, res: Response, next: NextFunction): Promise; + get(req: Request, res: Response, next: NextFunction): Promise; + update(req: Request, res: Response, next: NextFunction): Promise; + delete(req: Request, res: Response, next: NextFunction): Promise; + testNotifications( + req: Request, + res: Response, + next: NextFunction + ): Promise; +} +class MonitorController { + private monitorService: MonitorService; + private checkService: CheckService; + private notificationService: NotificationService; + constructor( + monitorService: MonitorService, + checkService: CheckService, + notificationService: NotificationService + ) { + this.monitorService = monitorService; + this.checkService = checkService; + this.notificationService = notificationService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + if (!userContext.currentTeamId) { + throw new ApiError("No team ID", 400); + } + + const monitor = await this.monitorService.create( + userContext.orgId, + userContext.sub, + userContext.currentTeamId, + req.body + ); + + res.status(201).json({ + message: "Monitor created successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + let result; + if (req.validatedQuery.embedChecks === true) { + const page = req.validatedQuery.page || 0; + const rowsPerPage = req.validatedQuery.rowsPerPage || 10; + const type = req.validatedQuery.type; + + result = await this.monitorService.getAllEmbedChecks( + teamId, + page, + rowsPerPage, + type + ); + } else { + result = await this.monitorService.getAll(teamId); + } + + res.status(200).json({ + message: "Monitors retrieved successfully", + data: result, + }); + } catch (error) { + next(error); + } + }; + + getChecks = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const monitorId = req.params.id; + if (!monitorId) { + throw new ApiError("Monitor ID is required", 400); + } + + const monitor = await this.monitorService.get(teamId, monitorId); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + + const page = Number(req.validatedQuery.page); + const rowsPerPage = Number(req.validatedQuery.rowsPerPage); + + const { count, checks } = await this.checkService.getMonitorChecks( + monitorId, + page, + rowsPerPage + ); + res.status(200).json({ + message: "Checks retrieved successfully", + data: { count, checks }, + }); + } catch (error) { + next(error); + } + }; + + toggleActive = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const monitorId = req.params.id; + if (!monitorId) { + throw new ApiError("Monitor ID is required", 400); + } + + const monitor = await this.monitorService.toggleActive( + userContext.sub, + teamId, + monitorId + ); + + const paused = monitor.isActive ? "resumed" : "paused"; + + res.status(200).json({ + message: `Monitor ${paused} successfully`, + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const monitorId = req.params.id; + if (!monitorId) { + throw new ApiError("Monitor ID is required", 400); + } + + let monitor; + + const status = req.query.status; + if (status && typeof status !== "string") { + throw new ApiError("Status query parameter must be a string", 400); + } + + if (req.query.embedChecks === "true") { + const range = req.query.range; + if (!range || typeof range !== "string") + throw new ApiError("Range query parameter is required", 400); + + monitor = await this.monitorService.getEmbedChecks( + teamId, + monitorId, + range, + status + ); + } else { + monitor = await this.monitorService.get(teamId, monitorId); + } + + res.status(200).json({ + message: "Monitor retrieved successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const monitorId = req.params.id; + if (!monitorId) { + throw new ApiError("Monitor ID is required", 400); + } + + const monitor = await this.monitorService.update( + userContext.sub, + teamId, + monitorId, + req.body + ); + res.status(200).json({ + message: "Monitor updated successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + await this.monitorService.delete(teamId, id); + + res.status(200).json({ + message: "Monitor deleted successfully", + }); + } catch (error) { + next(error); + } + }; + + testNotifications = async ( + req: Request, + res: Response, + next: NextFunction + ) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const monitorId = req.params.id; + if (!monitorId) { + throw new ApiError("Monitor ID is required", 400); + } + + const results = await this.notificationService.testNotificationChannels( + monitorId, + teamId + ); + return res.status(200).json({ + message: "Notification test sent successfully", + data: results, + }); + } catch (error) { + next(error); + } + }; +} + +export default MonitorController; diff --git a/server/src/controllers/NotificationChannelController.ts b/server/src/controllers/NotificationChannelController.ts new file mode 100644 index 000000000..331fcaf77 --- /dev/null +++ b/server/src/controllers/NotificationChannelController.ts @@ -0,0 +1,178 @@ +import { Request, Response, NextFunction } from "express"; +import NotificationChannelService from "@/services/business/NotificationChannelService.js"; +import { NotificationService } from "@/services/index.js"; +import ApiError from "@/utils/ApiError.js"; +class NotificationChannelController { + private notificationChannelService: NotificationChannelService; + private notificationService: NotificationService; + constructor( + notificationChannelService: NotificationChannelService, + notificationService: NotificationService + ) { + this.notificationChannelService = notificationChannelService; + this.notificationService = notificationService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const channel = await this.notificationChannelService.create( + tokenizedUser, + req.body + ); + res.status(201).json({ message: "OK", data: channel }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const notificationChannels = await this.notificationChannelService.getAll( + teamId + ); + res.status(200).json({ + message: "OK", + data: notificationChannels, + }); + } catch (error) { + next(error); + } + }; + + toggleActive = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const notificationChannel = + await this.notificationChannelService.toggleActive( + teamId, + tokenizedUser, + id + ); + res.status(200).json({ message: "OK", data: notificationChannel }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const updatedChannel = await this.notificationChannelService.update( + teamId, + tokenizedUser, + id, + req.body + ); + res.status(200).json({ message: "OK", data: updatedChannel }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const notificationChannel = await this.notificationChannelService.get( + teamId, + id + ); + res.status(200).json({ message: "OK", data: notificationChannel }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = tokenizedUser.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + await this.notificationChannelService.delete(teamId, id); + res.status(200).json({ message: "OK" }); + } catch (error) { + next(error); + } + }; + + test = async (req: Request, res: Response, next: NextFunction) => { + try { + const notificationChannel = req.body; + const success = await this.notificationService.testNotificationChannel( + notificationChannel + ); + if (success) { + res.status(200).json({ message: "OK" }); + } else { + throw new ApiError("Failed to send", 500); + } + } catch (error) { + next(error); + } + }; +} + +export default NotificationChannelController; diff --git a/server/src/controllers/ProfileController.ts b/server/src/controllers/ProfileController.ts new file mode 100644 index 000000000..fa9a162bb --- /dev/null +++ b/server/src/controllers/ProfileController.ts @@ -0,0 +1,69 @@ +import { Request, Response, NextFunction } from "express"; +import type { IUser } from "@/db/models/index.js"; +import { UserService } from "@/services/index.js"; +import ApiError from "@/utils/ApiError.js"; + +export interface IProfileController { + get(req: Request, res: Response, next: NextFunction): Promise; + update(req: Request, res: Response, next: NextFunction): Promise; +} + +class ProfileController { + private userService: UserService; + constructor(userService: UserService) { + this.userService = userService; + } + + private parseUser(user: IUser): Partial { + return { + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + }; + } + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + // TODO convert to get by id + const user = await this.userService.get(userContext.email); + if (!user) { + throw new ApiError("User not found", 404); + } + + res.status(200).json({ + message: "OK", + data: this.parseUser(user), + }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const updateData = req.body; + + const updatedUser = await this.userService.update( + userContext.sub, + updateData + ); + + res + .status(200) + .json({ message: "OK", data: this.parseUser(updatedUser) }); + } catch (error: any) { + next(error); + } + }; +} +export default ProfileController; diff --git a/server/src/controllers/QueueController.ts b/server/src/controllers/QueueController.ts new file mode 100644 index 000000000..68cbc12ed --- /dev/null +++ b/server/src/controllers/QueueController.ts @@ -0,0 +1,29 @@ +import { Request, Response, NextFunction } from "express"; +import QueueService from "@/services/business/QueueService.js"; +class QueueController { + private queueService: QueueService; + constructor(queueService: QueueService) { + this.queueService = queueService; + } + + getJobs = async (req: Request, res: Response, next: NextFunction) => { + try { + const jobs = await this.queueService.getJobs(); + res.status(200).json({ message: "ok", data: jobs }); + } catch (error) { + next(error); + } + }; + + getMetrics = async (req: Request, res: Response, next: NextFunction) => { + const metrics = await this.queueService.getMetrics(); + res.status(200).json({ message: "ok", data: metrics }); + }; + + flush = async (req: Request, res: Response, next: NextFunction) => { + const result = await this.queueService.flush(); + res.status(200).json({ message: "ok", flushed: result }); + }; +} + +export default QueueController; diff --git a/server/src/controllers/RecoveryController.ts b/server/src/controllers/RecoveryController.ts new file mode 100644 index 000000000..7c6ee590e --- /dev/null +++ b/server/src/controllers/RecoveryController.ts @@ -0,0 +1,109 @@ +import { Request, Response, NextFunction } from "express"; +import { + AuthService, + RecoveryService, + EmailService, +} from "@/services/index.js"; +import ApiError from "@/utils/ApiError.js"; +import { User } from "@/db/models/index.js"; +import { config } from "@/config/index.js"; +import { encode } from "@/utils/JWTUtils.js"; + +import { getChildLogger } from "@/logger/Logger.js"; +const logger = getChildLogger("RecoveryController"); + +export interface IRecoveryController { + requestRecovery: ( + req: Request, + res: Response, + next: NextFunction + ) => Promise; + resetPassword: ( + req: Request, + res: Response, + next: NextFunction + ) => Promise; +} + +class RecoveryController implements IRecoveryController { + private recoveryService: RecoveryService; + private authService: AuthService; + private emailService: EmailService; + constructor( + recoveryService: RecoveryService, + authService: AuthService, + emailService: EmailService + ) { + this.recoveryService = recoveryService; + this.authService = authService; + this.emailService = emailService; + } + + requestRecovery = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email } = req.body; + const user = await User.findOne({ email }).lean(); + + if (user) { + try { + const recoveryToken = await this.recoveryService.create(user._id); + // Don't wait for email to be sent + this.emailService + .sendGeneric( + user.email, + "Password Recovery", + `Your recovery link is: ${config.ORIGIN}/recovery/${recoveryToken}` + ) + .catch((error) => { + logger.error(error); + }); + } catch (error) { + // These should not be propagated to the user + logger.error(error); + } + } + res.status(200).json({ message: "OK" }); + } catch (error: any) { + next(error); + } + }; + + resetPassword = async (req: Request, res: Response, next: NextFunction) => { + try { + const { token, password } = req.body; + const recoveryToken = await this.recoveryService.get(token); + if (!recoveryToken) { + throw new ApiError("Invalid recovery token", 400); + } + await this.authService.changePassword(recoveryToken.userId, password); + await this.recoveryService.delete(recoveryToken._id); + + const user = await User.findById(recoveryToken.userId); + if (!user) { + throw new ApiError("User not found", 404); + } + const { tokenizedUser, returnableUser } = await this.authService.login({ + email: user.email, + password, + }); + + const userToken = encode(tokenizedUser); + + res.cookie("token", userToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(200).json({ + message: "Reset successful", + data: returnableUser, + }); + } catch (error: any) { + next(error); + } + }; +} + +export default RecoveryController; diff --git a/server/src/controllers/RoleController.ts b/server/src/controllers/RoleController.ts new file mode 100644 index 000000000..39d4ab087 --- /dev/null +++ b/server/src/controllers/RoleController.ts @@ -0,0 +1,80 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "@/utils/ApiError.js"; +import RoleService from "@/services/business/RoleService.js"; + +export interface IRoleController { + getAll: (req: Request, res: Response, next: NextFunction) => void; + getTeam: (req: Request, res: Response, next: NextFunction) => void; + get: (req: Request, res: Response, next: NextFunction) => void; +} + +class RoleController implements IRoleController { + private roleService: RoleService; + constructor(roleService: RoleService) { + this.roleService = roleService; + } + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + if (!tokenizedUser.orgId) { + throw new ApiError("No organization ID", 400); + } + + const type = req.query.type; + + const roles = await this.roleService.getAll( + tokenizedUser.orgId, + type as string + ); + return res.status(200).json({ + message: "Roles retrieved successfully", + data: roles, + }); + } catch (error) { + next(error); + } + }; + + getTeam = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + + if (!userContext) { + throw new ApiError("Unauthorized", 401); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const teamId = req.params.id; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const roles = await this.roleService.getTeam(orgId, teamId); + return res.status(200).json({ + message: "Roles retrieved successfully", + data: roles, + }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + } catch (error) { + next(error); + } + }; +} + +export default RoleController; diff --git a/server/src/controllers/StatusPageController.ts b/server/src/controllers/StatusPageController.ts new file mode 100644 index 000000000..50e3c1119 --- /dev/null +++ b/server/src/controllers/StatusPageController.ts @@ -0,0 +1,137 @@ +import { Request, Response, NextFunction } from "express"; +import StatusPageService from "@/services/business/StatusPageService.js"; +import ApiError from "@/utils/ApiError.js"; +class StatusPageController { + private statusPageService: StatusPageService; + constructor(statusPageService: StatusPageService) { + this.statusPageService = statusPageService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + const statusPage = await this.statusPageService.create( + userContext, + req.body + ); + res.status(201).json({ message: "OK", data: statusPage }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const statusPages = await this.statusPageService.getAll(teamId); + res.status(200).json({ + message: "OK", + data: statusPages, + }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const updatedChannel = await this.statusPageService.update( + teamId, + userContext, + id, + req.body + ); + res.status(200).json({ message: "OK", data: updatedChannel }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const statusPage = await this.statusPageService.get(teamId, id); + res.status(200).json({ message: "OK", data: statusPage }); + } catch (error) { + next(error); + } + }; + + getPublic = async (req: Request, res: Response, next: NextFunction) => { + try { + const url = req.params.url; + if (!url) { + throw new ApiError("No URL parameter", 400); + } + + const statusPage = await this.statusPageService.getPublic(url); + res.status(200).json({ message: "OK", data: statusPage }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const teamId = userContext.currentTeamId; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + await this.statusPageService.delete(teamId, id); + res.status(200).json({ message: "OK" }); + } catch (error) { + next(error); + } + }; +} + +export default StatusPageController; diff --git a/server/src/controllers/TeamController.ts b/server/src/controllers/TeamController.ts new file mode 100644 index 000000000..6134cc27d --- /dev/null +++ b/server/src/controllers/TeamController.ts @@ -0,0 +1,178 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "@/utils/ApiError.js"; +import TeamService from "@/services/business/TeamService.js"; + +export interface ITeamController { + create: (req: Request, res: Response, next: NextFunction) => void; + getOrg: (req: Request, res: Response, next: NextFunction) => void; + getJoined: (req: Request, res: Response, next: NextFunction) => void; + get: (req: Request, res: Response, next: NextFunction) => void; + update: (req: Request, res: Response, next: NextFunction) => void; + delete: (req: Request, res: Response, next: NextFunction) => void; +} + +class TeamController implements ITeamController { + private teamService: TeamService; + constructor(teamService: TeamService) { + this.teamService = teamService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const userId = userContext.sub; + if (!userId) { + throw new ApiError("No user ID", 400); + } + + const roleId = req.body.roleId; + if (!roleId) { + throw new ApiError("No role ID", 400); + } + + await this.teamService.create(orgId, userId, req.body, roleId); + + return res.status(201).json({ + message: "Team created successfully", + }); + } catch (error) { + next(error); + } + }; + + getOrg = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const userId = userContext.sub; + if (!userId) { + throw new ApiError("No user ID", 400); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const teams = await this.teamService.getOrg(orgId); + + return res + .status(200) + .json({ message: "Teams retrieved successfully", data: teams }); + } catch (error) { + next(error); + } + }; + + getJoined = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const userId = userContext.sub; + if (!userId) { + throw new ApiError("No user ID", 400); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const teams = await this.teamService.getJoined(orgId, userId); + + return res + .status(200) + .json({ message: "Teams retrieved successfully", data: teams }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + const teamId = req.params.id; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const team = await this.teamService.get(teamId, orgId); + return res + .status(200) + .json({ message: "Team retrieved successfully", data: team }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + + const result = await this.teamService.update(userContext, id, req.body); + return res.status(200).json({ + message: "Team updated successfully", + data: result, + }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req.params.id; + if (!teamId) { + throw new ApiError("No team ID", 400); + } + + const userContext = req.user; + if (!userContext) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const success = await this.teamService.delete(userContext.orgId, teamId); + if (!success) { + throw new ApiError("Failed to delete team", 500); + } + return res.status(200).json({ + message: "Team deleted successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default TeamController; diff --git a/server/src/controllers/TeamMemberController.ts b/server/src/controllers/TeamMemberController.ts new file mode 100644 index 000000000..e11b63d6c --- /dev/null +++ b/server/src/controllers/TeamMemberController.ts @@ -0,0 +1,165 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "@/utils/ApiError.js"; +import { TeamMemberService } from "@/services/index.js"; +import { invalidateCachesForUser } from "@/middleware/AddUserContext.js"; + +export interface ITeamMemberController { + create: (req: Request, res: Response, next: NextFunction) => void; + getAll: (req: Request, res: Response, next: NextFunction) => void; + get: (req: Request, res: Response, next: NextFunction) => void; + update: (req: Request, res: Response, next: NextFunction) => void; + delete: (req: Request, res: Response, next: NextFunction) => void; +} + +class TeamMemberController implements ITeamMemberController { + private teamMemberService: TeamMemberService; + constructor(teamMemberService: TeamMemberService) { + this.teamMemberService = teamMemberService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + + if (!userContext) { + throw new ApiError("Unauthorized", 401); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const tm = await this.teamMemberService.create(orgId, req.body); + return res.status(201).json({ + message: "Team member created successfully", + data: tm, + }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + + if (!userContext) { + throw new ApiError("Unauthorized", 401); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const teamId = req.query.teamId as string | undefined; + const type = req.query.type as string; + + const teamMembers = await this.teamMemberService.getAll( + orgId, + teamId, + type + ); + + return res.status(200).json({ + message: "Team members retrieved successfully", + data: teamMembers, + }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + throw new ApiError("Unauthorized", 401); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const teamMemberId = req.params.id; + if (!teamMemberId) { + throw new ApiError("No team member ID", 400); + } + + const teamMember = await this.teamMemberService.get(orgId, teamMemberId); + + return res + .status(200) + .json({ message: "Team retrieved successfully", data: teamMember }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + throw new ApiError("Unauthorized", 401); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + const { roleId } = req.body; + if (!roleId) { + throw new ApiError("No role ID provided", 400); + } + + const teamMembershipId = req.params.id as string; + if (!teamMembershipId) { + throw new ApiError("No team membership ID provided", 400); + } + + await this.teamMemberService.update(orgId, teamMembershipId, roleId); + + return res.status(200).json({ + message: "Team updated successfully", + data: null, + }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const userContext = req.user; + if (!userContext) { + throw new ApiError("Unauthorized", 401); + } + + const orgId = userContext.orgId; + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + + const teamMemberId = req.params.id; + if (!req.params.id) { + throw new ApiError("No team member ID", 400); + } + + const deleted = await this.teamMemberService.delete(orgId, req.params.id); + + if (!deleted) { + throw new ApiError("Failed to delete team member", 500); + } + + return res.status(200).json({ + message: "Team deleted successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default TeamMemberController; diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts new file mode 100644 index 000000000..7f78b6025 --- /dev/null +++ b/server/src/controllers/index.ts @@ -0,0 +1,15 @@ +export { default as AuthController } from "./AuthController.js"; +export { default as InviteController } from "./InviteController.js"; +export { default as MaintenanceController } from "./MaintenanceController.js"; +export { default as MonitorController } from "./MonitorController.js"; +export { default as NotificationChannelController } from "./NotificationChannelController.js"; +export { default as QueueController } from "./QueueController.js"; +export { default as TeamController } from "./TeamController.js"; +export { default as RoleController } from "./RoleController.js"; +export { default as TeamMemberController } from "./TeamMemberController.js"; +export { default as ChecksController } from "./ChecksController.js"; +export { default as StatusPageController } from "./StatusPageController.js"; +export { default as DiagnosticController } from "./DiagnosticController.js"; +export { default as RecoveryController } from "./RecoveryController.js"; +export { default as ProfileController } from "./ProfileController.js"; +export { default as IncidentsController } from "./IncidentController.js"; diff --git a/server/src/controllers/v1/announcementsController.js b/server/src/controllers/v1/announcementsController.js deleted file mode 100755 index 0425b44aa..000000000 --- a/server/src/controllers/v1/announcementsController.js +++ /dev/null @@ -1,77 +0,0 @@ -import { createAnnouncementValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "announcementController"; - -/** - * Controller for managing announcements in the system. - * This class handles the creation of new announcements. - * - * @class AnnouncementController - */ - -class AnnouncementController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies) { - super(commonDependencies); - this.createAnnouncement = this.createAnnouncement.bind(this); - this.getAnnouncement = this.getAnnouncement.bind(this); - } - - get serviceName() { - return AnnouncementController.SERVICE_NAME; - } - - /** - * Handles the creation of a new announcement. - * - * @async - * @param {Object} req - The request object, containing the announcement data in the body. - * @param {Object} res - The response object used to send the result back to the client. - * @param {Function} next - The next middleware function in the stack for error handling. - * - * @returns {Promise} A promise that resolves once the response is sent. - */ - createAnnouncement = asyncHandler( - async (req, res, next) => { - await createAnnouncementValidation.validateAsync(req.body); - const { title, message } = req.body; - const announcementData = { - title: title.trim(), - message: message.trim(), - userId: req.user._id, - }; - - const newAnnouncement = await this.db.createAnnouncement(announcementData); - return res.success({ - msg: this.stringService.createAnnouncement, - data: newAnnouncement, - }); - }, - SERVICE_NAME, - "createAnnouncement" - ); - - /** - * Handles retrieving announcements with pagination. - * - * @async - * @param {Object} res - The response object used to send the result back to the client. - * - `data`: The list of announcements to be sent back to the client. - * - `msg`: A message about the success of the request. - * @param {Function} next - The next middleware function in the stack for error handling. - */ - getAnnouncement = asyncHandler( - async (req, res, next) => { - const allAnnouncements = await this.db.getAnnouncements(); - return res.success({ - msg: this.stringService.getAnnouncement, - data: allAnnouncements, - }); - }, - SERVICE_NAME, - "getAnnouncement" - ); -} - -export default AnnouncementController; diff --git a/server/src/controllers/v1/authController.js b/server/src/controllers/v1/authController.js deleted file mode 100755 index e52de3849..000000000 --- a/server/src/controllers/v1/authController.js +++ /dev/null @@ -1,478 +0,0 @@ -import BaseController from "./baseController.js"; -import { - registrationBodyValidation, - loginValidation, - editUserBodyValidation, - recoveryValidation, - recoveryTokenBodyValidation, - newPasswordValidation, - getUserByIdParamValidation, - editUserByIdParamValidation, - editUserByIdBodyValidation, - editSuperadminUserByIdBodyValidation, - editUserPasswordByIdBodyValidation, -} from "../../validation/joi.js"; - -const SERVICE_NAME = "authController"; -/** - * Authentication Controller - * - * Handles all authentication-related HTTP requests including user registration, - * login, password recovery, and user management operations. - * - * @class AuthController - * @description Manages user authentication and authorization operations - */ -class AuthController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates an instance of AuthController. - * - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.settingsService - Service for application settings - * @param {Object} dependencies.emailService - Service for email operations - * @param {Object} dependencies.jobQueue - Service for job queue operations - * @param {Object} dependencies.userService - User business logic service - */ - constructor(commonDependencies, { settingsService, emailService, jobQueue, userService }) { - super(commonDependencies); - this.settingsService = settingsService; - this.emailService = emailService; - this.jobQueue = jobQueue; - this.userService = userService; - } - - get serviceName() { - return AuthController.SERVICE_NAME; - } - - /** - * Registers a new user in the system. - * - * @async - * @function registerUser - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing user registration data - * @param {string} req.body.firstName - User's first name - * @param {string} req.body.lastName - User's last name - * @param {string} req.body.email - User's email address (will be converted to lowercase) - * @param {string} req.body.password - User's password - * @param {string} [req.body.inviteToken] - Invite token for registration (required if superadmin exists) - * @param {string} [req.body.teamId] - Team ID (auto-assigned if superadmin) - * @param {Array} [req.body.role] - User roles (auto-assigned if superadmin) - * @param {Object} [req.file] - Profile image file uploaded via multer - * @param {Object} res - Express response object - * @returns {Promise} Success response with user data and JWT token - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 409 - Conflict if user already exists - * @example - * // Register first user (becomes superadmin) - * POST /auth/register - * { - * "firstName": "John", - * "lastName": "Doe", - * "email": "john@example.com", - * "password": "SecurePass123!" - * } - * - * // Register subsequent user (requires invite token) - * POST /auth/register - * { - * "firstName": "Jane", - * "lastName": "Smith", - * "email": "jane@example.com", - * "password": "SecurePass123!", - * "inviteToken": "abc123..." - * } - */ - registerUser = this.asyncHandler( - async (req, res) => { - if (req.body?.email) { - req.body.email = req.body.email?.toLowerCase(); - } - await registrationBodyValidation.validateAsync(req.body); - const { user, token } = await this.userService.registerUser(req.body, req.file); - res.success({ - msg: this.stringService.authCreateUser, - data: { user, token }, - }); - }, - SERVICE_NAME, - "registerUser" - ); - - /** - * Authenticates a user and returns a JWT token. - * - * @async - * @function loginUser - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing login credentials - * @param {string} req.body.email - User's email address (will be converted to lowercase) - * @param {string} req.body.password - User's password - * @param {Object} res - Express response object - * @returns {Promise} Success response with user data and JWT token - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 401 - Unauthorized if credentials are incorrect - * @example - * POST /auth/login - * { - * "email": "john@example.com", - * "password": "SecurePass123!" - * } - */ - loginUser = this.asyncHandler( - async (req, res) => { - if (req.body?.email) { - req.body.email = req.body.email?.toLowerCase(); - } - await loginValidation.validateAsync(req.body); - const { user, token } = await this.userService.loginUser(req.body.email, req.body.password); - - return res.success({ - msg: this.stringService.authLoginUser, - data: { - user, - token, - }, - }); - }, - SERVICE_NAME, - "loginUser" - ); - - /** - * Updates the current user's profile information. - * - * @async - * @function editUser - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing user update data - * @param {string} [req.body.firstName] - Updated first name - * @param {string} [req.body.lastName] - Updated last name - * @param {string} [req.body.password] - Current password (required for password change) - * @param {string} [req.body.newPassword] - New password (required for password change) - * @param {boolean} [req.body.deleteProfileImage] - Flag to delete profile image - * @param {Object} [req.file] - New profile image file - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {Object} res - Express response object - * @returns {Promise} Success response with updated user data - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 403 - Forbidden if current password is incorrect - * @example - * PUT /auth/user - * { - * "firstName": "John Updated", - * "lastName": "Doe Updated" - * } - * - * // Change password - * PUT /auth/user - * { - * "password": "OldPass123!", - * "newPassword": "NewPass123!" - * } - */ - editUser = this.asyncHandler( - async (req, res) => { - await editUserBodyValidation.validateAsync(req.body); - - const updatedUser = await this.userService.editUser(req.body, req.file, req.user); - - res.success({ - msg: this.stringService.authUpdateUser, - data: updatedUser, - }); - }, - SERVICE_NAME, - "editUser" - ); - - /** - * Checks if a superadmin account exists in the system. - * - * @async - * @function checkSuperadminExists - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @returns {Promise} Success response with boolean indicating superadmin existence - * @example - * GET /auth/users/superadmin - * // Response: { "data": true } or { "data": false } - */ - checkSuperadminExists = this.asyncHandler( - async (req, res) => { - const superAdminExists = await this.userService.checkSuperadminExists(); - return res.success({ - msg: this.stringService.authAdminExists, - data: superAdminExists, - }); - }, - SERVICE_NAME, - "checkSuperadminExists" - ); - - /** - * Initiates password recovery process by sending a recovery email. - * - * @async - * @function requestRecovery - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing email - * @param {string} req.body.email - Email address for password recovery - * @param {Object} res - Express response object - * @returns {Promise} Success response with message ID - * @throws {Error} 422 - Validation error if email is invalid - * @throws {Error} 404 - Not found if user doesn't exist - * @example - * POST /auth/recovery/request - * { - * "email": "john@example.com" - * } - */ - requestRecovery = this.asyncHandler( - async (req, res) => { - await recoveryValidation.validateAsync(req.body); - const email = req?.body?.email; - const msgId = await this.userService.requestRecovery(email); - return res.success({ - msg: this.stringService.authCreateRecoveryToken, - data: msgId, - }); - }, - SERVICE_NAME, - "requestRecovery" - ); - - /** - * Validates a password recovery token. - * - * @async - * @function validateRecovery - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing recovery token - * @param {string} req.body.recoveryToken - Recovery token to validate - * @param {Object} res - Express response object - * @returns {Promise} Success response if token is valid - * @throws {Error} 422 - Validation error if token format is invalid - * @throws {Error} 400 - Bad request if token is invalid or expired - * @example - * POST /auth/recovery/validate - * { - * "recoveryToken": "abc123..." - * } - */ - validateRecovery = this.asyncHandler( - async (req, res) => { - await recoveryTokenBodyValidation.validateAsync(req.body); - await this.userService.validateRecovery(req.body.recoveryToken); - return res.success({ - msg: this.stringService.authVerifyRecoveryToken, - }); - }, - SERVICE_NAME, - "validateRecovery" - ); - - /** - * Resets user password using a valid recovery token. - * - * @async - * @function resetPassword - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing new password and recovery token - * @param {string} req.body.password - New password - * @param {string} req.body.recoveryToken - Valid recovery token - * @param {Object} res - Express response object - * @returns {Promise} Success response with user data and JWT token - * @throws {Error} 422 - Validation error if password format is invalid - * @throws {Error} 400 - Bad request if token is invalid or expired - * @example - * POST /auth/recovery/reset - * { - * "password": "NewSecurePass123!", - * "recoveryToken": "abc123..." - * } - */ - resetPassword = this.asyncHandler( - async (req, res) => { - await newPasswordValidation.validateAsync(req.body); - const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); - return res.success({ - msg: this.stringService.authResetPassword, - data: { user, token }, - }); - }, - SERVICE_NAME, - "resetPassword" - ); - - /** - * Deletes the current user's account and associated data. - * - * @async - * @function deleteUser - * @param {Object} req - Express request object - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user._id - User ID - * @param {string} req.user.email - User email - * @param {string} req.user.teamId - User's team ID - * @param {Array} req.user.role - User roles - * @param {Object} res - Express response object - * @returns {Promise} Success response confirming user deletion - * @throws {Error} 400 - Bad request if user is demo user - * @throws {Error} 404 - Not found if user doesn't exist - * @example - * DELETE /auth/user - * // Requires JWT authentication - */ - deleteUser = this.asyncHandler( - async (req, res) => { - await this.userService.deleteUser(req.user); - return res.success({ - msg: this.stringService.authDeleteUser, - }); - }, - SERVICE_NAME, - "deleteUser" - ); - - /** - * Retrieves all users in the system (admin/superadmin only). - * - * @async - * @function getAllUsers - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @returns {Promise} Success response with array of users - * @throws {Error} 403 - Forbidden if user doesn't have admin/superadmin role - * @example - * GET /auth/users - * // Requires JWT authentication with admin/superadmin role - */ - getAllUsers = this.asyncHandler( - async (req, res) => { - const allUsers = await this.userService.getAllUsers(); - return res.success({ - msg: this.stringService.authGetAllUsers, - data: allUsers, - }); - }, - SERVICE_NAME, - "getAllUsers" - ); - - /** - * Retrieves a specific user by ID (superadmin only). - * - * @async - * @function getUserById - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.userId - ID of the user to retrieve - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {Array} req.user.role - Current user's roles - * @param {Object} res - Express response object - * @returns {Promise} Success response with user data - * @throws {Error} 422 - Validation error if userId is invalid - * @throws {Error} 403 - Forbidden if user doesn't have superadmin role - * @throws {Error} 404 - Not found if user doesn't exist - * @example - * GET /auth/users/507f1f77bcf86cd799439011 - * // Requires JWT authentication with superadmin role - */ - getUserById = this.asyncHandler( - async (req, res) => { - await getUserByIdParamValidation.validateAsync(req.params); - const userId = req?.params?.userId; - const roles = req?.user?.role; - - if (!userId) { - throw new Error("No user ID in request"); - } - - if (!roles || roles.length === 0) { - throw new Error("No roles in request"); - } - - const user = await this.userService.getUserById(roles, userId); - - return res.success({ msg: "ok", data: user }); - }, - SERVICE_NAME, - "getUserById" - ); - - /** - * Updates a specific user by ID (superadmin only). - * - * @async - * @function editUserById - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.userId - ID of the user to update - * @param {Object} req.body - Request body containing user update data - * @param {string} [req.body.firstName] - Updated first name - * @param {string} [req.body.lastName] - Updated last name - * @param {Array} [req.body.role] - Updated user roles - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user._id - Current user's ID - * @param {Array} req.user.role - Current user's roles - * @param {Object} res - Express response object - * @returns {Promise} Success response confirming user update - * @throws {Error} 422 - Validation error if parameters or body are invalid - * @throws {Error} 403 - Forbidden if user doesn't have superadmin role - * @throws {Error} 404 - Not found if user doesn't exist - * @example - * PUT /auth/users/507f1f77bcf86cd799439011 - * { - * "firstName": "Updated Name", - * "role": ["admin"] - * } - * // Requires JWT authentication with superadmin role - */ - editUserById = this.asyncHandler( - async (req, res) => { - const roles = req?.user?.role; - if (!roles.includes("superadmin")) { - throw this.errorService.createError("Unauthorized", 403); - } - - const userId = req.params.userId; - const user = { ...req.body }; - - await editUserByIdParamValidation.validateAsync(req.params); - // If this is superadmin self edit, allow "superadmin" role - if (userId === req.user._id) { - await editSuperadminUserByIdBodyValidation.validateAsync(req.body); - } else { - await editUserByIdBodyValidation.validateAsync(req.body); - } - - await this.userService.editUserById(userId, user); - return res.success({ msg: "ok" }); - }, - SERVICE_NAME, - "editUserById" - ); - editUserPasswordById = this.asyncHandler( - async (req, res) => { - const roles = req?.user?.role; - if (!roles.includes("superadmin")) { - throw this.errorService.createError("Unauthorized", 403); - } - - const userId = req.params.userId; - await editUserByIdParamValidation.validateAsync(req.params); - await editUserPasswordByIdBodyValidation.validateAsync(req.body); - const updatedPassword = req.body.password; - await this.userService.setPasswordByUserId(userId, updatedPassword); - return res.success({ msg: "Password reset successfully" }); - }, - SERVICE_NAME, - "editUserPasswordById" - ); -} - -export default AuthController; diff --git a/server/src/controllers/v1/baseController.js b/server/src/controllers/v1/baseController.js deleted file mode 100644 index e8612cec5..000000000 --- a/server/src/controllers/v1/baseController.js +++ /dev/null @@ -1,83 +0,0 @@ -import { AppError } from "../../service/v1/infrastructure/errorService.js"; - -export const createCommonDependencies = (db, errorService, logger, stringService) => { - return { - db, - errorService, - logger, - stringService, - }; -}; - -class BaseController { - constructor({ db, logger, errorService, ...additionalDependencies }) { - this.db = db; - this.logger = logger; - this.errorService = errorService; - Object.assign(this, additionalDependencies); - - this.asyncHandler = (fn, serviceName, methodName) => { - return async (req, res, next) => { - try { - await fn(req, res, next); - } catch (error) { - // Handle validation errors - if (error.isJoi) { - const validationError = this.errorService.createValidationError(error.message, error.details, serviceName, methodName); - return next(validationError); - } - - if (error.name === "ValidationError") { - const validationError = this.errorService.createValidationError("Database validation failed", error.errors, serviceName, methodName); - return next(validationError); - } - - if (error.name === "CastError") { - const notFoundError = this.errorService.createNotFoundError( - "Invalid resource identifier", - { field: error.path, value: error.value }, - serviceName, - methodName - ); - return next(notFoundError); - } - - if (error.code === "11000") { - const conflictError = this.errorService.createConflictError("Resource already exists", { - originalError: error.message, - code: error.code, - }); - conflictError.service = serviceName; - conflictError.method = methodName; - return next(conflictError); - } - - if (error instanceof AppError) { - error.service = error.service || serviceName; - error.method = error.method || methodName; - return next(error); - } - - if (error.status) { - const appError = this.errorService.createError(error.message, error.status, serviceName, methodName, { - originalError: error.message, - stack: error.stack, - }); - return next(appError); - } - - // For unknown errors, create a server error - const appError = this.errorService.createServerError(error.message || "An unexpected error occurred", { - originalError: error.message, - stack: error.stack, - }); - appError.service = serviceName; - appError.method = methodName; - appError.stack = error.stack; // Preserve original stack - return next(appError); - } - }; - }; - } -} -export default BaseController; diff --git a/server/src/controllers/v1/checkController.js b/server/src/controllers/v1/checkController.js deleted file mode 100755 index ea293d74d..000000000 --- a/server/src/controllers/v1/checkController.js +++ /dev/null @@ -1,360 +0,0 @@ -import BaseController from "./baseController.js"; -import { - getChecksParamValidation, - getChecksQueryValidation, - getTeamChecksQueryValidation, - deleteChecksParamValidation, - deleteChecksByTeamIdParamValidation, - updateChecksTTLBodyValidation, - ackCheckBodyValidation, - ackAllChecksParamValidation, - ackAllChecksBodyValidation, -} from "../../validation/joi.js"; - -const SERVICE_NAME = "checkController"; - -/** - * Check Controller - * - * Handles all check-related HTTP requests including retrieving checks, - * acknowledging checks, deleting checks, and managing check TTL settings. - * - * @class CheckController - * @description Manages check operations and monitoring data - */ -class CheckController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates an instance of CheckController. - * - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.settingsService - Service for application settings - * @param {Object} dependencies.checkService - Check business logic service - */ - constructor(commonDependencies, { settingsService, checkService }) { - super(commonDependencies); - this.settingsService = settingsService; - this.checkService = checkService; - } - - get serviceName() { - return CheckController.SERVICE_NAME; - } - - /** - * Retrieves checks for a specific monitor with filtering and pagination. - * - * @async - * @function getChecksByMonitor - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.monitorId - ID of the monitor to get checks for - * @param {Object} req.query - Query parameters for filtering and pagination - * @param {string} [req.query.type] - Type of checks to filter by - * @param {string} [req.query.sortOrder] - Sort order (asc/desc) - * @param {string} [req.query.dateRange] - Date range filter - * @param {string} [req.query.filter] - General filter string - * @param {boolean} [req.query.ack] - Filter by acknowledgment status - * @param {number} [req.query.page] - Page number for pagination - * @param {number} [req.query.rowsPerPage] - Number of rows per page - * @param {string} [req.query.status] - Filter by check status - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with checks data - * @throws {Error} 422 - Validation error if parameters are invalid - * @throws {Error} 404 - Not found if monitor doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to monitor - * @example - * GET /checks/monitor/507f1f77bcf86cd799439011?page=1&rowsPerPage=10&status=down - * // Requires JWT authentication - */ - getChecksByMonitor = this.asyncHandler( - async (req, res) => { - await getChecksParamValidation.validateAsync(req.params); - await getChecksQueryValidation.validateAsync(req.query); - - const result = await this.checkService.getChecksByMonitor({ - monitorId: req?.params?.monitorId, - query: req?.query, - teamId: req?.user?.teamId, - }); - - return res.success({ - msg: this.stringService.checkGet, - data: result, - }); - }, - SERVICE_NAME, - "getChecksByMonitor" - ); - - /** - * Retrieves all checks for the current user's team with filtering and pagination. - * - * @async - * @function getChecksByTeam - * @param {Object} req - Express request object - * @param {Object} req.query - Query parameters for filtering and pagination - * @param {string} [req.query.sortOrder] - Sort order (asc/desc) - * @param {string} [req.query.dateRange] - Date range filter - * @param {string} [req.query.filter] - General filter string - * @param {boolean} [req.query.ack] - Filter by acknowledgment status - * @param {number} [req.query.page] - Page number for pagination - * @param {number} [req.query.rowsPerPage] - Number of rows per page - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with checks data - * @throws {Error} 422 - Validation error if query parameters are invalid - * @example - * GET /checks/team?page=1&rowsPerPage=20&status=down&ack=false - * // Requires JWT authentication - */ - getChecksByTeam = this.asyncHandler( - async (req, res) => { - await getTeamChecksQueryValidation.validateAsync(req.query); - const checkData = await this.checkService.getChecksByTeam({ - teamId: req?.user?.teamId, - query: req?.query, - }); - return res.success({ - msg: this.stringService.checkGet, - data: checkData, - }); - }, - SERVICE_NAME, - "getChecksByTeam" - ); - - /** - * Retrieves a summary of checks for the current user's team. - * - * @async - * @function getChecksSummaryByTeamId - * @param {Object} req - Express request object - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with checks summary - * @example - * GET /checks/summary - * // Requires JWT authentication - * // Response includes counts by status, time ranges, etc. - */ - getChecksSummaryByTeamId = this.asyncHandler( - async (req, res) => { - const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId }); - return res.success({ - msg: this.stringService.checkGetSummary, - data: summary, - }); - }, - SERVICE_NAME, - "getChecksSummaryByTeamId" - ); - - /** - * Acknowledges a specific check by ID. - * - * @async - * @function ackCheck - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.checkId - ID of the check to acknowledge - * @param {Object} req.body - Request body containing acknowledgment data - * @param {boolean} req.body.ack - Acknowledgment status (true/false) - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with updated check data - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 404 - Not found if check doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to check - * @example - * PUT /checks/507f1f77bcf86cd799439011/ack - * { - * "ack": true - * } - * // Requires JWT authentication - */ - ackCheck = this.asyncHandler( - async (req, res) => { - await ackCheckBodyValidation.validateAsync(req.body); - - const updatedCheck = await this.checkService.ackCheck({ - checkId: req?.params?.checkId, - teamId: req?.user?.teamId, - ack: req?.body?.ack, - }); - - return res.success({ - msg: this.stringService.checkUpdateStatus, - data: updatedCheck, - }); - }, - SERVICE_NAME, - "ackCheck" - ); - - /** - * Acknowledges all checks for a specific monitor or path. - * - * @async - * @function ackAllChecks - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.monitorId - ID of the monitor - * @param {string} req.params.path - Path for acknowledgment (e.g., "monitor") - * @param {Object} req.body - Request body containing acknowledgment data - * @param {boolean} req.body.ack - Acknowledgment status (true/false) - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with updated checks data - * @throws {Error} 422 - Validation error if parameters or body are invalid - * @throws {Error} 404 - Not found if monitor doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to monitor - * @example - * PUT /checks/monitor/507f1f77bcf86cd799439011/ack - * { - * "ack": true - * } - * // Requires JWT authentication - */ - ackAllChecks = this.asyncHandler( - async (req, res) => { - await ackAllChecksParamValidation.validateAsync(req.params); - await ackAllChecksBodyValidation.validateAsync(req.body); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new Error("No team ID in request"); - } - - const updatedChecks = await this.checkService.ackAllChecks({ - monitorId: req?.params?.monitorId, - path: req?.params?.path, - teamId: req?.user?.teamId, - ack: req?.body?.ack, - }); - - return res.success({ - msg: this.stringService.checkUpdateStatus, - data: updatedChecks, - }); - }, - SERVICE_NAME, - "ackAllChecks" - ); - - /** - * Deletes all checks for a specific monitor. - * - * @async - * @function deleteChecks - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.monitorId - ID of the monitor whose checks to delete - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with deletion count - * @throws {Error} 422 - Validation error if monitorId is invalid - * @throws {Error} 404 - Not found if monitor doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to monitor - * @example - * DELETE /checks/monitor/507f1f77bcf86cd799439011 - * // Requires JWT authentication - * // Response: { "data": { "deletedCount": 150 } } - */ - deleteChecks = this.asyncHandler( - async (req, res) => { - await deleteChecksParamValidation.validateAsync(req.params); - - const deletedCount = await this.checkService.deleteChecks({ - monitorId: req.params.monitorId, - teamId: req?.user?.teamId, - }); - - return res.success({ - msg: this.stringService.checkDelete, - data: { deletedCount }, - }); - }, - SERVICE_NAME, - "deleteChecks" - ); - - /** - * Deletes all checks for the current user's team. - * - * @async - * @function deleteChecksByTeamId - * @param {Object} req - Express request object - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with deletion count - * @throws {Error} 422 - Validation error if parameters are invalid - * @example - * DELETE /checks/team - * // Requires JWT authentication - * // Response: { "data": { "deletedCount": 1250 } } - */ - deleteChecksByTeamId = this.asyncHandler( - async (req, res) => { - await deleteChecksByTeamIdParamValidation.validateAsync(req.params); - - const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); - - return res.success({ - msg: this.stringService.checkDelete, - data: { deletedCount }, - }); - }, - SERVICE_NAME, - "deleteChecksByTeamId" - ); - - /** - * Updates the TTL (Time To Live) setting for checks in the current user's team. - * - * @async - * @function updateChecksTTL - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing TTL data - * @param {number} req.body.ttl - TTL value in days - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response confirming TTL update - * @throws {Error} 422 - Validation error if TTL value is invalid - * @example - * PUT /checks/ttl - * { - * "ttl": 30 - * } - * // Requires JWT authentication - * // Sets check TTL to 30 days - */ - updateChecksTTL = this.asyncHandler( - async (req, res) => { - await updateChecksTTLBodyValidation.validateAsync(req.body); - - await this.checkService.updateChecksTTL({ - teamId: req?.user?.teamId, - ttl: req?.body?.ttl, - }); - - return res.success({ - msg: this.stringService.checkUpdateTTL, - }); - }, - SERVICE_NAME, - "updateChecksTtl" - ); -} - -export default CheckController; diff --git a/server/src/controllers/v1/controllerUtils.js b/server/src/controllers/v1/controllerUtils.js deleted file mode 100755 index f21ba154b..000000000 --- a/server/src/controllers/v1/controllerUtils.js +++ /dev/null @@ -1,12 +0,0 @@ -const fetchMonitorCertificate = async (sslChecker, monitor) => { - const monitorUrl = new URL(monitor.url); - const hostname = monitorUrl.hostname; - const cert = await sslChecker(hostname); - // Throw an error if no cert or if cert.validTo is not present - if (cert?.validTo === null || cert?.validTo === undefined) { - throw new Error("Certificate not found"); - } - return cert; -}; - -export { fetchMonitorCertificate }; diff --git a/server/src/controllers/v1/diagnosticController.js b/server/src/controllers/v1/diagnosticController.js deleted file mode 100755 index 0723a9f6c..000000000 --- a/server/src/controllers/v1/diagnosticController.js +++ /dev/null @@ -1,61 +0,0 @@ -const SERVICE_NAME = "diagnosticController"; -import BaseController from "./baseController.js"; -/** - * Diagnostic Controller - * - * Handles system diagnostic and monitoring requests including system statistics, - * performance metrics, and health checks. - * - * @class DiagnosticController - * @description Manages system diagnostics and performance monitoring - */ -class DiagnosticController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates an instance of DiagnosticController. - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.diagnosticService - Service for system diagnostics and monitoring - */ - constructor(commonDependencies, { diagnosticService }) { - super(commonDependencies); - this.diagnosticService = diagnosticService; - } - - get serviceName() { - return DiagnosticController.SERVICE_NAME; - } - /** - * Retrieves comprehensive system statistics and performance metrics. - * - * @async - * @function getSystemStats - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @returns {Promise} Success response with system diagnostics data - * @description Returns system performance metrics, memory usage, CPU statistics, - * and other diagnostic information useful for monitoring system health. - * @example - * GET /diagnostics/stats - * // Response includes: - * // - Memory usage (heap, external, arrayBuffers) - * // - CPU usage statistics - * // - System uptime - * // - Performance metrics - * // - Database connection status - * // - Active processes/connections - */ - getSystemStats = this.asyncHandler( - async (req, res) => { - const diagnostics = await this.diagnosticService.getSystemStats(); - return res.success({ - msg: "OK", - data: diagnostics, - }); - }, - SERVICE_NAME, - "getSystemStats" - ); -} - -export default DiagnosticController; diff --git a/server/src/controllers/v1/inviteController.js b/server/src/controllers/v1/inviteController.js deleted file mode 100755 index 1ec586f31..000000000 --- a/server/src/controllers/v1/inviteController.js +++ /dev/null @@ -1,101 +0,0 @@ -import { inviteBodyValidation, inviteVerificationBodyValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; -const SERVICE_NAME = "inviteController"; - -/** - * Controller for handling user invitation operations - * Manages invite token generation, email sending, and token verification - */ -class InviteController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates a new InviteController instance - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies.inviteService - Service for invite-related operations - */ - constructor(commonDependencies, { inviteService }) { - super(commonDependencies); - this.inviteService = inviteService; - } - - get serviceName() { - return InviteController.SERVICE_NAME; - } - - /** - * Generates an invite token for a user invitation - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing invite details - * @param {Object} req.user - Authenticated user object - * @param {string} req.user.teamId - Team ID of the authenticated user - * @param {Object} res - Express response object - * @returns {Promise} Response with invite token data - */ - getInviteToken = this.asyncHandler( - async (req, res) => { - const invite = req.body; - const teamId = req?.user?.teamId; - invite.teamId = teamId; - await inviteBodyValidation.validateAsync(invite); - const inviteToken = await this.inviteService.getInviteToken({ invite, teamId }); - return res.success({ - msg: this.stringService.inviteIssued, - data: inviteToken, - }); - }, - SERVICE_NAME, - "getInviteToken" - ); - - /** - * Sends an invitation email to a user - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing invite details - * @param {Object} req.user - Authenticated user object - * @param {string} req.user.teamId - Team ID of the authenticated user - * @param {string} req.user.firstName - First name of the authenticated user - * @param {Object} res - Express response object - * @returns {Promise} Response with invite token data - */ - sendInviteEmail = this.asyncHandler( - async (req, res) => { - const inviteRequest = req.body; - inviteRequest.teamId = req?.user?.teamId; - await inviteBodyValidation.validateAsync(inviteRequest); - - const inviteToken = await this.inviteService.sendInviteEmail({ - inviteRequest, - firstName: req?.user?.firstName, - }); - return res.success({ - msg: this.stringService.inviteIssued, - data: inviteToken, - }); - }, - SERVICE_NAME, - "sendInviteEmail" - ); - - /** - * Verifies an invite token and returns invite details - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing the invite token - * @param {string} req.body.token - The invite token to verify - * @param {Object} res - Express response object - * @returns {Promise} Response with verified invite data - */ - verifyInviteToken = this.asyncHandler( - async (req, res) => { - await inviteVerificationBodyValidation.validateAsync(req.body); - const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); - return res.success({ - msg: this.stringService.inviteVerified, - data: invite, - }); - }, - SERVICE_NAME, - "verifyInviteToken" - ); -} - -export default InviteController; diff --git a/server/src/controllers/v1/logController.js b/server/src/controllers/v1/logController.js deleted file mode 100644 index 3537246bb..000000000 --- a/server/src/controllers/v1/logController.js +++ /dev/null @@ -1,26 +0,0 @@ -import BaseController from "./baseController.js"; -const SERVICE_NAME = "LogController"; - -class LogController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies) { - super(commonDependencies); - } - - get serviceName() { - return LogController.SERVICE_NAME; - } - - getLogs = this.asyncHandler( - async (req, res) => { - const logs = await this.logger.getLogs(); - res.success({ - msg: "Logs fetched successfully", - data: logs, - }); - }, - SERVICE_NAME, - "getLogs" - ); -} -export default LogController; diff --git a/server/src/controllers/v1/maintenanceWindowController.js b/server/src/controllers/v1/maintenanceWindowController.js deleted file mode 100644 index be373ace5..000000000 --- a/server/src/controllers/v1/maintenanceWindowController.js +++ /dev/null @@ -1,146 +0,0 @@ -import { - createMaintenanceWindowBodyValidation, - editMaintenanceWindowByIdParamValidation, - editMaintenanceByIdWindowBodyValidation, - getMaintenanceWindowByIdParamValidation, - getMaintenanceWindowsByMonitorIdParamValidation, - getMaintenanceWindowsByTeamIdQueryValidation, - deleteMaintenanceWindowByIdParamValidation, -} from "../../validation/joi.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "maintenanceWindowController"; - -class MaintenanceWindowController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { settingsService, maintenanceWindowService }) { - super(commonDependencies); - this.settingsService = settingsService; - this.maintenanceWindowService = maintenanceWindowService; - } - - get serviceName() { - return MaintenanceWindowController.SERVICE_NAME; - } - - createMaintenanceWindows = this.asyncHandler( - async (req, res) => { - await createMaintenanceWindowBodyValidation.validateAsync(req.body); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); - - return res.success({ - msg: this.stringService.maintenanceWindowCreate, - }); - }, - SERVICE_NAME, - "createMaintenanceWindows" - ); - - getMaintenanceWindowById = this.asyncHandler( - async (req, res) => { - await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); - - const teamId = req.user.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId }); - - return res.success({ - msg: this.stringService.maintenanceWindowGetById, - data: maintenanceWindow, - }); - }, - SERVICE_NAME, - "getMaintenanceWindowById" - ); - - getMaintenanceWindowsByTeamId = this.asyncHandler( - async (req, res) => { - await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); - - const teamId = req?.user?.teamId; - - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query }); - - return res.success({ - msg: this.stringService.maintenanceWindowGetByTeam, - data: maintenanceWindows, - }); - }, - SERVICE_NAME, - "getMaintenanceWindowsByTeamId" - ); - - getMaintenanceWindowsByMonitorId = this.asyncHandler( - async (req, res) => { - await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId }); - - return res.success({ - msg: this.stringService.maintenanceWindowGetByUser, - data: maintenanceWindows, - }); - }, - SERVICE_NAME, - "getMaintenanceWindowsByMonitorId" - ); - - deleteMaintenanceWindow = this.asyncHandler( - async (req, res) => { - await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId }); - - return res.success({ - msg: this.stringService.maintenanceWindowDelete, - }); - }, - SERVICE_NAME, - "deleteMaintenanceWindow" - ); - - editMaintenanceWindow = this.asyncHandler( - async (req, res) => { - await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); - await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); - - const teamId = req.user.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId }); - return res.success({ - msg: this.stringService.maintenanceWindowEdit, - data: editedMaintenanceWindow, - }); - }, - SERVICE_NAME, - "editMaintenanceWindow" - ); -} - -export default MaintenanceWindowController; diff --git a/server/src/controllers/v1/monitorController.js b/server/src/controllers/v1/monitorController.js deleted file mode 100644 index 5ac44fa94..000000000 --- a/server/src/controllers/v1/monitorController.js +++ /dev/null @@ -1,475 +0,0 @@ -import { - getMonitorByIdParamValidation, - getMonitorByIdQueryValidation, - getMonitorsByTeamIdParamValidation, - getMonitorsByTeamIdQueryValidation, - createMonitorBodyValidation, - editMonitorBodyValidation, - pauseMonitorParamValidation, - getMonitorStatsByIdParamValidation, - getMonitorStatsByIdQueryValidation, - getCertificateParamValidation, - getHardwareDetailsByIdParamValidation, - getHardwareDetailsByIdQueryValidation, -} from "../../validation/joi.js"; -import sslChecker from "ssl-checker"; -import { fetchMonitorCertificate } from "./controllerUtils.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "monitorController"; -class MonitorController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { settingsService, jobQueue, emailService, monitorService }) { - super(commonDependencies); - this.settingsService = settingsService; - this.jobQueue = jobQueue; - this.emailService = emailService; - this.monitorService = monitorService; - } - - get serviceName() { - return MonitorController.SERVICE_NAME; - } - - async verifyTeamAccess(teamId, monitorId) { - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - } - - getAllMonitors = this.asyncHandler( - async (req, res) => { - const monitors = await this.monitorService.getAllMonitors(); - return res.success({ - msg: this.stringService.monitorGetAll, - data: monitors, - }); - }, - SERVICE_NAME, - "getAllMonitors" - ); - - getUptimeDetailsById = this.asyncHandler( - async (req, res) => { - const monitorId = req?.params?.monitorId; - const dateRange = req?.query?.dateRange; - const normalize = req?.query?.normalize; - - const teamId = req?.user?.teamId; - - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const data = await this.monitorService.getUptimeDetailsById({ - teamId, - monitorId, - dateRange, - normalize, - }); - return res.success({ - msg: this.stringService.monitorGetByIdSuccess, - data: data, - }); - }, - SERVICE_NAME, - "getUptimeDetailsById" - ); - - getMonitorStatsById = this.asyncHandler( - async (req, res) => { - await getMonitorStatsByIdParamValidation.validateAsync(req.params); - await getMonitorStatsByIdQueryValidation.validateAsync(req.query); - - let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query; - const monitorId = req?.params?.monitorId; - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const monitorStats = await this.monitorService.getMonitorStatsById({ - teamId, - monitorId, - limit, - sortOrder, - dateRange, - numToDisplay, - normalize, - }); - - return res.success({ - msg: this.stringService.monitorStatsById, - data: monitorStats, - }); - }, - SERVICE_NAME, - "getMonitorStatsById" - ); - - /** - * Get hardware details for a specific monitor by ID - * @async - * @param {Express.Request} req - Express request object containing monitorId in params - * @param {Express.Response} res - Express response object - * @param {Express.NextFunction} next - Express next middleware function - * @returns {Promise} - * @throws {Error} - Throws error if monitor not found or other database errors - */ - getHardwareDetailsById = this.asyncHandler( - async (req, res) => { - await getHardwareDetailsByIdParamValidation.validateAsync(req.params); - await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); - - const monitorId = req?.params?.monitorId; - const dateRange = req?.query?.dateRange; - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const monitor = await this.monitorService.getHardwareDetailsById({ - teamId, - monitorId, - dateRange, - }); - - return res.success({ - msg: this.stringService.monitorGetByIdSuccess, - data: monitor, - }); - }, - SERVICE_NAME, - "getHardwareDetailsById" - ); - - getMonitorCertificate = this.asyncHandler( - async (req, res) => { - await getCertificateParamValidation.validateAsync(req.params); - - const { monitorId } = req.params; - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - const certificate = await fetchMonitorCertificate(sslChecker, monitor); - - return res.success({ - msg: this.stringService.monitorCertificate, - data: { - certificateDate: new Date(certificate.validTo), - }, - }); - }, - SERVICE_NAME, - "getMonitorCertificate" - ); - - getMonitorById = this.asyncHandler( - async (req, res) => { - await getMonitorByIdParamValidation.validateAsync(req.params); - await getMonitorByIdQueryValidation.validateAsync(req.query); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId }); - - return res.success({ - msg: this.stringService.monitorGetByIdSuccess, - data: monitor, - }); - }, - SERVICE_NAME, - "getMonitorById" - ); - - createMonitor = this.asyncHandler( - async (req, res) => { - await createMonitorBodyValidation.validateAsync(req.body); - - const userId = req?.user?._id; - const teamId = req?.user?.teamId; - - const monitor = await this.monitorService.createMonitor({ teamId, userId, body: req.body }); - - return res.success({ - msg: this.stringService.monitorCreate, - data: monitor, - }); - }, - SERVICE_NAME, - "createMonitor" - ); - - createBulkMonitors = this.asyncHandler( - async (req, res) => { - if (!req.file) { - throw this.errorService.createBadRequestError("No file uploaded"); - } - - if (!req.file.mimetype.includes("csv")) { - throw this.errorService.createBadRequestError("File is not a CSV"); - } - - if (req.file.size === 0) { - throw this.errorService.createBadRequestError("File is empty"); - } - - const userId = req?.user?._id; - const teamId = req?.user?.teamId; - - if (!userId || !teamId) { - throw this.errorService.createBadRequestError("Missing userId or teamId"); - } - - const fileData = req?.file?.buffer?.toString("utf-8"); - if (!fileData) { - throw this.errorService.createBadRequestError("Cannot get file from buffer"); - } - - const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId }); - - return res.success({ - msg: this.stringService.bulkMonitorsCreate, - data: monitors, - }); - }, - SERVICE_NAME, - "createBulkMonitors" - ); - - deleteMonitor = this.asyncHandler( - async (req, res) => { - await getMonitorByIdParamValidation.validateAsync(req.params); - const monitorId = req.params.monitorId; - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); - - return res.success({ msg: this.stringService.monitorDelete, data: deletedMonitor }); - }, - SERVICE_NAME, - "deleteMonitor" - ); - - deleteAllMonitors = this.asyncHandler( - async (req, res) => { - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); - - return res.success({ msg: `Deleted ${deletedCount} monitors` }); - }, - SERVICE_NAME, - "deleteAllMonitors" - ); - - editMonitor = this.asyncHandler( - async (req, res) => { - await getMonitorByIdParamValidation.validateAsync(req.params); - await editMonitorBodyValidation.validateAsync(req.body); - const monitorId = req?.params?.monitorId; - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); - - return res.success({ - msg: this.stringService.monitorEdit, - data: editedMonitor, - }); - }, - SERVICE_NAME, - "editMonitor" - ); - - pauseMonitor = this.asyncHandler( - async (req, res) => { - await pauseMonitorParamValidation.validateAsync(req.params); - - const monitorId = req.params.monitorId; - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); - - return res.success({ - msg: monitor.isActive ? this.stringService.monitorResume : this.stringService.monitorPause, - data: monitor, - }); - }, - SERVICE_NAME, - "pauseMonitor" - ); - - addDemoMonitors = this.asyncHandler( - async (req, res) => { - const { _id, teamId } = req.user; - const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId }); - - return res.success({ - msg: this.stringService.monitorDemoAdded, - data: demoMonitors?.length ?? 0, - }); - }, - SERVICE_NAME, - "addDemoMonitors" - ); - - sendTestEmail = this.asyncHandler( - async (req, res) => { - const { to } = req.body; - if (!to || typeof to !== "string") { - throw this.errorService.createBadRequestError(this.stringService.errorForValidEmailAddress); - } - - const messageId = await this.monitorService.sendTestEmail({ to }); - return res.success({ - msg: this.stringService.sendTestEmail, - data: { messageId }, - }); - }, - SERVICE_NAME, - "sendTestEmail" - ); - - getMonitorsByTeamId = this.asyncHandler( - async (req, res) => { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - const teamId = req?.user?.teamId; - - const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, limit, type, page, rowsPerPage, filter, field, order }); - - return res.success({ - msg: this.stringService.monitorGetByTeamId, - data: monitors, - }); - }, - SERVICE_NAME, - "getMonitorsByTeamId" - ); - - getMonitorsAndSummaryByTeamId = this.asyncHandler( - async (req, res) => { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - - const explain = req?.query?.explain; - const type = req?.query?.type; - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain }); - - return res.success({ - msg: "OK", // TODO - data: result, - }); - }, - SERVICE_NAME, - "getMonitorsAndSummaryByTeamId" - ); - - getMonitorsWithChecksByTeamId = this.asyncHandler( - async (req, res) => { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - - const explain = req?.query?.explain; - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ - teamId, - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - explain, - }); - - return res.success({ - msg: "OK", - data: monitors, - }); - }, - SERVICE_NAME, - "getMonitorsWithChecksByTeamId" - ); - - exportMonitorsToCSV = this.asyncHandler( - async (req, res) => { - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const csv = await this.monitorService.exportMonitorsToCSV({ teamId }); - - return res.file({ - data: csv, - headers: { - "Content-Type": "text/csv", - "Content-Disposition": "attachment; filename=monitors.csv", - }, - }); - }, - SERVICE_NAME, - "exportMonitorsToCSV" - ); - - getAllGames = this.asyncHandler( - async (req, res) => { - return res.success({ - msg: "OK", - data: this.monitorService.getAllGames(), - }); - }, - SERVICE_NAME, - "getAllGames" - ); - - getGroupsByTeamId = this.asyncHandler( - async (req, res) => { - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const groups = await this.monitorService.getGroupsByTeamId({ teamId }); - - return res.success({ - msg: "OK", - data: groups, - }); - }, - SERVICE_NAME, - "getGroupsByTeamId" - ); -} - -export default MonitorController; diff --git a/server/src/controllers/v1/notificationController.js b/server/src/controllers/v1/notificationController.js deleted file mode 100644 index 37c2b4e60..000000000 --- a/server/src/controllers/v1/notificationController.js +++ /dev/null @@ -1,180 +0,0 @@ -import { createNotificationBodyValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "NotificationController"; - -class NotificationController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { notificationService, statusService }) { - super(commonDependencies); - this.notificationService = notificationService; - this.statusService = statusService; - } - - get serviceName() { - return NotificationController.SERVICE_NAME; - } - - testNotification = this.asyncHandler( - async (req, res) => { - const notification = req.body; - - const success = await this.notificationService.sendTestNotification(notification); - - if (!success) { - throw this.errorService.createServerError("Sending notification failed"); - } - - return res.success({ - msg: "Notification sent successfully", - }); - }, - SERVICE_NAME, - "testNotification" - ); - - createNotification = this.asyncHandler( - async (req, res) => { - await createNotificationBodyValidation.validateAsync(req.body, { - abortEarly: false, - }); - - const body = req.body; - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const userId = req?.user?._id; - if (!userId) { - throw this.errorService.createBadRequestError("User ID is required"); - } - body.userId = userId; - body.teamId = teamId; - - const notification = await this.db.notificationModule.createNotification(body); - return res.success({ - msg: "Notification created successfully", - data: notification, - }); - }, - SERVICE_NAME, - "createNotification" - ); - - getNotificationsByTeamId = this.asyncHandler( - async (req, res) => { - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const notifications = await this.db.notificationModule.getNotificationsByTeamId(teamId); - - return res.success({ - msg: "Notifications fetched successfully", - data: notifications, - }); - }, - SERVICE_NAME, - "getNotificationsByTeamId" - ); - - deleteNotification = this.asyncHandler( - async (req, res) => { - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - if (!notification.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - await this.db.notificationModule.deleteNotificationById(req.params.id); - return res.success({ - msg: "Notification deleted successfully", - }); - }, - SERVICE_NAME, - "deleteNotification" - ); - - getNotificationById = this.asyncHandler( - async (req, res) => { - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - if (!notification.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - return res.success({ - msg: "Notification fetched successfully", - data: notification, - }); - }, - SERVICE_NAME, - "getNotificationById" - ); - - editNotification = this.asyncHandler( - async (req, res) => { - await createNotificationBodyValidation.validateAsync(req.body, { - abortEarly: false, - }); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - if (!notification.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - const editedNotification = await this.db.notificationModule.editNotification(req.params.id, req.body); - return res.success({ - msg: "Notification updated successfully", - data: editedNotification, - }); - }, - SERVICE_NAME, - "editNotification" - ); - - testAllNotifications = this.asyncHandler( - async (req, res) => { - const monitorId = req.body.monitorId; - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - const notifications = monitor.notifications; - if (notifications.length === 0) throw this.errorService.createBadRequestError("No notifications"); - const result = await this.notificationService.testAllNotifications(notifications); - if (!result) throw this.errorService.createServerError("Failed to send all notifications"); - return res.success({ - msg: "All notifications sent successfully", - }); - }, - SERVICE_NAME, - "testAllNotifications" - ); -} - -export default NotificationController; diff --git a/server/src/controllers/v1/queueController.js b/server/src/controllers/v1/queueController.js deleted file mode 100755 index d2c3fd06f..000000000 --- a/server/src/controllers/v1/queueController.js +++ /dev/null @@ -1,87 +0,0 @@ -import BaseController from "./baseController.js"; -const SERVICE_NAME = "JobQueueController"; - -class JobQueueController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { jobQueue }) { - super(commonDependencies); - this.jobQueue = jobQueue; - } - - get serviceName() { - return JobQueueController.SERVICE_NAME; - } - - getMetrics = this.asyncHandler( - async (req, res) => { - const metrics = await this.jobQueue.getMetrics(); - res.success({ - msg: this.stringService.queueGetMetrics, - data: metrics, - }); - }, - SERVICE_NAME, - "getMetrics" - ); - - getJobs = this.asyncHandler( - async (req, res) => { - const jobs = await this.jobQueue.getJobs(); - return res.success({ - msg: this.stringService.queueGetJobs, - data: jobs, - }); - }, - SERVICE_NAME, - "getJobs" - ); - - getAllMetrics = this.asyncHandler( - async (req, res) => { - const jobs = await this.jobQueue.getJobs(); - const metrics = await this.jobQueue.getMetrics(); - return res.success({ - msg: this.stringService.queueGetAllMetrics, - data: { jobs, metrics }, - }); - }, - SERVICE_NAME, - "getAllMetrics" - ); - - addJob = this.asyncHandler( - async (req, res) => { - await this.jobQueue.addJob(Math.random().toString(36).substring(7)); - return res.success({ - msg: this.stringService.queueAddJob, - }); - }, - SERVICE_NAME, - "addJob" - ); - - flushQueue = this.asyncHandler( - async (req, res) => { - const result = await this.jobQueue.flushQueues(); - return res.success({ - msg: this.stringService.jobQueueFlush, - data: result, - }); - }, - SERVICE_NAME, - "flushQueue" - ); - - checkQueueHealth = this.asyncHandler( - async (req, res) => { - const stuckQueues = await this.jobQueue.checkQueueHealth(); - return res.success({ - msg: this.stringService.queueHealthCheck, - data: stuckQueues, - }); - }, - SERVICE_NAME, - "checkQueueHealth" - ); -} -export default JobQueueController; diff --git a/server/src/controllers/v1/settingsController.js b/server/src/controllers/v1/settingsController.js deleted file mode 100644 index 3db73a05a..000000000 --- a/server/src/controllers/v1/settingsController.js +++ /dev/null @@ -1,122 +0,0 @@ -import { updateAppSettingsBodyValidation } from "../../validation/joi.js"; -import { sendTestEmailBodyValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "SettingsController"; - -class SettingsController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { settingsService, emailService }) { - super(commonDependencies); - this.settingsService = settingsService; - this.emailService = emailService; - } - - get serviceName() { - return SettingsController.SERVICE_NAME; - } - - buildAppSettings = (dbSettings) => { - const sanitizedSettings = { ...dbSettings }; - delete sanitizedSettings.version; - - const returnSettings = { - pagespeedKeySet: false, - emailPasswordSet: false, - }; - - if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") { - returnSettings.pagespeedKeySet = true; - delete sanitizedSettings.pagespeedApiKey; - } - if (typeof sanitizedSettings.systemEmailPassword !== "undefined") { - returnSettings.emailPasswordSet = true; - delete sanitizedSettings.systemEmailPassword; - } - returnSettings.settings = sanitizedSettings; - return returnSettings; - }; - - getAppSettings = this.asyncHandler( - async (req, res) => { - const dbSettings = await this.settingsService.getDBSettings(); - - const returnSettings = this.buildAppSettings(dbSettings); - return res.success({ - msg: this.stringService.getAppSettings, - data: returnSettings, - }); - }, - SERVICE_NAME, - "getAppSettings" - ); - - updateAppSettings = this.asyncHandler( - async (req, res) => { - await updateAppSettingsBodyValidation.validateAsync(req.body); - - const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); - const returnSettings = this.buildAppSettings(updatedSettings); - return res.success({ - msg: this.stringService.updateAppSettings, - data: returnSettings, - }); - }, - SERVICE_NAME, - "updateAppSettings" - ); - - sendTestEmail = this.asyncHandler( - async (req, res) => { - await sendTestEmailBodyValidation.validateAsync(req.body); - - const { - to, - systemEmailHost, - systemEmailPort, - systemEmailAddress, - systemEmailPassword, - systemEmailUser, - systemEmailConnectionHost, - systemEmailSecure, - systemEmailPool, - systemEmailIgnoreTLS, - systemEmailRequireTLS, - systemEmailRejectUnauthorized, - systemEmailTLSServername, - } = req.body; - - const subject = this.stringService.testEmailSubject; - const context = { testName: "Monitoring System" }; - - const html = await this.emailService.buildEmail("testEmailTemplate", context); - const messageId = await this.emailService.sendEmail(to, subject, html, { - systemEmailHost, - systemEmailPort, - systemEmailUser, - systemEmailAddress, - systemEmailPassword, - systemEmailConnectionHost, - systemEmailSecure, - systemEmailPool, - systemEmailIgnoreTLS, - systemEmailRequireTLS, - systemEmailRejectUnauthorized, - systemEmailTLSServername, - }); - - if (!messageId) { - throw this.errorService.createServerError("Failed to send test email."); - } - - return res.success({ - msg: this.stringService.sendTestEmail, - data: { messageId }, - }); - }, - SERVICE_NAME, - "sendTestEmail" - ); -} - -export default SettingsController; diff --git a/server/src/controllers/v1/statusPageController.js b/server/src/controllers/v1/statusPageController.js deleted file mode 100644 index de6a1ca7c..000000000 --- a/server/src/controllers/v1/statusPageController.js +++ /dev/null @@ -1,108 +0,0 @@ -import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "statusPageController"; - -class StatusPageController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies) { - super(commonDependencies); - } - - get serviceName() { - return StatusPageController.SERVICE_NAME; - } - - createStatusPage = this.asyncHandler( - async (req, res) => { - await createStatusPageBodyValidation.validateAsync(req.body); - await imageValidation.validateAsync(req.file); - - const { _id, teamId } = req.user; - const statusPage = await this.db.statusPageModule.createStatusPage({ - statusPageData: req.body, - image: req.file, - userId: _id, - teamId, - }); - return res.success({ - msg: this.stringService.statusPageCreate, - data: statusPage, - }); - }, - SERVICE_NAME, - "createStatusPage" - ); - - updateStatusPage = this.asyncHandler( - async (req, res) => { - await createStatusPageBodyValidation.validateAsync(req.body); - await imageValidation.validateAsync(req.file); - - const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file); - if (statusPage === null) { - throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound); - } - return res.success({ - msg: this.stringService.statusPageUpdate, - data: statusPage, - }); - }, - SERVICE_NAME, - "updateStatusPage" - ); - - getStatusPage = this.asyncHandler( - async (req, res) => { - const statusPage = await this.db.statusPageModule.getStatusPage(); - return res.success({ - msg: this.stringService.statusPageByUrl, - data: statusPage, - }); - }, - SERVICE_NAME, - "getStatusPage" - ); - - getStatusPageByUrl = this.asyncHandler( - async (req, res) => { - await getStatusPageParamValidation.validateAsync(req.params); - await getStatusPageQueryValidation.validateAsync(req.query); - - const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url); - return res.success({ - msg: this.stringService.statusPageByUrl, - data: statusPage, - }); - }, - SERVICE_NAME, - "getStatusPageByUrl" - ); - - getStatusPagesByTeamId = this.asyncHandler( - async (req, res) => { - const teamId = req.user.teamId; - const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId); - - return res.success({ - msg: this.stringService.statusPageByTeamId, - data: statusPages, - }); - }, - SERVICE_NAME, - "getStatusPagesByTeamId" - ); - - deleteStatusPage = this.asyncHandler( - async (req, res) => { - await this.db.statusPageModule.deleteStatusPage(req.params.url); - return res.success({ - msg: this.stringService.statusPageDelete, - }); - }, - SERVICE_NAME, - "deleteStatusPage" - ); -} - -export default StatusPageController; diff --git a/server/src/controllers/v2/AuthController.ts b/server/src/controllers/v2/AuthController.ts deleted file mode 100644 index 3afed5312..000000000 --- a/server/src/controllers/v2/AuthController.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { encode, decode } from "../../utils/JWTUtils.js"; -import AuthService from "../../service/v2/business/AuthService.js"; -import ApiError from "../../utils/ApiError.js"; -import InviteService from "../../service/v2/business/InviteService.js"; -import { IInvite } from "../../db/v2/models/index.js"; - -class AuthController { - private authService: AuthService; - private inviteService: InviteService; - constructor(authService: AuthService, inviteService: InviteService) { - this.authService = authService; - this.inviteService = inviteService; - } - - register = async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, firstName, lastName, password } = req.body; - - if (!email || !firstName || !lastName || !password) { - throw new Error("Email, firstName, lastName, and password are required"); - } - - const result = await this.authService.register({ - email, - firstName, - lastName, - password, - }); - - const token = encode(result); - - res.cookie("token", token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week - }); - - res.status(201).json({ - message: "User created successfully", - }); - } catch (error) { - next(error); - } - }; - - registerWithInvite = async (req: Request, res: Response, next: NextFunction) => { - try { - const token = req.params.token; - if (!token) { - throw new ApiError("Invite token is required", 400); - } - - const invite: IInvite = await this.inviteService.get(token); - - const { firstName, lastName, password } = req.body; - const email = invite?.email; - const roles = invite?.roles; - - if (!email || !firstName || !lastName || !password || !roles || roles.length === 0) { - throw new Error("Email, firstName, lastName, password, and roles are required"); - } - - const result = await this.authService.registerWithInvite({ - email, - firstName, - lastName, - password, - roles, - }); - - if (!result) { - throw new Error("Registration failed"); - } - - await this.inviteService.delete(invite._id.toString()); - - const jwt = encode(result); - - res.cookie("token", jwt, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week - }); - - res.status(201).json({ message: "User created successfully" }); - } catch (error) { - next(error); - } - }; - - login = async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, password } = req.body; - // Validation - if (!email || !password) { - return res.status(400).json({ message: "Email and password are required" }); - } - const result = await this.authService.login({ email, password }); - - const token = encode(result); - - res.cookie("token", token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week - }); - - res.status(200).json({ - message: "Login successful", - }); - } catch (error) { - next(error); - } - }; - - logout = (req: Request, res: Response) => { - res.clearCookie("token", { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - }); - res.status(200).json({ message: "Logout successful" }); - }; - - me = (req: Request, res: Response, next: NextFunction) => { - return res.status(200).json({ message: "OK" }); - }; - - cleanup = async (req: Request, res: Response) => { - try { - await this.authService.cleanup(); - res.status(200).json({ message: "Cleanup successful" }); - } catch (error) {} - }; - - cleanMonitors = async (req: Request, res: Response) => { - try { - await this.authService.cleanMonitors(); - res.status(200).json({ message: "Monitors cleanup successful" }); - } catch (error) { - res.status(500).json({ message: "Internal server error" }); - } - }; -} - -export default AuthController; diff --git a/server/src/controllers/v2/InviteController.ts b/server/src/controllers/v2/InviteController.ts deleted file mode 100644 index 4d72bf8d4..000000000 --- a/server/src/controllers/v2/InviteController.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import InviteService from "../../service/v2/business/InviteService.js"; - -class InviteController { - private inviteService: InviteService; - constructor(inviteService: InviteService) { - this.inviteService = inviteService; - } - - create = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const invite = await this.inviteService.create(tokenizedUser, req.body); - res.status(201).json({ message: "OK", data: invite }); - } catch (error: any) { - next(error); - } - }; - - getAll = async (req: Request, res: Response, next: NextFunction) => { - try { - const invites = await this.inviteService.getAll(); - res.status(200).json({ - message: "OK", - data: invites, - }); - } catch (error) { - next(error); - } - }; - - get = async (req: Request, res: Response, next: NextFunction) => { - try { - const token = req.params.token; - if (!token) { - return res.status(400).json({ message: "Token parameter is required" }); - } - const invite = await this.inviteService.get(token); - res.status(200).json({ message: "OK", data: invite }); - } catch (error) { - next(error); - } - }; - - delete = async (req: Request, res: Response, next: NextFunction) => { - try { - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - await this.inviteService.delete(id); - res.status(204).json({ message: "OK" }); - } catch (error: any) { - next(error); - } - }; -} - -export default InviteController; diff --git a/server/src/controllers/v2/MaintenanceController.ts b/server/src/controllers/v2/MaintenanceController.ts deleted file mode 100644 index 7c6be11d5..000000000 --- a/server/src/controllers/v2/MaintenanceController.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import MaintenanceService from "../../service/v2/business/MaintenanceService.js"; - -class MaintenanceController { - private maintenanceService: MaintenanceService; - constructor(maintenanceService: MaintenanceService) { - this.maintenanceService = maintenanceService; - } - - create = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const maintenance = await this.maintenanceService.create(tokenizedUser, req.body); - res.status(201).json({ message: "OK", data: maintenance }); - } catch (error) { - next(error); - } - }; - - getAll = async (req: Request, res: Response, next: NextFunction) => { - try { - const maintenances = await this.maintenanceService.getAll(); - res.status(200).json({ - message: "OK", - data: maintenances, - }); - } catch (error) { - next(error); - } - }; - - toggleActive = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - const maintenance = await this.maintenanceService.toggleActive(tokenizedUser, id); - res.status(200).json({ message: "OK", data: maintenance }); - } catch (error) { - next(error); - } - }; - - update = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - const updatedMaintenance = await this.maintenanceService.update(tokenizedUser, id, req.body); - res.status(200).json({ message: "OK", data: updatedMaintenance }); - } catch (error) { - next(error); - } - }; - - get = async (req: Request, res: Response, next: NextFunction) => { - try { - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - const maintenance = await this.maintenanceService.get(id); - res.status(200).json({ message: "OK", data: maintenance }); - } catch (error) { - next(error); - } - }; - - delete = async (req: Request, res: Response, next: NextFunction) => { - try { - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - await this.maintenanceService.delete(id); - res.status(204).json({ message: "OK" }); - } catch (error) { - next(error); - } - }; -} - -export default MaintenanceController; diff --git a/server/src/controllers/v2/MonitorController.ts b/server/src/controllers/v2/MonitorController.ts deleted file mode 100644 index 7c0c63d2c..000000000 --- a/server/src/controllers/v2/MonitorController.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import ApiError from "../../utils/ApiError.js"; -import MonitorService from "../../service/v2/business/MonitorService.js"; -import { MonitorType } from "../../db/v2/models/monitors/Monitor.js"; -import CheckService from "../../service/v2/business/CheckService.js"; -class MonitorController { - private monitorService: MonitorService; - private checkService: CheckService; - constructor(monitorService: MonitorService, checkService: CheckService) { - this.monitorService = monitorService; - this.checkService = checkService; - } - - create = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const monitor = await this.monitorService.create(tokenizedUser, req.body); - res.status(201).json({ - message: "Monitor created successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - getAll = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - let monitors; - if (req.query.embedChecks === "true") { - const page = Math.max(1, Number(req.query.page) || 1); - const limit = Math.max(1, Number(req.query.limit) || 10); - const type: MonitorType[] = req.query.type as MonitorType[]; - - monitors = await this.monitorService.getAllEmbedChecks(page, limit, type); - } else { - monitors = await this.monitorService.getAll(); - } - - res.status(200).json({ - message: "Monitors retrieved successfully", - data: monitors, - }); - } catch (error) { - next(error); - } - }; - - getChecks = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const page = Number(req.query.page); - const rowsPerPage = Number(req.query.rowsPerPage); - - if (isNaN(page)) throw new ApiError("Page query parameter must be a number", 400); - if (isNaN(rowsPerPage)) throw new ApiError("rowsPerPage query parameter must be a number", 400); - - if (page < 0) throw new ApiError("Page must be greater than 0", 400); - if (rowsPerPage < 0) throw new ApiError("rowsPerPage must be greater than 0", 400); - - const { count, checks } = await this.checkService.getChecks(id, page, rowsPerPage); - res.status(200).json({ - message: "Checks retrieved successfully", - data: { count, checks }, - }); - } catch (error) { - next(error); - } - }; - - toggleActive = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const monitor = await this.monitorService.toggleActive(id, tokenizedUser); - res.status(200).json({ - message: "Monitor paused/unpaused successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - get = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const range = req.query.range; - if (!range || typeof range !== "string") throw new ApiError("Range query parameter is required", 400); - - let monitor; - - const status = req.query.status; - if (status && typeof status !== "string") { - throw new ApiError("Status query parameter must be a string", 400); - } - - if (req.query.embedChecks === "true") { - monitor = await this.monitorService.getEmbedChecks(id, range, status); - } else { - monitor = await this.monitorService.get(id); - } - - res.status(200).json({ - message: "Monitor retrieved successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - update = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const monitor = await this.monitorService.update(tokenizedUser, id, req.body); - res.status(200).json({ - message: "Monitor updated successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - delete = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - await this.monitorService.delete(id); - - res.status(200).json({ - message: "Monitor deleted successfully", - }); - } catch (error) { - next(error); - } - }; -} - -export default MonitorController; diff --git a/server/src/controllers/v2/NotificationChannelController.ts b/server/src/controllers/v2/NotificationChannelController.ts deleted file mode 100644 index e13f572f4..000000000 --- a/server/src/controllers/v2/NotificationChannelController.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import NotificationService from "../../service/v2/business/NotificationChannelService.js"; - -class NotificationChannelController { - private notificationService: NotificationService; - constructor(notificationService: NotificationService) { - this.notificationService = notificationService; - } - - create = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const channel = await this.notificationService.create(tokenizedUser, req.body); - res.status(201).json({ message: "OK", data: channel }); - } catch (error) { - next(error); - } - }; - - getAll = async (req: Request, res: Response, next: NextFunction) => { - try { - const notificationChannels = await this.notificationService.getAll(); - res.status(200).json({ - message: "OK", - data: notificationChannels, - }); - } catch (error) { - next(error); - } - }; - - toggleActive = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - const notificationChannel = await this.notificationService.toggleActive(tokenizedUser, id); - res.status(200).json({ message: "OK", data: notificationChannel }); - } catch (error) { - next(error); - } - }; - - update = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - const updatedChannel = await this.notificationService.update(tokenizedUser, id, req.body); - res.status(200).json({ message: "OK", data: updatedChannel }); - } catch (error) { - next(error); - } - }; - - get = async (req: Request, res: Response, next: NextFunction) => { - try { - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - const notificationChannel = await this.notificationService.get(id); - res.status(200).json({ message: "OK", data: notificationChannel }); - } catch (error) { - next(error); - } - }; - - delete = async (req: Request, res: Response, next: NextFunction) => { - try { - const id = req.params.id; - if (!id) { - return res.status(400).json({ message: "ID parameter is required" }); - } - await this.notificationService.delete(id); - res.status(204).json({ message: "OK" }); - } catch (error) { - next(error); - } - }; -} - -export default NotificationChannelController; diff --git a/server/src/controllers/v2/QueueController.ts b/server/src/controllers/v2/QueueController.ts deleted file mode 100644 index 510703894..000000000 --- a/server/src/controllers/v2/QueueController.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import QueueService from "../../service/v2/business/QueueService.js"; -class QueueController { - private queueService: QueueService; - constructor(queueService: QueueService) { - this.queueService = queueService; - } - - getJobs = async (req: Request, res: Response, next: NextFunction) => { - try { - const jobs = await this.queueService.getJobs(); - res.status(200).json({ message: "ok", data: jobs }); - } catch (error) { - next(error); - } - }; - - getMetrics = async (req: Request, res: Response, next: NextFunction) => { - const metrics = await this.queueService.getMetrics(); - res.status(200).json({ message: "ok", data: metrics }); - }; - - flush = async (req: Request, res: Response, next: NextFunction) => { - const result = await this.queueService.flush(); - res.status(200).json({ message: "ok", flushed: result }); - }; -} - -export default QueueController; diff --git a/server/src/db/MongoDB.ts b/server/src/db/MongoDB.ts new file mode 100644 index 000000000..aa751eff2 --- /dev/null +++ b/server/src/db/MongoDB.ts @@ -0,0 +1,43 @@ +import mongoose from "mongoose"; +import { getChildLogger } from "@/logger/Logger.js"; +const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/saas"; + +const RETRY_INTERVAL_MS = 2000; // wait 2 seconds between retries +const MAX_RETRIES = 10; // number of retries before giving up +const logger = getChildLogger("MongoDB"); + +export const connectDatabase = async (): Promise => { + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + await mongoose.connect(MONGODB_URI); + logger.info("Connected to MongoDB"); + return true; + } catch (error) { + logger.error( + `MongoDB connection attempt ${attempt} failed. Retrying in ${RETRY_INTERVAL_MS}ms...`, + error + ); + + if (attempt === MAX_RETRIES) { + logger.error( + "Could not connect to MongoDB after max retries. Exiting." + ); + process.exit(1); + } + + await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL_MS)); + } + } + + return false; +}; +export const disconnectDatabase = async (): Promise => { + try { + await mongoose.disconnect(); + logger.info("Disconnected from MongoDB"); + return true; + } catch (error) { + logger.error("MongoDB disconnection error:", error); + return false; + } +}; diff --git a/server/src/db/models/auth/Invite.ts b/server/src/db/models/auth/Invite.ts new file mode 100644 index 000000000..e85dc2dce --- /dev/null +++ b/server/src/db/models/auth/Invite.ts @@ -0,0 +1,44 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IInvite extends Document { + _id: Types.ObjectId; + orgId: Types.ObjectId; + orgRoleId?: Types.ObjectId; + teamId: Types.ObjectId; + teamRoleId: Types.ObjectId; + email: string; + tokenHash: string; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + expiry: Date; + createdAt: Date; + updatedAt: Date; +} + +const InviteSchema = new Schema( + { + orgId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", required: true }, + orgRoleId: { type: Schema.Types.ObjectId, ref: "Role" }, + teamRoleId: { type: Schema.Types.ObjectId, ref: "Role", required: true }, + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + + tokenHash: { type: String, required: true, unique: true }, + expiry: { + type: Date, + default: () => new Date(Date.now() + 24 * 60 * 60 * 1000), + expires: 0, + }, + createdBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + }, + { timestamps: true } +); + +export const Invite = mongoose.model("Invite", InviteSchema); diff --git a/server/src/db/models/auth/Org.ts b/server/src/db/models/auth/Org.ts new file mode 100644 index 000000000..8e3ce7f81 --- /dev/null +++ b/server/src/db/models/auth/Org.ts @@ -0,0 +1,18 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +export interface IOrg extends Document { + _id: Types.ObjectId; + name: string; + ownerId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const orgSchema = new Schema( + { + name: { type: String, required: true }, + ownerId: { type: Schema.Types.ObjectId, ref: "User", required: true }, + }, + { timestamps: true } +); + +export const Org = mongoose.model("Org", orgSchema); diff --git a/server/src/db/models/auth/OrgMembership.ts b/server/src/db/models/auth/OrgMembership.ts new file mode 100644 index 000000000..1281df1cb --- /dev/null +++ b/server/src/db/models/auth/OrgMembership.ts @@ -0,0 +1,51 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { invalidateCachesForUser } from "@/middleware/AddUserContext.js"; + +export interface IOrgMembership extends Document { + _id: Types.ObjectId; + orgId: Types.ObjectId; + userId: Types.ObjectId; + roleId?: Types.ObjectId; + role: string; + createdAt: Date; + updatedAt: Date; +} + +const orgMembershipSchema = new Schema( + { + orgId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, + userId: { type: Schema.Types.ObjectId, ref: "User", required: true }, + roleId: { type: Schema.Types.ObjectId, ref: "Role" }, + }, + { timestamps: true } +); + +orgMembershipSchema.post("save", async function (doc) { + invalidateCachesForUser(doc.userId.toString()); +}); + +orgMembershipSchema.post( + "deleteOne", + { document: true, query: false }, + async function (doc) { + invalidateCachesForUser(doc.userId.toString()); + } +); + +orgMembershipSchema.post("findOneAndDelete", function (doc: IOrgMembership) { + if (!doc) return; + invalidateCachesForUser(doc.userId.toString()); +}); + +orgMembershipSchema.post( + "findOneAndUpdate", + async function (doc: IOrgMembership) { + if (!doc) return; + invalidateCachesForUser(doc.userId.toString()); + } +); + +export const OrgMembership = mongoose.model( + "OrgMembership", + orgMembershipSchema +); diff --git a/server/src/db/models/auth/Role.ts b/server/src/db/models/auth/Role.ts new file mode 100644 index 000000000..b8d6cdd4a --- /dev/null +++ b/server/src/db/models/auth/Role.ts @@ -0,0 +1,26 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export const ScopeTypes = ["organization", "team"] as const; +export type ScopeType = (typeof ScopeTypes)[number]; + +export interface IRole extends Document { + _id: Types.ObjectId; + organizationId: Types.ObjectId; + name: string; + scope: ScopeType; + permissions: string[]; + createdAt: Date; + updatedAt: Date; +} + +const roleSchema = new Schema( + { + organizationId: { type: Schema.Types.ObjectId, ref: "Org" }, + name: { type: String, required: true }, + scope: { type: String, enum: ScopeTypes, required: true }, + permissions: { type: [String], default: [] }, + }, + { timestamps: true } +); + +export const Role = mongoose.model("Role", roleSchema); diff --git a/server/src/db/models/auth/Team.ts b/server/src/db/models/auth/Team.ts new file mode 100644 index 000000000..c4ada54c2 --- /dev/null +++ b/server/src/db/models/auth/Team.ts @@ -0,0 +1,22 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +export interface ITeam extends Document { + _id: Types.ObjectId; + orgId: Types.ObjectId; + name: string; + description?: string; + isSystem?: boolean; + createdAt: Date; + updatedAt: Date; +} + +const teamSchema = new Schema( + { + orgId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, + name: { type: String, required: true }, + description: { type: String, required: false }, + isSystem: { type: Boolean, default: false }, + }, + { timestamps: true } +); + +export const Team = mongoose.model("Team", teamSchema); diff --git a/server/src/db/models/auth/TeamMembership.ts b/server/src/db/models/auth/TeamMembership.ts new file mode 100644 index 000000000..aef296aa8 --- /dev/null +++ b/server/src/db/models/auth/TeamMembership.ts @@ -0,0 +1,51 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { invalidateCachesForUser } from "@/middleware/AddUserContext.js"; +export interface ITeamMembership extends Document { + _id: Types.ObjectId; + orgId: Types.ObjectId; + teamId: Types.ObjectId; + userId: Types.ObjectId; + roleId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const teamMembershipSchema = new Schema( + { + orgId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", required: true }, + userId: { type: Schema.Types.ObjectId, ref: "User", required: true }, + roleId: { type: Schema.Types.ObjectId, ref: "Role" }, + }, + { timestamps: true } +); + +teamMembershipSchema.post("save", async function (doc) { + invalidateCachesForUser(doc.userId.toString()); +}); + +teamMembershipSchema.post( + "deleteOne", + { document: true, query: false }, + async function (doc) { + invalidateCachesForUser(doc.userId.toString()); + } +); + +teamMembershipSchema.post("findOneAndDelete", function (doc: ITeamMembership) { + if (!doc) return; + invalidateCachesForUser(doc.userId.toString()); +}); + +teamMembershipSchema.post( + "findOneAndUpdate", + async function (doc: ITeamMembership) { + if (!doc) return; + invalidateCachesForUser(doc.userId.toString()); + } +); + +export const TeamMembership = mongoose.model( + "TeamMembership", + teamMembershipSchema +); diff --git a/server/src/db/models/auth/User.ts b/server/src/db/models/auth/User.ts new file mode 100644 index 000000000..1fea4930c --- /dev/null +++ b/server/src/db/models/auth/User.ts @@ -0,0 +1,67 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { IRole, ITeam } from "@/db/models/index.js"; + +export interface ITokenizedUser { + sub: string; + email: string; + orgId: string; +} +export interface IUserContext { + sub: string; + email: string; + orgId: string; + teamIds?: string[]; + teams?: ITeam[]; + currentTeamId?: string; + roles?: { + orgRole?: IRole; + teamRole: IRole; + }; +} + +export interface IUserReturnable { + id: string; + email: string; + firstName: string; + lastName: string; + org: { + name: string; + permissions: string[]; + }; + teams: { + id: string; + name: string; + permissions: string[]; + }[]; +} + +export interface IUser extends Document { + _id: Types.ObjectId; + firstName: string; + lastName: string; + email: string; + passwordHash: string; + lastLoginAt?: Date; + createdAt: Date; + updatedAt: Date; +} + +export interface IUserProfile { + firstName?: string; + lastName?: string; + password?: string; + confirmPassword?: string; +} + +const userSchema = new Schema( + { + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + passwordHash: { type: String, required: true }, + lastLoginAt: { type: Date }, + }, + { timestamps: true } +); + +export const User = mongoose.model("User", userSchema); diff --git a/server/src/db/models/checks/Check.ts b/server/src/db/models/checks/Check.ts new file mode 100644 index 000000000..ba0e60bab --- /dev/null +++ b/server/src/db/models/checks/Check.ts @@ -0,0 +1,315 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { + MonitorType, + MonitorTypes, + MonitorStatus, + MonitorStatuses, +} from "../monitors/Monitor.js"; +import type { Response } from "got"; +export type GotTimings = Response["timings"]; +export interface ITimingPhases { + wait: number; + dns: number; + tcp: number; + tls: number; + request: number; + firstByte: number; + download: number; + total: number; +} + +export interface ICpuInfo { + physical_core: number; + logical_core: number; + frequency: number; + current_frequency: number; + temperature: number[]; // per-core temps + free_percent: number; + usage_percent: number; +} + +export interface IMemoryInfo { + total_bytes: number; + available_bytes: number; + used_bytes: number; + usage_percent: number; +} + +export interface IHostInfo { + os?: string; + platform?: string; + kernel_version?: string; + pretty_name?: string; +} + +export interface IDiskInfo { + device: string; + total_bytes: number; + free_bytes: number; + used_bytes: number; + usage_percent: number; + total_inodes?: number; + free_inodes?: number; + used_inodes?: number; + inodes_usage_percent?: number; + read_bytes?: number; + write_bytes?: number; + read_time?: number; + write_time?: number; +} + +export interface INetInfo { + name: string; + bytes_sent: number; + bytes_recv: number; + packets_sent: number; + packets_recv: number; + err_in: number; + err_out: number; + drop_in: number; + drop_out: number; + fifo_in: number; + fifo_out: number; +} + +export interface ICaptureInfo { + version?: string; + mode?: string; +} + +export interface ISystemInfo { + cpu: ICpuInfo; + memory: IMemoryInfo; + disk: IDiskInfo[]; + host: IHostInfo; + net: INetInfo[]; +} + +export interface ILighthouseAudit { + id?: string; + title?: string; + score?: number | null; + displayValue?: string; + numericValue?: number; + numericUnit?: string; +} +export interface ILighthouseCategories { + accessibility?: { score?: number | null }; + "best-practices"?: { score?: number | null }; + seo?: { score?: number | null }; + performance?: { score?: number | null }; +} + +export interface ILighthouseResult { + categories?: ILighthouseCategories; + audits?: Record; +} + +export interface ICheckLighthouseFields { + accessibility: number; + bestPractices: number; + seo: number; + performance: number; + audits: { + cls: ILighthouseAudit; + si: ILighthouseAudit; + fcp: ILighthouseAudit; + lcp: ILighthouseAudit; + tbt: ILighthouseAudit; + }; +} + +export interface ICheck extends Document { + _id: Types.ObjectId; + metadata: { + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + type: MonitorType; + }; + status: MonitorStatus; + httpStatusCode?: number; + message: string; + responseTime?: number; + timings?: GotTimings; + errorMessage?: string; + ackAt?: Date; + ackBy?: Types.ObjectId; + system?: ISystemInfo; + capture?: ICaptureInfo; + lighthouse?: ICheckLighthouseFields; + createdAt: Date; + updatedAt: Date; + expiry: Date; +} + +const CheckSchema = new Schema( + { + metadata: { + monitorId: { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + required: true, + }, + type: { + type: String, + required: true, + enum: MonitorTypes, + }, + }, + status: { + type: String, + required: true, + enum: MonitorStatuses, + }, + httpStatusCode: { type: Number }, + + message: { type: String, trim: true }, + responseTime: { type: Number }, + timings: { + start: { type: Date }, + socket: { type: Date }, + lookup: { type: Date }, + connect: { type: Date }, + secureConnect: { type: Date }, + response: { type: Date }, + end: { type: Date }, + phases: { + wait: { type: Number }, + dns: { type: Number }, + tcp: { type: Number }, + tls: { type: Number }, + request: { type: Number }, + firstByte: { type: Number }, + download: { type: Number }, + total: { type: Number }, + }, + }, + system: { + type: { + cpu: { + physical_core: { type: Number }, + logical_core: { type: Number }, + frequency: { type: Number }, + current_frequency: { type: Number }, + temperature: [{ type: Number }], + free_percent: { type: Number }, + usage_percent: { type: Number }, + }, + memory: { + total_bytes: { type: Number }, + available_bytes: { type: Number }, + used_bytes: { type: Number }, + usage_percent: { type: Number }, + }, + disk: [ + { + device: { type: String }, + total_bytes: { type: Number }, + free_bytes: { type: Number }, + used_bytes: { type: Number }, + usage_percent: { type: Number }, + total_inodes: { type: Number }, + free_inodes: { type: Number }, + used_inodes: { type: Number }, + inodes_usage_percent: { type: Number }, + read_bytes: { type: Number }, + write_bytes: { type: Number }, + read_time: { type: Number }, + write_time: { type: Number }, + }, + ], + host: { + os: { type: String }, + platform: { type: String }, + kernel_version: { type: String }, + pretty_name: { type: String }, + }, + net: [ + { + name: { type: String }, + bytes_sent: { type: Number }, + bytes_recv: { type: Number }, + packets_sent: { type: Number }, + packets_recv: { type: Number }, + err_in: { type: Number }, + err_out: { type: Number }, + drop_in: { type: Number }, + drop_out: { type: Number }, + fifo_in: { type: Number }, + fifo_out: { type: Number }, + }, + ], + }, + required: false, + }, + + capture: { + type: { + version: { type: String }, + mode: { type: String }, + }, + required: false, + }, + lighthouse: { + accessibility: { type: Number, required: false }, + bestPractices: { type: Number, required: false }, + seo: { type: Number, required: false }, + performance: { type: Number, required: false }, + audits: { + cls: { + type: Object, + }, + si: { + type: Object, + }, + fcp: { + type: Object, + }, + lcp: { + type: Object, + }, + tbt: { + type: Object, + }, + }, + type: { + accessibility: { type: Number }, + bestPractices: { type: Number }, + seo: { type: Number }, + performance: { type: Number }, + audits: { + cls: { type: Object }, + si: { type: Object }, + fcp: { type: Object }, + lcp: { type: Object }, + tbt: { type: Object }, + }, + }, + required: false, + }, + + errorMessage: { type: String, trim: true }, + ackAt: { type: Date }, + ackBy: { type: Schema.Types.ObjectId, ref: "User" }, + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, + }, + }, + { + timestamps: true, + timeseries: { + timeField: "createdAt", + metaField: "metadata", + granularity: "seconds", + }, + } +); + +export const Check = mongoose.model("Check", CheckSchema); diff --git a/server/src/db/models/incidents/Incident.ts b/server/src/db/models/incidents/Incident.ts new file mode 100644 index 000000000..aa4ef25f1 --- /dev/null +++ b/server/src/db/models/incidents/Incident.ts @@ -0,0 +1,39 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export const ResolutionTypes = ["auto", "manual"] as const; +export type ResolutionType = (typeof ResolutionTypes)[number]; + +export interface IIncident { + _id: Types.ObjectId; + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + startedAt: Date; + startCheck: Types.ObjectId; + endedAt?: Date; + endCheck?: Types.ObjectId; + resolved: boolean; + resolvedBy?: Types.ObjectId; + resolutionType?: ResolutionType; + resolutionNote?: string; +} + +const IncidentSchema = new Schema( + { + monitorId: { type: Schema.Types.ObjectId, ref: "Monitor", required: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", required: true }, + startedAt: { type: Date, required: true }, + startCheck: { type: Schema.Types.ObjectId, ref: "Check", required: true }, + endedAt: { type: Date }, + endCheck: { type: Schema.Types.ObjectId, ref: "Check" }, + resolved: { type: Boolean, default: false }, + resolvedBy: { type: Schema.Types.ObjectId, ref: "User" }, + resolutionType: { + type: String, + enum: ResolutionTypes, + }, + resolutionNote: { type: String }, + }, + { timestamps: true } +); + +export const Incident = mongoose.model("Incident", IncidentSchema); diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts new file mode 100644 index 000000000..571c8b068 --- /dev/null +++ b/server/src/db/models/index.ts @@ -0,0 +1,58 @@ +export { Check } from "./checks/Check.js"; +export type { + ICheck, + ISystemInfo, + ICaptureInfo, + ILighthouseResult, +} from "./checks/Check.js"; + +export { Monitor, MonitorStatuses } from "./monitors/Monitor.js"; +export type { IMonitor } from "./monitors/Monitor.js"; + +export { MonitorStats } from "./monitors/MonitorStats.js"; +export type { IMonitorStats } from "./monitors/MonitorStats.js"; + +export { User } from "./auth/User.js"; +export type { + IUser, + ITokenizedUser, + IUserReturnable, + IUserContext, + IUserProfile, +} from "./auth/User.js"; + +export { + NotificationChannel, + ChannelTypes, +} from "./notification-channel/NotificationChannel.js"; +export type { INotificationChannel } from "./notification-channel/NotificationChannel.js"; + +export { Invite } from "./auth/Invite.js"; +export type { IInvite } from "./auth/Invite.js"; + +export { Role } from "./auth/Role.js"; +export type { IRole } from "./auth/Role.js"; + +export { Maintenance, MaintenanceRepeats } from "./maintenance/Maintenance.js"; +export type { IMaintenance } from "./maintenance/Maintenance.js"; + +export { Org } from "./auth/Org.js"; +export type { IOrg } from "./auth/Org.js"; + +export { OrgMembership } from "./auth/OrgMembership.js"; +export type { IOrgMembership } from "./auth/OrgMembership.js"; + +export { Team } from "./auth/Team.js"; +export type { ITeam } from "./auth/Team.js"; + +export { TeamMembership } from "./auth/TeamMembership.js"; +export type { ITeamMembership } from "./auth/TeamMembership.js"; + +export { StatusPage } from "./status-page/StatusPage.js"; +export type { IStatusPage } from "./status-page/StatusPage.js"; + +export { RecoveryToken } from "./recovery/RecoveryToken.js"; +export type { IRecoveryToken } from "./recovery/RecoveryToken.js"; + +export { Incident, ResolutionTypes } from "./incidents/Incident.js"; +export type { IIncident, ResolutionType } from "./incidents/Incident.js"; diff --git a/server/src/db/models/maintenance/Maintenance.ts b/server/src/db/models/maintenance/Maintenance.ts new file mode 100644 index 000000000..2d209d3f7 --- /dev/null +++ b/server/src/db/models/maintenance/Maintenance.ts @@ -0,0 +1,54 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +export const MaintenanceRepeats = ["no repeat", "daily", "weekly"] as const; +export type MaintenanceRepeat = (typeof MaintenanceRepeats)[number]; +export interface IMaintenance extends Document { + _id: Types.ObjectId; + orgId: Types.ObjectId; + teamId: Types.ObjectId; + name: string; + isActive: boolean; + repeat: MaintenanceRepeat; + monitors: Types.ObjectId[]; + startTime: Date; + endTime: Date; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const MaintenanceSchema = new Schema( + { + orgId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", required: true }, + name: { type: String, required: true, trim: true }, + isActive: { type: Boolean, required: true, default: true }, + repeat: { + type: String, + enum: MaintenanceRepeats, + required: false, + default: null, + }, + monitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + startTime: { type: Date, required: true }, + endTime: { type: Date, required: true }, + createdBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + }, + { timestamps: true } +); + +MaintenanceSchema.index({ isActive: 1 }); +MaintenanceSchema.index({ startTime: 1 }); +MaintenanceSchema.index({ endTime: 1 }); + +export const Maintenance = mongoose.model( + "Maintenance", + MaintenanceSchema +); diff --git a/server/src/db/models/monitors/Monitor.ts b/server/src/db/models/monitors/Monitor.ts new file mode 100644 index 000000000..35a3e4cff --- /dev/null +++ b/server/src/db/models/monitors/Monitor.ts @@ -0,0 +1,134 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { Check, MonitorStats } from "@/db/models/index.js"; + +export const MonitorTypes = [ + "http", + "https", + "ping", + "infrastructure", + "pagespeed", +] as const; +export type MonitorType = (typeof MonitorTypes)[number]; + +export const MonitorStatuses = [ + "up", + "down", + "paused", + "initializing", +] as const; +export type MonitorStatus = (typeof MonitorStatuses)[number]; +export interface IMonitor extends Document { + _id: Types.ObjectId; + orgId: Types.ObjectId; + teamId: Types.ObjectId; + name: string; + url: string; + secret?: string; + type: MonitorType; + interval: number; // in ms + isActive: boolean; + status: MonitorStatus; + n: number; // Number of consecutive successes required to change status + lastCheckedAt?: Date; + latestChecks: { + status: MonitorStatus; + responseTime: number; + checkedAt: Date; + }[]; + notificationChannels?: Types.ObjectId[]; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const MonitorSchema = new Schema( + { + orgId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", required: true }, + name: { type: String, required: true, trim: true, maxlength: 100 }, + url: { type: String, required: true, trim: true }, + secret: { type: String, required: false }, + type: { + type: String, + required: true, + enum: MonitorTypes, + }, + interval: { type: Number, required: true, default: 60000 }, + isActive: { type: Boolean, required: true, default: true }, + status: { + type: String, + required: true, + default: "initializing", + enum: MonitorStatuses, + }, + n: { type: Number, required: true, default: 1 }, + lastCheckedAt: { type: Date }, + latestChecks: { + type: [ + { + status: { + type: String, + required: true, + enum: MonitorStatuses, + }, + responseTime: { type: Number, required: true }, + checkedAt: { type: Date, required: true }, + }, + ], + default: [], + }, + notificationChannels: { + type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel" }], + default: [], + }, + + createdBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + }, + { timestamps: true } +); + +MonitorSchema.pre( + "deleteOne", + { document: true, query: false }, + async function (next) { + try { + const monitorId = this._id; + await Check.deleteMany({ "metadata.monitorId": monitorId }); + await MonitorStats.deleteMany({ monitorId }); + next(); + } catch (error: any) { + next(error); + } + } +); + +MonitorSchema.pre( + "deleteMany", + { document: false, query: true }, + async function (next) { + try { + const filter = this.getFilter(); + const monitors = await this.model.find(filter).select("_id"); + const monitorIds = monitors.map((m) => m._id); + if (monitorIds.length > 0) { + await Check.deleteMany({ "metadata.monitorId": { $in: monitorIds } }); + await MonitorStats.deleteMany({ monitorId: { $in: monitorIds } }); + } + next(); + } catch (error: any) { + next(error); + } + } +); + +MonitorSchema.index({ isActive: 1 }); +MonitorSchema.index({ status: 1 }); +MonitorSchema.index({ type: 1 }); +MonitorSchema.index({ lastCheckedAt: 1 }); +MonitorSchema.index({ isActive: 1, status: 1 }); +MonitorSchema.index({ createdBy: 1 }); +MonitorSchema.index({ updatedBy: 1 }); + +export const Monitor = mongoose.model("Monitor", MonitorSchema); diff --git a/server/src/db/models/monitors/MonitorStats.ts b/server/src/db/models/monitors/MonitorStats.ts new file mode 100644 index 000000000..328e7c4ed --- /dev/null +++ b/server/src/db/models/monitors/MonitorStats.ts @@ -0,0 +1,80 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { MonitorStatus, MonitorStatuses } from "./Monitor.js"; + +export interface IMonitorStats extends mongoose.Document { + monitorId: mongoose.Types.ObjectId; + avgResponseTime: number; + maxResponseTime: number; + totalChecks: number; + totalUpChecks: number; + totalDownChecks: number; + uptimePercentage: number; + lastCheckTimestamp: number; + lastResponseTime: number; + timeOfLastFailure: number; + currentStreak: number; + currentStreakStatus: MonitorStatus; + currentStreakStartedAt: number; + createdAt: Date; + updatedAt: Date; +} + +const MonitorStatsSchema = new Schema( + { + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + }, + avgResponseTime: { + type: Number, + default: 0, + }, + maxResponseTime: { + type: Number, + default: 0, + }, + lastResponseTime: { + type: Number, + default: 0, + }, + totalChecks: { + type: Number, + default: 0, + }, + totalUpChecks: { + type: Number, + default: 0, + }, + totalDownChecks: { + type: Number, + default: 0, + }, + uptimePercentage: { + type: Number, + default: 0, + }, + lastCheckTimestamp: { + type: Number, + default: 0, + }, + timeOfLastFailure: { + type: Number, + default: 0, + }, + currentStreak: { type: Number, required: false, default: 0 }, + currentStreakStatus: { + type: String, + required: false, + enum: MonitorStatuses, + }, + currentStreakStartedAt: { type: Number, required: false }, + }, + { timestamps: true } +); + +export const MonitorStats = mongoose.model( + "MonitorStats", + MonitorStatsSchema +); diff --git a/server/src/db/models/notification-channel/NotificationChannel.ts b/server/src/db/models/notification-channel/NotificationChannel.ts new file mode 100644 index 000000000..1cd0bed1d --- /dev/null +++ b/server/src/db/models/notification-channel/NotificationChannel.ts @@ -0,0 +1,57 @@ +import mongoose, { Schema, Types } from "mongoose"; +export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const; +export type ChannelType = (typeof ChannelTypes)[number]; + +export interface INotificationChannelConfig { + url?: string; // For webhook, slack, discord + emailAddress?: string; // For email +} + +export interface INotificationChannel { + _id: Types.ObjectId; + orgId: Types.ObjectId; + teamId: Types.ObjectId; + name: string; + type: ChannelType; + config: INotificationChannelConfig; + isActive: boolean; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const NotificationChannelConfigSchema = new Schema( + { + url: { type: String, required: false }, + emailAddress: { type: String, required: false }, + }, + { _id: false, strict: "throw" } +); + +const NotificationChannelSchema = new Schema( + { + orgId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", required: true }, + name: { type: String, required: true, trim: true }, + type: { + type: String, + required: true, + enum: ChannelTypes, + }, + config: { type: NotificationChannelConfigSchema, required: true }, + isActive: { type: Boolean, required: true, default: true }, + createdBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + }, + { timestamps: true } +); + +NotificationChannelSchema.index({ isActive: 1 }); +NotificationChannelSchema.index({ type: 1 }); +NotificationChannelSchema.index({ type: 1, isActive: 1 }); + +export const NotificationChannel = mongoose.model( + "NotificationChannel", + NotificationChannelSchema +); diff --git a/server/src/db/models/recovery/RecoveryToken.ts b/server/src/db/models/recovery/RecoveryToken.ts new file mode 100644 index 000000000..2e6cccaec --- /dev/null +++ b/server/src/db/models/recovery/RecoveryToken.ts @@ -0,0 +1,35 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IRecoveryToken { + _id: string; + userId: Types.ObjectId; + tokenHash: string; + expiry: Date; + createdAt: Date; + updatedAt: Date; +} + +const RecoveryTokenSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + unique: true, + }, + tokenHash: { type: String, required: true }, + expiry: { + type: Date, + default: () => new Date(Date.now() + 24 * 60 * 60 * 1000), + expires: 0, + }, + }, + { + timestamps: true, + } +); + +export const RecoveryToken = mongoose.model( + "RecoveryToken", + RecoveryTokenSchema +); diff --git a/server/src/db/models/status-page/StatusPage.ts b/server/src/db/models/status-page/StatusPage.ts new file mode 100644 index 000000000..0866118cb --- /dev/null +++ b/server/src/db/models/status-page/StatusPage.ts @@ -0,0 +1,38 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IStatusPage { + _id: Types.ObjectId; + orgId: Types.ObjectId; + teamId: Types.ObjectId; + name: string; + description?: string; + url: string; + isPublished: boolean; + monitors: Types.ObjectId[]; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const StatusPageSchema = new Schema( + { + orgId: { type: Schema.Types.ObjectId, ref: "Org", required: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", required: true }, + name: { type: String, required: true, trim: true }, + description: { type: String, required: false, trim: true }, + url: { type: String, required: true, trim: true }, + isPublished: { type: Boolean, required: true, default: false }, + monitors: [{ type: Schema.Types.ObjectId, ref: "Monitor", required: true }], + createdBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + }, + { timestamps: true } +); + +StatusPageSchema.index({ isPublished: 1 }); + +export const StatusPage = mongoose.model( + "StatusPage", + StatusPageSchema +); diff --git a/server/src/db/v1/MongoDB.js b/server/src/db/v1/MongoDB.js deleted file mode 100755 index 3bdae3525..000000000 --- a/server/src/db/v1/MongoDB.js +++ /dev/null @@ -1,98 +0,0 @@ -import mongoose from "mongoose"; -import AppSettings from "./models/AppSettings.js"; -import { runMigrations } from "./migration/index.js"; -class MongoDB { - static SERVICE_NAME = "MongoDB"; - - constructor({ - logger, - envSettings, - checkModule, - inviteModule, - statusPageModule, - userModule, - hardwareCheckModule, - maintenanceWindowModule, - monitorModule, - networkCheckModule, - notificationModule, - pageSpeedCheckModule, - recoveryModule, - settingsModule, - }) { - this.logger = logger; - this.envSettings = envSettings; - this.userModule = userModule; - this.inviteModule = inviteModule; - this.recoveryModule = recoveryModule; - this.pageSpeedCheckModule = pageSpeedCheckModule; - this.hardwareCheckModule = hardwareCheckModule; - this.checkModule = checkModule; - this.maintenanceWindowModule = maintenanceWindowModule; - this.monitorModule = monitorModule; - this.notificationModule = notificationModule; - this.settingsModule = settingsModule; - this.statusPageModule = statusPageModule; - this.networkCheckModule = networkCheckModule; - } - - get serviceName() { - return MongoDB.SERVICE_NAME; - } - - connect = async () => { - try { - const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; - await mongoose.connect(connectionString); - // If there are no AppSettings, create one // TODO why is this here? - await AppSettings.findOneAndUpdate( - {}, // empty filter to match any document - {}, // empty update - { - new: true, - setDefaultsOnInsert: true, - } - ); - // Sync indexes - const models = mongoose.modelNames(); - for (const modelName of models) { - const model = mongoose.model(modelName); - await model.syncIndexes(); - } - - this.logger.info({ - message: "Connected to MongoDB", - service: this.SERVICE_NAME, - method: "connect", - }); - - await runMigrations(); - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "connect", - stack: error.stack, - }); - throw error; - } - }; - - disconnect = async () => { - try { - this.logger.info({ message: "Disconnecting from MongoDB" }); - await mongoose.disconnect(); - this.logger.info({ message: "Disconnected from MongoDB" }); - return; - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "disconnect", - stack: error.stack, - }); - } - }; -} - -export default MongoDB; diff --git a/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts b/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts deleted file mode 100644 index 857d36381..000000000 --- a/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Monitor from "../models/Monitor.js"; -async function migrateStatusWindowThreshold() { - try { - const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } }); - for (const monitor of monitors) { - monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100; - await monitor.save(); - console.log(`Migrated monitor ${monitor._id}: statusWindowThreshold set to ${monitor.statusWindowThreshold}`); - } - console.log("StatusWindowThreshold migration complete."); - return true; - } catch (err) { - console.error("Migration error:", err); - return false; - } -} - -export { migrateStatusWindowThreshold }; diff --git a/server/src/db/v1/migration/index.ts b/server/src/db/v1/migration/index.ts deleted file mode 100644 index 4bf56895a..000000000 --- a/server/src/db/v1/migration/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; - -const runMigrations = async () => { - await migrateStatusWindowThreshold(); -}; - -export { runMigrations }; diff --git a/server/src/db/v1/models/AppSettings.js b/server/src/db/v1/models/AppSettings.js deleted file mode 100755 index 2313d638c..000000000 --- a/server/src/db/v1/models/AppSettings.js +++ /dev/null @@ -1,80 +0,0 @@ -import mongoose from "mongoose"; - -const AppSettingsSchema = mongoose.Schema( - { - checkTTL: { - type: Number, - default: 30, - }, - language: { - type: String, - default: "gb", - }, - pagespeedApiKey: { - type: String, - }, - systemEmailHost: { - type: String, - }, - systemEmailPort: { - type: Number, - }, - systemEmailAddress: { - type: String, - }, - systemEmailPassword: { - type: String, - }, - systemEmailUser: { - type: String, - }, - systemEmailConnectionHost: { - type: String, - default: "localhost", - }, - systemEmailTLSServername: { - type: String, - }, - systemEmailSecure: { - type: Boolean, - default: false, - }, - systemEmailPool: { - type: Boolean, - default: false, - }, - systemEmailIgnoreTLS: { - type: Boolean, - default: false, - }, - systemEmailRequireTLS: { - type: Boolean, - default: false, - }, - systemEmailRejectUnauthorized: { - type: Boolean, - default: true, - }, - singleton: { - type: Boolean, - required: true, - unique: true, - default: true, - }, - version: { - type: Number, - default: 1, - }, - globalThresholds: { - cpu: { type: Number }, - memory: { type: Number }, - disk: { type: Number }, - temperature: { type: Number }, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("AppSettings", AppSettingsSchema); diff --git a/server/src/db/v1/models/Check.js b/server/src/db/v1/models/Check.js deleted file mode 100755 index 789cebaa5..000000000 --- a/server/src/db/v1/models/Check.js +++ /dev/null @@ -1,176 +0,0 @@ -import mongoose from "mongoose"; - -const cpuSchema = mongoose.Schema({ - physical_core: { type: Number, default: 0 }, - logical_core: { type: Number, default: 0 }, - frequency: { type: Number, default: 0 }, - temperature: { type: [Number], default: [] }, - free_percent: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const memorySchema = mongoose.Schema({ - total_bytes: { type: Number, default: 0 }, - available_bytes: { type: Number, default: 0 }, - used_bytes: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const diskSchema = mongoose.Schema({ - read_speed_bytes: { type: Number, default: 0 }, - write_speed_bytes: { type: Number, default: 0 }, - total_bytes: { type: Number, default: 0 }, - free_bytes: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const hostSchema = mongoose.Schema({ - os: { type: String, default: "" }, - platform: { type: String, default: "" }, - kernel_version: { type: String, default: "" }, -}); - -const errorSchema = mongoose.Schema({ - metric: { type: [String], default: [] }, - err: { type: String, default: "" }, -}); - -const captureSchema = mongoose.Schema({ - version: { type: String, default: "" }, - mode: { type: String, default: "" }, -}); - -const networkInterfaceSchema = mongoose.Schema({ - name: { type: String }, - bytes_sent: { type: Number, default: 0 }, - bytes_recv: { type: Number, default: 0 }, - packets_sent: { type: Number, default: 0 }, - packets_recv: { type: Number, default: 0 }, - err_in: { type: Number, default: 0 }, - err_out: { type: Number, default: 0 }, - drop_in: { type: Number, default: 0 }, - drop_out: { type: Number, default: 0 }, - fifo_in: { type: Number, default: 0 }, - fifo_out: { type: Number, default: 0 }, -}); - -const CheckSchema = new mongoose.Schema( - { - // Common fields - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - index: true, - }, - type: { - type: String, - enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"], - required: true, - index: true, - }, - - status: { - type: Boolean, - index: true, - }, - - responseTime: { - type: Number, - }, - - timings: { - type: Object, - default: {}, - }, - - statusCode: { - type: Number, - index: true, - }, - - message: { - type: String, - }, - - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, - - ack: { - type: Boolean, - default: false, - }, - - ackAt: { - type: Date, - }, - - // Hardware fields - cpu: { - type: cpuSchema, - default: () => ({}), - }, - memory: { - type: memorySchema, - default: () => ({}), - }, - disk: { - type: [diskSchema], - default: () => [], - }, - host: { - type: hostSchema, - default: () => ({}), - }, - - errors: { - type: [errorSchema], - default: () => [], - }, - - capture: { - type: captureSchema, - default: () => ({}), - }, - - net: { - type: [networkInterfaceSchema], - default: () => [], - }, - - // PageSpeed fields - accessibility: { - type: Number, - }, - bestPractices: { - type: Number, - }, - seo: { - type: Number, - }, - performance: { - type: Number, - }, - audits: { - type: Object, - }, - }, - { timestamps: true } -); - -CheckSchema.index({ updatedAt: 1 }); -CheckSchema.index({ monitorId: 1, updatedAt: 1 }); -CheckSchema.index({ monitorId: 1, updatedAt: -1 }); -CheckSchema.index({ teamId: 1, updatedAt: -1 }); - -export default mongoose.model("Check", CheckSchema); diff --git a/server/src/db/v1/models/InviteToken.js b/server/src/db/v1/models/InviteToken.js deleted file mode 100755 index 079600647..000000000 --- a/server/src/db/v1/models/InviteToken.js +++ /dev/null @@ -1,35 +0,0 @@ -import mongoose from "mongoose"; -const InviteTokenSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - role: { - type: Array, - required: true, - default: ["user"], - }, - token: { - type: String, - required: true, - }, - expiry: { - type: Date, - default: Date.now, - expires: 3600, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("InviteToken", InviteTokenSchema); diff --git a/server/src/db/v1/models/MaintenanceWindow.js b/server/src/db/v1/models/MaintenanceWindow.js deleted file mode 100755 index b68abb413..000000000 --- a/server/src/db/v1/models/MaintenanceWindow.js +++ /dev/null @@ -1,68 +0,0 @@ -import mongoose from "mongoose"; -/** - * MaintenanceWindow Schema - * @module MaintenanceWindow - * @typedef {Object} MaintenanceWindow - * @property {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor. This is a reference to the Monitor model and is immutable. - * @property {Boolean} active - Indicates whether the maintenance window is active. - * @property {Number} repeat - Indicates how often this maintenance window should repeat. - * @property {Date} start - The start date and time of the maintenance window. - * @property {Date} end - The end date and time of the maintenance window. - * @property {Date} expiry - The expiry date and time of the maintenance window. This is used for MongoDB's TTL index to automatically delete the document at this time. This field is set to the same value as `end` when `oneTime` is `true`. - * - * @example - * - * let maintenanceWindow = new MaintenanceWindow({ - * monitorId: monitorId, - * active: active, - * repeat: repeat, - * start: start, - * end: end, - * }); - * - * if (repeat === 0) { - * maintenanceWindow.expiry = end; - * } - * - */ - -const MaintenanceWindow = mongoose.Schema( - { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - }, - active: { - type: Boolean, - default: true, - }, - name: { - type: String, - }, - repeat: { - type: Number, - }, - start: { - type: Date, - }, - end: { - type: Date, - }, - expiry: { - type: Date, - index: { expires: "0s" }, - }, - }, - - { - timestamps: true, - } -); - -export default mongoose.model("MaintenanceWindow", MaintenanceWindow); diff --git a/server/src/db/v1/models/Monitor.js b/server/src/db/v1/models/Monitor.js deleted file mode 100755 index e6e1a0024..000000000 --- a/server/src/db/v1/models/Monitor.js +++ /dev/null @@ -1,208 +0,0 @@ -import mongoose from "mongoose"; -import Check from "./Check.js"; -import MonitorStats from "./MonitorStats.js"; -import StatusPage from "./StatusPage.js"; - -const MonitorSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - name: { - type: String, - required: true, - }, - description: { - type: String, - }, - status: { - type: Boolean, - default: undefined, - }, - statusWindow: { - type: [Boolean], - default: [], - }, - statusWindowSize: { - type: Number, - default: 5, - }, - statusWindowThreshold: { - type: Number, - default: 60, - }, - type: { - type: String, - required: true, - enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"], - }, - ignoreTlsErrors: { - type: Boolean, - default: false, - }, - jsonPath: { - type: String, - }, - expectedValue: { - type: String, - }, - matchMethod: { - type: String, - enum: ["equal", "include", "regex", ""], - }, - url: { - type: String, - required: true, - }, - port: { - type: Number, - }, - isActive: { - type: Boolean, - default: true, - }, - interval: { - // in milliseconds - type: Number, - default: 60000, - }, - uptimePercentage: { - type: Number, - default: undefined, - }, - notifications: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Notification", - }, - ], - secret: { - type: String, - }, - thresholds: { - type: { - usage_cpu: { type: Number }, - usage_memory: { type: Number }, - usage_disk: { type: Number }, - usage_temperature: { type: Number }, - }, - _id: false, - }, - alertThreshold: { - type: Number, - default: 5, - }, - cpuAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, - memoryAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, - diskAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, - tempAlertThreshold: { - type: Number, - default: function () { - return this.alertThreshold; - }, - }, - gameId: { - type: String, - }, - group: { - type: String, - trim: true, - maxLength: 50, - default: null, - set: function (value) { - return value && value.trim() ? value.trim() : null; - }, - }, - }, - { - timestamps: true, - } -); - -MonitorSchema.pre("findOneAndDelete", async function (next) { - // Delete checks and stats - try { - const doc = await this.model.findOne(this.getFilter()); - - if (!doc) { - throw new Error("Monitor not found"); - } - - await Check.deleteMany({ monitorId: doc._id }); - - // Deal with status pages - await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); - - await MonitorStats.deleteMany({ monitorId: doc?._id.toString() }); - next(); - } catch (error) { - next(error); - } -}); - -MonitorSchema.pre("deleteMany", async function (next) { - const filter = this.getFilter(); - const monitors = await this.model.find(filter).select(["_id", "type"]).lean(); - - for (const monitor of monitors) { - await Check.deleteMany({ monitorId: monitor._id }); - await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } }); - await MonitorStats.deleteMany({ monitorId: monitor._id.toString() }); - } - next(); -}); - -MonitorSchema.pre("save", function (next) { - if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { - this.cpuAlertThreshold = this.alertThreshold; - } - if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) { - this.memoryAlertThreshold = this.alertThreshold; - } - if (!this.diskAlertThreshold || this.isModified("alertThreshold")) { - this.diskAlertThreshold = this.alertThreshold; - } - if (!this.tempAlertThreshold || this.isModified("alertThreshold")) { - this.tempAlertThreshold = this.alertThreshold; - } - next(); -}); - -MonitorSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if (update.alertThreshold) { - update.cpuAlertThreshold = update.alertThreshold; - update.memoryAlertThreshold = update.alertThreshold; - update.diskAlertThreshold = update.alertThreshold; - update.tempAlertThreshold = update.alertThreshold; - } - next(); -}); - -MonitorSchema.index({ teamId: 1, type: 1 }); - -export default mongoose.model("Monitor", MonitorSchema); diff --git a/server/src/db/v1/models/MonitorStats.js b/server/src/db/v1/models/MonitorStats.js deleted file mode 100755 index c081c8a4f..000000000 --- a/server/src/db/v1/models/MonitorStats.js +++ /dev/null @@ -1,49 +0,0 @@ -import mongoose from "mongoose"; - -const MonitorStatsSchema = new mongoose.Schema( - { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - avgResponseTime: { - type: Number, - default: 0, - }, - totalChecks: { - type: Number, - default: 0, - }, - totalUpChecks: { - type: Number, - default: 0, - }, - totalDownChecks: { - type: Number, - default: 0, - }, - uptimePercentage: { - type: Number, - default: 0, - }, - lastCheckTimestamp: { - type: Number, - default: 0, - }, - lastResponseTime: { - type: Number, - default: 0, - }, - timeOfLastFailure: { - type: Number, - default: 0, - }, - }, - { timestamps: true } -); - -const MonitorStats = mongoose.model("MonitorStats", MonitorStatsSchema); - -export default MonitorStats; diff --git a/server/src/db/v1/models/Notification.js b/server/src/db/v1/models/Notification.js deleted file mode 100755 index 72c716a26..000000000 --- a/server/src/db/v1/models/Notification.js +++ /dev/null @@ -1,74 +0,0 @@ -import mongoose from "mongoose"; - -const NotificationSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - type: { - type: String, - enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"], - }, - notificationName: { - type: String, - required: true, - }, - address: { - type: String, - }, - phone: { - type: String, - }, - // Matrix-specific fields - homeserverUrl: { - type: String, - }, - roomId: { - type: String, - }, - accessToken: { - type: String, - }, - }, - { - timestamps: true, - } -); - -NotificationSchema.pre("save", function (next) { - if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { - this.cpuAlertThreshold = this.alertThreshold; - } - if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) { - this.memoryAlertThreshold = this.alertThreshold; - } - if (!this.diskAlertThreshold || this.isModified("alertThreshold")) { - this.diskAlertThreshold = this.alertThreshold; - } - if (!this.tempAlertThreshold || this.isModified("alertThreshold")) { - this.tempAlertThreshold = this.alertThreshold; - } - next(); -}); - -NotificationSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if (update.alertThreshold) { - update.cpuAlertThreshold = update.alertThreshold; - update.memoryAlertThreshold = update.alertThreshold; - update.diskAlertThreshold = update.alertThreshold; - update.tempAlertThreshold = update.alertThreshold; - } - next(); -}); - -export default mongoose.model("Notification", NotificationSchema); diff --git a/server/src/db/v1/models/RecoveryToken.js b/server/src/db/v1/models/RecoveryToken.js deleted file mode 100755 index 2219a4bca..000000000 --- a/server/src/db/v1/models/RecoveryToken.js +++ /dev/null @@ -1,25 +0,0 @@ -import mongoose from "mongoose"; - -const RecoveryTokenSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - token: { - type: String, - required: true, - }, - expiry: { - type: Date, - default: Date.now, - expires: 600, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("RecoveryToken", RecoveryTokenSchema); diff --git a/server/src/db/v1/models/StatusPage.js b/server/src/db/v1/models/StatusPage.js deleted file mode 100755 index 621749fb8..000000000 --- a/server/src/db/v1/models/StatusPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import mongoose from "mongoose"; - -const StatusPageSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - type: { - type: String, - required: true, - default: "uptime", - enum: ["uptime"], - }, - companyName: { - type: String, - required: true, - default: "", - }, - url: { - type: String, - unique: true, - required: true, - default: "", - }, - timezone: { - type: String, - required: false, - }, - color: { - type: String, - required: false, - default: "#4169E1", - }, - monitors: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - required: true, - }, - ], - subMonitors: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - required: true, - }, - ], - logo: { - data: Buffer, - contentType: String, - }, - isPublished: { - type: Boolean, - default: false, - }, - showCharts: { - type: Boolean, - default: true, - }, - showUptimePercentage: { - type: Boolean, - default: true, - }, - showAdminLoginLink: { - type: Boolean, - default: false, - }, - customCSS: { - type: String, - default: "", - }, - }, - { timestamps: true } -); - -export default mongoose.model("StatusPage", StatusPageSchema); diff --git a/server/src/db/v1/models/Team.js b/server/src/db/v1/models/Team.js deleted file mode 100755 index 95337c201..000000000 --- a/server/src/db/v1/models/Team.js +++ /dev/null @@ -1,14 +0,0 @@ -import mongoose from "mongoose"; -const TeamSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - }, - { - timestamps: true, - } -); -export default mongoose.model("Team", TeamSchema); diff --git a/server/src/db/v1/models/User.js b/server/src/db/v1/models/User.js deleted file mode 100755 index bf3012b26..000000000 --- a/server/src/db/v1/models/User.js +++ /dev/null @@ -1,105 +0,0 @@ -import mongoose from "mongoose"; -import bcrypt from "bcryptjs"; -import Monitor from "./Monitor.js"; -import Team from "./Team.js"; -import Notification from "./Notification.js"; - -const UserSchema = mongoose.Schema( - { - firstName: { - type: String, - required: true, - }, - lastName: { - type: String, - required: true, - }, - email: { - type: String, - required: true, - unique: true, - }, - password: { - type: String, - required: true, - }, - avatarImage: { - type: String, - }, - profileImage: { - data: Buffer, - contentType: String, - }, - isActive: { - type: Boolean, - default: true, - }, - isVerified: { - type: Boolean, - default: false, - }, - role: { - type: [String], - default: "user", - enum: ["user", "admin", "superadmin", "demo"], - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - }, - checkTTL: { - type: Number, - }, - }, - { - timestamps: true, - } -); - -UserSchema.pre("save", function (next) { - if (!this.isModified("password")) { - return next(); - } - const salt = bcrypt.genSaltSync(10); - this.password = bcrypt.hashSync(this.password, salt); - next(); -}); - -UserSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if ("password" in update) { - const salt = bcrypt.genSaltSync(10); - update.password = bcrypt.hashSync(update.password, salt); - } - - next(); -}); - -UserSchema.pre("findOneAndDelete", async function (next) { - try { - const userToDelete = await this.model.findOne(this.getFilter()); - if (!userToDelete) return next(); - if (userToDelete.role.includes("superadmin")) { - await Team.deleteOne({ _id: userToDelete.teamId }); - await Monitor.deleteMany({ userId: userToDelete._id }); - await this.model.deleteMany({ - teamId: userToDelete.teamId, - _id: { $ne: userToDelete._id }, - }); - await Notification.deleteMany({ teamId: userToDelete.teamId }); - } - next(); - } catch (error) { - next(error); - } -}); - -UserSchema.methods.comparePassword = async function (submittedPassword) { - const res = await bcrypt.compare(submittedPassword, this.password); - return res; -}; - -const User = mongoose.model("User", UserSchema); - -export default User; diff --git a/server/src/db/v1/modules/checkModule.js b/server/src/db/v1/modules/checkModule.js deleted file mode 100755 index dfca02bed..000000000 --- a/server/src/db/v1/modules/checkModule.js +++ /dev/null @@ -1,283 +0,0 @@ -import { ObjectId } from "mongodb"; -import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js"; - -const SERVICE_NAME = "checkModule"; -const dateRangeLookup = { - recent: new Date(new Date().setDate(new Date().getDate() - 2)), - hour: new Date(new Date().setHours(new Date().getHours() - 1)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: undefined, -}; - -class CheckModule { - constructor({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }) { - this.logger = logger; - this.Check = Check; - this.HardwareCheck = HardwareCheck; - this.PageSpeedCheck = PageSpeedCheck; - this.Monitor = Monitor; - this.User = User; - } - - createChecks = async (checks) => { - try { - await this.Check.insertMany(checks, { ordered: false }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createCheck"; - throw error; - } - }; - - getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => { - try { - status = status === "true" ? true : status === "false" ? false : undefined; - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - - const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; - - // Match - const matchStage = { - monitorId: new ObjectId(monitorId), - ...(typeof status !== "undefined" && { status }), - ...(typeof ack !== "undefined" && ackStage), - ...(dateRangeLookup[dateRange] && { - createdAt: { - $gte: dateRangeLookup[dateRange], - }, - }), - }; - - if (filter !== undefined) { - switch (filter) { - case "all": - break; - case "down": - break; - case "resolve": - matchStage.statusCode = 5000; - break; - default: - this.logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecks", - }); - break; - } - } - - //Sort - sortOrder = sortOrder === "asc" ? 1 : -1; - - // Pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - const checks = await this.Check.aggregate([ - { $match: matchStage }, - { $sort: { createdAt: sortOrder } }, - { - $facet: { - summary: [{ $count: "checksCount" }], - checks: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - checksCount: { - $ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0], - }, - checks: { - $ifNull: ["$checks", []], - }, - }, - }, - ]); - return checks[0]; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecks"; - throw error; - } - }; - - getChecksByTeam = async ({ sortOrder, dateRange, filter, ack, page, rowsPerPage, teamId }) => { - try { - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - - const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; - - const matchStage = { - teamId: new ObjectId(teamId), - status: false, - ...(typeof ack !== "undefined" && ackStage), - ...(dateRangeLookup[dateRange] && { - createdAt: { - $gte: dateRangeLookup[dateRange], - }, - }), - }; - // Add filter to match stage - if (filter !== undefined) { - switch (filter) { - case "all": - break; - case "down": - break; - case "resolve": - matchStage.statusCode = 5000; - break; - default: - this.logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecksByTeam", - }); - break; - } - } - - sortOrder = sortOrder === "asc" ? 1 : -1; - - // pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - const aggregatePipeline = [ - { $match: matchStage }, - - { $sort: { createdAt: sortOrder } }, - { - $facet: { - summary: [{ $count: "checksCount" }], - checks: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, - checks: "$checks", - }, - }, - ]; - - const checks = await this.Check.aggregate(aggregatePipeline); - return checks[0]; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecksByTeam"; - throw error; - } - }; - - ackCheck = async (checkId, teamId, ack) => { - try { - const updatedCheck = await this.Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true }); - - if (!updatedCheck) { - throw new Error("Check not found"); - } - - return updatedCheck; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "ackCheck"; - throw error; - } - }; - - ackAllChecks = async (monitorId, teamId, ack, path) => { - try { - const updatedChecks = await this.Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } }); - return updatedChecks.modifiedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "ackAllChecks"; - throw error; - } - }; - - getChecksSummaryByTeamId = async ({ teamId }) => { - try { - const matchStage = { - teamId: new ObjectId(teamId), - }; - const checks = await this.Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage })); - return checks[0].summary; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getChecksSummaryByTeamId"; - throw error; - } - }; - deleteChecks = async (monitorId) => { - try { - const result = await this.Check.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteChecks"; - throw error; - } - }; - - deleteChecksByTeamId = async (teamId) => { - try { - // Find all monitor IDs for this team (only get _id field for efficiency) - const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 }); - const monitorIds = teamMonitors.map((monitor) => monitor._id); - - // Delete all checks for these monitors in one operation - const deleteResult = await this.Check.deleteMany({ monitorId: { $in: monitorIds } }); - - return deleteResult.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteChecksByTeamId"; - throw error; - } - }; - - updateChecksTTL = async (teamId, ttl) => { - try { - await this.Check.collection.dropIndex("expiry_1"); - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "updateChecksTTL", - stack: error.stack, - }); - } - - try { - await this.Check.collection.createIndex( - { expiry: 1 }, - { expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary - ); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateChecksTTL"; - throw error; - } - // Update user - try { - await this.User.updateMany({ teamId: teamId }, { checkTTL: ttl }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateChecksTTL"; - throw error; - } - }; -} - -export default CheckModule; diff --git a/server/src/db/v1/modules/checkModuleQueries.js b/server/src/db/v1/modules/checkModuleQueries.js deleted file mode 100644 index f2ad2a922..000000000 --- a/server/src/db/v1/modules/checkModuleQueries.js +++ /dev/null @@ -1,44 +0,0 @@ -const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => { - return [ - { $match: matchStage }, - { - $facet: { - summary: [ - { - $group: { - _id: null, - totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } }, - resolvedChecks: { - $sum: { - $cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0], - }, - }, - downChecks: { - $sum: { - $cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0], - }, - }, - cannotResolveChecks: { - $sum: { - $cond: [{ $eq: ["$statusCode", 5000] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, - }, - }, - ]; -}; - -export { buildChecksSummaryByTeamIdPipeline }; diff --git a/server/src/db/v1/modules/inviteModule.js b/server/src/db/v1/modules/inviteModule.js deleted file mode 100755 index 444e51efd..000000000 --- a/server/src/db/v1/modules/inviteModule.js +++ /dev/null @@ -1,56 +0,0 @@ -const SERVICE_NAME = "inviteModule"; - -class InviteModule { - constructor({ InviteToken, crypto, stringService }) { - this.InviteToken = InviteToken; - this.crypto = crypto; - this.stringService = stringService; - } - - requestInviteToken = async (userData) => { - try { - await this.InviteToken.deleteMany({ email: userData.email }); - userData.token = this.crypto.randomBytes(32).toString("hex"); - let inviteToken = new this.InviteToken(userData); - await inviteToken.save(); - return inviteToken; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "requestInviteToken"; - throw error; - } - }; - - getInviteToken = async (token) => { - try { - const invite = await this.InviteToken.findOne({ - token, - }); - if (invite === null) { - throw new Error(this.stringService.authInviteNotFound); - } - return invite; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getInviteToken"; - throw error; - } - }; - getInviteTokenAndDelete = async (token) => { - try { - const invite = await this.InviteToken.findOneAndDelete({ - token, - }); - if (invite === null) { - throw new Error(this.stringService.authInviteNotFound); - } - return invite; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getInviteTokenAndDelete"; - throw error; - } - }; -} - -export default InviteModule; diff --git a/server/src/db/v1/modules/maintenanceWindowModule.js b/server/src/db/v1/modules/maintenanceWindowModule.js deleted file mode 100755 index 61523c9f0..000000000 --- a/server/src/db/v1/modules/maintenanceWindowModule.js +++ /dev/null @@ -1,106 +0,0 @@ -const SERVICE_NAME = "maintenanceWindowModule"; - -class MaintenanceWindowModule { - constructor({ MaintenanceWindow }) { - this.MaintenanceWindow = MaintenanceWindow; - } - createMaintenanceWindow = async (maintenanceWindowData) => { - try { - const maintenanceWindow = new this.MaintenanceWindow({ - ...maintenanceWindowData, - }); - - // If the maintenance window is a one time window, set the expiry to the end date - if (maintenanceWindowData.oneTime) { - maintenanceWindow.expiry = maintenanceWindowData.end; - } - const result = await maintenanceWindow.save(); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createMaintenanceWindow"; - throw error; - } - }; - getMaintenanceWindowById = async ({ id, teamId }) => { - try { - const maintenanceWindow = await this.MaintenanceWindow.findOne({ - _id: id, - teamId: teamId, - }); - return maintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowById"; - throw error; - } - }; - - getMaintenanceWindowsByTeamId = async (teamId, query) => { - try { - let { active, page, rowsPerPage, field, order } = query || {}; - const maintenanceQuery = { teamId }; - - if (active !== undefined) maintenanceQuery.active = active; - - const maintenanceWindowCount = await this.MaintenanceWindow.countDocuments(maintenanceQuery); - - // Pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - // Sorting - let sort = {}; - if (field !== undefined && order !== undefined) { - sort[field] = order === "asc" ? 1 : -1; - } - - const maintenanceWindows = await this.MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort); - - return { maintenanceWindows, maintenanceWindowCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowByUserId"; - throw error; - } - }; - getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { - try { - const maintenanceWindows = await this.MaintenanceWindow.find({ - monitorId: monitorId, - teamId: teamId, - }); - return maintenanceWindows; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowsByMonitorId"; - throw error; - } - }; - - deleteMaintenanceWindowById = async ({ id, teamId }) => { - try { - const maintenanceWindow = await this.MaintenanceWindow.findOneAndDelete({ _id: id, teamId }); - return maintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMaintenanceWindowById"; - throw error; - } - }; - - editMaintenanceWindowById = async ({ id, body }) => { - try { - const editedMaintenanceWindow = await this.MaintenanceWindow.findByIdAndUpdate(id, body, { new: true }); - return editedMaintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editMaintenanceWindowById"; - throw error; - } - }; -} - -export default MaintenanceWindowModule; diff --git a/server/src/db/v1/modules/monitorModule.js b/server/src/db/v1/modules/monitorModule.js deleted file mode 100755 index 39168f0b2..000000000 --- a/server/src/db/v1/modules/monitorModule.js +++ /dev/null @@ -1,593 +0,0 @@ -import { - buildUptimeDetailsPipeline, - buildHardwareDetailsPipeline, - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildMonitorsAndSummaryByTeamIdPipeline, - buildMonitorsWithChecksByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, -} from "./monitorModuleQueries.js"; - -const SERVICE_NAME = "monitorModule"; - -class MonitorModule { - constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { - this.Monitor = Monitor; - this.MonitorStats = MonitorStats; - this.Check = Check; - this.stringService = stringService; - this.fs = fs; - this.path = path; - this.fileURLToPath = fileURLToPath; - this.ObjectId = ObjectId; - this.NormalizeData = NormalizeData; - this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; - - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - - this.demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json"); - } - - // Helper - calculateUptimeDuration = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const latestCheck = new Date(checks[0].createdAt); - let latestDownCheck = 0; - - for (let i = checks.length - 1; i >= 0; i--) { - if (checks[i].status === false) { - latestDownCheck = new Date(checks[i].createdAt); - break; - } - } - - // If no down check is found, uptime is from the last check to now - if (latestDownCheck === 0) { - return Date.now() - new Date(checks[checks.length - 1].createdAt); - } - - // Otherwise the uptime is from the last check to the last down check - return latestCheck - latestDownCheck; - }; - - // Helper - getLastChecked = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - // Data is sorted newest->oldest, so last check is the most recent - return new Date() - new Date(checks[0].createdAt); - }; - getLatestResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - return checks[0]?.responseTime ?? 0; - }; - - // Helper - getAverageResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - const validChecks = checks.filter((check) => typeof check.responseTime === "number"); - if (validChecks.length === 0) { - return 0; - } - const aggResponseTime = validChecks.reduce((sum, check) => { - return sum + check.responseTime; - }, 0); - return aggResponseTime / validChecks.length; - }; - - // Helper - getUptimePercentage = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const upCount = checks.reduce((count, check) => { - return check.status === true ? count + 1 : count; - }, 0); - return (upCount / checks.length) * 100; - }; - - // Helper - getIncidents = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - return checks.reduce((acc, check) => { - return check.status === false ? (acc += 1) : acc; - }, 0); - }; - - // Helper - getDateRange = (dateRange) => { - const startDates = { - recent: new Date(new Date().setHours(new Date().getHours() - 2)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: new Date(0), - }; - return { - start: startDates[dateRange], - end: new Date(), - }; - }; - - //Helper - getMonitorChecks = async (monitorId, dateRange, sortOrder) => { - const indexSpec = { - monitorId: 1, - updatedAt: sortOrder, // This will be 1 or -1 - }; - - const [checksAll, checksForDateRange] = await Promise.all([ - this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), - this.Check.find({ - monitorId, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }) - .hint(indexSpec) - .lean(), - ]); - - return { checksAll, checksForDateRange }; - }; - - // Helper - processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => { - let processedChecks = checks; - if (numToDisplay && checks.length > numToDisplay) { - const n = Math.ceil(checks.length / numToDisplay); - processedChecks = checks.filter((_, index) => index % n === 0); - } - return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks; - }; - - // Helper - groupChecksByTime = (checks, dateRange) => { - return checks.reduce((acc, check) => { - // Validate the date - const checkDate = new Date(check.createdAt); - if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { - return acc; - } - - const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0]; - - if (!acc[time]) { - acc[time] = { time, checks: [] }; - } - acc[time].checks.push(check); - return acc; - }, {}); - }; - - // Helper - calculateGroupStats = (group) => { - const totalChecks = group.checks.length; - - const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime)); - - return { - time: group.time, - uptimePercentage: this.getUptimePercentage(group.checks), - totalChecks, - totalIncidents: group.checks.filter((check) => !check.status).length, - avgResponseTime: - checksWithResponseTime.length > 0 - ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length - : 0, - }; - }; - - getAllMonitors = async () => { - try { - const monitors = await this.Monitor.find(); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllMonitors"; - throw error; - } - }; - - getMonitorById = async (monitorId) => { - try { - const monitor = await this.Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - const error = new Error(this.stringService.getDbFindMonitorById(monitorId)); - error.status = 404; - throw error; - } - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorById"; - throw error; - } - }; - - getMonitorsByIds = async (monitorIds) => { - try { - const objectIds = monitorIds.map((id) => new this.ObjectId(id)); - return await this.Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean(); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsByIds"; - throw error; - } - }; - getUptimeDetailsById = async ({ monitorId, dateRange }) => { - try { - const dates = this.getDateRange(dateRange); - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - - const dateString = formatLookup[dateRange]; - - const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); - - const monitorData = results[0]; - - monitorData.groupedUpChecks = this.NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100); - - monitorData.groupedDownChecks = this.NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100); - - const normalizedGroupChecks = this.NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100); - - monitorData.groupedChecks = normalizedGroupChecks; - const monitorStats = await this.MonitorStats.findOne({ monitorId }); - return { monitorData, monitorStats }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUptimeDetailsById"; - throw error; - } - }; - - getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => { - try { - // Get monitor, if we can't find it, abort with error - const monitor = await this.Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - throw new Error(this.stringService.getDbFindMonitorById(monitorId)); - } - - // Get query params - const sort = sortOrder === "asc" ? 1 : -1; - - // Get Checks for monitor in date range requested - const dates = this.getDateRange(dateRange); - const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort); - - // Build monitor stats - const monitorStats = { - ...monitor.toObject(), - uptimeDuration: this.calculateUptimeDuration(checksAll), - lastChecked: this.getLastChecked(checksAll), - latestResponseTime: this.getLatestResponseTime(checksAll), - periodIncidents: this.getIncidents(checksForDateRange), - periodTotalChecks: checksForDateRange.length, - checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize), - }; - - if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") { - // HTTP/PING Specific stats - monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange); - monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange); - const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange); - monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats); - } - - return monitorStats; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorStatsById"; - throw error; - } - }; - - getHardwareDetailsById = async ({ monitorId, dateRange }) => { - try { - const monitor = await this.Monitor.findById(monitorId); - const dates = this.getDateRange(dateRange); - - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - const dateString = formatLookup[dateRange]; - - const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); - - const stats = hardwareStats[0]; - - return { - ...monitor.toObject(), - stats, - }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getHardwareDetailsById"; - throw error; - } - }; - - getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })); - const summary = summaryResult[0]; - - const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })); - - const filteredMonitors = await this.Monitor.aggregate( - buildFilteredMonitorsByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - - const normalizedFilteredMonitors = filteredMonitors.map((monitor) => { - if (!monitor.checks) { - return monitor; - } - monitor.checks = this.NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - - return { summary, monitors, filteredMonitors: normalizedFilteredMonitors }; - }; - - getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { - try { - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - if (explain === true) { - return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); - } - - const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })); - const { monitors, summary } = queryResult?.[0] ?? {}; - return { monitors, summary }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsAndSummaryByTeamId"; - throw error; - } - }; - - getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => { - try { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - if (explain === true) { - return this.Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ).explain("executionStats"); - } - - const queryResult = await this.Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - const monitors = queryResult[0]?.monitors; - const count = queryResult[0]?.count; - const normalizedFilteredMonitors = monitors.map((monitor) => { - if (!monitor.checks) { - return monitor; - } - monitor.checks = this.NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - return { count, monitors: normalizedFilteredMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsWithChecksByTeamId"; - throw error; - } - }; - createMonitor = async ({ body, teamId, userId }) => { - try { - const monitor = new this.Monitor({ ...body, teamId, userId }); - const saved = await monitor.save(); - return saved; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createMonitor"; - throw error; - } - }; - - createBulkMonitors = async (req) => { - try { - const monitors = req.map((item) => new this.Monitor({ ...item, notifications: undefined })); - await this.Monitor.bulkSave(monitors); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createBulkMonitors"; - throw error; - } - }; - - deleteMonitor = async ({ teamId, monitorId }) => { - try { - const deletedMonitor = await this.Monitor.findOneAndDelete({ _id: monitorId, teamId }); - - if (!deletedMonitor) { - throw new Error(this.stringService.getDbFindMonitorById(monitorId)); - } - - return deletedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitor"; - throw error; - } - }; - - deleteAllMonitors = async (teamId) => { - try { - const monitors = await this.Monitor.find({ teamId }); - const { deletedCount } = await this.Monitor.deleteMany({ teamId }); - - return { monitors, deletedCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteAllMonitors"; - throw error; - } - }; - - deleteMonitorsByUserId = async (userId) => { - try { - const result = await this.Monitor.deleteMany({ userId: userId }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitorsByUserId"; - throw error; - } - }; - - editMonitor = async ({ monitorId, body }) => { - try { - const editedMonitor = await this.Monitor.findByIdAndUpdate(monitorId, body, { - new: true, - }); - return editedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editMonitor"; - throw error; - } - }; - - addDemoMonitors = async (userId, teamId) => { - try { - const demoMonitors = JSON.parse(this.fs.readFileSync(this.demoMonitorsPath, "utf8")); - - const demoMonitorsToInsert = demoMonitors.map((monitor) => { - return { - userId, - teamId, - name: monitor.name, - description: monitor.name, - type: "http", - url: monitor.url, - interval: 60000, - }; - }); - const insertedMonitors = await this.Monitor.insertMany(demoMonitorsToInsert); - return insertedMonitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "addDemoMonitors"; - throw error; - } - }; - - pauseMonitor = async ({ monitorId }) => { - try { - const monitor = await this.Monitor.findOneAndUpdate( - { _id: monitorId }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - status: "$$REMOVE", - }, - }, - ], - { new: true } - ); - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "pauseMonitor"; - throw error; - } - }; - - getGroupsByTeamId = async ({ teamId }) => { - try { - const groups = await this.Monitor.distinct("group", { - teamId: new this.ObjectId(teamId), - group: { $ne: null, $ne: "" }, - }); - - return groups.filter(Boolean).sort(); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getGroupsByTeamId"; - throw error; - } - }; -} - -export default MonitorModule; diff --git a/server/src/db/v1/modules/monitorModuleQueries.js b/server/src/db/v1/modules/monitorModuleQueries.js deleted file mode 100755 index 32aed1529..000000000 --- a/server/src/db/v1/modules/monitorModuleQueries.js +++ /dev/null @@ -1,1214 +0,0 @@ -import { ObjectId } from "mongodb"; - -const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { - return [ - { - $match: { - monitorId: new ObjectId(monitorId), - updatedAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $sort: { - updatedAt: 1, - }, - }, - { - $facet: { - // For the response time chart, should return checks for date window - // Grouped by: {day: hour}, {week: day}, {month: day} - uptimePercentage: [ - { - $group: { - _id: null, - upChecks: { - $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] }, - }, - totalChecks: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - percentage: { - $cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }], - }, - }, - }, - ], - groupedAvgResponseTime: [ - { - $group: { - _id: null, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - ], - groupedChecks: [ - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - { - $sort: { - _id: 1, - }, - }, - ], - // Up checks grouped by: {day: hour}, {week: day}, {month: day} - groupedUpChecks: [ - { - $match: { - status: true, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - totalChecks: { - $sum: 1, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - { - $sort: { _id: 1 }, - }, - ], - // Down checks grouped by: {day: hour}, {week: day}, {month: day} for the date window - groupedDownChecks: [ - { - $match: { - status: false, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - totalChecks: { - $sum: 1, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - { - $sort: { _id: 1 }, - }, - ], - }, - }, - { - $lookup: { - from: "monitors", - let: { monitor_id: { $toObjectId: monitorId } }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$_id", "$$monitor_id"] }, - }, - }, - { - $project: { - _id: 1, - teamId: 1, - name: 1, - status: 1, - interval: 1, - type: 1, - url: 1, - isActive: 1, - notifications: 1, - }, - }, - ], - as: "monitor", - }, - }, - { - $project: { - groupedAvgResponseTime: { - $arrayElemAt: ["$groupedAvgResponseTime.avgResponseTime", 0], - }, - - groupedChecks: "$groupedChecks", - groupedUpChecks: "$groupedUpChecks", - groupedDownChecks: "$groupedDownChecks", - groupedUptimePercentage: { $arrayElemAt: ["$uptimePercentage.percentage", 0] }, - monitor: { $arrayElemAt: ["$monitor", 0] }, - }, - }, - ]; -}; - -const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { - return [ - { - $match: { - monitorId: monitor._id, - type: "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $sort: { - createdAt: 1, - }, - }, - { - $facet: { - aggregateData: [ - { - $group: { - _id: null, - latestCheck: { - $last: "$$ROOT", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - upChecks: [ - { - $match: { - status: true, - }, - }, - { - $group: { - _id: null, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - checks: [ - { - $limit: 1, - }, - { - $project: { - diskCount: { - $size: { $ifNull: ["$disk", []] }, - }, - netCount: { $size: { $ifNull: ["$net", []] } }, - }, - }, - { - $lookup: { - from: "checks", - let: { - diskCount: "$diskCount", - netCount: "$netCount", - }, - pipeline: [ - { - $match: { - $expr: { - $and: [{ $eq: ["$monitorId", monitor._id] }, { $gte: ["$createdAt", dates.start] }, { $lte: ["$createdAt", dates.end] }], - }, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - avgCpuUsage: { - $avg: "$cpu.usage_percent", - }, - avgMemoryUsage: { - $avg: "$memory.usage_percent", - }, - avgTemperatures: { - $push: { - $ifNull: ["$cpu.temperature", [0]], - }, - }, - disks: { - $push: "$disk", - }, - net: { - $push: "$net", - }, - updatedAts: { - $push: "$updatedAt", - }, - }, - }, - { - $project: { - _id: 1, - avgCpuUsage: 1, - avgMemoryUsage: 1, - avgTemperature: { - $map: { - input: { - $range: [ - 0, - { - $size: { - // Handle null temperatures array - $ifNull: [ - { $arrayElemAt: ["$avgTemperatures", 0] }, - [0], // Default to single-element array if null - ], - }, - }, - ], - }, - as: "index", - in: { - $avg: { - $map: { - input: "$avgTemperatures", - as: "tempArray", - in: { - $ifNull: [ - { $arrayElemAt: ["$$tempArray", "$$index"] }, - 0, // Default to 0 if element is null - ], - }, - }, - }, - }, - }, - }, - disks: { - $map: { - input: { - $range: [0, "$$diskCount"], - }, - as: "diskIndex", - in: { - name: { - $concat: [ - "disk", - { - $toString: "$$diskIndex", - }, - ], - }, - readSpeed: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.read_speed_bytes", "$$diskIndex"], - }, - }, - }, - }, - writeSpeed: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.write_speed_bytes", "$$diskIndex"], - }, - }, - }, - }, - totalBytes: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.total_bytes", "$$diskIndex"], - }, - }, - }, - }, - freeBytes: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.free_bytes", "$$diskIndex"], - }, - }, - }, - }, - usagePercent: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.usage_percent", "$$diskIndex"], - }, - }, - }, - }, - }, - }, - }, - net: { - $map: { - input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$net", 0] }, []] } }] }, - as: "netIndex", - in: { - name: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.name", - }, - }, - "$$netIndex", - ], - }, - bytesSentPerSecond: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_sent" } }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], - }, - as: "iface", - in: "$$iface.bytes_sent", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaBytesRecv: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_recv" } }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], - }, - as: "iface", - in: "$$iface.bytes_recv", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaPacketsSent: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.packets_sent" } }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], - }, - as: "iface", - in: "$$iface.packets_sent", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaPacketsRecv: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.packets_recv", - }, - }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [ - { $ifNull: ["$net", []] }, - { - $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], - }, - ], - }, - as: "iface", - in: "$$iface.packets_recv", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [ - { $ifNull: ["$updatedAts", []] }, - { - $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], - }, - ], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaErrIn: { - $let: { - vars: { - first: { - $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.err_in" } }, "$$netIndex"], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], - }, - as: "iface", - in: "$$iface.err_in", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaErrOut: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.err_out", - }, - }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [ - { $ifNull: ["$net", []] }, - { - $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], - }, - ], - }, - as: "iface", - in: "$$iface.err_out", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [ - { $ifNull: ["$updatedAts", []] }, - { - $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], - }, - ], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaDropIn: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.drop_in", - }, - }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [ - { $ifNull: ["$net", []] }, - { - $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], - }, - ], - }, - as: "iface", - in: "$$iface.drop_in", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [ - { $ifNull: ["$updatedAts", []] }, - { - $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], - }, - ], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaDropOut: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.drop_out", - }, - }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [ - { $ifNull: ["$net", []] }, - { - $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], - }, - ], - }, - as: "iface", - in: "$$iface.drop_out", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [ - { $ifNull: ["$updatedAts", []] }, - { - $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], - }, - ], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - }, - }, - }, - firstUpdatedAt: 1, - lastUpdatedAt: 1, - }, - }, - ], - as: "hourlyStats", - }, - }, - { - $unwind: "$hourlyStats", - }, - { - $replaceRoot: { - newRoot: "$hourlyStats", - }, - }, - ], - }, - }, - { $unwind: "$checks" }, - { $sort: { "checks._id": 1 } }, - { - $group: { - _id: "$_id", - checks: { $push: "$checks" }, - aggregateData: { $first: "$aggregateData" }, - upChecks: { $first: "$upChecks" }, - }, - }, - { - $project: { - aggregateData: { - $arrayElemAt: ["$aggregateData", 0], - }, - upChecks: { - $arrayElemAt: ["$upChecks", 0], - }, - checks: 1, - }, - }, - ]; -}; - -const buildMonitorStatsPipeline = (monitor) => { - return [ - { - $match: { - monitorId: monitor._id, - }, - }, - { - $project: { - avgResponseTime: 1, - uptimePercentage: 1, - totalChecks: 1, - timeSinceLastCheck: { - $subtract: [Date.now(), "$lastCheckTimestamp"], - }, - lastCheckTimestamp: 1, - uptBurnt: { $toString: "$uptBurnt" }, - }, - }, - ]; -}; - -const buildMonitorSummaryByTeamIdPipeline = ({ matchStage }) => { - return [ - { $match: matchStage }, - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ]; -}; - -const buildMonitorsByTeamIdPipeline = ({ matchStage, field, order }) => { - const sort = { [field]: order === "asc" ? 1 : -1 }; - - return [ - { $match: matchStage }, - { $sort: sort }, - { - $project: { - _id: 1, - name: 1, - type: 1, - port: 1, - }, - }, - ]; -}; - -const buildMonitorsAndSummaryByTeamIdPipeline = ({ matchStage }) => { - return [ - { $match: matchStage }, - { - $facet: { - summary: [ - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - monitors: [ - { $sort: { name: 1 } }, - { - $project: { - _id: 1, - name: 1, - type: 1, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, - monitors: 1, - }, - }, - ]; -}; - -const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => { - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : []; - - // Match name - if (typeof filter !== "undefined" && field === "name") { - matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - } - - // Match isActive - if (typeof filter !== "undefined" && field === "isActive") { - matchStage.isActive = filter === "true" ? true : false; - } - - if (typeof filter !== "undefined" && field === "status") { - matchStage.status = filter === "true" ? true : false; - } - - // Match type - if (typeof filter !== "undefined" && field === "type") { - matchStage.type = filter; - } - - const monitorsPipeline = [ - { $sort: sort }, - { $skip: skip }, - ...limitStage, - { - $project: { - _id: 1, - name: 1, - description: 1, - type: 1, - url: 1, - isActive: 1, - createdAt: 1, - updatedAt: 1, - uptimePercentage: 1, - status: 1, - }, - }, - ]; - - // Add checks - if (limit) { - const checksCollection = "checks"; - monitorsPipeline.push({ - $lookup: { - from: checksCollection, - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { updatedAt: -1 } }, - { $limit: limit }, - { - $project: { - _id: 1, - status: 1, - responseTime: 1, - statusCode: 1, - createdAt: 1, - updatedAt: 1, - originalResponseTime: 1, - }, - }, - ], - as: "checks", - }, - }); - } - - const pipeline = [ - { $match: matchStage }, - { - $facet: { - count: [{ $count: "monitorsCount" }], - monitors: monitorsPipeline, - }, - }, - { - $project: { - count: { $arrayElemAt: ["$count", 0] }, - monitors: 1, - }, - }, - ]; - return pipeline; -}; - -const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => { - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : []; - - if (typeof filter !== "undefined" && field === "name") { - matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - } - - if (typeof filter !== "undefined" && field === "status") { - matchStage.status = filter === "true"; - } - - const pipeline = [{ $match: matchStage }, { $sort: sort }, { $skip: skip }, ...limitStage]; - - // Add checks - if (limit) { - const checksCollection = "checks"; - - pipeline.push({ - $lookup: { - from: checksCollection, - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - { $limit: limit }, - ], - as: "checks", - }, - }); - } - - return pipeline; -}; - -const buildGetMonitorsByTeamIdPipeline = (req) => { - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build the match stage - const matchStage = { teamId: new ObjectId(req.params.teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - return [ - { $match: matchStage }, - { - $facet: { - summary: [ - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - monitors: [ - { $sort: sort }, - { - $project: { - _id: 1, - name: 1, - }, - }, - ], - filteredMonitors: [ - ...(filter !== undefined - ? [ - { - $match: { - $or: [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }], - }, - }, - ] - : []), - { $sort: sort }, - { $skip: skip }, - ...(rowsPerPage ? [{ $limit: rowsPerPage }] : []), - ...(limit - ? [ - { - $lookup: { - from: "checks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "standardchecks", - }, - }, - ] - : []), - - { - $addFields: { - checks: { - $switch: { - branches: [ - { - case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] }, - then: "$standardchecks", - }, - ], - default: [], - }, - }, - }, - }, - { - $project: { - standardchecks: 0, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, - filteredMonitors: 1, - monitors: 1, - }, - }, - ]; -}; - -export { - buildUptimeDetailsPipeline, - buildHardwareDetailsPipeline, - buildMonitorStatsPipeline, - buildGetMonitorsByTeamIdPipeline, - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildMonitorsAndSummaryByTeamIdPipeline, - buildMonitorsWithChecksByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, -}; diff --git a/server/src/db/v1/modules/notificationModule.js b/server/src/db/v1/modules/notificationModule.js deleted file mode 100755 index 3c6edb54a..000000000 --- a/server/src/db/v1/modules/notificationModule.js +++ /dev/null @@ -1,100 +0,0 @@ -// import Notification from "../../models/Notification.js"; -// import Monitor from "../../models/Monitor.js"; -const SERVICE_NAME = "notificationModule"; - -class NotificationModule { - constructor({ Notification, Monitor }) { - this.Notification = Notification; - this.Monitor = Monitor; - } - createNotification = async (notificationData) => { - try { - const notification = await new this.Notification({ ...notificationData }).save(); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createNotification"; - throw error; - } - }; - getNotificationsByTeamId = async (teamId) => { - try { - const notifications = await this.Notification.find({ teamId }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByTeamId"; - throw error; - } - }; - getNotificationsByIds = async (notificationIds) => { - try { - const notifications = await this.Notification.find({ _id: { $in: notificationIds } }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByIds"; - throw error; - } - }; - getNotificationsByMonitorId = async (monitorId) => { - try { - const notifications = await this.Notification.find({ monitorId }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByMonitorId"; - throw error; - } - }; - deleteNotificationsByMonitorId = async (monitorId) => { - try { - const result = await this.Notification.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteNotificationsByMonitorId"; - throw error; - } - }; - deleteNotificationById = async (id) => { - try { - const notification = await this.Notification.findById(id); - if (!notification) { - throw new Error("Notification not found"); - } - - const result = await this.Notification.findByIdAndDelete(id); - await this.Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteNotificationById"; - throw error; - } - }; - getNotificationById = async (id) => { - try { - const notification = await this.Notification.findById(id); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationById"; - throw error; - } - }; - editNotification = async (id, notificationData) => { - try { - const notification = await this.Notification.findByIdAndUpdate(id, notificationData, { - new: true, - }); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editNotification"; - throw error; - } - }; -} - -export default NotificationModule; diff --git a/server/src/db/v1/modules/recoveryModule.js b/server/src/db/v1/modules/recoveryModule.js deleted file mode 100755 index cc22306a3..000000000 --- a/server/src/db/v1/modules/recoveryModule.js +++ /dev/null @@ -1,80 +0,0 @@ -const SERVICE_NAME = "recoveryModule"; - -class RecoveryModule { - constructor({ User, RecoveryToken, crypto, stringService }) { - this.User = User; - this.RecoveryToken = RecoveryToken; - this.crypto = crypto; - this.stringService = stringService; - } - - requestRecoveryToken = async (email) => { - try { - // Delete any existing tokens - await this.RecoveryToken.deleteMany({ email }); - let recoveryToken = new this.RecoveryToken({ - email, - token: this.crypto.randomBytes(32).toString("hex"), - }); - await recoveryToken.save(); - return recoveryToken; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "requestRecoveryToken"; - throw error; - } - }; - validateRecoveryToken = async (candidateToken) => { - try { - const recoveryToken = await this.RecoveryToken.findOne({ - token: candidateToken, - }); - if (recoveryToken !== null) { - return recoveryToken; - } else { - throw new Error(this.stringService.dbTokenNotFound); - } - } catch (error) { - error.service = SERVICE_NAME; - error.method = "validateRecoveryToken"; - throw error; - } - }; - - resetPassword = async (password, candidateToken) => { - try { - const newPassword = password; - - // Validate token again - const recoveryToken = await this.validateRecoveryToken(candidateToken); - const user = await this.User.findOne({ email: recoveryToken.email }); - - if (user === null) { - throw new Error(this.stringService.dbUserNotFound); - } - - const match = await user.comparePassword(newPassword); - - if (match === true) { - throw new Error("Password cannot be the same as the old password"); - } - - user.password = newPassword; - await user.save(); - await this.RecoveryToken.deleteMany({ email: recoveryToken.email }); - // Fetch the user again without the password - const userWithoutPassword = await this.User.findOne({ - email: recoveryToken.email, - }) - .select("-password") - .select("-profileImage"); - return userWithoutPassword; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "resetPassword"; - throw error; - } - }; -} - -export default RecoveryModule; diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js deleted file mode 100755 index 37b851bf2..000000000 --- a/server/src/db/v1/modules/settingsModule.js +++ /dev/null @@ -1,36 +0,0 @@ -// import AppSettings from "../../models/AppSettings.js"; -const SERVICE_NAME = "SettingsModule"; - -class SettingsModule { - constructor({ AppSettings }) { - this.AppSettings = AppSettings; - } - - updateAppSettings = async (newSettings) => { - try { - const update = { $set: { ...newSettings } }; - - if (newSettings.pagespeedApiKey === "") { - update.$unset = { pagespeedApiKey: "" }; - delete update.$set.pagespeedApiKey; - } - - if (newSettings.systemEmailPassword === "") { - update.$unset = { systemEmailPassword: "" }; - delete update.$set.systemEmailPassword; - } - - await this.AppSettings.findOneAndUpdate({}, update, { - upsert: true, - }); - const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - return settings; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateAppSettings"; - throw error; - } - }; -} - -export default SettingsModule; diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js deleted file mode 100755 index aadde0c81..000000000 --- a/server/src/db/v1/modules/statusPageModule.js +++ /dev/null @@ -1,298 +0,0 @@ -// import StatusPage from "../../models/StatusPage.js"; -// import { NormalizeData } from "../../../utils/dataUtils.js"; -// import ServiceRegistry from "../../../service/system/serviceRegistry.js"; -// import StringService from "../../../service/system/stringService.js"; - -const SERVICE_NAME = "statusPageModule"; - -class StatusPageModule { - constructor({ StatusPage, NormalizeData, stringService }) { - this.StatusPage = StatusPage; - this.NormalizeData = NormalizeData; - this.stringService = stringService; - } - - createStatusPage = async ({ statusPageData, image, userId, teamId }) => { - try { - const statusPage = new this.StatusPage({ - ...statusPageData, - userId, - teamId, - }); - if (image) { - statusPage.logo = { - data: image.buffer, - contentType: image.mimetype, - }; - } - await statusPage.save(); - return statusPage; - } catch (error) { - if (error?.code === 11000) { - // Handle duplicate URL errors - error.status = 400; - error.message = this.stringService.statusPageUrlNotUnique; - } - error.service = SERVICE_NAME; - error.method = "createStatusPage"; - throw error; - } - }; - - updateStatusPage = async (statusPageData, image) => { - try { - if (image) { - statusPageData.logo = { - data: image.buffer, - contentType: image.mimetype, - }; - } else { - statusPageData.logo = null; - } - - if (statusPageData.deleteSubmonitors === "true") { - statusPageData.subMonitors = []; - } - const statusPage = await this.StatusPage.findOneAndUpdate({ url: statusPageData.url }, statusPageData, { - new: true, - }); - - return statusPage; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateStatusPage"; - throw error; - } - }; - - getStatusPageByUrl = async (url) => { - // TODO This is deprecated, can remove and have controller call getStatusPage - try { - return this.getStatusPage(url); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPageByUrl"; - throw error; - } - }; - - getStatusPagesByTeamId = async (teamId) => { - try { - const statusPages = await this.StatusPage.find({ teamId }); - return statusPages; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPagesByTeamId"; - throw error; - } - }; - - getStatusPage = async (url) => { - try { - const preliminaryStatusPage = await this.StatusPage.findOne({ url }); - if (!preliminaryStatusPage) { - const error = new Error(this.stringService.statusPageNotFound); - error.status = 404; - throw error; - } - - if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) { - const { _id, color, companyName, isPublished, logo, originalMonitors, showCharts, showUptimePercentage, timezone, showAdminLoginLink, url } = - preliminaryStatusPage; - return { - statusPage: { - _id, - color, - companyName, - isPublished, - logo, - originalMonitors, - showCharts, - showUptimePercentage, - timezone, - showAdminLoginLink, - url, - }, - monitors: [], - }; - } - - const statusPageQuery = await this.StatusPage.aggregate([ - { $match: { url: url } }, - { - $set: { - originalMonitors: "$monitors", - }, - }, - { - $lookup: { - from: "monitors", - localField: "monitors", - foreignField: "_id", - as: "monitors", - }, - }, - { - $unwind: { - path: "$monitors", - preserveNullAndEmptyArrays: true, - }, - }, - { - $lookup: { - from: "maintenancewindows", - let: { monitorId: "$monitors._id" }, - pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }], - as: "monitors.maintenanceWindows", - }, - }, - { - $lookup: { - from: "checks", - let: { monitorId: "$monitors._id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - { $limit: 25 }, - ], - as: "monitors.checks", - }, - }, - { - $lookup: { - from: "monitorstats", - localField: "monitors._id", - foreignField: "monitorId", - as: "monitors.stats", - }, - }, - { - $addFields: { - "monitors.orderIndex": { - $indexOfArray: ["$originalMonitors", "$monitors._id"], - }, - "monitors.isMaintenance": { - $reduce: { - input: "$monitors.maintenanceWindows", - initialValue: false, - in: { - $or: [ - "$$value", - { - $and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }], - }, - ], - }, - }, - }, - "monitors.uptimePercentage": { - $arrayElemAt: ["$monitors.stats.uptimePercentage", 0], - }, - }, - }, - { $match: { "monitors.orderIndex": { $ne: -1 } } }, - { $sort: { "monitors.orderIndex": 1 } }, - - { - $group: { - _id: "$_id", - statusPage: { $first: "$$ROOT" }, - monitors: { $push: "$monitors" }, - }, - }, - { - $project: { - statusPage: { - _id: 1, - color: 1, - companyName: 1, - isPublished: 1, - logo: 1, - originalMonitors: 1, - showCharts: 1, - showUptimePercentage: 1, - timezone: 1, - showAdminLoginLink: 1, - url: 1, - }, - monitors: { - _id: 1, - userId: 1, - teamId: 1, - name: 1, - description: 1, - status: 1, - type: 1, - ignoreTlsErrors: 1, - jsonPath: 1, - expectedValue: 1, - matchMethod: 1, - url: 1, - port: 1, - isActive: 1, - interval: 1, - uptimePercentage: 1, - notifications: 1, - secret: 1, - thresholds: 1, - alertThreshold: 1, - cpuAlertThreshold: 1, - memoryAlertThreshold: 1, - diskAlertThreshold: 1, - tempAlertThreshold: 1, - checks: 1, - isMaintenance: 1, - createdAt: 1, - updatedAt: 1, - }, - }, - }, - ]); - if (!statusPageQuery.length) { - const error = new Error(this.stringService.statusPageNotFound); - error.status = 404; - throw error; - } - - const { statusPage, monitors } = statusPageQuery[0]; - - const normalizedMonitors = monitors.map((monitor) => { - return { - ...monitor, - checks: this.NormalizeData(monitor.checks, 10, 100), - }; - }); - - return { statusPage, monitors: normalizedMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPageByUrl"; - throw error; - } - }; - - deleteStatusPage = async (url) => { - try { - await this.StatusPage.deleteOne({ url }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteStatusPage"; - throw error; - } - }; - deleteStatusPagesByMonitorId = async (monitorId) => { - try { - await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteStatusPageByMonitorId"; - throw error; - } - }; -} - -export default StatusPageModule; diff --git a/server/src/db/v1/modules/userModule.js b/server/src/db/v1/modules/userModule.js deleted file mode 100755 index 83b3b15f9..000000000 --- a/server/src/db/v1/modules/userModule.js +++ /dev/null @@ -1,171 +0,0 @@ -const SERVICE_NAME = "userModule"; -const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key - -class UserModule { - constructor({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }) { - this.User = User; - this.Team = Team; - this.GenerateAvatarImage = GenerateAvatarImage; - this.ParseBoolean = ParseBoolean; - this.stringService = stringService; - } - - checkSuperadmin = async () => { - try { - const superAdmin = await this.User.findOne({ role: "superadmin" }); - if (superAdmin !== null) { - return true; - } - return false; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "checkSuperadmin"; - throw error; - } - }; - - insertUser = async (userData, imageFile) => { - try { - if (imageFile) { - // 1. Save the full size image - userData.profileImage = { - data: imageFile.buffer, - contentType: imageFile.mimetype, - }; - - // 2. Get the avatar sized image - const avatar = await this.GenerateAvatarImage(imageFile); - userData.avatarImage = avatar; - } - - // Handle creating team if superadmin - if (userData.role.includes("superadmin")) { - const team = new this.Team({ - email: userData.email, - }); - userData.teamId = team._id; - userData.checkTTL = 60 * 60 * 24 * 30; - await team.save(); - } - - const newUser = new this.User(userData); - await newUser.save(); - return await this.User.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find - } catch (error) { - if (error.code === DUPLICATE_KEY_CODE) { - error.message = this.stringService.dbUserExists; - } - error.service = SERVICE_NAME; - error.method = "insertUser"; - throw error; - } - }; - getUserByEmail = async (email) => { - try { - // Need the password to be able to compare, removed .select() - // We can strip the hash before returning the user - const user = await this.User.findOne({ email: email }).select("-profileImage"); - if (!user) { - throw new Error(this.stringService.dbUserNotFound); - } - return user; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUserByEmail"; - throw error; - } - }; - - updateUser = async ({ userId, user, file }) => { - if (!userId) { - throw new Error("No user in request"); - } - - try { - const candidateUser = { ...user }; - - if (this.ParseBoolean(candidateUser.deleteProfileImage) === true) { - candidateUser.profileImage = null; - candidateUser.avatarImage = null; - } else if (file) { - // 1. Save the full size image - candidateUser.profileImage = { - data: file.buffer, - contentType: file.mimetype, - }; - - // 2. Get the avatar sized image - const avatar = await this.GenerateAvatarImage(file); - candidateUser.avatarImage = avatar; - } - - const updatedUser = await this.User.findByIdAndUpdate( - userId, - candidateUser, - { new: true } // Returns updated user instead of pre-update user - ) - .select("-password") - .select("-profileImage"); - return updatedUser; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateUser"; - throw error; - } - }; - deleteUser = async (userId) => { - try { - const deletedUser = await this.User.findByIdAndDelete(userId); - if (!deletedUser) { - throw new Error(this.stringService.dbUserNotFound); - } - return deletedUser; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteUser"; - throw error; - } - }; - - getAllUsers = async () => { - try { - const users = await this.User.find().select("-password").select("-profileImage"); - return users; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllUsers"; - throw error; - } - }; - - getUserById = async (roles, userId) => { - try { - if (!roles.includes("superadmin")) { - throw new Error("User is not a superadmin"); - } - - const user = await this.User.findById(userId).select("-password").select("-profileImage"); - if (!user) { - throw new Error("User not found"); - } - - return user; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUserById"; - throw error; - } - }; - - editUserById = async (userId, user) => { - try { - await this.User.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage"); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editUserById"; - throw error; - } - }; -} - -export default UserModule; diff --git a/server/src/db/v1/utils/seedDb.js b/server/src/db/v1/utils/seedDb.js deleted file mode 100755 index cdfe3675c..000000000 --- a/server/src/db/v1/utils/seedDb.js +++ /dev/null @@ -1,75 +0,0 @@ -import Monitor from "../models/Monitor.js"; -import Check from "../models/Check.js"; -import { logger } from "../../../utils/logger.js"; - -const generateRandomUrl = () => { - const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; - const paths = ["api", "status", "health", "ping", "check"]; - return `https://${domains[Math.floor(Math.random() * domains.length)]}/${paths[Math.floor(Math.random() * paths.length)]}`; -}; - -const generateChecks = (monitorId, teamId, count) => { - const checks = []; - const endTime = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago - const startTime = new Date(endTime - count * 60 * 1000); // count minutes before endTime - - for (let i = 0; i < count; i++) { - const timestamp = new Date(startTime.getTime() + i * 60 * 1000); - const status = Math.random() > 0.05; // 95% chance of being up - - checks.push({ - monitorId, - teamId, - status, - responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms - createdAt: timestamp, - updatedAt: timestamp, - }); - } - - return checks; -}; - -const seedDb = async (userId, teamId) => { - try { - logger.info({ - message: "Deleting all monitors and checks", - service: "seedDb", - method: "seedDb", - }); - await Monitor.deleteMany({}); - await Check.deleteMany({}); - logger.info({ - message: "Adding monitors", - service: "DB", - method: "seedDb", - }); - for (let i = 0; i < 300; i++) { - const monitor = await Monitor.create({ - name: `Monitor ${i}`, - url: generateRandomUrl(), - type: "http", - userId, - teamId, - interval: 60000, - active: false, - }); - logger.info({ - message: `Adding monitor and checks for monitor ${i}`, - service: "DB", - method: "seedDb", - }); - const checks = generateChecks(monitor._id, teamId, 10000); - await Check.insertMany(checks); - } - } catch (error) { - logger.error({ - message: "Error seeding DB", - service: "DB", - method: "seedDb", - stack: error.stack, - }); - } -}; - -export default seedDb; diff --git a/server/src/db/v2/index.ts b/server/src/db/v2/index.ts deleted file mode 100644 index 2cd194283..000000000 --- a/server/src/db/v2/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import mongoose from "mongoose"; - -const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate"; - -export const connectDatabase = async (): Promise => { - try { - await mongoose.connect(MONGODB_URI); - console.log("Connected to MongoDB"); - return true; - } catch (error) { - console.error("MongoDB connection error:", error); - process.exit(1); - } -}; - -export const disconnectDatabase = async (): Promise => { - try { - await mongoose.disconnect(); - console.log("Disconnected from MongoDB"); - return true; - } catch (error) { - console.error("MongoDB disconnection error:", error); - return false; - } -}; diff --git a/server/src/db/v2/models/auth/Role.ts b/server/src/db/v2/models/auth/Role.ts deleted file mode 100644 index 1031de8b2..000000000 --- a/server/src/db/v2/models/auth/Role.ts +++ /dev/null @@ -1,50 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - -export interface IRole extends Document { - _id: Types.ObjectId; - name: string; - description?: string; - permissions: string[]; - isSystem: boolean; - isActive: boolean; - createdAt: Date; - updatedAt: Date; -} - -const roleSchema = new Schema( - { - name: { - type: String, - required: true, - trim: true, - maxlength: 50, - }, - description: { - type: String, - trim: true, - maxlength: 200, - }, - - permissions: [ - { - type: String, - required: true, - }, - ], - isSystem: { - type: Boolean, - default: false, - }, - isActive: { - type: Boolean, - default: true, - }, - }, - { - timestamps: true, - } -); - -roleSchema.index({ name: 1 }, { unique: true }); - -export const Role = mongoose.model("Role_v2", roleSchema); diff --git a/server/src/db/v2/models/auth/User.ts b/server/src/db/v2/models/auth/User.ts deleted file mode 100644 index 25b9794c7..000000000 --- a/server/src/db/v2/models/auth/User.ts +++ /dev/null @@ -1,102 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - -export interface ITokenizedUser { - sub: string; - roles: string[]; -} -export interface IUser extends Document { - _id: Types.ObjectId; - email: string; - firstName: string; - lastName: string; - passwordHash: string; - roles: Types.ObjectId[]; - profile: { - avatar?: string; - bio?: string; - timezone: string; - locale: string; - }; - preferences: { - theme: "light" | "dark" | "system"; - }; - lastLoginAt?: Date; - isActive: boolean; - isVerified: boolean; - createdAt: Date; - updatedAt: Date; -} - -const userSchema = new Schema( - { - email: { - type: String, - required: true, - unique: true, - trim: true, - lowercase: true, - }, - firstName: { - type: String, - required: true, - trim: true, - maxlength: 50, - }, - lastName: { - type: String, - required: true, - trim: true, - maxlength: 50, - }, - passwordHash: { - type: String, - required: true, - }, - roles: [ - { - type: Schema.Types.ObjectId, - ref: "Role_v2", - }, - ], - profile: { - avatar: { - type: String, - }, - bio: { - type: String, - maxlength: 200, - }, - timezone: { - type: String, - default: "UTC", - }, - locale: { - type: String, - default: "en", - }, - }, - preferences: { - theme: { - type: String, - enum: ["light", "dark", "system"], - default: "system", - }, - }, - lastLoginAt: { - type: Date, - }, - isActive: { - type: Boolean, - default: true, - }, - isVerified: { - type: Boolean, - default: false, - }, - }, - { - timestamps: true, - } -); - -export const User = mongoose.model("User_v2", userSchema); diff --git a/server/src/db/v2/models/checks/Check.ts b/server/src/db/v2/models/checks/Check.ts deleted file mode 100644 index 83bbc9f91..000000000 --- a/server/src/db/v2/models/checks/Check.ts +++ /dev/null @@ -1,297 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; -import { MonitorType, MonitorTypes, MonitorStatus, MonitorStatuses } from "../monitors/Monitor.js"; -import type { Response } from "got"; -export type GotTimings = Response["timings"]; -export interface ITimingPhases { - wait: number; - dns: number; - tcp: number; - tls: number; - request: number; - firstByte: number; - download: number; - total: number; -} - -export interface ICpuInfo { - physical_core: number; - logical_core: number; - frequency: number; - current_frequency: number; - temperature: number[]; // per-core temps - free_percent: number; - usage_percent: number; -} - -export interface IMemoryInfo { - total_bytes: number; - available_bytes: number; - used_bytes: number; - usage_percent: number; -} - -export interface IHostInfo { - os?: string; - platform?: string; - kernel_version?: string; - pretty_name?: string; -} - -export interface IDiskInfo { - device: string; - total_bytes: number; - free_bytes: number; - used_bytes: number; - usage_percent: number; - total_inodes?: number; - free_inodes?: number; - used_inodes?: number; - inodes_usage_percent?: number; - read_bytes?: number; - write_bytes?: number; - read_time?: number; - write_time?: number; -} - -export interface INetInfo { - name: string; - bytes_sent: number; - bytes_recv: number; - packets_sent: number; - packets_recv: number; - err_in: number; - err_out: number; - drop_in: number; - drop_out: number; - fifo_in: number; - fifo_out: number; -} - -export interface ICaptureInfo { - version?: string; - mode?: string; -} - -export interface ISystemInfo { - cpu: ICpuInfo; - memory: IMemoryInfo; - disk: IDiskInfo[]; - host: IHostInfo; - net: INetInfo[]; -} - -export interface ILighthouseAudit { - id?: string; - title?: string; - score?: number | null; - displayValue?: string; - numericValue?: number; - numericUnit?: string; -} -export interface ILighthouseCategories { - accessibility?: { score?: number | null }; - "best-practices"?: { score?: number | null }; - seo?: { score?: number | null }; - performance?: { score?: number | null }; -} - -export interface ILighthouseResult { - categories?: ILighthouseCategories; - audits?: Record; -} - -export interface ICheckLighthouseFields { - accessibility: number; - bestPractices: number; - seo: number; - performance: number; - audits: { - cls: ILighthouseAudit; - si: ILighthouseAudit; - fcp: ILighthouseAudit; - lcp: ILighthouseAudit; - tbt: ILighthouseAudit; - }; -} - -export interface ICheck extends Document { - _id: Types.ObjectId; - monitorId: Types.ObjectId; - type: MonitorType; - status: MonitorStatus; - message: string; - responseTime?: number; // in ms - timings?: GotTimings; - httpStatusCode?: number; - errorMessage?: string; - ack: boolean; - ackAt?: Date; - ackBy?: Types.ObjectId; - expiry: Date; - createdAt: Date; - updatedAt: Date; - system?: ISystemInfo; - capture?: ICaptureInfo; - lighthouse?: ICheckLighthouseFields; -} - -const CheckSchema = new Schema( - { - monitorId: { type: Schema.Types.ObjectId, ref: "Monitor_v2", required: true }, - - type: { - type: String, - required: true, - enum: MonitorTypes, - }, - status: { - type: String, - required: true, - enum: MonitorStatuses, - }, - message: { type: String, trim: true }, - responseTime: { type: Number }, - timings: { - start: { type: Date }, - socket: { type: Date }, - lookup: { type: Date }, - connect: { type: Date }, - secureConnect: { type: Date }, - response: { type: Date }, - end: { type: Date }, - phases: { - wait: { type: Number }, - dns: { type: Number }, - tcp: { type: Number }, - tls: { type: Number }, - request: { type: Number }, - firstByte: { type: Number }, - download: { type: Number }, - total: { type: Number }, - }, - }, - system: { - type: { - cpu: { - physical_core: { type: Number }, - logical_core: { type: Number }, - frequency: { type: Number }, - current_frequency: { type: Number }, - temperature: [{ type: Number }], - free_percent: { type: Number }, - usage_percent: { type: Number }, - }, - memory: { - total_bytes: { type: Number }, - available_bytes: { type: Number }, - used_bytes: { type: Number }, - usage_percent: { type: Number }, - }, - disk: [ - { - device: { type: String }, - total_bytes: { type: Number }, - free_bytes: { type: Number }, - used_bytes: { type: Number }, - usage_percent: { type: Number }, - total_inodes: { type: Number }, - free_inodes: { type: Number }, - used_inodes: { type: Number }, - inodes_usage_percent: { type: Number }, - read_bytes: { type: Number }, - write_bytes: { type: Number }, - read_time: { type: Number }, - write_time: { type: Number }, - }, - ], - host: { - os: { type: String }, - platform: { type: String }, - kernel_version: { type: String }, - pretty_name: { type: String }, - }, - net: [ - { - name: { type: String }, - bytes_sent: { type: Number }, - bytes_recv: { type: Number }, - packets_sent: { type: Number }, - packets_recv: { type: Number }, - err_in: { type: Number }, - err_out: { type: Number }, - drop_in: { type: Number }, - drop_out: { type: Number }, - fifo_in: { type: Number }, - fifo_out: { type: Number }, - }, - ], - }, - required: false, - }, - - capture: { - type: { - version: { type: String }, - mode: { type: String }, - }, - required: false, - }, - lighthouse: { - accessibility: { type: Number, required: false }, - bestPractices: { type: Number, required: false }, - seo: { type: Number, required: false }, - performance: { type: Number, required: false }, - audits: { - cls: { - type: Object, - }, - si: { - type: Object, - }, - fcp: { - type: Object, - }, - lcp: { - type: Object, - }, - tbt: { - type: Object, - }, - }, - type: { - accessibility: { type: Number }, - bestPractices: { type: Number }, - seo: { type: Number }, - performance: { type: Number }, - audits: { - cls: { type: Object }, - si: { type: Object }, - fcp: { type: Object }, - lcp: { type: Object }, - tbt: { type: Object }, - }, - }, - required: false, - }, - - httpStatusCode: { type: Number }, - errorMessage: { type: String, trim: true }, - ack: { type: Boolean, required: true, default: false }, - ackAt: { type: Date }, - ackBy: { type: Schema.Types.ObjectId, ref: "User_v2" }, - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, - }, - }, - { timestamps: true } -); - -CheckSchema.index({ monitorId: 1, createdAt: -1 }); -CheckSchema.index({ status: 1 }); -CheckSchema.index({ status: 1, ack: 1 }); -CheckSchema.index({ ack: 1, ackAt: 1 }); -CheckSchema.index({ createdAt: -1 }); - -export const Check = mongoose.model("Check_v2", CheckSchema); diff --git a/server/src/db/v2/models/index.ts b/server/src/db/v2/models/index.ts deleted file mode 100644 index fbc05c10f..000000000 --- a/server/src/db/v2/models/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -export { User } from "./auth/User.js"; -export type { IUser } from "./auth/User.js"; -export type { ITokenizedUser } from "./auth/User.js"; -export { Role } from "./auth/Role.js"; -export type { IRole } from "./auth/Role.js"; -export { connectDatabase, disconnectDatabase } from "../index.js"; -export { Monitor } from "./monitors/Monitor.js"; -export { MonitorStatuses } from "./monitors/Monitor.js"; -export type { IMonitor } from "./monitors/Monitor.js"; -export { Check } from "./checks/Check.js"; -export type { - ICheck, - ISystemInfo, - ICaptureInfo, - INetInfo, - IDiskInfo, - IHostInfo, - IMemoryInfo, - ICpuInfo, - ILighthouseAudit, - ITimingPhases, - ILighthouseCategories, - ILighthouseResult, - ICheckLighthouseFields, -} from "./checks/Check.js"; -export type { IMonitorStats } from "./monitors/MonitorStats.js"; -export { MonitorStats } from "./monitors/MonitorStats.js"; -export type { INotificationChannel } from "./notification-channel/NotificationChannel.js"; -export { NotificationChannel } from "./notification-channel/NotificationChannel.js"; -export type { IMaintenance } from "./maintenance/Maintenance.js"; -export { Maintenance } from "./maintenance/Maintenance.js"; -export type { IInvite } from "./invite/Invite.js"; -export { Invite } from "./invite/Invite.js"; diff --git a/server/src/db/v2/models/invite/Invite.ts b/server/src/db/v2/models/invite/Invite.ts deleted file mode 100644 index 2bc569900..000000000 --- a/server/src/db/v2/models/invite/Invite.ts +++ /dev/null @@ -1,43 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - -export interface IInvite extends Document { - _id: Types.ObjectId; - email: string; - tokenHash: string; - roles: Types.ObjectId[]; - createdBy: Types.ObjectId; - updatedBy: Types.ObjectId; - expiry: Date; - createdAt: Date; - updatedAt: Date; -} - -const InviteSchema = new Schema( - { - email: { - type: String, - required: true, - unique: true, - trim: true, - lowercase: true, - }, - roles: [ - { - type: Schema.Types.ObjectId, - ref: "Role_v2", - required: true, - }, - ], - tokenHash: { type: String, required: true, unique: true }, - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24, - }, - createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, - updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, - }, - { timestamps: true } -); - -export const Invite = mongoose.model("Invite_v2", InviteSchema); diff --git a/server/src/db/v2/models/maintenance/Maintenance.ts b/server/src/db/v2/models/maintenance/Maintenance.ts deleted file mode 100644 index c2ce33a2b..000000000 --- a/server/src/db/v2/models/maintenance/Maintenance.ts +++ /dev/null @@ -1,39 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - -export interface IMaintenance extends Document { - _id: Types.ObjectId; - name: string; - isActive: boolean; - monitors: Types.ObjectId[]; - startTime: Date; - endTime: Date; - createdBy: Types.ObjectId; - updatedBy: Types.ObjectId; - createdAt: Date; - updatedAt: Date; -} - -const MaintenanceSchema = new Schema( - { - name: { type: String, required: true, trim: true }, - isActive: { type: Boolean, required: true, default: true }, - monitors: [ - { - type: Schema.Types.ObjectId, - ref: "Monitor_v2", - required: true, - }, - ], - startTime: { type: Date, required: true }, - endTime: { type: Date, required: true }, - createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, - updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, - }, - { timestamps: true } -); - -MaintenanceSchema.index({ isActive: 1 }); -MaintenanceSchema.index({ startTime: 1 }); -MaintenanceSchema.index({ endTime: 1 }); - -export const Maintenance = mongoose.model("Maintenance_v2", MaintenanceSchema); diff --git a/server/src/db/v2/models/monitors/Monitor.ts b/server/src/db/v2/models/monitors/Monitor.ts deleted file mode 100644 index 52cbae9d8..000000000 --- a/server/src/db/v2/models/monitors/Monitor.ts +++ /dev/null @@ -1,96 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; -import { Check, MonitorStats } from "../index.js"; - -export const MonitorTypes = ["http", "https", "ping", "infrastructure", "pagespeed"] as const; -export type MonitorType = (typeof MonitorTypes)[number]; - -export const MonitorStatuses = ["up", "down", "paused", "initializing"] as const; -export type MonitorStatus = (typeof MonitorStatuses)[number]; -export interface IMonitor extends Document { - _id: Types.ObjectId; - name: string; - url: string; - secret?: string; - type: MonitorType; - interval: number; // in ms - isActive: boolean; - status: MonitorStatus; - n: number; // Number of consecutive successes required to change status - lastCheckedAt?: Date; - latestChecks: { - status: MonitorStatus; - responseTime: number; - checkedAt: Date; - }[]; - notificationChannels?: Types.ObjectId[]; - createdBy: Types.ObjectId; - updatedBy: Types.ObjectId; - createdAt: Date; - updatedAt: Date; -} - -const MonitorSchema = new Schema( - { - name: { type: String, required: true, trim: true, maxlength: 100 }, - url: { type: String, required: true, trim: true }, - secret: { type: String, required: false }, - type: { - type: String, - required: true, - enum: MonitorTypes, - }, - interval: { type: Number, required: true, default: 60000 }, - isActive: { type: Boolean, required: true, default: true }, - status: { - type: String, - required: true, - default: "initializing", - enum: MonitorStatuses, - }, - n: { type: Number, required: true, default: 1 }, - lastCheckedAt: { type: Date }, - latestChecks: { - type: [ - { - status: { - type: String, - required: true, - enum: MonitorStatuses, - }, - responseTime: { type: Number, required: true }, - checkedAt: { type: Date, required: true }, - }, - ], - default: [], - }, - notificationChannels: { - type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel_v2" }], - default: [], - }, - - createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, - updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, - }, - { timestamps: true } -); - -MonitorSchema.pre("deleteOne", { document: true, query: false }, async function (next) { - try { - const monitorId = this._id; - await Check.deleteMany({ monitorId }); - await MonitorStats.deleteMany({ monitorId }); - next(); - } catch (error: any) { - next(error); - } -}); - -MonitorSchema.index({ isActive: 1 }); -MonitorSchema.index({ status: 1 }); -MonitorSchema.index({ type: 1 }); -MonitorSchema.index({ lastCheckedAt: 1 }); -MonitorSchema.index({ isActive: 1, status: 1 }); -MonitorSchema.index({ createdBy: 1 }); -MonitorSchema.index({ updatedBy: 1 }); - -export const Monitor = mongoose.model("Monitor_v2", MonitorSchema); diff --git a/server/src/db/v2/models/monitors/MonitorStats.ts b/server/src/db/v2/models/monitors/MonitorStats.ts deleted file mode 100644 index 56e76f9ab..000000000 --- a/server/src/db/v2/models/monitors/MonitorStats.ts +++ /dev/null @@ -1,77 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; -import { MonitorStatus, MonitorStatuses } from "./Monitor.js"; - -export interface IMonitorStats extends mongoose.Document { - monitorId: mongoose.Types.ObjectId; - avgResponseTime: number; - maxResponseTime: number; - totalChecks: number; - totalUpChecks: number; - totalDownChecks: number; - uptimePercentage: number; - lastCheckTimestamp: number; - lastResponseTime: number; - timeOfLastFailure: number; - currentStreak: number; - currentStreakStatus: MonitorStatus; - currentStreakStartedAt: number; - createdAt: Date; - updatedAt: Date; -} - -const MonitorStatsSchema = new Schema( - { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor_v2", - immutable: true, - index: true, - }, - avgResponseTime: { - type: Number, - default: 0, - }, - maxResponseTime: { - type: Number, - default: 0, - }, - lastResponseTime: { - type: Number, - default: 0, - }, - totalChecks: { - type: Number, - default: 0, - }, - totalUpChecks: { - type: Number, - default: 0, - }, - totalDownChecks: { - type: Number, - default: 0, - }, - uptimePercentage: { - type: Number, - default: 0, - }, - lastCheckTimestamp: { - type: Number, - default: 0, - }, - timeOfLastFailure: { - type: Number, - default: 0, - }, - currentStreak: { type: Number, required: false, default: 0 }, - currentStreakStatus: { - type: String, - required: false, - enum: MonitorStatuses, - }, - currentStreakStartedAt: { type: Number, required: false }, - }, - { timestamps: true } -); - -export const MonitorStats = mongoose.model("MonitorStats_v2", MonitorStatsSchema); diff --git a/server/src/db/v2/models/notification-channel/NotificationChannel.ts b/server/src/db/v2/models/notification-channel/NotificationChannel.ts deleted file mode 100644 index e77639608..000000000 --- a/server/src/db/v2/models/notification-channel/NotificationChannel.ts +++ /dev/null @@ -1,50 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; -export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const; -export type ChannelType = (typeof ChannelTypes)[number]; - -export interface INotificationChannelConfig { - url?: string; // For webhook, slack, discord - emailAddress?: string; // For email -} - -export interface INotificationChannel { - _id: Types.ObjectId; - name: string; - type: ChannelType; - config: INotificationChannelConfig; - isActive: boolean; - createdBy: Types.ObjectId; - updatedBy: Types.ObjectId; - createdAt: Date; - updatedAt: Date; -} - -const NotificationChannelConfigSchema = new Schema( - { - url: { type: String, required: false }, - emailAddress: { type: String, required: false }, - }, - { _id: false, strict: "throw" } -); - -const NotificationChannelSchema = new Schema( - { - name: { type: String, required: true, trim: true }, - type: { - type: String, - required: true, - enum: ChannelTypes, - }, - config: { type: NotificationChannelConfigSchema, required: true }, - isActive: { type: Boolean, required: true, default: true }, - createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, - updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, - }, - { timestamps: true } -); - -NotificationChannelSchema.index({ isActive: 1 }); -NotificationChannelSchema.index({ type: 1 }); -NotificationChannelSchema.index({ type: 1, isActive: 1 }); - -export const NotificationChannel = mongoose.model("NotificationChannel_v2", NotificationChannelSchema); diff --git a/server/src/index.js b/server/src/index.js deleted file mode 100755 index e8b756d0e..000000000 --- a/server/src/index.js +++ /dev/null @@ -1,60 +0,0 @@ -import { initializeServices } from "./config/services.js"; -import { initializeControllers } from "./config/controllers.js"; -import { createApp } from "./app.js"; -import { initShutdownListener } from "./shutdown.js"; -import { fileURLToPath } from "url"; -import path from "path"; -import fs from "fs"; - -import Logger from "./utils/logger.js"; -import SettingsService from "./service/v1/system/settingsService.js"; -import AppSettings from "./db/v1/models/AppSettings.js"; - -const SERVICE_NAME = "Server"; -let logger; - -const startApp = async () => { - // FE path - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8")); - const frontendPath = path.join(__dirname, "..", "public"); - - // Create services - const settingsService = new SettingsService(AppSettings); - const envSettings = settingsService.loadSettings(); - - // Create logger - logger = new Logger({ envSettings }); - - // Initialize services - const services = await initializeServices({ logger, envSettings, settingsService }); - - // Initialize controllers - const controllers = initializeControllers(services); - - const app = createApp({ - services, - controllers, - envSettings, - frontendPath, - openApiSpec, - }); - - const port = envSettings.port || 52345; - const server = app.listen(port, () => { - logger.info({ message: `Server started on port:${port}` }); - }); - - initShutdownListener(server, services); -}; - -startApp().catch((error) => { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "startApp", - stack: error.stack, - }); - process.exit(1); -}); diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 000000000..ded9b0262 --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,42 @@ +import express from "express"; +import { connectDatabase } from "@/db/MongoDB.js"; +import { initServices, initControllers, initRoutes } from "./init/index.js"; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import { addUserContext } from "./middleware/AddUserContext.js"; +import { verifyToken } from "./middleware/VerifyToken.js"; +import { config } from "@/config/index.js"; +import { getChildLogger } from "@/logger/Logger.js"; +import ApiError from "./utils/ApiError.js"; +const indexLogger = getChildLogger("index"); + +const createApp = async () => { + await connectDatabase(); + const services = await initServices(); + const controllers = initControllers(services); + const app = express(); + app.use(express.json()); + app.use(cookieParser()); + app.use( + cors({ + origin: config.ORIGIN, + methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", + allowedHeaders: ["Content-Type", "Authorization", "x-team-id"], + credentials: true, + }) + ); + const routes = initRoutes(controllers, app); + app.use("/api/v1/health", verifyToken, addUserContext, (req, res) => { + res.json({ + status: "OK", + }); + }); + + const port = 52345; + app.listen(port, () => { + indexLogger.info(`Server is running on http://localhost:${port}`); + indexLogger.error(new ApiError("Test error logging", 500)); + }); +}; + +createApp(); diff --git a/server/src/init/controllers.ts b/server/src/init/controllers.ts new file mode 100644 index 000000000..0e063a9ef --- /dev/null +++ b/server/src/init/controllers.ts @@ -0,0 +1,70 @@ +import { + AuthController, + InviteController, + MaintenanceController, + MonitorController, + NotificationChannelController, + QueueController, + TeamController, + RoleController, + TeamMemberController, + ChecksController, + StatusPageController, + DiagnosticController, + RecoveryController, + ProfileController, + IncidentsController, +} from "@/controllers/index.js"; +import { UserService } from "@/services/index.js"; + +export const initControllers = (services: any) => { + const controllers: Record = {}; + + controllers.authController = new AuthController( + services.authService, + services.inviteService + ); + controllers.inviteController = new InviteController(services.inviteService); + controllers.maintenanceController = new MaintenanceController( + services.maintenanceService + ); + controllers.monitorController = new MonitorController( + services.monitorService, + services.checkService, + services.notificationService + ); + controllers.notificationChannelController = new NotificationChannelController( + services.notificationChannelService, + services.notificationService + ); + controllers.queueController = new QueueController(services.jobQueue); + + controllers.teamController = new TeamController(services.teamService); + + controllers.roleController = new RoleController(services.roleService); + + controllers.teamMemberController = new TeamMemberController( + services.teamMemberService + ); + + controllers.checksController = new ChecksController(services.checkService); + controllers.statusPageController = new StatusPageController( + services.statusPageService + ); + controllers.diagnosticController = new DiagnosticController( + services.diagnosticService + ); + controllers.recoveryController = new RecoveryController( + services.recoveryService, + services.authService, + services.emailService + ); + controllers.profileController = new ProfileController(services.userService); + controllers.incidentsController = new IncidentsController( + services.incidentService, + services.notificationService, + services.monitorService + ); + + return controllers; +}; diff --git a/server/src/init/index.ts b/server/src/init/index.ts new file mode 100644 index 000000000..074670b7f --- /dev/null +++ b/server/src/init/index.ts @@ -0,0 +1,3 @@ +export { initControllers } from "./controllers.js"; +export { initServices } from "./services.js"; +export { initRoutes } from "./routes.js"; diff --git a/server/src/init/routes.ts b/server/src/init/routes.ts new file mode 100644 index 000000000..367e49b57 --- /dev/null +++ b/server/src/init/routes.ts @@ -0,0 +1,67 @@ +import type { Express } from "express"; +import { + AuthRoutes, + InviteRoutes, + MaintenanceRoutes, + MonitorRoutes, + NotificationChannelRoutes, + QueueRoutes, + TeamRoutes, + RoleRoutes, + TeamMemberRoutes, + ChecksRoutes, + StatusPageRoutes, + DiagnosticRoutes, + RecoveryRoutes, + ProfileRoutes, + IncidentsRoutes, +} from "@/routes/index.js"; +import { errorHandler } from "@/middleware/ErrorHandler.js"; +export const initRoutes = (controllers: any, app: Express) => { + const authRoutes = new AuthRoutes(controllers.authController); + const inviteRoutes = new InviteRoutes(controllers.inviteController); + const maintenanceRoutes = new MaintenanceRoutes( + controllers.maintenanceController + ); + const monitorRoutes = new MonitorRoutes(controllers.monitorController); + const notificationChannelRoutes = new NotificationChannelRoutes( + controllers.notificationChannelController + ); + const queueRoutes = new QueueRoutes(controllers.queueController); + const teamRoutes = new TeamRoutes(controllers.teamController); + const roleRoutes = new RoleRoutes(controllers.roleController); + const teamMemberRoutes = new TeamMemberRoutes( + controllers.teamMemberController + ); + const checksRoutes = new ChecksRoutes(controllers.checksController); + const statusPageRoutes = new StatusPageRoutes( + controllers.statusPageController + ); + const diagnosticRoutes = new DiagnosticRoutes( + controllers.diagnosticController + ); + + const recoveryRoutes = new RecoveryRoutes(controllers.recoveryController); + const profileRoutes = new ProfileRoutes(controllers.profileController); + const incidentsRoutes = new IncidentsRoutes(controllers.incidentsController); + + app.use("/api/v1/auth", authRoutes.getRouter()); + app.use("/api/v1/invite", inviteRoutes.getRouter()); + app.use("/api/v1/maintenance", maintenanceRoutes.getRouter()); + app.use("/api/v1/monitors", monitorRoutes.getRouter()); + app.use( + "/api/v1/notification-channels", + notificationChannelRoutes.getRouter() + ); + app.use("/api/v1/queue", queueRoutes.getRouter()); + app.use("/api/v1/teams", teamRoutes.getRouter()); + app.use("/api/v1/roles", roleRoutes.getRouter()); + app.use("/api/v1/team-members", teamMemberRoutes.getRouter()); + app.use("/api/v1/checks", checksRoutes.getRouter()); + app.use("/api/v1/status-pages", statusPageRoutes.getRouter()); + app.use("/api/v1/diagnostic", diagnosticRoutes.getRouter()); + app.use("/api/v1/recovery", recoveryRoutes.getRouter()); + app.use("/api/v1/profile", profileRoutes.getRouter()); + app.use("/api/v1/incidents", incidentsRoutes.getRouter()); + app.use(errorHandler); +}; diff --git a/server/src/init/services.ts b/server/src/init/services.ts new file mode 100644 index 000000000..228864e40 --- /dev/null +++ b/server/src/init/services.ts @@ -0,0 +1,97 @@ +import { + CheckService, + InviteService, + MaintenanceService, + MonitorStatsService, + NotificationChannelService, + UserService, + DiscordService, + EmailService, + SlackService, + WebhookService, + NetworkService, + StatusService, + NotificationService, + JobQueue, + JobGenerator, + AuthService, + MonitorService, + QueueService, + ServiceRegistry, + TeamService, + RoleService, + TeamMemberService, + StatusPageService, + DiagnosticService, + RecoveryService, + IncidentService, +} from "@/services/index.js"; + +export const initServices = async () => { + const checkService = new CheckService(); + const inviteService = new InviteService(); + const maintenanceService = new MaintenanceService(); + const monitorStatsService = new MonitorStatsService(); + const notificationChannelService = new NotificationChannelService(); + const userService = new UserService(); + const discordService = new DiscordService(); + const emailService = new EmailService(userService); + const slackService = new SlackService(); + const webhookService = new WebhookService(); + const networkService = new NetworkService(); + const statusService = new StatusService(); + const notificationService = new NotificationService(userService); + const incidentService = new IncidentService(); + const jobGenerator = new JobGenerator( + networkService, + checkService, + monitorStatsService, + statusService, + notificationService, + incidentService, + maintenanceService + ); + const jobQueue = await JobQueue.create(jobGenerator); + const authService = new AuthService(jobQueue); + const monitorService = new MonitorService(jobQueue); + const queueService = new QueueService(jobQueue); + const teamService = new TeamService(jobQueue); + const roleService = new RoleService(); + const teamMemberService = new TeamMemberService(); + const statusPageService = new StatusPageService(); + const diagnosticService = new DiagnosticService(jobQueue); + const recoveryService = new RecoveryService(); + const services = { + checkService, + inviteService, + maintenanceService, + monitorStatsService, + notificationChannelService, + userService, + discordService, + emailService, + slackService, + webhookService, + networkService, + statusService, + notificationService, + jobGenerator, + jobQueue, + authService, + monitorService, + queueService, + teamService, + roleService, + teamMemberService, + statusPageService, + diagnosticService, + recoveryService, + incidentService, + }; + + Object.values(services).forEach((service) => { + ServiceRegistry.register(service.SERVICE_NAME, service); + }); + + return services; +}; diff --git a/server/src/locales/en.json b/server/src/locales/en.json deleted file mode 100755 index 373911a55..000000000 --- a/server/src/locales/en.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "dontHaveAccount": "Don't have account", - "email": "E-mail", - "forgotPassword": "Forgot Password", - "password": "password", - "signUp": "Sign up", - "submit": "Submit", - "title": "Title", - "continue": "Continue", - "enterEmail": "Enter your email", - "authLoginTitle": "Log In", - "authLoginEnterPassword": "Enter your password", - "commonPassword": "Password", - "commonBack": "Back", - "authForgotPasswordTitle": "Forgot password?", - "authForgotPasswordResetPassword": "Reset password", - "createPassword": "Create your password", - "createAPassword": "Create a password", - "authRegisterAlreadyHaveAccount": "Already have an account?", - "commonAppName": "BlueWave Uptime", - "authLoginEnterEmail": "Enter your email", - "authRegisterTitle": "Create an account", - "authRegisterStepOneTitle": "Create your account", - "authRegisterStepOneDescription": "Enter your details to get started", - "authRegisterStepTwoTitle": "Set up your profile", - "authRegisterStepTwoDescription": "Tell us more about yourself", - "authRegisterStepThreeTitle": "Almost done!", - "authRegisterStepThreeDescription": "Review your information", - "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", - "authForgotPasswordSendInstructions": "Send instructions", - "authForgotPasswordBackTo": "Back to", - "authCheckEmailTitle": "Check your email", - "authCheckEmailDescription": "We sent a password reset link to {{email}}", - "authCheckEmailResendEmail": "Resend email", - "authCheckEmailBackTo": "Back to", - "goBackTo": "Go back to", - "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", - "authCheckEmailClickToResend": "Click to resend", - "authSetNewPasswordTitle": "Set new password", - "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", - "authSetNewPasswordNewPassword": "New password", - "authSetNewPasswordConfirmPassword": "Confirm password", - "confirmPassword": "Confirm your password", - "authSetNewPasswordResetPassword": "Reset password", - "authSetNewPasswordBackTo": "Back to", - "authPasswordMustBeAtLeast": "Must be at least", - "authPasswordCharactersLong": "8 characters long", - "authPasswordMustContainAtLeast": "Must contain at least", - "authPasswordSpecialCharacter": "one special character", - "authPasswordOneNumber": "one number", - "authPasswordUpperCharacter": "one upper character", - "authPasswordLowerCharacter": "one lower character", - "authPasswordConfirmAndPassword": "Confirm password and password", - "authPasswordMustMatch": "must match", - "friendlyError": "Something went wrong...", - "unknownError": "An unknown error occurred", - "unauthorized": "Unauthorized access", - "authAdminExists": "Admin already exists", - "authInviteNotFound": "Invite not found", - "unknownService": "Unknown service", - "noAuthToken": "No auth token provided", - "invalidAuthToken": "Invalid auth token", - "expiredAuthToken": "Token expired", - "noRefreshToken": "No refresh token provided", - "invalidRefreshToken": "Invalid refresh token", - "expiredRefreshToken": "Refresh token expired", - "requestNewAccessToken": "Request new access token", - "invalidPayload": "Invalid payload", - "verifyOwnerNotFound": "Document not found", - "verifyOwnerUnauthorized": "Unauthorized access", - "insufficientPermissions": "Insufficient permissions", - "dbUserExists": "User already exists", - "dbUserNotFound": "User not found", - "dbTokenNotFound": "Token not found", - "dbResetPasswordBadMatch": "New password must be different from old password", - "dbFindMonitorById": "Monitor with id ${monitorId} not found", - "dbDeleteChecks": "No checks found for monitor with id ${monitorId}", - "authIncorrectPassword": "Incorrect password", - "authUnauthorized": "Unauthorized access", - "monitorGetById": "Monitor not found", - "monitorGetByUserId": "No monitors found for user", - "jobQueueWorkerClose": "Error closing worker", - "jobQueueDeleteJob": "Job not found in queue", - "jobQueueObliterate": "Error obliterating queue", - "pingCannotResolve": "No response", - "statusPageNotFound": "Status page not found", - "statusPageUrlNotUnique": "Status page url must be unique", - "dockerFail": "Failed to fetch Docker container information", - "dockerNotFound": "Docker container not found", - "portFail": "Failed to connect to port", - "alertCreate": "Alert created successfully", - "alertGetByUser": "Got alerts successfully", - "alertGetByMonitor": "Got alerts by Monitor successfully", - "alertGetById": "Got alert by Id successfully", - "alertEdit": "Alert edited successfully", - "alertDelete": "Alert deleted successfully", - "authCreateUser": "User created successfully", - "authLoginUser": "User logged in successfully", - "authLogoutUser": "User logged out successfully", - "authUpdateUser": "User updated successfully", - "authCreateRecoveryToken": "Recovery token created successfully", - "authVerifyRecoveryToken": "Recovery token verified successfully", - "authResetPassword": "Password reset successfully", - "authAdminCheck": "Admin check completed successfully", - "authDeleteUser": "User deleted successfully", - "authTokenRefreshed": "Auth token is refreshed", - "authGetAllUsers": "Got all users successfully", - "inviteIssued": "Invite sent successfully", - "inviteVerified": "Invite verified successfully", - "checkCreate": "Check created successfully", - "checkGet": "Got checks successfully", - "checkDelete": "Checks deleted successfully", - "checkUpdateTtl": "Checks TTL updated successfully", - "monitorGetAll": "Got all monitors successfully", - "monitorStatsById": "Got monitor stats by Id successfully", - "monitorGetByIdSuccess": "Got monitor by Id successfully", - "monitorGetByTeamId": "Got monitors by Team Id successfully", - "monitorGetByUserIdSuccess": "Got monitor for ${userId} successfully", - "monitorCreate": "Monitor created successfully", - "bulkMonitorsCreate": "Monitors created successfully", - "monitorDelete": "Monitor deleted successfully", - "monitorEdit": "Monitor edited successfully", - "monitorCertificate": "Got monitor certificate successfully", - "monitorDemoAdded": "Successfully added demo monitors", - "queueGetMetrics": "Got metrics successfully", - "queueGetJobs": "Got jobs successfully", - "queueAddJob": "Job added successfully", - "queueObliterate": "Queue obliterated", - "jobQueueDeleteJobSuccess": "Job removed successfully", - "jobQueuePauseJob": "Job paused successfully", - "jobQueueResumeJob": "Job resumed successfully", - "maintenanceWindowGetById": "Got Maintenance Window by Id successfully", - "maintenanceWindowCreate": "Maintenance Window created successfully", - "maintenanceWindowGetByTeam": "Got Maintenance Windows by Team successfully", - "maintenanceWindowDelete": "Maintenance Window deleted successfully", - "maintenanceWindowEdit": "Maintenance Window edited successfully", - "pingSuccess": "Success", - "getAppSettings": "Got app settings successfully", - "updateAppSettings": "Updated app settings successfully", - "statusPageByUrl": "Got status page by url successfully", - "statusPageCreate": "Status page created successfully", - "newTermsAdded": "New terms added to POEditor", - "dockerSuccess": "Docker container status fetched successfully", - "portSuccess": "Port connected successfully", - "monitorPause": "Monitor paused successfully", - "monitorResume": "Monitor resumed successfully", - "statusPageDelete": "Status page deleted successfully", - "statusPageUpdate": "Status page updated successfully", - "statusPageByTeamId": "Got status pages by team id successfully", - "httpNetworkError": "Network error", - "httpNotJson": "Response data is not json", - "httpExpectedValueFail": "Expected value did not match", - "httpJsonPathError": "Failed to parse json data", - "httpEmptyResult": "Result is empty", - "httpMatchSuccess": "Response data match successfully", - "httpMatchFail": "Failed to match response data", - "httpJsonPathFail": "JSONPath did not match expected value", - "webhookSendSuccess": "Webhook notification sent successfully", - "telegramRequiresBotTokenAndChatId": "Telegram notifications require both botToken and chatId", - "webhookUrlRequired": "Webhook URL is required", - "platformRequired": "Platform is required", - "testNotificationFailed": "Failed to send test notification", - "monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: UP\n📟 Status Code: {code}\n\u200B\n", - "monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n", - "sendTestEmail": "Test email sent successfully", - "errorForValidEmailAddress": "A valid recipient email address is required.", - "testEmailSubject": "Test E-mail from Checkmate", - "discordNotification": { - "monitor": "Monitor", - "status": "Status", - "statusCode": "Status Code", - "time": "Time", - "up": "UP", - "down": "DOWN", - "checkmate": "Checkmate", - "url": "URL", - "unknown": "Unknown", - "uptimeAlert": "Uptime Alert: Your monitor {monitorName} is back online", - "downtimeAlert": "Downtime Alert: Your monitor {monitorName} went offline" - } -} diff --git a/server/src/locales/en.json.bak b/server/src/locales/en.json.bak deleted file mode 100755 index eaef58eef..000000000 --- a/server/src/locales/en.json.bak +++ /dev/null @@ -1,147 +0,0 @@ -{ - "dontHaveAccount": "Don't have account", - "email": "E-mail", - "forgotPassword": "Forgot Password", - "password": "password", - "signUp": "Sign up", - "submit": "Submit", - "title": "Title", - "continue": "Continue", - "enterEmail": "Enter your email", - "authLoginTitle": "Log In", - "authLoginEnterPassword": "Enter your password", - "commonPassword": "Password", - "commonBack": "Back", - "authForgotPasswordTitle": "Forgot password?", - "authForgotPasswordResetPassword": "Reset password", - "createPassword": "Create your password", - "createAPassword": "Create a password", - "authRegisterAlreadyHaveAccount": "Already have an account?", - "commonAppName": "BlueWave Uptime", - "authLoginEnterEmail": "Enter your email", - "authRegisterTitle": "Create an account", - "authRegisterStepOneTitle": "Create your account", - "authRegisterStepOneDescription": "Enter your details to get started", - "authRegisterStepTwoTitle": "Set up your profile", - "authRegisterStepTwoDescription": "Tell us more about yourself", - "authRegisterStepThreeTitle": "Almost done!", - "authRegisterStepThreeDescription": "Review your information", - "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", - "authForgotPasswordSendInstructions": "Send instructions", - "authForgotPasswordBackTo": "Back to", - "authCheckEmailTitle": "Check your email", - "authCheckEmailDescription": "We sent a password reset link to {{email}}", - "authCheckEmailResendEmail": "Resend email", - "authCheckEmailBackTo": "Back to", - "goBackTo": "Go back to", - "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", - "authCheckEmailClickToResend": "Click to resend", - "authSetNewPasswordTitle": "Set new password", - "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", - "authSetNewPasswordNewPassword": "New password", - "authSetNewPasswordConfirmPassword": "Confirm password", - "confirmPassword": "Confirm your password", - "authSetNewPasswordResetPassword": "Reset password", - "authSetNewPasswordBackTo": "Back to", - "authPasswordMustBeAtLeast": "Must be at least", - "authPasswordCharactersLong": "8 characters long", - "authPasswordMustContainAtLeast": "Must contain at least", - "authPasswordSpecialCharacter": "one special character", - "authPasswordOneNumber": "one number", - "authPasswordUpperCharacter": "one upper character", - "authPasswordLowerCharacter": "one lower character", - "authPasswordConfirmAndPassword": "Confirm password and password", - "authPasswordMustMatch": "must match", - "friendlyError": "Something went wrong...", - "unknownError": "An unknown error occurred", - "unauthorized": "Unauthorized access", - "authAdminExists": "Admin already exists", - "authInviteNotFound": "Invite not found", - "unknownService": "Unknown service", - "noAuthToken": "No auth token provided", - "invalidAuthToken": "Invalid auth token", - "expiredAuthToken": "Token expired", - "noRefreshToken": "No refresh token provided", - "invalidRefreshToken": "Invalid refresh token", - "expiredRefreshToken": "Refresh token expired", - "requestNewAccessToken": "Request new access token", - "invalidPayload": "Invalid payload", - "verifyOwnerNotFound": "Document not found", - "verifyOwnerUnauthorized": "Unauthorized access", - "insufficientPermissions": "Insufficient permissions", - "dbUserExists": "User already exists", - "dbUserNotFound": "User not found", - "dbTokenNotFound": "Token not found", - "dbResetPasswordBadMatch": "New password must be different from old password", - "dbFindMonitorById": "Monitor with id ${monitorId} not found", - "dbDeleteChecks": "No checks found for monitor with id ${monitorId}", - "authIncorrectPassword": "Incorrect password", - "authUnauthorized": "Unauthorized access", - "monitorGetById": "Monitor not found", - "monitorGetByUserId": "No monitors found for user", - "jobQueueWorkerClose": "Error closing worker", - "jobQueueDeleteJob": "Job not found in queue", - "jobQueueObliterate": "Error obliterating queue", - "pingCannotResolve": "No response", - "statusPageNotFound": "Status page not found", - "statusPageUrlNotUnique": "Status page url must be unique", - "dockerFail": "Failed to fetch Docker container information", - "dockerNotFound": "Docker container not found", - "portFail": "Failed to connect to port", - "alertCreate": "Alert created successfully", - "alertGetByUser": "Got alerts successfully", - "alertGetByMonitor": "Got alerts by Monitor successfully", - "alertGetById": "Got alert by Id successfully", - "alertEdit": "Alert edited successfully", - "alertDelete": "Alert deleted successfully", - "authCreateUser": "User created successfully", - "authLoginUser": "User logged in successfully", - "authLogoutUser": "User logged out successfully", - "authUpdateUser": "User updated successfully", - "authCreateRecoveryToken": "Recovery token created successfully", - "authVerifyRecoveryToken": "Recovery token verified successfully", - "authResetPassword": "Password reset successfully", - "authAdminCheck": "Admin check completed successfully", - "authDeleteUser": "User deleted successfully", - "authTokenRefreshed": "Auth token is refreshed", - "authGetAllUsers": "Got all users successfully", - "inviteIssued": "Invite sent successfully", - "inviteVerified": "Invite verified successfully", - "checkCreate": "Check created successfully", - "checkGet": "Got checks successfully", - "checkDelete": "Checks deleted successfully", - "checkUpdateTtl": "Checks TTL updated successfully", - "monitorGetAll": "Got all monitors successfully", - "monitorStatsById": "Got monitor stats by Id successfully", - "monitorGetByIdSuccess": "Got monitor by Id successfully", - "monitorGetByTeamId": "Got monitors by Team Id successfully", - "monitorGetByUserIdSuccess": "Got monitor for ${userId} successfully", - "monitorCreate": "Monitor created successfully", - "monitorDelete": "Monitor deleted successfully", - "monitorEdit": "Monitor edited successfully", - "monitorCertificate": "Got monitor certificate successfully", - "monitorDemoAdded": "Successfully added demo monitors", - "queueGetMetrics": "Got metrics successfully", - "queueAddJob": "Job added successfully", - "queueObliterate": "Queue obliterated", - "jobQueueDeleteJobSuccess": "Job removed successfully", - "jobQueuePauseJob": "Job paused successfully", - "jobQueueResumeJob": "Job resumed successfully", - "maintenanceWindowGetById": "Got Maintenance Window by Id successfully", - "maintenanceWindowCreate": "Maintenance Window created successfully", - "maintenanceWindowGetByTeam": "Got Maintenance Windows by Team successfully", - "maintenanceWindowDelete": "Maintenance Window deleted successfully", - "maintenanceWindowEdit": "Maintenance Window edited successfully", - "pingSuccess": "Success", - "getAppSettings": "Got app settings successfully", - "updateAppSettings": "Updated app settings successfully", - "statusPageByUrl": "Got status page by url successfully", - "statusPageCreate": "Status page created successfully", - "newTermsAdded": "New terms added to POEditor", - "dockerSuccess": "Docker container status fetched successfully", - "portSuccess": "Port connected successfully", - "monitorPause": "Monitor paused successfully", - "monitorResume": "Monitor resumed successfully", - "statusPageDelete": "Status page deleted successfully", - "statusPageUpdate": "Status page updated successfully" -} diff --git a/server/src/locales/tr.json b/server/src/locales/tr.json deleted file mode 100755 index cc1ef4c64..000000000 --- a/server/src/locales/tr.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "dontHaveAccount": "Hesabınız yok mu", - "email": "E-posta", - "forgotPassword": "Parolamı Unuttum", - "password": "Parola", - "signUp": "Kayıt ol", - "submit": "Gönder", - "title": "Başlık", - "continue": "Devam et", - "enterEmail": "E-posta adresinizi girin", - "authLoginTitle": "Giriş Yap", - "authLoginEnterPassword": "Parolanızı girin", - "commonPassword": "Parola", - "commonBack": "Geri", - "authForgotPasswordTitle": "Parolanı mi unuttun?", - "authForgotPasswordResetPassword": "Parola sıfırla", - "createPassword": "Parolanızı oluşturun", - "createAPassword": "Bir parola oluşturun", - "authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?", - "commonAppName": "BlueWave Uptime", - "authLoginEnterEmail": "E-posta adresinizi girin", - "authRegisterTitle": "Hesap oluştur", - "authRegisterStepOneTitle": "Hesabınızı oluşturun", - "authRegisterStepOneDescription": "Başlamak için bilgilerinizi girin", - "authRegisterStepTwoTitle": "Profilinizi ayarlayın", - "authRegisterStepTwoDescription": "Kendiniz hakkında daha fazla bilgi verin", - "authRegisterStepThreeTitle": "Neredeyse bitti!", - "authRegisterStepThreeDescription": "Bilgilerinizi gözden geçirin", - "authForgotPasswordDescription": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.", - "authForgotPasswordSendInstructions": "Talimatları gönder", - "authForgotPasswordBackTo": "Geri dön", - "authCheckEmailTitle": "E-postanızı kontrol edin", - "authCheckEmailDescription": "{{email}} adresine bir şifre sıfırlama bağlantısı gönderdik", - "authCheckEmailResendEmail": "E-postayı yeniden gönder", - "authCheckEmailBackTo": "Geri dön", - "goBackTo": "Geri dön", - "authCheckEmailDidntReceiveEmail": "E-postayı almadınız mı?", - "authCheckEmailClickToResend": "Yeniden göndermek için tıklayın", - "authSetNewPasswordTitle": "Yeni şifre belirleyin", - "authSetNewPasswordDescription": "Yeni şifreniz, daha önce kullanılan şifrelerden farklı olmalıdır.", - "authSetNewPasswordNewPassword": "Yeni şifre", - "authSetNewPasswordConfirmPassword": "Parolayı onayla", - "confirmPassword": "Parolanızı onaylayın", - "authSetNewPasswordResetPassword": "Parolayı sıfırla", - "authSetNewPasswordBackTo": "Geri dön", - "authPasswordMustBeAtLeast": "En az", - "authPasswordCharactersLong": "8 karakter uzunluğunda olmalı", - "authPasswordMustContainAtLeast": "En az içermeli", - "authPasswordSpecialCharacter": "bir özel karakter", - "authPasswordOneNumber": "bir rakam", - "authPasswordUpperCharacter": "bir büyük harf", - "authPasswordLowerCharacter": "bir küçük harf", - "authPasswordConfirmAndPassword": "Onay şifresi ve şifre", - "authPasswordMustMatch": "eşleşmelidir", - "friendlyError": "Bir şeyler yanlış gitti...", - "unknownError": "Bilinmeyen bir hata oluştu", - "unauthorized": "Yetkisiz erişim", - "authAdminExists": "Yönetici zaten mevcut", - "authInviteNotFound": "Davet bulunamadı", - "unknownService": "Bilinmeyen servis", - "noAuthToken": "Kimlik doğrulama belirteci sağlanmadı", - "invalidAuthToken": "Geçersiz kimlik doğrulama belirteci", - "expiredAuthToken": "Belirteç süresi doldu", - "noRefreshToken": "Yenileme belirteci sağlanmadı", - "invalidRefreshToken": "Geçersiz yenileme belirteci", - "expiredRefreshToken": "Yenileme belirteci süresi doldu", - "requestNewAccessToken": "Yeni erişim belirteci isteyin", - "invalidPayload": "Geçersiz veri", - "verifyOwnerNotFound": "Belge bulunamadı", - "verifyOwnerUnauthorized": "Yetkisiz erişim", - "insufficientPermissions": "Yetersiz izinler", - "dbUserExists": "Kullanıcı zaten mevcut", - "dbUserNotFound": "Kullanıcı bulunamadı", - "dbTokenNotFound": "Belirteç bulunamadı", - "dbResetPasswordBadMatch": "Yeni şifre eski şifreden farklı olmalıdır", - "dbFindMonitorById": "${monitorId} kimlikli monitör bulunamadı", - "dbDeleteChecks": "${monitorId} kimlikli monitör için kontrol bulunamadı", - "authIncorrectPassword": "Geçersiz parola", - "authUnauthorized": "Yetkisiz erişim", - "monitorGetById": "Monitör bulunamadı", - "monitorGetByUserId": "Kullanıcı için monitör bulunamadı", - "jobQueueWorkerClose": "İşçi kapatılırken hata oluştu", - "jobQueueDeleteJob": "İş kuyrukta bulunamadı", - "jobQueueObliterate": "Kuyruk yok edilirken hata oluştu", - "pingCannotResolve": "Yanıt yok", - "statusPageNotFound": "Durum sayfası bulunamadı", - "statusPageUrlNotUnique": "Durum sayfası URL'si benzersiz olmalıdır", - "dockerFail": "Docker konteyner bilgisi alınamadı", - "dockerNotFound": "Docker konteyner bulunamadı", - "portFail": "Porta bağlanılamadı", - "alertCreate": "Uyarı başarıyla oluşturuldu", - "alertGetByUser": "Uyarılar başarıyla alındı", - "alertGetByMonitor": "Monitöre göre uyarılar başarıyla alındı", - "alertGetById": "Kimliğe göre uyarı başarıyla alındı", - "alertEdit": "Uyarı başarıyla düzenlendi", - "alertDelete": "Uyarı başarıyla silindi", - "authCreateUser": "Kullanıcı başarıyla oluşturuldu", - "authLoginUser": "Kullanıcı başarıyla giriş yaptı", - "authLogoutUser": "Kullanıcı başarıyla çıkış yaptı", - "authUpdateUser": "Kullanıcı başarıyla güncellendi", - "authCreateRecoveryToken": "Kurtarma belirteci başarıyla oluşturuldu", - "authVerifyRecoveryToken": "Kurtarma belirteci başarıyla doğrulandı", - "authResetPassword": "Şifre başarıyla sıfırlandı", - "authAdminCheck": "Yönetici kontrolü başarıyla tamamlandı", - "authDeleteUser": "Kullanıcı başarıyla silindi", - "authTokenRefreshed": "Kimlik doğrulama belirteci yenilendi", - "authGetAllUsers": "Tüm kullanıcılar başarıyla alındı", - "inviteIssued": "Davet başarıyla gönderildi", - "inviteVerified": "Davet başarıyla doğrulandı", - "checkCreate": "Kontrol başarıyla oluşturuldu", - "checkGet": "Kontroller başarıyla alındı", - "checkDelete": "Kontroller başarıyla silindi", - "checkUpdateTtl": "Kontrol TTL başarıyla güncellendi", - "monitorGetAll": "Tüm monitörler başarıyla alındı", - "monitorStatsById": "Kimliğe göre monitör istatistikleri başarıyla alındı", - "monitorGetByIdSuccess": "Kimliğe göre monitör başarıyla alındı", - "monitorGetByTeamId": "Takım kimliğine göre monitörler başarıyla alındı", - "monitorGetByUserIdSuccess": "${userId} için monitör başarıyla alındı", - "monitorCreate": "Monitör başarıyla oluşturuldu", - "monitorDelete": "Monitör başarıyla silindi", - "monitorEdit": "Monitör başarıyla düzenlendi", - "monitorCertificate": "Monitör sertifikası başarıyla alındı", - "monitorDemoAdded": "Demo monitörler başarıyla eklendi", - "queueGetMetrics": "Metrikler başarıyla alındı", - "queueAddJob": "İş başarıyla eklendi", - "queueObliterate": "Kuyruk yok edildi", - "jobQueueDeleteJobSuccess": "İş başarıyla kaldırıldı", - "jobQueuePauseJob": "İş başarıyla duraklatıldı", - "jobQueueResumeJob": "İş başarıyla devam ettirildi", - "maintenanceWindowGetById": "Kimliğe göre bakım penceresi başarıyla alındı", - "maintenanceWindowCreate": "Bakım penceresi başarıyla oluşturuldu", - "maintenanceWindowGetByTeam": "Takıma göre bakım pencereleri başarıyla alındı", - "maintenanceWindowDelete": "Bakım penceresi başarıyla silindi", - "maintenanceWindowEdit": "Bakım penceresi başarıyla düzenlendi", - "pingSuccess": "Başarılı", - "getAppSettings": "Uygulama ayarları başarıyla alındı", - "updateAppSettings": "Uygulama ayarları başarıyla güncellendi", - "statusPageByUrl": "URL'ye göre durum sayfası başarıyla alındı", - "statusPageCreate": "Durum sayfası başarıyla oluşturuldu", - "dockerSuccess": "Docker konteyner durumu başarıyla alındı", - "portSuccess": "Porta başarıyla bağlanıldı", - "newTermsAdded": "POEditor'a yeni terimler eklendi", - "monitorPause": "Monitör başarıyla duraklatıldı", - "monitorResume": "Monitör başarıyla devam ettirildi", - "monitorDelete2": "" -} diff --git a/server/src/logger/Logger.ts b/server/src/logger/Logger.ts new file mode 100644 index 000000000..996cbf701 --- /dev/null +++ b/server/src/logger/Logger.ts @@ -0,0 +1,51 @@ +import winston from "winston"; +import MemoryTransport from "@/logger/MemoryTransport.js"; +import { config } from "@/config/index.js"; + +const { combine, timestamp, errors, colorize, printf, json } = winston.format; + +const buildLogMsg = (info: winston.Logform.TransformableInfo) => { + if (!info) return "Logging error: no info provided"; + const timestamp = info.timestamp; + const level = info.level; + const message = info.message; + const service = info.service; + const stack = info.stack || ""; + return `[${timestamp}]${ + service ? ` [${service}]` : "" + } ${level}: ${message} ${stack}`; +}; + +export const memoryTransport = new MemoryTransport({ + maxItems: Number(process.env.IN_MEMORY_LOG_LIMIT) || 100, + format: combine(timestamp(), errors({ stack: true }), json()), +}); + +const logger = winston.createLogger({ + level: config.LOG_LEVEL || "info", + + transports: [ + new winston.transports.Console({ + format: combine( + timestamp(), + colorize({ all: true }), + errors({ stack: true }), + printf((info) => { + return buildLogMsg(info); + }) + ), + }), + memoryTransport, + new winston.transports.File({ + filename: "logs/server.log", + level: "warn", + format: combine(timestamp(), json()), + }), + ], +}); + +export const getChildLogger = (serviceName: string): winston.Logger => { + return logger.child({ service: serviceName }); +}; + +export default logger; diff --git a/server/src/logger/MemoryTransport.ts b/server/src/logger/MemoryTransport.ts new file mode 100644 index 000000000..6af33caa2 --- /dev/null +++ b/server/src/logger/MemoryTransport.ts @@ -0,0 +1,33 @@ +import Transport from "winston-transport"; + +interface MemoryCacheTransportOptions extends Transport.TransportStreamOptions { + maxItems?: number; +} + +export default class MemoryTransport extends Transport { + private logs: any[] = []; + private maxItems: number; + + constructor(opts?: MemoryCacheTransportOptions) { + super(opts); + this.maxItems = opts?.maxItems || 500; + } + + log = (info: any, callback: () => void) => { + setImmediate(() => this.emit("logged", info)); + this.logs.unshift(info); + if (this.logs.length > this.maxItems) { + this.logs.shift(); + } + callback(); + }; + + getLogs = (limit?: number) => { + if (!limit) return this.logs; + return this.logs.slice(-limit); + }; + + clear() { + this.logs = []; + } +} diff --git a/server/src/middleware/AddUserContext.ts b/server/src/middleware/AddUserContext.ts new file mode 100644 index 000000000..fc4eb29cd --- /dev/null +++ b/server/src/middleware/AddUserContext.ts @@ -0,0 +1,148 @@ +import { Request, Response, NextFunction } from "express"; +import { OrgMembership, TeamMembership, IRole } from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; + +interface IRolesObj { + orgRole?: IRole; + teamRole: IRole; +} +interface IRolesCacheData { + data: IRolesObj; + expiresAt: number; +} + +interface ITeamIdsCacheData { + data: string[]; + expiresAt: number; +} + +const CACHE_TTL = 5 * 60 * 1000; +const rolesCache = new Map(); +const teamIdsCache = new Map(); + +export const invalidateCachesForUser = (userId: string) => { + for (const key of rolesCache.keys()) { + if (key.startsWith(userId + ":")) { + rolesCache.delete(key); + } + } + + for (const key of teamIdsCache.keys()) { + if (key.startsWith(userId)) { + teamIdsCache.delete(key); + } + } +}; + +const getTeamIds = async (userId: string): Promise => { + // Try to get from cache + if (teamIdsCache.has(userId)) { + const cached = teamIdsCache.get(userId); + + if (!cached) { + throw new ApiError("TeamID cache retrieval error", 500); + } + + if (cached.expiresAt > Date.now()) { + return cached.data; + } + } + + // If not in cache or expired, fetch from DB + const teamMemberships = await TeamMembership.find({ userId }) + .select("teamId") + .lean(); + const teamIds = teamMemberships.map((t) => t.teamId.toString()); + teamIdsCache.set(userId, { + data: teamIds, + expiresAt: Date.now() + CACHE_TTL, + }); + return teamIds; +}; + +const getRoles = async ( + userId: string, + orgId: string, + currentTeamId: string +) => { + // Try to get from cache + if (rolesCache.has(`${userId}:${orgId}:${currentTeamId}`)) { + const cached = rolesCache.get(`${userId}:${orgId}:${currentTeamId}`); + + if (!cached) { + throw new ApiError("Roles cache retrieval error", 500); + } + + if (cached.expiresAt > Date.now()) { + return cached.data; + } + } + + const [orgRole, teamRole] = await Promise.all([ + OrgMembership.findOne({ userId, orgId }) + .populate<{ roleId: IRole }>("roleId") + .select("roleId") + .lean(), + + TeamMembership.findOne({ userId, teamId: currentTeamId }) + .populate<{ roleId: IRole }>("roleId") + .select("roleId") + .lean(), + ]); + + if (!orgRole) { + throw new ApiError("User has no role in the organization", 403); + } + + if (!teamRole) { + throw new ApiError("User has no role in the team", 403); + } + + const cacheData = { orgRole: orgRole.roleId, teamRole: teamRole.roleId }; + + rolesCache.set(`${userId}:${orgId}:${currentTeamId}`, { + data: cacheData, + expiresAt: Date.now() + CACHE_TTL, + }); + return cacheData; +}; + +export const addUserContext = async ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + if (!req.user) { + throw new ApiError("User not authenticated", 401); + } + + // Get identifiers + const sub = req.user.sub; + const orgId = req.user.orgId; + const currentTeamId = req.headers["x-team-id"] as string; + + // Get user team IDs + const teamIds = await getTeamIds(sub); + + // Short circuit if user doesn't belong to selected team + if (!teamIds.includes(currentTeamId)) { + throw new ApiError("User does not belong to the selected team", 403); + } + + // Get user roles to context + const { orgRole, teamRole } = await getRoles(sub, orgId, currentTeamId); + + // Populate user context + req.user.teamIds = teamIds; + req.user.roles = { + orgRole, + teamRole, + }; + req.user.currentTeamId = currentTeamId; + + return next(); + } catch (error) { + next(error); + } +}; diff --git a/server/src/middleware/ErrorHandler.ts b/server/src/middleware/ErrorHandler.ts new file mode 100644 index 000000000..77999105d --- /dev/null +++ b/server/src/middleware/ErrorHandler.ts @@ -0,0 +1,18 @@ +import { Request, Response, NextFunction } from "express"; +import { getChildLogger } from "@/logger/Logger.js"; +import ApiError from "@/utils/ApiError.js"; +const logger = getChildLogger("ErrorHandler"); + +export const errorHandler = ( + err: Error | ApiError, + req: Request, + res: Response, + next: NextFunction +) => { + logger.error(err); + if (err instanceof ApiError) { + res.status(err.status).json({ message: err.message }); + } else { + res.status(500).json({ message: err.message || "Internal Server Error" }); + } +}; diff --git a/server/src/middleware/VerifyPermission.ts b/server/src/middleware/VerifyPermission.ts new file mode 100644 index 000000000..657c93024 --- /dev/null +++ b/server/src/middleware/VerifyPermission.ts @@ -0,0 +1,70 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "@/utils/ApiError.js"; +import { TeamMembership } from "@/db/models/index.js"; +import type { IRole } from "@/db/models/index.js"; +export const hasPermission = ( + permissions: string[], + requiredPermissions: string[] +) => { + if (permissions.includes("*")) return true; + + const matches = (requiredPermission: string, userPermission: string) => { + if (userPermission === requiredPermission) return true; + if (userPermission.endsWith(".*")) { + const prefix = userPermission.slice(0, -2); + return requiredPermission.startsWith(prefix + "."); + } + return false; + }; + + return requiredPermissions.every((requiredPermission) => { + return permissions.some((permission) => + matches(requiredPermission, permission) + ); + }); +}; + +const verifyTeamPermission = (resourceActions: string[]) => { + return async (req: Request, res: Response, next: NextFunction) => { + const tokenizedUser = req.user; + + if (!tokenizedUser) { + throw new ApiError("No user", 400); + } + + const userId = tokenizedUser.sub; + if (!userId) { + throw new ApiError("No user ID", 400); + } + + const orgPermissions = tokenizedUser.roles?.orgRole?.permissions || []; + const teamPermissions = tokenizedUser.roles?.teamRole.permissions || []; + + const allPermissions = [...orgPermissions, ...teamPermissions]; + + const allowed = hasPermission(allPermissions, resourceActions); + if (!allowed) { + throw new ApiError("Insufficient permissions", 403); + } + next(); + }; +}; + +const verifyOrgPermission = (resourceActions: string[]) => { + return async (req: Request, res: Response, next: NextFunction) => { + const userContext = req.user; + + if (!userContext) { + throw new ApiError("No user", 400); + } + + const orgPermissions = userContext.roles?.orgRole?.permissions || []; + const allowed = hasPermission(orgPermissions, resourceActions); + if (!allowed) { + throw new ApiError("Insufficient permissions", 403); + } + next(); + }; +}; + +export { verifyTeamPermission, verifyOrgPermission }; diff --git a/server/src/middleware/VerifyToken.ts b/server/src/middleware/VerifyToken.ts new file mode 100644 index 000000000..2df15e408 --- /dev/null +++ b/server/src/middleware/VerifyToken.ts @@ -0,0 +1,20 @@ +import { Request, Response, NextFunction } from "express"; +import { decode } from "@/utils/JWTUtils.js"; +import ApiError from "@/utils/ApiError.js"; +const verifyToken = (req: Request, res: Response, next: NextFunction) => { + const token = req.cookies.token; + if (!token) { + const error = new ApiError("No token provided", 401); + return next(error); + } + + try { + const decoded = decode(token); + req.user = decoded; + next(); + } catch (error) { + next(error); + } +}; + +export { verifyToken }; diff --git a/server/src/middleware/v1/handleErrors.js b/server/src/middleware/v1/handleErrors.js deleted file mode 100755 index 43671fbe2..000000000 --- a/server/src/middleware/v1/handleErrors.js +++ /dev/null @@ -1,22 +0,0 @@ -import { logger } from "../../utils/logger.js"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; - -const handleErrors = (error, req, res, next) => { - const status = error.status || 500; - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - const message = error.message || stringService.authIncorrectPassword; - const service = error.service || stringService.unknownService; - logger.error({ - message: message, - service: service, - method: error.method, - stack: error.stack, - }); - res.error({ - status, - msg: message, - }); -}; - -export { handleErrors }; diff --git a/server/src/middleware/v1/isAllowed.js b/server/src/middleware/v1/isAllowed.js deleted file mode 100755 index cc240f531..000000000 --- a/server/src/middleware/v1/isAllowed.js +++ /dev/null @@ -1,57 +0,0 @@ -import jwt from "jsonwebtoken"; -const TOKEN_PREFIX = "Bearer "; -const SERVICE_NAME = "allowedRoles"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; -import SettingsService from "../../service/v1/system/settingsService.js"; - -const isAllowed = (allowedRoles) => { - return (req, res, next) => { - const token = req.headers["authorization"]; - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - // If no token is pressent, return an error - if (!token) { - const error = new Error(stringService.noAuthToken); - error.status = 401; - error.service = SERVICE_NAME; - next(error); - return; - } - - // If the token is improperly formatted, return an error - if (!token.startsWith(TOKEN_PREFIX)) { - const error = new Error(stringService.invalidAuthToken); - error.status = 400; - error.service = SERVICE_NAME; - next(error); - return; - } - // Parse the token - try { - const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); - const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); - var decoded = jwt.verify(parsedToken, jwtSecret); - const userRoles = decoded.role; - - // Check if the user has the required role - if (userRoles.some((role) => allowedRoles.includes(role))) { - next(); - return; - } else { - const error = new Error(stringService.insufficientPermissions); - error.status = 403; - error.service = SERVICE_NAME; - next(error); - return; - } - } catch (error) { - error.status = 401; - error.method = "isAllowed"; - error.service = SERVICE_NAME; - next(error); - return; - } - }; -}; - -export { isAllowed }; diff --git a/server/src/middleware/v1/languageMiddleware.js b/server/src/middleware/v1/languageMiddleware.js deleted file mode 100755 index 819858a21..000000000 --- a/server/src/middleware/v1/languageMiddleware.js +++ /dev/null @@ -1,27 +0,0 @@ -import { logger } from "../../utils/logger.js"; - -const languageMiddleware = (stringService, translationService) => async (req, res, next) => { - try { - const acceptLanguage = req.headers["accept-language"] || "en"; - const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase(); - - translationService.setLanguage(language); - stringService.setLanguage(language); - - next(); - } catch (error) { - logger.error({ - message: error.message, - service: "languageMiddleware", - }); - const acceptLanguage = req.headers["accept-language"] || "en"; - const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase(); - - translationService.setLanguage(language); - stringService.setLanguage(language); - - next(); - } -}; - -export default languageMiddleware; diff --git a/server/src/middleware/v1/rateLimiter.js b/server/src/middleware/v1/rateLimiter.js deleted file mode 100644 index 70b080d23..000000000 --- a/server/src/middleware/v1/rateLimiter.js +++ /dev/null @@ -1,17 +0,0 @@ -import rateLimit from "express-rate-limit"; - -export const generalApiLimiter = rateLimit({ - window: 60 * 1000, - limit: 600, - standardHeaders: true, - legacyHeaders: false, - ipv6Subnet: 64, -}); - -export const authApiLimiter = rateLimit({ - window: 60 * 1000, - limit: 15, - standardHeaders: true, - legacyHeaders: false, - ipv6Subnet: 64, -}); diff --git a/server/src/middleware/v1/responseHandler.js b/server/src/middleware/v1/responseHandler.js deleted file mode 100755 index 5169e7160..000000000 --- a/server/src/middleware/v1/responseHandler.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Middleware that adds standardized response methods to the Express response object. - * This allows for consistent API responses throughout the application. - * - * @param {import('express').Request} req - Express request object - * @param {import('express').Response} res - Express response object - * @param {import('express').NextFunction} next - Express next middleware function - */ -const responseHandler = (req, res, next) => { - /** - * Sends a standardized success response - * - * @param {Object} options - Success response options - * @param {number} [options.status=200] - HTTP status code - * @param {string} [options.msg="OK"] - Success message - * @param {*} [options.data=null] - Response data payload - * @returns {Object} Express response object - */ - res.success = ({ status = 200, msg = "OK", data = null, headers = {} }) => { - // Set custom headers if provided - Object.entries(headers).forEach(([key, value]) => { - res.set(key, value); - }); - - return res.status(status).json({ - success: true, - msg: msg, - data: data, - }); - }; - - /** - * Sends a standardized error response - * - * @param {Object} options - Error response options - * @param {number} [options.status=500] - HTTP status code - * @param {string} [options.msg="Internal server error"] - Error message - * @param {*} [options.data=null] - Additional error data (if any) - * @returns {Object} Express response object - */ - res.error = ({ status = 500, msg = "Internal server error", data = null }) => { - return res.status(status).json({ - success: false, - msg, - data, - }); - }; - - /** - * Sends a raw file response (for CSV, PDF, etc.) - * @param {Object} options - * @param {Buffer|string} options.data - The file content - * @param {Object} options.headers - Headers to set (e.g. Content-Type, Content-Disposition) - * @param {number} [options.status=200] - HTTP status code - */ - res.file = ({ data, headers = {}, status = 200 }) => { - Object.entries(headers).forEach(([key, value]) => { - res.setHeader(key, value); - }); - return res.status(status).send(data); - }; - - next(); -}; - -export { responseHandler }; diff --git a/server/src/middleware/v1/sanitization.js b/server/src/middleware/v1/sanitization.js deleted file mode 100644 index a89325de1..000000000 --- a/server/src/middleware/v1/sanitization.js +++ /dev/null @@ -1,85 +0,0 @@ -import { JSDOM } from "jsdom"; -import DOMPurify from "isomorphic-dompurify"; - -// Initialize DOMPurify with jsdom -const window = new JSDOM("").window; -const purify = DOMPurify(window); - -/** - * Sanitizes user input to prevent XSS attacks - * @param {string} input - The input string to sanitize - * @param {Object} options - Sanitization options - * @returns {string} The sanitized string - */ -export const sanitizeInput = (input, options = {}) => { - if (typeof input !== "string") { - return input; - } - - // Default configuration - remove all HTML tags and attributes - const defaultConfig = { - ALLOWED_TAGS: [], - ALLOWED_ATTR: [], - KEEP_CONTENT: true, - ...options, - }; - - return purify.sanitize(input, defaultConfig); -}; - -/** - * Sanitizes an object recursively - * @param {Object} obj - The object to sanitize - * @param {Object} options - Sanitization options - * @returns {Object} The sanitized object - */ -export const sanitizeObject = (obj, options = {}) => { - if (typeof obj !== "object" || obj === null) { - return obj; - } - - if (Array.isArray(obj)) { - return obj.map((item) => sanitizeObject(item, options)); - } - - const sanitized = {}; - for (const [key, value] of Object.entries(obj)) { - if (typeof value === "string") { - sanitized[key] = sanitizeInput(value, options); - } else if (typeof value === "object" && value !== null) { - sanitized[key] = sanitizeObject(value, options); - } else { - sanitized[key] = value; - } - } - - return sanitized; -}; - -/** - * Express middleware for sanitizing request body - * @param {Object} options - Sanitization options - * @returns {Function} Express middleware function - */ -export const sanitizeBody = (options = {}) => { - return (req, res, next) => { - if (req.body && typeof req.body === "object") { - req.body = sanitizeObject(req.body, options); - } - next(); - }; -}; - -/** - * Express middleware for sanitizing query parameters - * @param {Object} options - Sanitization options - * @returns {Function} Express middleware function - */ -export const sanitizeQuery = (options = {}) => { - return (req, res, next) => { - if (req.query && typeof req.query === "object") { - req.query = sanitizeObject(req.query, options); - } - next(); - }; -}; diff --git a/server/src/middleware/v1/verifyJWT.js b/server/src/middleware/v1/verifyJWT.js deleted file mode 100755 index b57391dfd..000000000 --- a/server/src/middleware/v1/verifyJWT.js +++ /dev/null @@ -1,57 +0,0 @@ -import jwt from "jsonwebtoken"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import SettingsService from "../../service/v1/system/settingsService.js"; -import StringService from "../../service/v1/system/stringService.js"; -const SERVICE_NAME = "verifyJWT"; -const TOKEN_PREFIX = "Bearer "; - -/** - * Verifies the JWT token - * @function - * @param {express.Request} req - * @param {express.Response} res - * @param {express.NextFunction} next - * @returns {express.Response} - */ -const verifyJWT = (req, res, next) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - const token = req.headers["authorization"]; - // Make sure a token is provided - if (!token) { - const error = new Error(stringService.noAuthToken); - error.status = 401; - error.service = SERVICE_NAME; - next(error); - return; - } - // Make sure it is properly formatted - if (!token.startsWith(TOKEN_PREFIX)) { - const error = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token - error.status = 401; - error.service = SERVICE_NAME; - error.method = "verifyJWT"; - next(error); - return; - } - - const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); - // Verify the token's authenticity - const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); - jwt.verify(parsedToken, jwtSecret, (err, decoded) => { - if (err) { - const errorMessage = err.name === "TokenExpiredError" ? stringService.expiredAuthToken : stringService.invalidAuthToken; - err.details = { msg: errorMessage }; - err.status = 401; - err.service = SERVICE_NAME; - err.method = "verifyJWT"; - next(err); - return; - } else { - // Token is valid, carry on - req.user = decoded; - next(); - } - }); -}; - -export { verifyJWT }; diff --git a/server/src/middleware/v1/verifyOwnership.js b/server/src/middleware/v1/verifyOwnership.js deleted file mode 100755 index 741fb6436..000000000 --- a/server/src/middleware/v1/verifyOwnership.js +++ /dev/null @@ -1,59 +0,0 @@ -import { logger } from "../../utils/logger.js"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; -import { ObjectId } from "mongodb"; - -const SERVICE_NAME = "verifyOwnership"; - -const verifyOwnership = (Model, paramName) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - return async (req, res, next) => { - const userId = req.user._id; - let documentId = req.params[paramName]; - - try { - if (typeof documentId === "string") { - documentId = ObjectId.createFromHexString(documentId); - } - const doc = await Model.findById(documentId); - //If the document is not found, return a 404 error - if (!doc) { - logger.error({ - message: stringService.verifyOwnerNotFound, - service: SERVICE_NAME, - method: "verifyOwnership", - }); - const error = new Error(stringService.verifyOwnerNotFound); - error.status = 404; - throw error; - } - - // Special case for User model, as it will not have a `userId` field as other docs will - if (Model.modelName === "User") { - if (userId.toString() !== doc._id.toString()) { - const error = new Error(stringService.verifyOwnerUnauthorized); - error.status = 403; - throw error; - } - next(); - return; - } - - // If the userID does not match the document's userID, return a 403 error - if (userId.toString() !== doc.userId.toString()) { - const error = new Error("Unauthorized"); - error.status = 403; - throw error; - } - next(); - return; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "verifyOwnership"; - next(error); - return; - } - }; -}; - -export { verifyOwnership }; diff --git a/server/src/middleware/v1/verifySuperAdmin.js b/server/src/middleware/v1/verifySuperAdmin.js deleted file mode 100755 index 41ac0a3e0..000000000 --- a/server/src/middleware/v1/verifySuperAdmin.js +++ /dev/null @@ -1,66 +0,0 @@ -const jwt = require("jsonwebtoken"); -const logger = require("../../utils/logger.js"); -const SERVICE_NAME = "verifyAdmin"; -const TOKEN_PREFIX = "Bearer "; -import ServiceRegistry from "../service/serviceRegistry.js"; -import SettingsService from "../service/settingsService.js"; -import StringService from "../service/stringService.js"; -/** - * Verifies the JWT token - * @function - * @param {express.Request} req - * @param {express.Response} res - * @param {express.NextFunction} next - * @returns {express.Response} - */ -const verifySuperAdmin = (req, res, next) => { - const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); - const token = req.headers["authorization"]; - // Make sure a token is provided - if (!token) { - const error = new Error(stringService.noAuthToken); - error.status = 401; - error.service = SERVICE_NAME; - next(error); - return; - } - // Make sure it is properly formatted - if (!token.startsWith(TOKEN_PREFIX)) { - const error = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token - error.status = 400; - error.service = SERVICE_NAME; - error.method = "verifySuperAdmin"; - next(error); - return; - } - - const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); - // verify admin role is present - const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); - - jwt.verify(parsedToken, jwtSecret, (err, decoded) => { - if (err) { - logger.error({ - message: err.message, - service: SERVICE_NAME, - method: "verifySuperAdmin", - stack: err.stack, - details: stringService.invalidAuthToken, - }); - return res.status(401).json({ success: false, msg: stringService.invalidAuthToken }); - } - - if (decoded.role.includes("superadmin") === false) { - logger.error({ - message: stringService.invalidAuthToken, - service: SERVICE_NAME, - method: "verifySuperAdmin", - stack: err.stack, - }); - return res.status(401).json({ success: false, msg: stringService.unauthorized }); - } - next(); - }); -}; - -module.exports = { verifySuperAdmin }; diff --git a/server/src/middleware/v1/verifyTeamAccess.js b/server/src/middleware/v1/verifyTeamAccess.js deleted file mode 100644 index 60647bdc2..000000000 --- a/server/src/middleware/v1/verifyTeamAccess.js +++ /dev/null @@ -1,38 +0,0 @@ -const SERVICE_NAME = "verifyTeamAccess"; - -const verifyTeamAccess = (Model, paramName) => { - return async (req, res, next) => { - try { - const documentId = req.params[paramName]; - const doc = await Model.findById(documentId); - - if (!doc) { - const error = new Error("Document not found"); - error.status = 404; - throw error; - } - - if (!req?.user?.teamId || !doc.teamId) { - const error = new Error("Missing team information"); - error.status = 400; - throw error; - } - - if (req.user.teamId.toString() === doc.teamId.toString()) { - next(); - return; - } - - const error = new Error("Unauthorized"); - error.status = 403; - throw error; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "verifyTeamAccess"; - next(error); - return; - } - }; -}; - -export { verifyTeamAccess }; diff --git a/server/src/middleware/v2/VerifyPermissions.ts b/server/src/middleware/v2/VerifyPermissions.ts deleted file mode 100644 index 64d573e00..000000000 --- a/server/src/middleware/v2/VerifyPermissions.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import ApiError from "../../utils/ApiError.js"; -import { User, IUser, Role, IRole } from "../../db/v2/models/index.js"; - -const rolesCache = new Map(); -// const CACHE_TTL = 30 * 60 * 1000; // 30 minutes -const CACHE_TTL = 1; // 30 minutes -const MAX_CACHE_SIZE = 1000; - -const getCachedRoles = async (userId: string) => { - if (rolesCache.size >= MAX_CACHE_SIZE) { - const oldestKey = rolesCache.keys().next().value; - if (!oldestKey) return null; - rolesCache.delete(oldestKey); - } - - const cached = rolesCache.get(userId); - - if (cached && Date.now() - cached.timestamp < CACHE_TTL) { - return cached.roles; - } - - const user: IUser | null = await User.findById(userId); - if (!user) { - return null; - } - - const roles = await Role.find({ _id: { $in: user.roles } }); - rolesCache.set(userId, { roles, timestamp: Date.now() }); - return roles; -}; - -const hasPermission = (roles: IRole[], requiredPermissions: string[]) => { - const userPermissions = [...new Set(roles.flatMap((role) => role.permissions))]; - - if (userPermissions.includes("*")) return true; - - const matches = (requiredPermission: string, userPermission: string) => { - if (userPermission === requiredPermission) return true; - if (userPermission.endsWith(".*")) { - const prefix = userPermission.slice(0, -2); - return requiredPermission.startsWith(prefix + "."); - } - return false; - }; - - return requiredPermissions.every((requiredPermission) => { - return userPermissions.some((userPermission) => matches(requiredPermission, userPermission)); - }); -}; - -const verifyPermission = (resourceActions: string[]) => { - return async (req: Request, res: Response, next: NextFunction) => { - const tokenizedUser = req.user; - - if (!tokenizedUser) { - throw new ApiError("No user", 400); - } - - const userId = tokenizedUser.sub; - if (!userId) { - throw new ApiError("No user ID", 400); - } - - const userRoles = await getCachedRoles(userId); - if (!userRoles) { - throw new ApiError("User roles not found", 400); - } - const allowed = hasPermission(userRoles, resourceActions); - if (!allowed) { - throw new ApiError("Insufficient permissions", 403); - } - next(); - }; -}; - -export { verifyPermission }; diff --git a/server/src/middleware/v2/VerifyToken.ts b/server/src/middleware/v2/VerifyToken.ts deleted file mode 100644 index 7dabd51cc..000000000 --- a/server/src/middleware/v2/VerifyToken.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { decode } from "../../utils/JWTUtils.js"; -import ApiError from "../../utils/ApiError.js"; -const verifyToken = (req: Request, res: Response, next: NextFunction) => { - const token = req.cookies.token; - - if (!token) { - const error = new ApiError("No token provided", 401); - return next(error); - } - - try { - const decoded = decode(token); - req.user = decoded; - next(); - } catch (error) { - next(error); - } -}; - -export { verifyToken }; diff --git a/server/src/middleware/validation.ts b/server/src/middleware/validation.ts new file mode 100644 index 000000000..305d58b1c --- /dev/null +++ b/server/src/middleware/validation.ts @@ -0,0 +1,37 @@ +import { Request, Response, NextFunction } from "express"; +import { z, ZodObject, ZodRawShape, ZodError } from "zod"; +import { getChildLogger } from "@/logger/Logger.js"; + +const logger = getChildLogger("ValidationMiddleware"); + +export const validateBody = >(schema: T) => { + return (req: Request, res: Response, next: NextFunction) => { + try { + req.body = schema.strict().parse(req.body); // enforce strict keys + next(); + } catch (err) { + logger.error("Error validating request body:", err); + if (err instanceof ZodError) { + return res.status(400).json({ errors: z.treeifyError(err) }); + } + return res.status(500).json({ message: "Server error" }); + } + }; +}; + +export type TypedQueryRequest = Request & { validatedQuery: T }; + +export const validateQuery = >(schema: T) => { + return (req: Request, res: Response, next: NextFunction) => { + try { + req.validatedQuery = schema.strict().parse(req.query); + next(); + } catch (err) { + logger.error("Error validating request query:", err); + if (err instanceof ZodError) { + return res.status(400).json({ errors: z.treeifyError(err) }); + } + return res.status(500).json({ message: "Server error" }); + } + }; +}; diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts new file mode 100644 index 000000000..29835ed97 --- /dev/null +++ b/server/src/routes/auth.ts @@ -0,0 +1,48 @@ +import { Router } from "express"; + +import { AuthController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { validateBody } from "@/middleware/validation.js"; +import { + registerSchema, + registerWithInviteSchema, + loginSchema, +} from "@/validation/index.js"; + +class AuthRoutes { + private controller: AuthController; + private router: Router; + constructor(authController: AuthController) { + this.controller = authController; + this.router = Router(); + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/register", + validateBody(registerSchema), + this.controller.register + ); + this.router.post( + "/register/invite/:token", + validateBody(registerWithInviteSchema), + this.controller.registerWithInvite + ); + this.router.post( + "/login", + validateBody(loginSchema), + this.controller.login + ); + this.router.post("/logout", this.controller.logout); + this.router.get("/me", verifyToken, this.controller.me); + this.router.post("/cleanup", this.controller.cleanup); + this.router.post("/cleanup-monitors", this.controller.cleanMonitors); + }; + + getRouter() { + return this.router; + } +} + +export default AuthRoutes; diff --git a/server/src/routes/checks.ts b/server/src/routes/checks.ts new file mode 100644 index 000000000..4fdce568b --- /dev/null +++ b/server/src/routes/checks.ts @@ -0,0 +1,36 @@ +import { Router } from "express"; + +import { ChecksController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { validateQuery } from "@/middleware/validation.js"; +import { verifyOrgPermission } from "@/middleware/VerifyPermission.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; +import { checksStatusIdQuerySchema } from "@/validation/index.js"; + +class CheckRoutes { + private controller: ChecksController; + private router: Router; + constructor(checksController: ChecksController) { + this.controller = checksController; + this.router = Router(); + this.initRoutes(); + } + + initRoutes = () => { + this.router.get( + "/", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.checks.read]), + validateQuery(checksStatusIdQuerySchema), + this.controller.getChecksByStatus + ); + }; + + getRouter() { + return this.router; + } +} + +export default CheckRoutes; diff --git a/server/src/routes/diagnostic.ts b/server/src/routes/diagnostic.ts new file mode 100644 index 000000000..976d129b5 --- /dev/null +++ b/server/src/routes/diagnostic.ts @@ -0,0 +1,38 @@ +import { Router } from "express"; +import { DiagnosticController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { verifyOrgPermission } from "@/middleware/VerifyPermission.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; +class DiagnosticRoutes { + private router; + private controller; + constructor(diagnosticController: DiagnosticController) { + this.router = Router(); + this.controller = diagnosticController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.get( + "/logs", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.diagnostic.read]), + this.controller.getLogs + ); + this.router.get( + "/jobs", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.diagnostic.read]), + this.controller.getJobs + ); + }; + + getRouter() { + return this.router; + } +} + +export default DiagnosticRoutes; diff --git a/server/src/routes/incidents.ts b/server/src/routes/incidents.ts new file mode 100644 index 000000000..739e26d00 --- /dev/null +++ b/server/src/routes/incidents.ts @@ -0,0 +1,48 @@ +import { Router } from "express"; + +import { ChecksController, IncidentsController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { validateQuery, validateBody } from "@/middleware/validation.js"; +import { verifyOrgPermission } from "@/middleware/VerifyPermission.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; +import { + getIncidentsQuerySchema, + patchIncidentsBodySchema, +} from "@/validation/index.js"; + +class IncidentsRoutes { + private controller: IncidentsController; + private router: Router; + constructor(incidentsController: IncidentsController) { + this.controller = incidentsController; + this.router = Router(); + this.initRoutes(); + } + + initRoutes = () => { + this.router.get( + "/", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.incidents.read]), + validateQuery(getIncidentsQuerySchema), + this.controller.getIncidents + ); + + this.router.patch( + "/:id/resolve", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.incidents.update]), + validateBody(patchIncidentsBodySchema), + this.controller.resolveIncident + ); + }; + + getRouter() { + return this.router; + } +} + +export default IncidentsRoutes; diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts new file mode 100644 index 000000000..4598f9715 --- /dev/null +++ b/server/src/routes/index.ts @@ -0,0 +1,17 @@ +import RoleRoutes from "./roles.js"; + +export { default as AuthRoutes } from "./auth.js"; +export { default as InviteRoutes } from "./invite.js"; +export { default as MaintenanceRoutes } from "./maintenance.js"; +export { default as MonitorRoutes } from "./monitors.js"; +export { default as NotificationChannelRoutes } from "./notificationChannels.js"; +export { default as QueueRoutes } from "./queue.js"; +export { default as TeamRoutes } from "./team.js"; +export { default as RoleRoutes } from "./roles.js"; +export { default as TeamMemberRoutes } from "./teamMembers.js"; +export { default as ChecksRoutes } from "./checks.js"; +export { default as StatusPageRoutes } from "./statusPages.js"; +export { default as DiagnosticRoutes } from "./diagnostic.js"; +export { default as RecoveryRoutes } from "./recovery.js"; +export { default as ProfileRoutes } from "./profile.js"; +export { default as IncidentsRoutes } from "./incidents.js"; diff --git a/server/src/routes/invite.ts b/server/src/routes/invite.ts new file mode 100644 index 000000000..b32dd8abf --- /dev/null +++ b/server/src/routes/invite.ts @@ -0,0 +1,52 @@ +import { Router } from "express"; +import { InviteController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { verifyOrgPermission } from "@/middleware/VerifyPermission.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; +import { validateBody } from "@/middleware/validation.js"; +import { inviteSchema } from "@/validation/index.js"; +class InviteRoutes { + private router; + private controller; + constructor(inviteController: InviteController) { + this.router = Router(); + this.controller = inviteController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/", + verifyToken, + addUserContext, + validateBody(inviteSchema), + verifyOrgPermission([PERMISSIONS.invite.write]), + this.controller.create + ); + + this.router.get( + "/", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.invite.read]), + this.controller.getAll + ); + + this.router.get("/:id", verifyToken, this.controller.get); + + this.router.delete( + "/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.invite.delete]), + this.controller.delete + ); + }; + + getRouter() { + return this.router; + } +} + +export default InviteRoutes; diff --git a/server/src/routes/maintenance.ts b/server/src/routes/maintenance.ts new file mode 100644 index 000000000..543f15802 --- /dev/null +++ b/server/src/routes/maintenance.ts @@ -0,0 +1,72 @@ +import { Router } from "express"; +import { MaintenanceController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { verifyTeamPermission } from "@/middleware/VerifyPermission.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; + +class MaintenanceRoutes { + private router; + private controller; + constructor(maintenanceController: MaintenanceController) { + this.router = Router(); + this.controller = maintenanceController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.maintenance.write]), + this.controller.create + ); + + this.router.get( + "/", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.maintenance.read]), + this.controller.getAll + ); + + this.router.patch( + "/:id/active", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.maintenance.update]), + this.controller.toggleActive + ); + + this.router.patch( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.maintenance.update]), + this.controller.update + ); + + this.router.get( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.maintenance.read]), + this.controller.get + ); + + this.router.delete( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.maintenance.delete]), + this.controller.delete + ); + }; + + getRouter() { + return this.router; + } +} + +export default MaintenanceRoutes; diff --git a/server/src/routes/monitors.ts b/server/src/routes/monitors.ts new file mode 100644 index 000000000..aafe16bbb --- /dev/null +++ b/server/src/routes/monitors.ts @@ -0,0 +1,103 @@ +import { Router } from "express"; +import { MonitorController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { verifyTeamPermission } from "@/middleware/VerifyPermission.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; +import { validateBody, validateQuery } from "@/middleware/validation.js"; +import { + monitorSchema, + monitorIdChecksQuerySchema, + monitorPatchSchema, + monitorAllEmbedChecksQuerySchema, +} from "@/validation/index.js"; +import { verify } from "node:crypto"; + +class MonitorRoutes { + private router; + private controller; + constructor(monitorController: MonitorController) { + this.router = Router(); + this.controller = monitorController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.monitors.write]), + validateBody(monitorSchema), + this.controller.create + ); + + this.router.get( + "/", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.monitors.read]), + validateQuery(monitorAllEmbedChecksQuerySchema), + this.controller.getAll + ); + + this.router.get( + "/:id/checks", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.monitors.read]), + validateQuery(monitorIdChecksQuerySchema), + this.controller.getChecks + ); + + this.router.post( + "/:id/notifications/test", + verifyToken, + addUserContext, + verifyTeamPermission([ + PERMISSIONS.monitors.read, + PERMISSIONS.notifications.read, + ]), + this.controller.testNotifications + ); + + this.router.patch( + "/:id/active", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.monitors.write]), + this.controller.toggleActive + ); + + this.router.get( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.monitors.read]), + this.controller.get + ); + + this.router.patch( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.monitors.write]), + validateBody(monitorPatchSchema), + this.controller.update + ); + + this.router.delete( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.monitors.delete]), + this.controller.delete + ); + }; + + getRouter() { + return this.router; + } +} + +export default MonitorRoutes; diff --git a/server/src/routes/notificationChannels.ts b/server/src/routes/notificationChannels.ts new file mode 100644 index 000000000..c88c1bb86 --- /dev/null +++ b/server/src/routes/notificationChannels.ts @@ -0,0 +1,87 @@ +import { Router } from "express"; +import { NotificationChannelController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { verifyTeamPermission } from "@/middleware/VerifyPermission.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; +import { validateBody, validateQuery } from "@/middleware/validation.js"; +import { + notificationChannelSchema, + notificationPatchSchema, +} from "@/validation/index.js"; + +class NotificationChannelRoutes { + private router; + private controller; + constructor(notificationChannelController: NotificationChannelController) { + this.router = Router(); + this.controller = notificationChannelController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.notifications.write]), + validateBody(notificationChannelSchema), + this.controller.create + ); + + this.router.get( + "/", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.notifications.read]), + this.controller.getAll + ); + + this.router.patch( + "/:id/active", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.notifications.write]), + this.controller.toggleActive + ); + + this.router.patch( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.notifications.update]), + validateBody(notificationPatchSchema), + this.controller.update + ); + + this.router.get( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.notifications.read]), + this.controller.get + ); + + this.router.delete( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.notifications.delete]), + this.controller.delete + ); + this.router.post( + "/test", + verifyToken, + addUserContext, + validateBody(notificationChannelSchema), + verifyTeamPermission([PERMISSIONS.notifications.write]), + this.controller.test + ); + }; + + getRouter() { + return this.router; + } +} + +export default NotificationChannelRoutes; diff --git a/server/src/routes/profile.ts b/server/src/routes/profile.ts new file mode 100644 index 000000000..663a900d9 --- /dev/null +++ b/server/src/routes/profile.ts @@ -0,0 +1,34 @@ +import { Router } from "express"; +import { ProfileController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { validateBody } from "@/middleware/validation.js"; +import { profileSchema } from "@/validation/index.js"; + +class ProfileRoutes { + private router; + private controller; + constructor(profileController: ProfileController) { + this.router = Router(); + this.controller = profileController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.get("/", verifyToken, addUserContext, this.controller.get); + + this.router.patch( + "/", + verifyToken, + addUserContext, + validateBody(profileSchema), + this.controller.update + ); + }; + + getRouter() { + return this.router; + } +} + +export default ProfileRoutes; diff --git a/server/src/routes/queue.ts b/server/src/routes/queue.ts new file mode 100644 index 000000000..d2726b02d --- /dev/null +++ b/server/src/routes/queue.ts @@ -0,0 +1,24 @@ +import { QueueController } from "@/controllers/index.js"; +import { Router } from "express"; + +class QueueRoutes { + private router; + private controller; + constructor(queueController: QueueController) { + this.router = Router(); + this.controller = queueController; + this.initRoutes(); + } + + initRoutes() { + this.router.get("/jobs", this.controller.getJobs); + this.router.get("/metrics", this.controller.getMetrics); + this.router.post("/flush", this.controller.flush); + } + + getRouter() { + return this.router; + } +} + +export default QueueRoutes; diff --git a/server/src/routes/recovery.ts b/server/src/routes/recovery.ts new file mode 100644 index 000000000..9eaaedbf3 --- /dev/null +++ b/server/src/routes/recovery.ts @@ -0,0 +1,33 @@ +import { Router } from "express"; +import { RecoveryController } from "@/controllers/index.js"; +import { validateBody } from "@/middleware/validation.js"; +import { recoverySchema, resetPasswordSchema } from "@/validation/index.js"; +class RecoveryRoutes { + private router; + private controller; + constructor(recoveryController: RecoveryController) { + this.router = Router(); + this.controller = recoveryController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/", + validateBody(recoverySchema), + this.controller.requestRecovery + ); + + this.router.post( + "/reset", + validateBody(resetPasswordSchema), + this.controller.resetPassword + ); + }; + + getRouter() { + return this.router; + } +} + +export default RecoveryRoutes; diff --git a/server/src/routes/roles.ts b/server/src/routes/roles.ts new file mode 100644 index 000000000..0e5148344 --- /dev/null +++ b/server/src/routes/roles.ts @@ -0,0 +1,48 @@ +import { Router } from "express"; + +import { RoleController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { verifyOrgPermission } from "@/middleware/VerifyPermission.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; + +class RoleRoutes { + private controller: RoleController; + private router: Router; + constructor(roleController: RoleController) { + this.controller = roleController; + this.router = Router(); + this.initRoutes(); + } + + initRoutes = () => { + this.router.get( + "/", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.roles.read]), + this.controller.getAll + ); + this.router.get( + "/team/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.roles.read]), + this.controller.getTeam + ); + + this.router.get( + "/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.roles.read]), + this.controller.get + ); + }; + + getRouter() { + return this.router; + } +} + +export default RoleRoutes; diff --git a/server/src/routes/statusPages.ts b/server/src/routes/statusPages.ts new file mode 100644 index 000000000..dcfb016a7 --- /dev/null +++ b/server/src/routes/statusPages.ts @@ -0,0 +1,70 @@ +import { Router } from "express"; +import { StatusPageController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { verifyTeamPermission } from "@/middleware/VerifyPermission.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; +import { validateBody } from "@/middleware/validation.js"; +import { statusPageSchema } from "@/validation/index.js"; + +class StatusPageRoutes { + private router; + private controller; + constructor(statusPageController: StatusPageController) { + this.router = Router(); + this.controller = statusPageController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.statusPages.write]), + validateBody(statusPageSchema), + this.controller.create + ); + + this.router.get( + "/", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.statusPages.read]), + this.controller.getAll + ); + + this.router.patch( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.statusPages.update]), + validateBody(statusPageSchema.partial()), + this.controller.update + ); + + this.router.get( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.statusPages.read]), + this.controller.get + ); + + this.router.get("/public/:url", this.controller.getPublic); + + this.router.delete( + "/:id", + verifyToken, + addUserContext, + verifyTeamPermission([PERMISSIONS.statusPages.delete]), + this.controller.delete + ); + }; + + getRouter() { + return this.router; + } +} + +export default StatusPageRoutes; diff --git a/server/src/routes/team.ts b/server/src/routes/team.ts new file mode 100644 index 000000000..83c9281ca --- /dev/null +++ b/server/src/routes/team.ts @@ -0,0 +1,60 @@ +import { Router } from "express"; + +import { TeamController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { verifyOrgPermission } from "@/middleware/VerifyPermission.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; + +class TeamRoutes { + private controller: TeamController; + private router: Router; + constructor(teamController: TeamController) { + this.controller = teamController; + this.router = Router(); + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.teams.write]), + this.controller.create + ); + + this.router.get("/", verifyToken, this.controller.getOrg); + this.router.get("/joined", verifyToken, this.controller.getJoined); + + this.router.get( + "/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.teams.read]), + this.controller.get + ); + + this.router.patch( + "/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.teams.write]), + this.controller.update + ); + + this.router.delete( + "/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.teams.delete]), + this.controller.delete + ); + }; + + getRouter() { + return this.router; + } +} + +export default TeamRoutes; diff --git a/server/src/routes/teamMembers.ts b/server/src/routes/teamMembers.ts new file mode 100644 index 000000000..5eda056b2 --- /dev/null +++ b/server/src/routes/teamMembers.ts @@ -0,0 +1,59 @@ +import { Router } from "express"; + +import { TeamMemberController } from "@/controllers/index.js"; +import { verifyToken } from "@/middleware/VerifyToken.js"; +import { addUserContext } from "@/middleware/AddUserContext.js"; +import { verifyOrgPermission } from "@/middleware/VerifyPermission.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; + +class TeamMemberRoutes { + private controller: TeamMemberController; + private router: Router; + constructor(teamMemberController: TeamMemberController) { + this.controller = teamMemberController; + this.router = Router(); + this.initRoutes(); + } + + initRoutes = () => { + this.router.post( + "/", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.teams.write]), + this.controller.create + ); + + this.router.get("/", verifyToken, addUserContext, this.controller.getAll); + + this.router.get( + "/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.teams.read]), + this.controller.get + ); + + this.router.patch( + "/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.teams.write]), + this.controller.update + ); + + this.router.delete( + "/:id", + verifyToken, + addUserContext, + verifyOrgPermission([PERMISSIONS.teams.delete]), + this.controller.delete + ); + }; + + getRouter() { + return this.router; + } +} + +export default TeamMemberRoutes; diff --git a/server/src/routes/v1/announcementsRoute.js b/server/src/routes/v1/announcementsRoute.js deleted file mode 100755 index 011dfac4a..000000000 --- a/server/src/routes/v1/announcementsRoute.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { isAllowed } from "../middleware/isAllowed.js"; - -class AnnouncementRoutes { - constructor(controller) { - this.router = Router(); - this.announcementController = controller; - this.initRoutes(); - } - - initRoutes() { - /** - * @route POST / - * @desc Create a new announcement - * @access Private (Requires JWT verification) - */ - this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.announcementController.createAnnouncement); - - /** - * @route GET / - * @desc Get announcements - * @access Public - */ - this.router.get("/", this.announcementController.getAnnouncement); - } - - getRouter() { - return this.router; - } -} - -export default AnnouncementRoutes; diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/v1/authRoute.js deleted file mode 100755 index 63b4c4201..000000000 --- a/server/src/routes/v1/authRoute.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Router } from "express"; -import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; -import multer from "multer"; - -const upload = multer(); - -class AuthRoutes { - constructor(authController) { - this.router = Router(); - this.authController = authController; - this.initRoutes(); - } - - initRoutes() { - this.router.post("/register", upload.single("profileImage"), this.authController.registerUser); - this.router.post("/login", this.authController.loginUser); - - this.router.post("/recovery/request", this.authController.requestRecovery); - this.router.post("/recovery/validate", this.authController.validateRecovery); - this.router.post("/recovery/reset/", this.authController.resetPassword); - - this.router.get("/users/superadmin", this.authController.checkSuperadminExists); - - this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers); - this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById); - this.router.put("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById); - this.router.put("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById); - - this.router.put("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser); - this.router.delete("/user", verifyJWT, this.authController.deleteUser); - } - - getRouter() { - return this.router; - } -} - -export default AuthRoutes; diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/v1/checkRoute.js deleted file mode 100755 index 78f6b8a42..000000000 --- a/server/src/routes/v1/checkRoute.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Router } from "express"; - -import { isAllowed } from "../../middleware/v1/isAllowed.js"; - -class CheckRoutes { - constructor(checkController) { - this.router = Router(); - this.checkController = checkController; - this.initRoutes(); - } - - initRoutes() { - this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId); - this.router.get("/team", this.checkController.getChecksByTeam); - this.router.put("/team/ttl", isAllowed(["admin", "superadmin"]), this.checkController.updateChecksTTL); - this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId); - - this.router.put("/check/:checkId", this.checkController.ackCheck); - - this.router.get("/:monitorId", this.checkController.getChecksByMonitor); - this.router.delete("/:monitorId", this.checkController.deleteChecks); - - this.router.put("/:path/:monitorId?", this.checkController.ackAllChecks); - } - - getRouter() { - return this.router; - } -} - -export default CheckRoutes; diff --git a/server/src/routes/v1/diagnosticRoute.js b/server/src/routes/v1/diagnosticRoute.js deleted file mode 100755 index 2cf481e08..000000000 --- a/server/src/routes/v1/diagnosticRoute.js +++ /dev/null @@ -1,20 +0,0 @@ -import { Router } from "express"; -import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; - -class DiagnosticRoutes { - constructor(diagnosticController) { - this.router = Router(); - this.diagnosticController = diagnosticController; - this.initRoutes(); - } - initRoutes() { - this.router.get("/system", verifyJWT, isAllowed(["admin", "superadmin"]), this.diagnosticController.getSystemStats); - } - - getRouter() { - return this.router; - } -} - -export default DiagnosticRoutes; diff --git a/server/src/routes/v1/inviteRoute.js b/server/src/routes/v1/inviteRoute.js deleted file mode 100755 index a47550536..000000000 --- a/server/src/routes/v1/inviteRoute.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Router } from "express"; -import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; - -class InviteRoutes { - constructor(inviteController) { - this.router = Router(); - this.inviteController = inviteController; - this.initRoutes(); - } - - initRoutes() { - this.router.post("/send", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.sendInviteEmail); - this.router.post("/verify", this.inviteController.verifyInviteToken); - this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.getInviteToken); - } - - getRouter() { - return this.router; - } -} - -export default InviteRoutes; diff --git a/server/src/routes/v1/logRoutes.js b/server/src/routes/v1/logRoutes.js deleted file mode 100755 index 43da8b75a..000000000 --- a/server/src/routes/v1/logRoutes.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; -class LogRoutes { - constructor(logController) { - this.router = Router(); - this.logController = logController; - this.initRoutes(); - } - initRoutes() { - this.router.get("/", isAllowed(["admin", "superadmin"]), this.logController.getLogs); - } - - getRouter() { - return this.router; - } -} - -export default LogRoutes; diff --git a/server/src/routes/v1/maintenanceWindowRoute.js b/server/src/routes/v1/maintenanceWindowRoute.js deleted file mode 100755 index 345f2c13c..000000000 --- a/server/src/routes/v1/maintenanceWindowRoute.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Router } from "express"; - -class MaintenanceWindowRoutes { - constructor(maintenanceWindowController) { - this.router = Router(); - this.mwController = maintenanceWindowController; - this.initRoutes(); - } - initRoutes() { - this.router.post("/", this.mwController.createMaintenanceWindows); - this.router.get("/team/", this.mwController.getMaintenanceWindowsByTeamId); - - this.router.get("/monitor/:monitorId", this.mwController.getMaintenanceWindowsByMonitorId); - - this.router.get("/:id", this.mwController.getMaintenanceWindowById); - this.router.put("/:id", this.mwController.editMaintenanceWindow); - this.router.delete("/:id", this.mwController.deleteMaintenanceWindow); - } - - getRouter() { - return this.router; - } -} - -export default MaintenanceWindowRoutes; diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.js deleted file mode 100755 index dc0dd694b..000000000 --- a/server/src/routes/v1/monitorRoute.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; -import multer from "multer"; -import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js"; -const upload = multer({ - storage: multer.memoryStorage(), // Store file in memory as Buffer -}); - -class MonitorRoutes { - constructor(monitorController) { - this.router = Router(); - this.monitorController = monitorController; - this.initRoutes(); - } - - initRoutes() { - // Team routes - this.router.get("/team", this.monitorController.getMonitorsByTeamId); - this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); - this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); - this.router.get("/team/groups", this.monitorController.getGroupsByTeamId); - - // Uptime routes - this.router.get("/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById); - - // Hardware routes - this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById); - - // General monitor routes - this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor); - this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById); - - // Util routes - this.router.get("/certificate/:monitorId", (req, res, next) => { - this.monitorController.getMonitorCertificate(req, res, next, fetchMonitorCertificate); - }); - - // General monitor CRUD routes - this.router.get("/", this.monitorController.getAllMonitors); - this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); - this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); - - // Other static routes - this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors); - this.router.get("/export", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToCSV); - this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors); - this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail); - this.router.get("/games", this.monitorController.getAllGames); - - // Individual monitor CRUD routes - this.router.get("/:monitorId", this.monitorController.getMonitorById); - this.router.put("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor); - this.router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.deleteMonitor); - } - - getRouter() { - return this.router; - } -} - -export default MonitorRoutes; diff --git a/server/src/routes/v1/notificationRoute.js b/server/src/routes/v1/notificationRoute.js deleted file mode 100755 index f88b0b96c..000000000 --- a/server/src/routes/v1/notificationRoute.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Router } from "express"; -class NotificationRoutes { - constructor(notificationController) { - this.router = Router(); - this.notificationController = notificationController; - this.initializeRoutes(); - } - - initializeRoutes() { - this.router.post("/", this.notificationController.createNotification); - - this.router.post("/test/all", this.notificationController.testAllNotifications); - this.router.post("/test", this.notificationController.testNotification); - - this.router.get("/team", this.notificationController.getNotificationsByTeamId); - - this.router.get("/:id", this.notificationController.getNotificationById); - this.router.delete("/:id", this.notificationController.deleteNotification); - this.router.put("/:id", this.notificationController.editNotification); - } - - getRouter() { - return this.router; - } -} - -export default NotificationRoutes; diff --git a/server/src/routes/v1/queueRoute.js b/server/src/routes/v1/queueRoute.js deleted file mode 100755 index c6cfa05da..000000000 --- a/server/src/routes/v1/queueRoute.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; -class QueueRoutes { - constructor(queueController) { - this.router = Router(); - this.queueController = queueController; - this.initRoutes(); - } - initRoutes() { - this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs); - this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob); - - this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics); - this.router.get("/health", isAllowed(["admin", "superadmin"]), this.queueController.checkQueueHealth); - this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics); - this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue); - } - - getRouter() { - return this.router; - } -} - -export default QueueRoutes; diff --git a/server/src/routes/v1/settingsRoute.js b/server/src/routes/v1/settingsRoute.js deleted file mode 100755 index e88186af2..000000000 --- a/server/src/routes/v1/settingsRoute.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; - -class SettingsRoutes { - constructor(settingsController) { - this.router = Router(); - this.settingsController = settingsController; - this.initRoutes(); - } - - initRoutes() { - this.router.get("/", this.settingsController.getAppSettings); - this.router.put("/", isAllowed(["admin", "superadmin"]), this.settingsController.updateAppSettings); - this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.settingsController.sendTestEmail); - } - - getRouter() { - return this.router; - } -} - -export default SettingsRoutes; diff --git a/server/src/routes/v1/statusPageRoute.js b/server/src/routes/v1/statusPageRoute.js deleted file mode 100755 index 2c3b0b265..000000000 --- a/server/src/routes/v1/statusPageRoute.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Router } from "express"; -import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; -import multer from "multer"; -const upload = multer(); - -class StatusPageRoutes { - constructor(statusPageController) { - this.router = Router(); - this.statusPageController = statusPageController; - this.initRoutes(); - } - - initRoutes() { - this.router.get("/", this.statusPageController.getStatusPage); - this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId); - - this.router.post("/", upload.single("logo"), verifyJWT, this.statusPageController.createStatusPage); - this.router.put("/", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage); - - this.router.get("/:url", this.statusPageController.getStatusPageByUrl); - this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage); - } - - getRouter() { - return this.router; - } -} - -export default StatusPageRoutes; diff --git a/server/src/routes/v2/auth.ts b/server/src/routes/v2/auth.ts deleted file mode 100644 index 919d0fd76..000000000 --- a/server/src/routes/v2/auth.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Router } from "express"; - -import express from "express"; -import AuthController from "../../controllers/v2/AuthController.js"; -import { verifyToken } from "../../middleware/v2/VerifyToken.js"; - -const router = express.Router(); - -class AuthRoutes { - private controller: AuthController; - private router: Router; - constructor(authController: AuthController) { - this.controller = authController; - this.router = Router(); - this.initRoutes(); - } - - initRoutes = () => { - this.router.post("/register", this.controller.register); - this.router.post("/register/invite/:token", this.controller.registerWithInvite); - this.router.post("/login", this.controller.login); - this.router.post("/logout", this.controller.logout); - this.router.get("/me", verifyToken, this.controller.me); - this.router.post("/cleanup", this.controller.cleanup); - this.router.post("/cleanup-monitors", this.controller.cleanMonitors); - }; - - getRouter() { - return this.router; - } -} - -export default AuthRoutes; diff --git a/server/src/routes/v2/invite.ts b/server/src/routes/v2/invite.ts deleted file mode 100644 index 8066d0c77..000000000 --- a/server/src/routes/v2/invite.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Router } from "express"; -import InviteController from "../../controllers/v2/InviteController.js"; -import { verifyToken } from "../../middleware/v2/VerifyToken.js"; -import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js"; - -class InviteRoutes { - private router; - private controller; - constructor(inviteController: InviteController) { - this.router = Router(); - this.controller = inviteController; - this.initRoutes(); - } - - initRoutes = () => { - this.router.post("/", verifyToken, verifyPermission(["invite.create"]), this.controller.create); - - this.router.get("/", verifyToken, verifyPermission(["invite.view"]), this.controller.getAll); - - this.router.get("/:token", verifyToken, verifyPermission(["invite.view"]), this.controller.get); - - this.router.delete("/:id", verifyToken, verifyPermission(["invite.delete"]), this.controller.delete); - }; - - getRouter() { - return this.router; - } -} - -export default InviteRoutes; diff --git a/server/src/routes/v2/maintenance.ts b/server/src/routes/v2/maintenance.ts deleted file mode 100644 index 62bc5f1cf..000000000 --- a/server/src/routes/v2/maintenance.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Router } from "express"; -import MaintenanceController from "../../controllers/v2/MaintenanceController.js"; -import { verifyToken } from "../../middleware/v2/VerifyToken.js"; -import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js"; - -class MaintenanceRoutes { - private router; - private controller; - constructor(maintenanceController: MaintenanceController) { - this.router = Router(); - this.controller = maintenanceController; - this.initRoutes(); - } - - initRoutes = () => { - this.router.post("/", verifyToken, verifyPermission(["maintenance.create"]), this.controller.create); - - this.router.get("/", verifyToken, verifyPermission(["maintenance.view"]), this.controller.getAll); - - this.router.patch("/:id/active", verifyToken, verifyPermission(["maintenance.update"]), this.controller.toggleActive); - - this.router.patch("/:id", verifyToken, verifyPermission(["maintenance.update"]), this.controller.update); - - this.router.get("/:id", verifyToken, verifyPermission(["maintenance.view"]), this.controller.get); - - this.router.delete("/:id", verifyToken, verifyPermission(["maintenance.delete"]), this.controller.delete); - }; - - getRouter() { - return this.router; - } -} - -export default MaintenanceRoutes; diff --git a/server/src/routes/v2/monitors.ts b/server/src/routes/v2/monitors.ts deleted file mode 100644 index 61e5b61d6..000000000 --- a/server/src/routes/v2/monitors.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Router } from "express"; -import MonitorController from "../../controllers/v2/MonitorController.js"; -import { verifyToken } from "../../middleware/v2/VerifyToken.js"; -import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js"; - -class MonitorRoutes { - private router; - private controller; - constructor(monitorController: MonitorController) { - this.router = Router(); - this.controller = monitorController; - this.initRoutes(); - } - - initRoutes = () => { - this.router.post("/", verifyToken, verifyPermission(["monitors.create"]), this.controller.create); - - this.router.get("/", verifyToken, verifyPermission(["monitors.view"]), this.controller.getAll); - - this.router.get("/:id/checks", verifyToken, verifyPermission(["monitors.view"]), this.controller.getChecks); - - this.router.patch("/:id/active", verifyToken, verifyPermission(["monitors.update"]), this.controller.toggleActive); - - this.router.get("/:id", verifyToken, verifyPermission(["monitors.view"]), this.controller.get); - - this.router.patch("/:id", verifyToken, verifyPermission(["monitors.update"]), this.controller.update); - - this.router.delete("/:id", verifyToken, verifyPermission(["monitors.delete"]), this.controller.delete); - }; - - getRouter() { - return this.router; - } -} - -export default MonitorRoutes; diff --git a/server/src/routes/v2/notificationChannels.ts b/server/src/routes/v2/notificationChannels.ts deleted file mode 100644 index 1db32902a..000000000 --- a/server/src/routes/v2/notificationChannels.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Router } from "express"; -import NotificationController from "../../controllers/v2/NotificationChannelController.js"; -import { verifyToken } from "../../middleware/v2/VerifyToken.js"; -import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js"; - -class NotificationChannelRoutes { - private router; - private controller; - constructor(notificationController: NotificationController) { - this.router = Router(); - this.controller = notificationController; - this.initRoutes(); - } - - initRoutes = () => { - this.router.post("/", verifyToken, verifyPermission(["notifications.create"]), this.controller.create); - - this.router.get("/", verifyToken, verifyPermission(["notifications.view"]), this.controller.getAll); - - this.router.patch("/:id/active", verifyToken, verifyPermission(["notifications.update"]), this.controller.toggleActive); - - this.router.patch("/:id", verifyToken, verifyPermission(["notifications.update"]), this.controller.update); - - this.router.get("/:id", verifyToken, verifyPermission(["notifications.view"]), this.controller.get); - - this.router.delete("/:id", verifyToken, verifyPermission(["notifications.delete"]), this.controller.delete); - }; - - getRouter() { - return this.router; - } -} - -export default NotificationChannelRoutes; diff --git a/server/src/routes/v2/queue.ts b/server/src/routes/v2/queue.ts deleted file mode 100644 index 428776e61..000000000 --- a/server/src/routes/v2/queue.ts +++ /dev/null @@ -1,24 +0,0 @@ -import QueueController from "../../controllers/v2/QueueController.js"; -import { Router } from "express"; - -class QueueRoutes { - private router; - private controller; - constructor(queueController: QueueController) { - this.router = Router(); - this.controller = queueController; - this.initRoutes(); - } - - initRoutes() { - this.router.get("/jobs", this.controller.getJobs); - this.router.get("/metrics", this.controller.getMetrics); - this.router.post("/flush", this.controller.flush); - } - - getRouter() { - return this.router; - } -} - -export default QueueRoutes; diff --git a/server/src/service/v1/business/checkService.js b/server/src/service/v1/business/checkService.js deleted file mode 100644 index 9beb43326..000000000 --- a/server/src/service/v1/business/checkService.js +++ /dev/null @@ -1,149 +0,0 @@ -const SERVICE_NAME = "checkService"; - -class CheckService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ db, settingsService, stringService, errorService }) { - this.db = db; - this.settingsService = settingsService; - this.stringService = stringService; - this.errorService = errorService; - } - - get serviceName() { - return CheckService.SERVICE_NAME; - } - - getChecksByMonitor = async ({ monitorId, query, teamId }) => { - if (!monitorId) { - throw this.errorService.createBadRequestError("No monitor ID in request"); - } - - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; - const result = await this.db.checkModule.getChecksByMonitor({ - monitorId, - sortOrder, - dateRange, - filter, - ack, - page, - rowsPerPage, - status, - }); - return result; - }; - - getChecksByTeam = async ({ teamId, query }) => { - let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query; - - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const checkData = await this.db.checkModule.getChecksByTeam({ - sortOrder, - dateRange, - filter, - ack, - page, - rowsPerPage, - teamId, - }); - return checkData; - }; - - getChecksSummaryByTeamId = async ({ teamId }) => { - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const summary = await this.db.checkModule.getChecksSummaryByTeamId({ teamId }); - return summary; - }; - - ackCheck = async ({ checkId, teamId, ack }) => { - if (!checkId) { - throw this.errorService.createBadRequestError("No check ID in request"); - } - - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const updatedCheck = await this.db.checkModule.ackCheck(checkId, teamId, ack); - return updatedCheck; - }; - - ackAllChecks = async ({ monitorId, path, teamId, ack }) => { - if (path === "monitor") { - if (!monitorId) { - throw this.errorService.createBadRequestError("No monitor ID in request"); - } - - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - } - - const updatedChecks = await this.db.checkModule.ackAllChecks(monitorId, teamId, ack, path); - return updatedChecks; - }; - - deleteChecks = async ({ monitorId, teamId }) => { - if (!monitorId) { - throw this.errorService.createBadRequestError("No monitor ID in request"); - } - - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - const deletedCount = await this.db.checkModule.deleteChecks(monitorId); - return deletedCount; - }; - deleteChecksByTeamId = async ({ teamId }) => { - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const deletedCount = await this.db.checkModule.deleteChecksByTeamId(teamId); - return deletedCount; - }; - - updateChecksTTL = async ({ teamId, ttl }) => { - const SECONDS_PER_DAY = 86400; - const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY; - await this.db.checkModule.updateChecksTTL(teamId, newTTL); - }; -} - -export default CheckService; diff --git a/server/src/service/v1/business/diagnosticService.js b/server/src/service/v1/business/diagnosticService.js deleted file mode 100644 index 353604932..000000000 --- a/server/src/service/v1/business/diagnosticService.js +++ /dev/null @@ -1,94 +0,0 @@ -import v8 from "v8"; -import os from "os"; - -const SERVICE_NAME = "diagnosticService"; - -class DiagnosticService { - static SERVICE_NAME = SERVICE_NAME; - - constructor() { - /** - * Performance Observer for monitoring system performance metrics. - * Clears performance marks after each measurement to prevent memory leaks. - */ - const obs = new PerformanceObserver((items) => { - // Get the first entry but we don't need to store it - items.getEntries()[0]; - performance.clearMarks(); - }); - obs.observe({ entryTypes: ["measure"] }); - } - - get serviceName() { - return DiagnosticService.SERVICE_NAME; - } - - getCPUUsage = async () => { - const startUsage = process.cpuUsage(); - const timingPeriod = 1000; // measured in ms - await new Promise((resolve) => setTimeout(resolve, timingPeriod)); - const endUsage = process.cpuUsage(startUsage); - const cpuUsage = { - userUsageMs: endUsage.user / 1000, - systemUsageMs: endUsage.system / 1000, - usagePercentage: ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100, - }; - return cpuUsage; - }; - - getSystemStats = async () => { - // Memory Usage - const totalMemory = os.totalmem(); - const freeMemory = os.freemem(); - - const osStats = { - freeMemoryBytes: freeMemory, // bytes - totalMemoryBytes: totalMemory, // bytes - }; - - const used = process.memoryUsage(); - const memoryUsage = {}; - for (let key in used) { - memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB - } - - // CPU Usage - const cpuMetrics = await this.getCPUUsage(); - - // V8 Heap Statistics - const heapStats = v8.getHeapStatistics(); - const v8Metrics = { - totalHeapSizeBytes: heapStats.total_heap_size, // bytes - usedHeapSizeBytes: heapStats.used_heap_size, // bytes - heapSizeLimitBytes: heapStats.heap_size_limit, // bytes - }; - - // Event Loop Delay - let eventLoopDelay = 0; - performance.mark("start"); - await new Promise((resolve) => setTimeout(resolve, 0)); - performance.mark("end"); - performance.measure("eventLoopDelay", "start", "end"); - const entries = performance.getEntriesByName("eventLoopDelay"); - if (entries.length > 0) { - eventLoopDelay = entries[0].duration; - } - - // Uptime - const uptimeMs = process.uptime() * 1000; // ms - - // Combine Metrics - const diagnostics = { - osStats, - memoryUsage, - cpuUsage: cpuMetrics, - v8HeapStats: v8Metrics, - eventLoopDelayMs: eventLoopDelay, - uptimeMs, - }; - - return diagnostics; - }; -} - -export default DiagnosticService; diff --git a/server/src/service/v1/business/inviteService.js b/server/src/service/v1/business/inviteService.js deleted file mode 100644 index 369ed195a..000000000 --- a/server/src/service/v1/business/inviteService.js +++ /dev/null @@ -1,44 +0,0 @@ -const SERVICE_NAME = "inviteService"; - -class InviteService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ db, settingsService, emailService, stringService, errorService }) { - this.db = db; - this.settingsService = settingsService; - this.emailService = emailService; - this.stringService = stringService; - this.errorService = errorService; - } - - get serviceName() { - return InviteService.SERVICE_NAME; - } - - getInviteToken = async ({ invite, teamId }) => { - invite.teamId = teamId; - const inviteToken = await this.db.inviteModule.requestInviteToken(invite); - return inviteToken; - }; - - sendInviteEmail = async ({ inviteRequest, firstName }) => { - const inviteToken = await this.db.inviteModule.requestInviteToken({ ...inviteRequest }); - const { clientHost } = this.settingsService.getSettings(); - - const html = await this.emailService.buildEmail("employeeActivationTemplate", { - name: firstName, - link: `${clientHost}/register/${inviteToken.token}`, - }); - const result = await this.emailService.sendEmail(inviteRequest.email, "Welcome to Uptime Monitor", html); - if (!result) { - throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings."); - } - }; - - verifyInviteToken = async ({ inviteToken }) => { - const invite = await this.db.inviteModule.getInviteToken(inviteToken); - return invite; - }; -} - -export default InviteService; diff --git a/server/src/service/v1/business/maintenanceWindowService.js b/server/src/service/v1/business/maintenanceWindowService.js deleted file mode 100644 index 9d52702e9..000000000 --- a/server/src/service/v1/business/maintenanceWindowService.js +++ /dev/null @@ -1,66 +0,0 @@ -const SERVICE_NAME = "maintenanceWindowService"; - -class MaintenanceWindowService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ db, settingsService, stringService, errorService }) { - this.db = db; - this.settingsService = settingsService; - this.stringService = stringService; - this.errorService = errorService; - } - - get serviceName() { - return MaintenanceWindowService.SERVICE_NAME; - } - - createMaintenanceWindow = async ({ teamId, body }) => { - const monitorIds = body.monitors; - const monitors = await this.db.monitorModule.getMonitorsByIds(monitorIds); - - const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId)); - - if (unauthorizedMonitors.length > 0) { - throw this.errorService.createAuthorizationError(); - } - - const dbTransactions = monitorIds.map((monitorId) => { - return this.db.maintenanceWindowModule.createMaintenanceWindow({ - teamId, - monitorId, - name: body.name, - active: body.active ? body.active : true, - repeat: body.repeat, - start: body.start, - end: body.end, - }); - }); - await Promise.all(dbTransactions); - }; - - getMaintenanceWindowById = async ({ id, teamId }) => { - const maintenanceWindow = await this.db.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId }); - return maintenanceWindow; - }; - - getMaintenanceWindowsByTeamId = async ({ teamId, query }) => { - const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query); - return maintenanceWindows; - }; - - getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { - const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId }); - return maintenanceWindows; - }; - - deleteMaintenanceWindow = async ({ id, teamId }) => { - await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId }); - }; - - editMaintenanceWindow = async ({ id, teamId, body }) => { - const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId }); - return editedMaintenanceWindow; - }; -} - -export default MaintenanceWindowService; diff --git a/server/src/service/v1/business/monitorService.js b/server/src/service/v1/business/monitorService.js deleted file mode 100644 index 2b643ee64..000000000 --- a/server/src/service/v1/business/monitorService.js +++ /dev/null @@ -1,277 +0,0 @@ -import { createMonitorsBodyValidation } from "../../../validation/joi.js"; - -const SERVICE_NAME = "MonitorService"; -class MonitorService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService, games }) { - this.db = db; - this.settingsService = settingsService; - this.jobQueue = jobQueue; - this.stringService = stringService; - this.emailService = emailService; - this.papaparse = papaparse; - this.logger = logger; - this.errorService = errorService; - this.games = games; - } - - get serviceName() { - return MonitorService.SERVICE_NAME; - } - - verifyTeamAccess = async ({ teamId, monitorId }) => { - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - if (!monitor?.teamId?.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - }; - - getAllMonitors = async () => { - const monitors = await this.db.monitorModule.getAllMonitors(); - return monitors; - }; - - getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const data = await this.db.monitorModule.getUptimeDetailsById({ - monitorId, - dateRange, - normalize, - }); - - return data; - }; - - getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitorStats = await this.db.monitorModule.getMonitorStatsById({ - monitorId, - limit, - sortOrder, - dateRange, - numToDisplay, - normalize, - }); - - return monitorStats; - }; - - getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange }); - - return monitor; - }; - - getMonitorById = async ({ teamId, monitorId }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - return monitor; - }; - - createMonitor = async ({ teamId, userId, body }) => { - const monitor = await this.db.monitorModule.createMonitor({ - body, - teamId, - userId, - }); - - this.jobQueue.addJob(monitor._id, monitor); - }; - - createBulkMonitors = async ({ fileData, userId, teamId }) => { - const { parse } = this.papaparse; - - return new Promise((resolve, reject) => { - parse(fileData, { - header: true, - skipEmptyLines: true, - transform: (value, header) => { - if (value === "") return undefined; // Empty fields become undefined - - // Handle 'port' and 'interval' fields, check if they're valid numbers - if (["port", "interval"].includes(header)) { - const num = parseInt(value, 10); - if (isNaN(num)) { - throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`); - } - return num; - } - - return value; - }, - complete: async ({ data, errors }) => { - try { - if (errors.length > 0) { - throw this.errorService.createServerError("Error parsing CSV"); - } - - if (!data || data.length === 0) { - throw this.errorService.createServerError("CSV file contains no data rows"); - } - - const enrichedData = data.map((monitor) => ({ - userId, - teamId, - ...monitor, - description: monitor.description || monitor.name || monitor.url, - name: monitor.name || monitor.url, - type: monitor.type || "http", - })); - - await createMonitorsBodyValidation.validateAsync(enrichedData); - - const monitors = await this.db.monitorModule.createBulkMonitors(enrichedData); - - await Promise.all( - monitors.map(async (monitor) => { - this.jobQueue.addJob(monitor._id, monitor); - }) - ); - - resolve(monitors); - } catch (error) { - reject(error); - } - }, - }); - }); - }; - - deleteMonitor = async ({ teamId, monitorId }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId }); - await this.jobQueue.deleteJob(monitor); - await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id); - return monitor; - }; - - deleteAllMonitors = async ({ teamId }) => { - const { monitors, deletedCount } = await this.db.monitorModule.deleteAllMonitors(teamId); - await Promise.all( - monitors.map(async (monitor) => { - try { - await this.jobQueue.deleteJob(monitor); - await this.db.checkModule.deleteChecks(monitor._id); - await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor._id); - await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor._id); - } catch (error) { - this.logger.warn({ - message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, - service: SERVICE_NAME, - method: "deleteAllMonitors", - stack: error.stack, - }); - } - }) - ); - return deletedCount; - }; - - editMonitor = async ({ teamId, monitorId, body }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body }); - await this.jobQueue.updateJob(editedMonitor); - }; - - pauseMonitor = async ({ teamId, monitorId }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.pauseMonitor({ monitorId }); - monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor); - return monitor; - }; - - addDemoMonitors = async ({ userId, teamId }) => { - const demoMonitors = await this.db.monitorModule.addDemoMonitors(userId, teamId); - - await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor))); - return demoMonitors; - }; - - sendTestEmail = async ({ to }) => { - const subject = this.stringService.testEmailSubject; - const context = { testName: "Monitoring System" }; - - const html = await this.emailService.buildEmail("testEmailTemplate", context); - const messageId = await this.emailService.sendEmail(to, subject, html); - - if (!messageId) { - throw this.errorService.createServerError("Failed to send test email."); - } - - return messageId; - }; - - getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - teamId, - }); - return monitors; - }; - - getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => { - const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({ - type, - explain, - teamId, - }); - return result; - }; - - getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => { - const result = await this.db.monitorModule.getMonitorsWithChecksByTeamId({ - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - teamId, - explain, - }); - return result; - }; - - exportMonitorsToCSV = async ({ teamId }) => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); - - if (!monitors || monitors.length === 0) { - throw this.errorService.createNotFoundError("No monitors to export"); - } - - const csvData = monitors?.filteredMonitors?.map((monitor) => ({ - name: monitor.name, - description: monitor.description, - type: monitor.type, - url: monitor.url, - interval: monitor.interval, - port: monitor.port, - ignoreTlsErrors: monitor.ignoreTlsErrors, - isActive: monitor.isActive, - })); - - const csv = this.papaparse.unparse(csvData); - return csv; - }; - - getAllGames = () => { - return this.games; - }; - - getGroupsByTeamId = async ({ teamId }) => { - const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId }); - return groups; - }; -} - -export default MonitorService; diff --git a/server/src/service/v1/business/userService.js b/server/src/service/v1/business/userService.js deleted file mode 100644 index 7d7f7b014..000000000 --- a/server/src/service/v1/business/userService.js +++ /dev/null @@ -1,219 +0,0 @@ -const SERVICE_NAME = "userService"; - -class UserService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ crypto, db, emailService, settingsService, logger, stringService, jwt, errorService, jobQueue }) { - this.db = db; - this.emailService = emailService; - this.settingsService = settingsService; - this.logger = logger; - this.stringService = stringService; - this.jwt = jwt; - this.errorService = errorService; - this.jobQueue = jobQueue; - this.crypto = crypto; - } - - get serviceName() { - return UserService.SERVICE_NAME; - } - - issueToken = (payload, appSettings) => { - const tokenTTL = appSettings?.jwtTTL ?? "2h"; - const tokenSecret = appSettings?.jwtSecret; - const payloadData = payload; - return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); - }; - - registerUser = async (user, file) => { - // Create a new user - // If superAdmin exists, a token should be attached to all further register requests - const superAdminExists = await this.db.userModule.checkSuperadmin(); - if (superAdminExists) { - const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(user.inviteToken); - user.role = invitedUser.role; - user.teamId = invitedUser.teamId; - } else { - // This is the first account, create JWT secret to use if one is not supplied by env - const jwtSecret = this.crypto.randomBytes(64).toString("hex"); - await this.db.settingsModule.updateAppSettings({ jwtSecret }); - } - - const newUser = await this.db.userModule.insertUser({ ...user }, file); - - this.logger.debug({ - message: "New user created", - service: SERVICE_NAME, - method: "registerUser", - details: newUser._id, - }); - - const userForToken = { ...newUser._doc }; - delete userForToken.profileImage; - delete userForToken.avatarImage; - - const appSettings = await this.settingsService.getSettings(); - - const token = this.issueToken(userForToken, appSettings); - - try { - const html = await this.emailService.buildEmail("welcomeEmailTemplate", { - name: newUser.firstName, - }); - this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => { - this.logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "registerUser", - stack: error.stack, - }); - }); - } catch (error) { - this.logger.warn({ - message: error.message, - service: SERVICE_NAME, - method: "registerUser", - stack: error.stack, - }); - } - - return { user: newUser, token }; - }; - - loginUser = async (email, password) => { - // Check if user exists - const user = await this.db.userModule.getUserByEmail(email); - // Compare password - const match = await user.comparePassword(password); - if (match !== true) { - throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword); - } - - // Remove password from user object. Should this be abstracted to DB layer? - const userWithoutPassword = { ...user._doc }; - delete userWithoutPassword.password; - delete userWithoutPassword.avatarImage; - - // Happy path, return token - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(userWithoutPassword, appSettings); - // reset avatar image - userWithoutPassword.avatarImage = user.avatarImage; - return { user: userWithoutPassword, token }; - }; - - editUser = async (updates, file, currentUser) => { - // Change Password check - if (updates?.password && updates?.newPassword) { - // Get user's email - // Add user email to body for DB operation - updates.email = currentUser.email; - // Get user - const user = await this.db.userModule.getUserByEmail(currentUser.email); - // Compare passwords - const match = await user.comparePassword(updates?.password); - // If not a match, throw a 403 - // 403 instead of 401 to avoid triggering axios interceptor - if (!match) { - throw this.errorService.createAuthorizationError(this.stringService.authIncorrectPassword); - } - // If a match, update the password - updates.password = updates.newPassword; - } - - const updatedUser = await this.db.userModule.updateUser({ userId: currentUser?._id, user: updates, file: file }); - return updatedUser; - }; - - checkSuperadminExists = async () => { - const superAdminExists = await this.db.userModule.checkSuperadmin(); - return superAdminExists; - }; - - requestRecovery = async (email) => { - const user = await this.db.userModule.getUserByEmail(email); - const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email); - const name = user.firstName; - const { clientHost } = this.settingsService.getSettings(); - const url = `${clientHost}/set-new-password/${recoveryToken.token}`; - - const html = await this.emailService.buildEmail("passwordResetTemplate", { - name, - email, - url, - }); - const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html); - return msgId; - }; - - validateRecovery = async (recoveryToken) => { - await this.db.recoveryModule.validateRecoveryToken(recoveryToken); - }; - - resetPassword = async (password, recoveryToken) => { - const user = await this.db.recoveryModule.resetPassword(password, recoveryToken); - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(user._doc, appSettings); - return { user, token }; - }; - - deleteUser = async (user) => { - const email = user?.email; - if (!email) { - throw this.errorService.createBadRequestError("No email in request"); - } - - const teamId = user?.teamId; - const userId = user?._id; - - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - if (!userId) { - throw this.errorService.createBadRequestError("No user ID in request"); - } - - const roles = user?.role; - if (roles.includes("demo")) { - throw this.errorService.createBadRequestError("Demo user cannot be deleted"); - } - - // 1. Find all the monitors associated with the team ID if superadmin - const result = await this.db.monitorModule.getMonitorsByTeamId({ - teamId: teamId, - }); - - if (roles.includes("superadmin")) { - // 2. Remove all jobs, delete checks and alerts - result?.monitors.length > 0 && - (await Promise.all( - result.monitors.map(async (monitor) => { - await this.jobQueue.deleteJob(monitor); - }) - )); - } - // 6. Delete the user by id - await this.db.userModule.deleteUser(userId); - }; - - getAllUsers = async () => { - const users = await this.db.userModule.getAllUsers(); - return users; - }; - - getUserById = async (roles, userId) => { - const user = await this.db.userModule.getUserById(roles, userId); - return user; - }; - - editUserById = async (userId, user) => { - await this.db.userModule.editUserById(userId, user); - }; - setPasswordByUserId = async (userId, password) => { - const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null }); - return updatedUser; - }; -} -export default UserService; diff --git a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js deleted file mode 100644 index a3cdea8e7..000000000 --- a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ /dev/null @@ -1,176 +0,0 @@ -import Scheduler from "super-simple-scheduler"; -const SERVICE_NAME = "JobQueue"; - -class SuperSimpleQueue { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ envSettings, db, logger, helper }) { - this.envSettings = envSettings; - this.db = db; - this.logger = logger; - this.helper = helper; - } - - get serviceName() { - return SuperSimpleQueue.SERVICE_NAME; - } - - static async create({ envSettings, db, logger, helper }) { - const instance = new SuperSimpleQueue({ envSettings, db, logger, helper }); - await instance.init(); - return instance; - } - - init = async () => { - try { - this.scheduler = new Scheduler({ - // storeType: "mongo", - // storeType: "redis", - logLevel: "debug", - debug: true, - // dbUri: this.envSettings.dbConnectionString, - }); - this.scheduler.start(); - - this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); - const monitors = await this.db.monitorModule.getAllMonitors(); - for (const monitor of monitors) { - const randomOffset = Math.floor(Math.random() * 100); - setTimeout(() => { - this.addJob(monitor._id, monitor); - }, randomOffset); - } - - return true; - } catch (error) { - this.logger.error({ - message: "Failed to initialize SuperSimpleQueue", - service: SERVICE_NAME, - method: "init", - details: error, - }); - return false; - } - }; - - addJob = async (monitorId, monitor) => { - this.scheduler.addJob({ - id: monitorId.toString(), - template: "monitor-job", - repeat: monitor.interval, - active: monitor.isActive, - data: monitor.toObject(), - }); - }; - - deleteJob = async (monitor) => { - this.scheduler.removeJob(monitor._id.toString()); - }; - - pauseJob = async (monitor) => { - const result = this.scheduler.pauseJob(monitor._id.toString()); - if (result === false) { - throw new Error("Failed to resume monitor"); - } - this.logger.debug({ - message: `Paused monitor ${monitor._id}`, - service: SERVICE_NAME, - method: "pauseJob", - }); - }; - - resumeJob = async (monitor) => { - const result = this.scheduler.resumeJob(monitor._id.toString()); - if (result === false) { - throw new Error("Failed to resume monitor"); - } - this.logger.debug({ - message: `Resumed monitor ${monitor._id}`, - service: SERVICE_NAME, - method: "resumeJob", - }); - }; - - updateJob = async (monitor) => { - this.scheduler.updateJob(monitor._id.toString(), { repeat: monitor.interval, data: monitor.toObject() }); - }; - - shutdown = async () => { - this.scheduler.stop(); - }; - - getMetrics = async () => { - const jobs = await this.scheduler.getJobs(); - const metrics = jobs.reduce( - (acc, job) => { - acc.totalRuns += job.runCount || 0; - acc.totalFailures += job.failCount || 0; - acc.jobs++; - if (job.failCount > 0 && job.lastFailedAt >= job.lsatRunAt) { - acc.failingJobs++; - } - - if (job.lockedAt) { - acc.activeJobs++; - } - - if (job.failCount > 0) { - acc.jobsWithFailures.push({ - monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - failedAt: job.lastFailedAt, - failCount: job.failCount, - failReason: job.lastFailReason, - }); - } - return acc; - }, - { - jobs: 0, - activeJobs: 0, - failingJobs: 0, - jobsWithFailures: [], - totalRuns: 0, - totalFailures: 0, - } - ); - return metrics; - }; - - getJobs = async () => { - const jobs = await this.scheduler.getJobs(); - return jobs.map((job) => { - return { - monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - monitorInterval: job?.data?.interval || null, - active: job.active, - lockedAt: job.lockedAt, - runCount: job.runCount || 0, - failCount: job.failCount || 0, - failReason: job.lastFailReason, - lastRunAt: job.lastRunAt, - lastFinishedAt: job.lastFinishedAt, - lastRunTook: job.lockedAt ? null : job.lastFinishedAt - job.lastRunAt, - lastFailedAt: job.lastFailedAt, - }; - }); - }; - - flushQueues = async () => { - const stopRes = this.scheduler.stop(); - const flushRes = this.scheduler.flushJobs(); - const initRes = await this.init(); - return { - success: stopRes && flushRes && initRes, - }; - }; - - obliterate = async () => { - console.log("obliterate not implemented"); - }; -} - -export default SuperSimpleQueue; diff --git a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js deleted file mode 100644 index 2833a59f5..000000000 --- a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ /dev/null @@ -1,115 +0,0 @@ -const SERVICE_NAME = "JobQueueHelper"; - -class SuperSimpleQueueHelper { - static SERVICE_NAME = SERVICE_NAME; - - /** - * @param {{ - * db: import("../database.js").Database, - * logger: import("../logger.js").Logger, - * networkService: import("../networkService.js").NetworkService, - * statusService: import("../statusService.js").StatusService, - * notificationService: import("../notificationService.js").NotificationService - * }} - */ - constructor({ db, logger, networkService, statusService, notificationService }) { - this.db = db; - this.logger = logger; - this.networkService = networkService; - this.statusService = statusService; - this.notificationService = notificationService; - } - - get serviceName() { - return SuperSimpleQueueHelper.SERVICE_NAME; - } - - getMonitorJob = () => { - return async (monitor) => { - try { - const monitorId = monitor._id; - const teamId = monitor.teamId; - if (!monitorId) { - throw new Error("No monitor id"); - } - - const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); - if (maintenanceWindowActive) { - this.logger.info({ - message: `Monitor ${monitorId} is in maintenance window`, - service: SERVICE_NAME, - method: "getMonitorJob", - }); - return; - } - const networkResponse = await this.networkService.requestStatus(monitor); - - if (!networkResponse) { - throw new Error("No network response"); - } - - const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); - - this.notificationService - .handleNotifications({ - ...networkResponse, - monitor: updatedMonitor, - prevStatus, - statusChanged, - }) - .catch((error) => { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "getMonitorJob", - details: `Error sending notifications for job ${monitor._id}: ${error.message}`, - stack: error.stack, - }); - }); - } catch (error) { - this.logger.warn({ - message: error.message, - service: error.service || SERVICE_NAME, - method: error.method || "getMonitorJob", - stack: error.stack, - }); - throw error; - } - }; - }; - - async isInMaintenanceWindow(monitorId, teamId) { - const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ - monitorId: monitorId.toString(), - teamId: teamId.toString(), - }); - // Check for active maintenance window: - const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { - if (window.active) { - const start = new Date(window.start); - const end = new Date(window.end); - const now = new Date(); - const repeatInterval = window.repeat || 0; - - // If start is < now and end > now, we're in maintenance - if (start <= now && end >= now) return true; - - // If maintenance window was set in the past with a repeat, - // we need to advance start and end to see if we are in range - - while (start < now && repeatInterval !== 0) { - start.setTime(start.getTime() + repeatInterval); - end.setTime(end.getTime() + repeatInterval); - if (start <= now && end >= now) { - return true; - } - } - return false; - } - return acc; - }, false); - return maintenanceWindowIsActive; - } -} - -export default SuperSimpleQueueHelper; diff --git a/server/src/service/v1/infrastructure/bufferService.js b/server/src/service/v1/infrastructure/bufferService.js deleted file mode 100755 index 459d25af3..000000000 --- a/server/src/service/v1/infrastructure/bufferService.js +++ /dev/null @@ -1,76 +0,0 @@ -const SERVICE_NAME = "BufferService"; - -class BufferService { - static SERVICE_NAME = SERVICE_NAME; - constructor({ db, logger, envSettings }) { - this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 1000 : 1000 * 60 * 1; // 1 minute - this.db = db; - this.logger = logger; - this.SERVICE_NAME = SERVICE_NAME; - this.buffer = []; - this.scheduleNextFlush(); - this.logger.info({ - message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`, - service: this.SERVICE_NAME, - method: "constructor", - }); - } - - get serviceName() { - return BufferService.SERVICE_NAME; - } - - addToBuffer({ check }) { - try { - this.buffer.push(check); - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "addToBuffer", - stack: error.stack, - }); - } - } - - scheduleNextFlush() { - this.bufferTimer = setTimeout(async () => { - try { - await this.flushBuffer(); - } catch (error) { - this.logger.error({ - message: `Error in flush cycle: ${error.message}`, - service: this.SERVICE_NAME, - method: "scheduleNextFlush", - stack: error.stack, - }); - } finally { - // Schedule the next flush only after the current one completes - this.scheduleNextFlush(); - } - }, this.BUFFER_TIMEOUT); - } - async flushBuffer() { - let items = this.buffer.length; - - try { - await this.db.checkModule.createChecks(this.buffer); - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "flushBuffer", - stack: error.stack, - }); - } - this.buffer = []; - // this.logger.debug({ - // message: `Flushed ${items} items`, - // service: this.SERVICE_NAME, - // method: "flushBuffer", - // }); - items = 0; - } -} - -export default BufferService; diff --git a/server/src/service/v1/infrastructure/emailService.js b/server/src/service/v1/infrastructure/emailService.js deleted file mode 100755 index ed661ff16..000000000 --- a/server/src/service/v1/infrastructure/emailService.js +++ /dev/null @@ -1,171 +0,0 @@ -import { fileURLToPath } from "url"; -import path from "path"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const SERVICE_NAME = "EmailService"; - -/** - * Represents an email service that can load templates, build, and send emails. - */ -class EmailService { - static SERVICE_NAME = SERVICE_NAME; - - /** - * Constructs an instance of the EmailService, initializing template loaders and the email transporter. - * @param {Object} settingsService - The settings service to get email configuration. - * @param {Object} fs - The file system module. - * @param {Object} path - The path module. - * @param {Function} compile - The Handlebars compile function. - * @param {Function} mjml2html - The MJML to HTML conversion function. - * @param {Object} nodemailer - The nodemailer module. - * @param {Object} logger - The logger module. - */ - constructor(settingsService, fs, path, compile, mjml2html, nodemailer, logger) { - this.settingsService = settingsService; - this.fs = fs; - this.path = path; - this.compile = compile; - this.mjml2html = mjml2html; - this.nodemailer = nodemailer; - this.logger = logger; - this.init(); - } - - get serviceName() { - return EmailService.SERVICE_NAME; - } - - init = async () => { - /** - * Loads an email template from the filesystem. - * - * @param {string} templateName - The name of the template to load. - * @returns {Function} A compiled template function that can be used to generate HTML email content. - */ - this.loadTemplate = (templateName) => { - try { - const templatePath = this.path.join(__dirname, `../../../templates/${templateName}.mjml`); - const templateContent = this.fs.readFileSync(templatePath, "utf8"); - return this.compile(templateContent); - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "loadTemplate", - stack: error.stack, - }); - } - }; - - /** - * A lookup object to access preloaded email templates. - * @type {Object.} - * TODO Load less used templates in their respective functions - */ - this.templateLookup = { - welcomeEmailTemplate: this.loadTemplate("welcomeEmail"), - employeeActivationTemplate: this.loadTemplate("employeeActivation"), - noIncidentsThisWeekTemplate: this.loadTemplate("noIncidentsThisWeek"), - serverIsDownTemplate: this.loadTemplate("serverIsDown"), - serverIsUpTemplate: this.loadTemplate("serverIsUp"), - passwordResetTemplate: this.loadTemplate("passwordReset"), - hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"), - testEmailTemplate: this.loadTemplate("testEmailTemplate"), - }; - - /** - * The email transporter used to send emails. - * @type {Object} - */ - }; - - buildEmail = async (template, context) => { - try { - const mjml = this.templateLookup[template](context); - const html = await this.mjml2html(mjml); - return html.html; - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "buildEmail", - stack: error.stack, - }); - } - }; - - sendEmail = async (to, subject, html, transportConfig) => { - let config; - if (typeof transportConfig !== "undefined") { - config = transportConfig; - } else { - config = await this.settingsService.getDBSettings(); - } - const { - systemEmailHost, - systemEmailPort, - systemEmailSecure, - systemEmailPool, - systemEmailUser, - systemEmailAddress, - systemEmailPassword, - systemEmailConnectionHost, - systemEmailTLSServername, - systemEmailIgnoreTLS, - systemEmailRequireTLS, - systemEmailRejectUnauthorized, - } = config; - - const emailConfig = { - host: systemEmailHost, - port: Number(systemEmailPort), - secure: systemEmailSecure, - auth: { - user: systemEmailUser || systemEmailAddress, - pass: systemEmailPassword, - }, - name: systemEmailConnectionHost || "localhost", - connectionTimeout: 5000, - pool: systemEmailPool, - tls: { - rejectUnauthorized: systemEmailRejectUnauthorized, - ignoreTLS: systemEmailIgnoreTLS, - requireTLS: systemEmailRequireTLS, - servername: systemEmailTLSServername, - }, - }; - this.transporter = this.nodemailer.createTransport(emailConfig); - - try { - await this.transporter.verify(); - } catch (error) { - this.logger.warn({ - message: "Email transporter verification failed", - service: SERVICE_NAME, - method: "verifyTransporter", - }); - return false; - } - - try { - const info = await this.transporter.sendMail({ - to: to, - from: systemEmailAddress, - subject: subject, - html: html, - }); - return info?.messageId; - } catch (error) { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "sendEmail", - stack: error.stack, - }); - } - }; -} - -export default EmailService; diff --git a/server/src/service/v1/infrastructure/errorService.js b/server/src/service/v1/infrastructure/errorService.js deleted file mode 100644 index 680bb3765..000000000 --- a/server/src/service/v1/infrastructure/errorService.js +++ /dev/null @@ -1,101 +0,0 @@ -export class AppError extends Error { - constructor(message, status = 500, service = null, method = null, details = null) { - super(message); - this.status = status; - this.service = service; - this.method = method; - this.details = details; - - Error.captureStackTrace(this, this.constructor); - } -} - -class ValidationError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 422, service, method, details); - } -} - -class AuthenticationError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 401, service, method, details); - } -} - -class AuthorizationError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 403, service, method, details); - } -} - -class NotFoundError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 404, service, method, details); - } -} - -class ConflictError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 409, service, method, details); - } -} - -class DatabaseError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 500, service, method, details); - } -} - -class BadRequestError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 400, service, method, details); - } -} - -const SERVICE_NAME = "ErrorService"; -class ErrorService { - static SERVICE_NAME = SERVICE_NAME; - constructor() {} - - get serviceName() { - return ErrorService.SERVICE_NAME; - } - - createError = (message, status = 500, service = null, method = null, details = null) => { - return new AppError(message, status, service, method, details); - }; - - createValidationError = (message, details = null, service = null, method = null) => { - return new ValidationError(message, details, service, method); - }; - - createAuthenticationError = (message = "Unauthorized", details = null, service = null, method = null) => { - return new AuthenticationError(message, details, service, method); - }; - - createAuthorizationError = (message, details = null, service = null, method = null) => { - return new AuthorizationError(message, details, service, method); - }; - - createNotFoundError = (message, details = null, service = null, method = null) => { - return new NotFoundError(message, details, service, method); - }; - - createConflictError = (message, details = null, service = null, method = null) => { - return new ConflictError(message, details, service, method); - }; - - createDatabaseError = (message, details = null, service = null, method = null) => { - return new DatabaseError(message, details, service, method); - }; - - createServerError = (message, details = null, service = null, method = null) => { - return this.createError(message, 500, service, method, details); - }; - - createBadRequestError = (message = "BadRequest", details = null, service = null, method = null) => { - return new BadRequestError(message, details, service, method); - }; -} - -export default ErrorService; diff --git a/server/src/service/v1/infrastructure/networkService.js b/server/src/service/v1/infrastructure/networkService.js deleted file mode 100644 index d364f373e..000000000 --- a/server/src/service/v1/infrastructure/networkService.js +++ /dev/null @@ -1,580 +0,0 @@ -import CacheableLookup from "cacheable-lookup"; -const SERVICE_NAME = "NetworkService"; - -class NetworkService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) { - this.TYPE_PING = "ping"; - this.TYPE_HTTP = "http"; - this.TYPE_PAGESPEED = "pagespeed"; - this.TYPE_HARDWARE = "hardware"; - this.TYPE_DOCKER = "docker"; - this.TYPE_PORT = "port"; - this.TYPE_GAME = "game"; - this.SERVICE_NAME = SERVICE_NAME; - this.NETWORK_ERROR = 5000; - this.PING_ERROR = 5001; - this.axios = axios; - this.https = https; - this.jmespath = jmespath; - this.GameDig = GameDig; - this.ping = ping; - this.logger = logger; - this.http = http; - this.Docker = Docker; - this.net = net; - this.stringService = stringService; - this.settingsService = settingsService; - - const cacheable = new CacheableLookup(); - - this.got = got.extend({ - dnsCache: cacheable, - timeout: { - request: 30000, - }, - retry: { limit: 1 }, - }); - } - - // Helper functions - async timeRequest(operation) { - const start = process.hrtime.bigint(); - try { - const response = await operation(); - const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); - return { response, responseTime: elapsedMs }; - } catch (error) { - const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); - return { response: null, responseTime: elapsedMs, error }; - } - } - - // Main entry point - async requestStatus(monitor) { - const type = monitor?.type || "unknown"; - switch (type) { - case this.TYPE_PING: - return await this.requestPing(monitor); - case this.TYPE_HTTP: - return await this.requestHttp(monitor); - case this.TYPE_PAGESPEED: - return await this.requestPageSpeed(monitor); - case this.TYPE_HARDWARE: - return await this.requestHardware(monitor); - case this.TYPE_DOCKER: - return await this.requestDocker(monitor); - case this.TYPE_PORT: - return await this.requestPort(monitor); - case this.TYPE_GAME: - return await this.requestGame(monitor); - default: - return await this.handleUnsupportedType(type); - } - } - - async requestPing(monitor) { - try { - if (!monitor?.url) { - throw new Error("Monitor URL is required"); - } - - const rawUrl = monitor.url; - const sanitizedHost = rawUrl - .replace(/^https?:\/\//, "") - .replace(/\/.*$/, "") - .replace(/:.*/, ""); - const { response, error } = await this.timeRequest(() => this.ping.promise.probe(sanitizedHost)); - - if (!response) { - if (error) { - throw error; - } - throw new Error("Ping failed - no result returned"); - } - - const pingResponse = { - monitorId: monitor._id, - type: "ping", - status: response.alive, - code: 200, - responseTime: response.time, - message: "Success", - payload: response, - }; - - if (error) { - pingResponse.status = false; - pingResponse.code = 200; - pingResponse.message = "Ping failed"; - return pingResponse; - } - - return pingResponse; - } catch (err) { - err.service = this.SERVICE_NAME; - err.method = "requestPing"; - throw err; - } - } - - async requestHttp(monitor) { - const { url, secret, _id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; - const httpResponse = { - monitorId: _id, - teamId: teamId, - type, - }; - - try { - if (!url) { - throw new Error("Monitor URL is required"); - } - const config = { - headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, - }; - - if (ignoreTlsErrors) { - config.agent = { - https: new this.https.Agent({ - rejectUnauthorized: false, - }), - }; - } - - const response = await this.got(url, config); - - let payload; - const contentType = response.headers["content-type"]; - - if (contentType && contentType.includes("application/json")) { - try { - payload = JSON.parse(response.body); - } catch { - payload = response.body; - } - } else { - payload = response.body; - } - - httpResponse.code = response.statusCode; - httpResponse.status = response.ok; - httpResponse.message = response.statusMessage; - httpResponse.responseTime = response.timings.phases.total || 0; - httpResponse.payload = payload; - httpResponse.timings = response.timings || {}; - - if (!expectedValue && !jsonPath) { - return httpResponse; - } - - if (expectedValue && !jsonPath) { - let ok = false; - if (matchMethod === "equal") ok = payload === expectedValue; - if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue); - if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload); - - if (ok === true) { - return httpResponse; - } else { - httpResponse.code = 500; - httpResponse.status = false; - httpResponse.message = this.stringService.httpExpectedValueFail; - return httpResponse; - } - } - - if (jsonPath) { - const contentType = response.headers["content-type"]; - const isJson = contentType?.includes("application/json"); - if (!isJson) { - httpResponse.status = false; - httpResponse.message = this.stringService.httpNotJson; - return httpResponse; - } - try { - const extracted = this.jmespath.search(payload, jsonPath); - if (expectedValue) { - let ok = false; - if (matchMethod === "equal") ok = extracted === expectedValue; - if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue); - if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted); - - if (ok) { - httpResponse.extracted = extracted; - return httpResponse; - } else { - httpResponse.status = false; - httpResponse.code = 500; - httpResponse.message = this.stringService.httpJsonPathFail; - httpResponse.extracted = extracted; - return httpResponse; - } - } else { - const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null; - if (!isFalsey) { - httpResponse.extracted = extracted; - return httpResponse; - } else { - httpResponse.status = false; - httpResponse.code = 500; - httpResponse.message = this.stringService.httpJsonPathFail; - httpResponse.extracted = extracted; - return httpResponse; - } - } - } catch { - httpResponse.status = false; - httpResponse.message = this.stringService.httpJsonPathError; - return httpResponse; - } - } - return httpResponse; - } catch (err) { - if (err.name === "HTTPError" || err.name === "RequestError") { - httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR; - httpResponse.status = false; - httpResponse.message = err?.response?.statusCode || err.message; - httpResponse.responseTime = err?.timings?.phases?.total || 0; - httpResponse.payload = null; - httpResponse.timings = err?.timings || {}; - return httpResponse; - } - err.service = this.SERVICE_NAME; - err.method = "requestHttp"; - throw err; - } - } - - async requestPageSpeed(monitor) { - try { - const url = monitor.url; - if (!url) { - throw new Error("Monitor URL is required"); - } - let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; - const dbSettings = await this.settingsService.getDBSettings(); - if (dbSettings?.pagespeedApiKey) { - pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`; - } else { - this.logger.warn({ - message: "PageSpeed API key not found, job not executed", - service: this.SERVICE_NAME, - method: "requestPagespeed", - details: { url }, - }); - } - return await this.requestHttp({ - ...monitor, - url: pageSpeedUrl, - }); - } catch (err) { - err.service = this.SERVICE_NAME; - err.method = "requestPageSpeed"; - throw err; - } - } - - async requestHardware(monitor) { - try { - return await this.requestHttp(monitor); - } catch (err) { - err.service = this.SERVICE_NAME; - err.method = "requestHardware"; - throw err; - } - } - - async requestDocker(monitor) { - try { - if (!monitor.url) { - throw new Error("Monitor URL is required"); - } - - const docker = new this.Docker({ - socketPath: "/var/run/docker.sock", - handleError: true, // Enable error handling - }); - - const dockerResponse = { - monitorId: monitor._id, - type: monitor.type, - }; - - const containers = await docker.listContainers({ all: true }); - - // Normalize input: strip leading slashes and convert to lowercase for comparison - const normalizedInput = monitor.url.replace(/^\/+/, "").toLowerCase(); - - // Priority-based matching to avoid ambiguity: - // 1. Exact full ID match (64-char) - let exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); - - // 2. Exact container name match (case-insensitive) - let exactNameMatch = containers.find((c) => - c.Names.some((name) => { - const cleanName = name.replace(/^\/+/, "").toLowerCase(); - return cleanName === normalizedInput; - }) - ); - - // 3. Partial ID match (fallback for backwards compatibility) - let partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput)); - - // Select container based on priority - let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; - - // Return negative response if no container - if (!targetContainer) { - this.logger.warn({ - message: `No container found for "${monitor.url}".`, - service: this.SERVICE_NAME, - method: "requestDocker", - details: { url: monitor.url }, - }); - - dockerResponse.code = 404; - dockerResponse.status = false; - dockerResponse.message = this.stringService.dockerNotFound; - return dockerResponse; - } - - // Return negative response if ambiguous matches exist - const matchTypes = []; - if (exactIdMatch) matchTypes.push("exact ID"); - if (exactNameMatch) matchTypes.push("exact name"); - if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID"); - - if (matchTypes.length > 1) { - this.logger.warn({ - message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`, - service: this.SERVICE_NAME, - method: "requestDocker", - details: { url: monitor.url }, - }); - dockerResponse.status = 404; - dockerResponse.status = false; - dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`; - return dockerResponse; - } - - const container = docker.getContainer(targetContainer.Id); - const { response, responseTime, error } = await this.timeRequest(() => container.inspect()); - - dockerResponse.responseTime = responseTime; - dockerResponse.status = response?.State?.Status === "running" ? true : false; - dockerResponse.code = 200; - dockerResponse.message = "Docker container status fetched successfully"; - - if (error) { - dockerResponse.status = false; - dockerResponse.code = error.statusCode || this.NETWORK_ERROR; - dockerResponse.message = error.reason || "Failed to fetch Docker container information"; - return dockerResponse; - } - - return dockerResponse; - } catch (err) { - err.service = this.SERVICE_NAME; - err.method = "requestDocker"; - throw err; - } - } - - async requestPort(monitor) { - try { - const { url, port } = monitor; - const { response, responseTime, error } = await this.timeRequest(async () => { - return new Promise((resolve, reject) => { - const socket = this.net.createConnection( - { - host: url, - port, - }, - () => { - socket.end(); - socket.destroy(); - resolve({ success: true }); - } - ); - - socket.setTimeout(5000); - socket.on("timeout", () => { - socket.destroy(); - reject(new Error("Connection timeout")); - }); - - socket.on("error", (err) => { - socket.destroy(); - reject(err); - }); - }); - }); - - const portResponse = { - code: 200, - status: response.success, - message: this.stringService.portSuccess, - monitorId: monitor._id, - type: monitor.type, - responseTime: responseTime, - }; - - if (error) { - portResponse.code = this.NETWORK_ERROR; - portResponse.status = false; - portResponse.message = this.stringService.portFail; - return portResponse; - } - - return portResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestTCP"; - throw error; - } - } - - async requestGame(monitor) { - try { - const { url, port, gameId } = monitor; - - const gameResponse = { - code: 200, - status: true, - message: "Success", - monitorId: monitor._id, - type: "game", - }; - - const state = await this.GameDig.query({ - type: gameId, - host: url, - port: port, - }).catch((error) => { - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: "requestGame", - details: { url, port, gameId }, - }); - }); - - if (!state) { - gameResponse.code = this.NETWORK_ERROR; - gameResponse.status = false; - gameResponse.message = "No response"; - return gameResponse; - } - - gameResponse.responseTime = state.ping; - gameResponse.payload = state; - return gameResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestPing"; - throw error; - } - } - async handleUnsupportedType(type) { - const err = new Error(`Unsupported type: ${type}`); - err.service = this.SERVICE_NAME; - err.method = "getStatus"; - throw err; - } - - // Other network requests unrelated to monitoring: - async requestWebhook(type, url, body) { - try { - const response = await this.axios.post(url, body, { - headers: { - "Content-Type": "application/json", - }, - }); - - return { - type: "webhook", - status: true, - code: response.status, - message: `Successfully sent ${type} notification`, - payload: response.data, - }; - } catch (error) { - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: "requestWebhook", - }); - - return { - type: "webhook", - status: false, - code: error.response?.status || this.NETWORK_ERROR, - message: `Failed to send ${type} notification`, - payload: error.response?.data, - }; - } - } - - async requestPagerDuty({ message, routingKey, monitorUrl }) { - try { - const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, { - routing_key: routingKey, - event_action: "trigger", - payload: { - summary: message, - severity: "critical", - source: monitorUrl, - timestamp: new Date().toISOString(), - }, - }); - - if (response?.data?.status !== "success") return false; - return true; - } catch (error) { - error.details = error.response?.data; - error.service = this.SERVICE_NAME; - error.method = "requestPagerDuty"; - throw error; - } - } - - async requestMatrix({ homeserverUrl, accessToken, roomId, message }) { - try { - const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; - const body = { - msgtype: "m.text", - body: message, - format: "org.matrix.custom.html", - formatted_body: message, - }; - const response = await this.axios.post(url, body, { - headers: { - "Content-Type": "application/json", - }, - }); - - return { - status: true, - code: response.status, - message: "Successfully sent Matrix notification", - }; - } catch (error) { - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: "requestMatrix", - }); - - return { - status: false, - code: error.response?.status || this.NETWORK_ERROR, - message: "Failed to send Matrix notification", - payload: error.response?.data, - }; - } - } -} - -export default NetworkService; diff --git a/server/src/service/v1/infrastructure/notificationProviders/matrix.js b/server/src/service/v1/infrastructure/notificationProviders/matrix.js deleted file mode 100644 index a8b33d4b8..000000000 --- a/server/src/service/v1/infrastructure/notificationProviders/matrix.js +++ /dev/null @@ -1,34 +0,0 @@ -const SERVICE_NAME = "Matrix"; - -class Matrix { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ networkService, logger }) { - this.networkService = networkService; - this.logger = logger; - } - - get serviceName() { - return Matrix.SERVICE_NAME; - } - - async send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }) { - const title = `Checkmate status for ${monitorName}`; - const formattedMessage = `## ${title}\n${message}`; - try { - await this.networkService.requestMatrix({ - homeserverUrl, - accessToken, - roomId, - message: formattedMessage, - }); - this.logger.info(`Successfully sent Matrix notification for ${friendlyName}`); - return true; - } catch (error) { - this.logger.error(`Failed to send Matrix notification for ${friendlyName}: ${error.message}`); - return false; - } - } -} - -export default Matrix; diff --git a/server/src/service/v1/infrastructure/notificationService.js b/server/src/service/v1/infrastructure/notificationService.js deleted file mode 100644 index 8c6fb063f..000000000 --- a/server/src/service/v1/infrastructure/notificationService.js +++ /dev/null @@ -1,128 +0,0 @@ -const SERVICE_NAME = "NotificationService"; -import Matrix from "./notificationProviders/matrix.js"; - -class NotificationService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ emailService, db, logger, networkService, stringService, notificationUtils }) { - this.emailService = emailService; - this.db = db; - this.logger = logger; - this.networkService = networkService; - this.stringService = stringService; - this.notificationUtils = notificationUtils; - } - - get serviceName() { - return NotificationService.SERVICE_NAME; - } - - sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => { - const { type, address } = notification; - - if (type === "email") { - const messageId = await this.emailService.sendEmail(address, subject, html); - if (!messageId) return false; - return true; - } - - // Create a body for webhooks - let body = { text: content }; - if (type === "discord") { - body = !discordContent ? { content } : discordContent; - } - if (type === "webhook") { - body = !webhookBody ? { content } : webhookBody; - } - if (type === "slack" || type === "discord" || type === "webhook") { - const response = await this.networkService.requestWebhook(type, address, body); - return response.status; - } - if (type === "pager_duty") { - const response = await this.networkService.requestPagerDuty({ - message: content, - monitorUrl: subject, - routingKey: address, - }); - - return response; - } - if (type === "matrix") { - const { friendlyName, homeserverUrl, accessToken, roomId } = notification; - const monitorName = subject; - const message = content; - const matrix = new Matrix({ networkService: this.networkService, logger: this.logger }); - const success = await matrix.send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }); - return success; - } - }; - - async handleNotifications(networkResponse) { - const { monitor, statusChanged, prevStatus } = networkResponse; - const { type } = monitor; - if (type !== "hardware" && statusChanged === false) return false; - // if prevStatus is undefined, monitor is resuming, we're done - if (type !== "hardware" && prevStatus === undefined) return false; - - const notificationIDs = networkResponse.monitor?.notifications ?? []; - if (notificationIDs.length === 0) return false; - if (networkResponse.monitor.type === "hardware") { - const thresholds = networkResponse?.monitor?.thresholds; - - if (thresholds === undefined) return false; // No thresholds set, we're done - const metrics = networkResponse?.payload?.data ?? null; - if (metrics === null) return false; // No metrics, we're done - - const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); - if (alerts.length === 0) return false; - - const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); - const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); - const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); - const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); - return success; - } - - // Status monitors - const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse); - const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse); - const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent }); - return success; - } - - async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) { - const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs); - // Map each notification to a test promise - const promises = notifications.map(async (notification) => { - try { - await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody }); - return true; - } catch (err) { - return false; - } - }); - - const results = await Promise.all(promises); - return results.every((r) => r === true); - } - - async getTestNotification() { - const html = await this.notificationUtils.buildTestEmail(); - const content = "This is a test notification"; - const subject = "Test Notification"; - return { subject, html, content }; - } - - async testAllNotifications(notificationIDs) { - const { subject, html, content } = await this.getTestNotification(); - return this.notifyAll({ notificationIDs, subject, html, content }); - } - - async sendTestNotification(notification) { - const { subject, html, content } = await this.getTestNotification(); - const success = await this.sendNotification({ notification, subject, content, html }); - return success; - } -} - -export default NotificationService; diff --git a/server/src/service/v1/infrastructure/notificationUtils.js b/server/src/service/v1/infrastructure/notificationUtils.js deleted file mode 100644 index 879ad927c..000000000 --- a/server/src/service/v1/infrastructure/notificationUtils.js +++ /dev/null @@ -1,198 +0,0 @@ -const SERVICE_NAME = "NotificationUtils"; - -class NotificationUtils { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ stringService, emailService }) { - this.stringService = stringService; - this.emailService = emailService; - } - - get serviceName() { - return NotificationUtils.SERVICE_NAME; - } - - buildTestEmail = async () => { - const context = { testName: "Monitoring System" }; - const html = await this.emailService.buildEmail("testEmailTemplate", context); - return html; - }; - - buildStatusEmail = async (networkResponse) => { - const { monitor, status, prevStatus } = networkResponse; - const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; - const context = { monitor: monitor.name, url: monitor.url }; - const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`; - const html = await this.emailService.buildEmail(template, context); - return { subject, html }; - }; - - buildWebhookMessage = (networkResponse) => { - const { monitor, status, code, timestamp } = networkResponse; - // Format timestamp using the local system timezone - const formatTime = (timestamp) => { - const date = new Date(timestamp); - - // Get timezone abbreviation and format the date - const timeZoneAbbr = date.toLocaleTimeString("en-US", { timeZoneName: "short" }).split(" ").pop(); - - // Format the date with readable format - return ( - date - .toLocaleString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }) - .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") + - " " + - timeZoneAbbr - ); - }; - - // Get formatted time - const formattedTime = timestamp ? formatTime(timestamp) : formatTime(new Date().getTime()); - - // Create different messages based on status with extra spacing - let messageText; - if (status === true) { - messageText = this.stringService.monitorUpAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } else { - messageText = this.stringService.monitorDownAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } - const dn = this.stringService.discordNotification; - let discordMessageText = { - embeds: [ - { - title: status - ? dn.uptimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown) - : dn.downtimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown), - color: status ? 5763719 : 15548997, - - fields: [ - { name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true }, - { name: dn.status, value: status ? dn.up : dn.down, inline: true }, - { name: dn.statusCode, value: String(code ?? dn.unknown), inline: true }, - { name: dn.time, value: formattedTime, inline: true }, - { name: dn.url, value: monitor?.url ?? dn.unknown, inline: false }, - ], - footer: { text: dn.checkmate }, - }, - ], - }; - return [messageText, discordMessageText]; - }; - - buildHardwareAlerts = async (networkResponse) => { - const monitor = networkResponse?.monitor; - const thresholds = networkResponse?.monitor?.thresholds; - const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; - - const metrics = networkResponse?.payload?.data; - const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; - - const alerts = { - cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, - memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, - disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, - }; - - const alertsToSend = []; - const discordEmbeds = []; - const monitorInfoFields = [ - { name: "Monitor", value: monitor.name, inline: true }, - { name: "URL", value: monitor.url, inline: false }, - ]; - const formatDiscordAlert = { - cpu: () => ({ - title: "CPU alert", - description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - color: 15548997, - fields: monitorInfoFields, - footer: { text: "Checkmate" }, - }), - - memory: () => ({ - title: "Memory alert", - description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - color: 15548997, - fields: monitorInfoFields, - footer: { text: "Checkmate" }, - }), - - disk: () => ({ - title: "Disk alert", - description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - color: 15548997, - footer: { text: "Checkmate" }, - fields: [ - ...monitorInfoFields, - ...(Array.isArray(disk) ? disk : []).map((d, idx) => ({ - name: `Disk ${idx}`, - value: `${(d?.usage_percent * 100).toFixed(0)}%`, - inline: true, - })), - ], - }), - }; - const alertTypes = ["cpu", "memory", "disk"]; - for (const type of alertTypes) { - // Iterate over each alert type to see if any need to be decremented - if (alerts[type] === true) { - monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered - - if (monitor[`${type}AlertThreshold`] <= 0) { - // If threshold drops below 0, reset and send notification - monitor[`${type}AlertThreshold`] = monitor.alertThreshold; - - const formatAlert = { - cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - memory: () => - `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - disk: () => - `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) - .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - }; - alertsToSend.push(formatAlert[type]()); - discordEmbeds.push(formatDiscordAlert[type]()); - } - } - } - await monitor.save(); - const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; - return [alertsToSend, discordPayload]; - }; - - buildHardwareEmail = async (networkResponse, alerts) => { - const { monitor } = networkResponse; - const template = "hardwareIncidentTemplate"; - const context = { monitor: monitor.name, url: monitor.url, alerts }; - const subject = `Monitor ${monitor.name} infrastructure alerts`; - const html = await this.emailService.buildEmail(template, context); - return { subject, html }; - }; - - buildHardwareNotificationMessage = (alerts, monitor) => { - const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; - const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts] : []; - return alertText.map((alert) => alert).join("\n"); - }; - buildHardwareWebhookBody = (alerts, monitor) => { - const content = alerts.map((alert) => alert).join("\n"); - const body = { text: content, name: monitor.name, url: monitor.url }; - return body; - }; -} - -export default NotificationUtils; diff --git a/server/src/service/v1/infrastructure/statusService.js b/server/src/service/v1/infrastructure/statusService.js deleted file mode 100755 index 248d1c910..000000000 --- a/server/src/service/v1/infrastructure/statusService.js +++ /dev/null @@ -1,317 +0,0 @@ -import MonitorStats from "../../../db/v1/models/MonitorStats.js"; -const SERVICE_NAME = "StatusService"; - -class StatusService { - static SERVICE_NAME = SERVICE_NAME; - - /** - * @param {{ - * buffer: import("./bufferService.js").BufferService - * }} - */ constructor({ db, logger, buffer }) { - this.db = db; - this.logger = logger; - this.buffer = buffer; - } - - get serviceName() { - return StatusService.SERVICE_NAME; - } - - async updateRunningStats({ monitor, networkResponse }) { - try { - const monitorId = monitor._id; - const { responseTime, status } = networkResponse; - // Get stats - let stats = await MonitorStats.findOne({ monitorId }); - if (!stats) { - stats = new MonitorStats({ - monitorId, - avgResponseTime: 0, - totalChecks: 0, - totalUpChecks: 0, - totalDownChecks: 0, - uptimePercentage: 0, - lastCheck: null, - }); - } - - // Update stats - - // Last response time - stats.lastResponseTime = responseTime; - - // Avg response time: - let avgResponseTime = stats.avgResponseTime; - if (typeof responseTime !== "undefined" && responseTime !== null) { - if (avgResponseTime === 0) { - avgResponseTime = responseTime; - } else { - avgResponseTime = (avgResponseTime * (stats.totalChecks - 1) + responseTime) / stats.totalChecks; - } - } - stats.avgResponseTime = avgResponseTime; - - // Total checks - stats.totalChecks++; - if (status === true) { - stats.totalUpChecks++; - // Update the timeSinceLastFailure if needed - if (stats.timeOfLastFailure === 0) { - stats.timeOfLastFailure = new Date().getTime(); - } - } else { - stats.totalDownChecks++; - stats.timeOfLastFailure = 0; - } - - // Calculate uptime percentage - let uptimePercentage; - if (stats.totalChecks > 0) { - uptimePercentage = stats.totalUpChecks / stats.totalChecks; - } else { - uptimePercentage = status === true ? 100 : 0; - } - stats.uptimePercentage = uptimePercentage; - - // latest check - stats.lastCheckTimestamp = new Date().getTime(); - - await stats.save(); - return true; - } catch (error) { - this.logger.error({ - service: this.SERVICE_NAME, - message: error.message, - method: "updateRunningStats", - stack: error.stack, - }); - return false; - } - } - - getStatusString = (status) => { - if (status === true) return "up"; - if (status === false) return "down"; - return "unknown"; - }; - /** - * Updates the status of a monitor based on the network response. - * - * @param {Object} networkResponse - The network response containing monitorId and status. - * @param {string} networkResponse.monitorId - The ID of the monitor. - * @param {string} networkResponse.status - The new status of the monitor. - * @returns {Promise} - A promise that resolves to an object containinfg the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred. - * @returns {Promise} returnObject - The object returned by the function. - * @returns {Object} returnObject.monitor - The monitor object. - * @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed. - * @returns {boolean} returnObject.prevStatus - The previous status of the monitor - */ - updateStatus = async (networkResponse) => { - const check = this.buildCheck(networkResponse); - await this.insertCheck(check); - try { - const { monitorId, status, code } = networkResponse; - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - // Update running stats - this.updateRunningStats({ monitor, networkResponse }); - - // If the status window size has changed, empty - while (monitor.statusWindow.length > monitor.statusWindowSize) { - monitor.statusWindow.shift(); - } - - // Update status sliding window - monitor.statusWindow.push(status); - if (monitor.statusWindow.length > monitor.statusWindowSize) { - monitor.statusWindow.shift(); - } - - if (monitor.status === undefined || monitor.status === null) { - monitor.status = status; - } - - let newStatus = monitor.status; - let statusChanged = false; - const prevStatus = monitor.status; - - // Return early if not enough data points - if (monitor.statusWindow.length < monitor.statusWindowSize) { - await monitor.save(); - return { - monitor, - statusChanged: false, - prevStatus, - code, - timestamp: Date.now(), - }; - } - - // Check if threshold has been met - const failures = monitor.statusWindow.filter((s) => s === false).length; - const failureRate = (failures / monitor.statusWindow.length) * 100; - - // If threshold has been met and the monitor is not already down, mark down: - if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) { - newStatus = false; - statusChanged = true; - } - // If the failure rate is below the threshold and the monitor is down, recover: - else if (failureRate < monitor.statusWindowThreshold && monitor.status === false) { - newStatus = true; - statusChanged = true; - } - - if (statusChanged) { - this.logger.info({ - service: this.SERVICE_NAME, - message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`, - prevStatus, - newStatus, - }); - } - - monitor.status = newStatus; - await monitor.save(); - - return { - monitor, - statusChanged, - prevStatus, - code, - timestamp: new Date().getTime(), - }; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "updateStatus"; - throw error; - } - }; - - /** - * Builds a check object from the network response. - * - * @param {Object} networkResponse - The network response object. - * @param {string} networkResponse.monitorId - The monitor ID. - * @param {string} networkResponse.type - The type of the response. - * @param {string} networkResponse.status - The status of the response. - * @param {number} networkResponse.responseTime - The response time. - * @param {number} networkResponse.code - The status code. - * @param {string} networkResponse.message - The message. - * @param {Object} networkResponse.payload - The payload of the response. - * @returns {Object} The check object. - */ - buildCheck = (networkResponse) => { - const { - monitorId, - teamId, - type, - status, - responseTime, - code, - message, - payload, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - timings, - } = networkResponse; - - const check = { - monitorId, - teamId, - type, - status, - statusCode: code, - responseTime, - timings: timings || {}, - message, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - }; - - if (type === "pagespeed") { - if (typeof payload === "undefined") { - this.logger.warn({ - message: "Failed to build check", - service: this.SERVICE_NAME, - method: "buildCheck", - details: "empty payload", - }); - return undefined; - } - const categories = payload?.lighthouseResult?.categories ?? {}; - const audits = payload?.lighthouseResult?.audits ?? {}; - const { - "cumulative-layout-shift": cls = 0, - "speed-index": si = 0, - "first-contentful-paint": fcp = 0, - "largest-contentful-paint": lcp = 0, - "total-blocking-time": tbt = 0, - } = audits; - check.accessibility = (categories?.accessibility?.score || 0) * 100; - check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; - check.seo = (categories?.seo?.score || 0) * 100; - check.performance = (categories?.performance?.score || 0) * 100; - check.audits = { cls, si, fcp, lcp, tbt }; - } - - if (type === "hardware") { - const { cpu, memory, disk, host, net } = payload?.data ?? {}; - const { errors } = payload?.errors ?? []; - check.cpu = cpu ?? {}; - check.memory = memory ?? {}; - check.disk = disk ?? {}; - check.host = host ?? {}; - check.errors = errors ?? []; - check.capture = payload?.capture ?? {}; - check.net = net ?? {}; - } - return check; - }; - - /** - * Inserts a check into the database based on the network response. - * - * @param {Object} networkResponse - The network response object. - * @param {string} networkResponse.monitorId - The monitor ID. - * @param {string} networkResponse.type - The type of the response. - * @param {string} networkResponse.status - The status of the response. - * @param {number} networkResponse.responseTime - The response time. - * @param {number} networkResponse.code - The status code. - * @param {string} networkResponse.message - The message. - * @param {Object} networkResponse.payload - The payload of the response. - * @returns {Promise} A promise that resolves when the check is inserted. - */ - insertCheck = async (check) => { - try { - if (typeof check === "undefined") { - this.logger.warn({ - message: "Failed to build check", - service: this.SERVICE_NAME, - method: "insertCheck", - }); - return false; - } - this.buffer.addToBuffer({ check }); - return true; - } catch (error) { - this.logger.error({ - message: error.message, - service: error.service || this.SERVICE_NAME, - method: error.method || "insertCheck", - details: error.details || `Error inserting check for monitor: ${check?.monitorId}`, - stack: error.stack, - }); - } - }; -} -export default StatusService; diff --git a/server/src/service/v1/system/serviceRegistry.js b/server/src/service/v1/system/serviceRegistry.js deleted file mode 100755 index 4e3e28464..000000000 --- a/server/src/service/v1/system/serviceRegistry.js +++ /dev/null @@ -1,63 +0,0 @@ -const SERVICE_NAME = "ServiceRegistry"; - -class ServiceRegistry { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ logger }) { - this.services = {}; - this.logger = logger; - } - - get serviceName() { - return ServiceRegistry.SERVICE_NAME; - } - - // Instance methods - register(name, service) { - this.logger.info({ - message: `Registering service ${name}`, - service: SERVICE_NAME, - method: "register", - }); - this.services[name] = service; - } - - get(name) { - if (!this.services[name]) { - this.logger.error({ - message: `Service ${name} is not registered`, - service: SERVICE_NAME, - method: "get", - }); - throw new Error(`Service ${name} is not registered`); - } - return this.services[name]; - } - - listServices() { - return Object.keys(this.services); - } - - static get(name) { - if (!ServiceRegistry.instance) { - throw new Error("ServiceRegistry not initialized"); - } - return ServiceRegistry.instance.get(name); - } - - static register(name, service) { - if (!ServiceRegistry.instance) { - throw new Error("ServiceRegistry not initialized"); - } - return ServiceRegistry.instance.register(name, service); - } - - static listServices() { - if (!ServiceRegistry.instance) { - throw new Error("ServiceRegistry not initialized"); - } - return ServiceRegistry.instance.listServices(); - } -} - -export default ServiceRegistry; diff --git a/server/src/service/v1/system/settingsService.js b/server/src/service/v1/system/settingsService.js deleted file mode 100755 index 70d8d192c..000000000 --- a/server/src/service/v1/system/settingsService.js +++ /dev/null @@ -1,67 +0,0 @@ -const SERVICE_NAME = "SettingsService"; - -const envConfig = { - jwtSecret: process.env.JWT_SECRET, - jwtTTL: process.env.TOKEN_TTL, - systemEmailHost: process.env.SYSTEM_EMAIL_HOST, - nodeEnv: process.env.NODE_ENV, - logLevel: process.env.LOG_LEVEL, - clientHost: process.env.CLIENT_HOST, - dbConnectionString: process.env.DB_CONNECTION_STRING, - port: process.env.PORT, -}; -/** - * SettingsService - * - * This service is responsible for loading and managing the application settings. - */ -class SettingsService { - static SERVICE_NAME = "SettingsService"; - - /** - * Constructs a new SettingsService - * @constructor - * @throws {Error} - */ constructor(AppSettings) { - this.AppSettings = AppSettings; - this.settings = { ...envConfig }; - } - - get serviceName() { - return SettingsService.SERVICE_NAME; - } - - /** - * Load settings from env settings - * @returns {Object>} The settings. - */ - loadSettings() { - return this.settings; - } - - /** - * Get the current settings. - * @returns {Object} The current settings. - * @throws Will throw an error if settings have not been loaded. - */ - getSettings() { - if (!this.settings) { - throw new Error("Settings have not been loaded"); - } - return this.settings; - } - - async getDBSettings() { - // Remove any old settings - await this.AppSettings.deleteMany({ version: { $exists: false } }); - - let settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); - if (settings === null) { - await this.AppSettings.create({}); - settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); - } - return settings; - } -} - -export default SettingsService; diff --git a/server/src/service/v1/system/stringService.js b/server/src/service/v1/system/stringService.js deleted file mode 100755 index 231550be7..000000000 --- a/server/src/service/v1/system/stringService.js +++ /dev/null @@ -1,452 +0,0 @@ -const SERVICE_NAME = "StringService"; - -class StringService { - static SERVICE_NAME = SERVICE_NAME; - - constructor(translationService) { - if (StringService.instance) { - return StringService.instance; - } - - this.translationService = translationService; - this._language = "en"; // default language - StringService.instance = this; - } - - get serviceName() { - return StringService.SERVICE_NAME; - } - - setLanguage(language) { - this._language = language; - } - - get language() { - return this._language; - } - - // Auth Messages - get dontHaveAccount() { - return this.translationService.getTranslation("dontHaveAccount"); - } - - get email() { - return this.translationService.getTranslation("email"); - } - - get forgotPassword() { - return this.translationService.getTranslation("forgotPassword"); - } - - get password() { - return this.translationService.getTranslation("password"); - } - - get signUp() { - return this.translationService.getTranslation("signUp"); - } - - get submit() { - return this.translationService.getTranslation("submit"); - } - - get title() { - return this.translationService.getTranslation("title"); - } - - get continue() { - return this.translationService.getTranslation("continue"); - } - - get enterEmail() { - return this.translationService.getTranslation("enterEmail"); - } - - get authLoginTitle() { - return this.translationService.getTranslation("authLoginTitle"); - } - - get authLoginEnterPassword() { - return this.translationService.getTranslation("authLoginEnterPassword"); - } - - get commonPassword() { - return this.translationService.getTranslation("commonPassword"); - } - - get commonBack() { - return this.translationService.getTranslation("commonBack"); - } - - get authForgotPasswordTitle() { - return this.translationService.getTranslation("authForgotPasswordTitle"); - } - - get authForgotPasswordResetPassword() { - return this.translationService.getTranslation("authForgotPasswordResetPassword"); - } - - get createPassword() { - return this.translationService.getTranslation("createPassword"); - } - - get createAPassword() { - return this.translationService.getTranslation("createAPassword"); - } - - get authRegisterAlreadyHaveAccount() { - return this.translationService.getTranslation("authRegisterAlreadyHaveAccount"); - } - - get commonAppName() { - return this.translationService.getTranslation("commonAppName"); - } - - get authLoginEnterEmail() { - return this.translationService.getTranslation("authLoginEnterEmail"); - } - - get authRegisterTitle() { - return this.translationService.getTranslation("authRegisterTitle"); - } - - get monitorGetAll() { - return this.translationService.getTranslation("monitorGetAll"); - } - - get monitorGetById() { - return this.translationService.getTranslation("monitorGetById"); - } - - get monitorGetByIdSuccess() { - return this.translationService.getTranslation("monitorGetByIdSuccess"); - } - - get monitorCreate() { - return this.translationService.getTranslation("monitorCreate"); - } - - get bulkMonitorsCreate() { - return this.translationService.getTranslation("bulkMonitorsCreate"); - } - - get monitorEdit() { - return this.translationService.getTranslation("monitorEdit"); - } - - get monitorDelete() { - return this.translationService.getTranslation("monitorDelete"); - } - - get monitorPause() { - return this.translationService.getTranslation("monitorPause"); - } - - get monitorResume() { - return this.translationService.getTranslation("monitorResume"); - } - - get monitorDemoAdded() { - return this.translationService.getTranslation("monitorDemoAdded"); - } - - get monitorStatsById() { - return this.translationService.getTranslation("monitorStatsById"); - } - - get monitorCertificate() { - return this.translationService.getTranslation("monitorCertificate"); - } - - // Maintenance Window Messages - get maintenanceWindowCreate() { - return this.translationService.getTranslation("maintenanceWindowCreate"); - } - - get maintenanceWindowGetById() { - return this.translationService.getTranslation("maintenanceWindowGetById"); - } - - get maintenanceWindowGetByTeam() { - return this.translationService.getTranslation("maintenanceWindowGetByTeam"); - } - - get maintenanceWindowDelete() { - return this.translationService.getTranslation("maintenanceWindowDelete"); - } - - get maintenanceWindowEdit() { - return this.translationService.getTranslation("maintenanceWindowEdit"); - } - - // Webhook Messages - get webhookUnsupportedPlatform() { - return this.translationService.getTranslation("webhookUnsupportedPlatform"); - } - - get webhookSendError() { - return this.translationService.getTranslation("webhookSendError"); - } - - get webhookSendSuccess() { - return this.translationService.getTranslation("webhookSendSuccess"); - } - - get telegramRequiresBotTokenAndChatId() { - return this.translationService.getTranslation("telegramRequiresBotTokenAndChatId"); - } - - get webhookUrlRequired() { - return this.translationService.getTranslation("webhookUrlRequired"); - } - - get platformRequired() { - return this.translationService.getTranslation("platformRequired"); - } - - get testNotificationFailed() { - return this.translationService.getTranslation("testNotificationFailed"); - } - - get monitorUpAlert() { - return this.translationService.getTranslation("monitorUpAlert"); - } - - get monitorDownAlert() { - return this.translationService.getTranslation("monitorDownAlert"); - } - - get discordNotification() { - return this.translationService.getTranslation("discordNotification"); - } - - getWebhookUnsupportedPlatform(platform) { - return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform); - } - - getWebhookSendError(platform) { - return this.translationService.getTranslation("webhookSendError").replace("{platform}", platform); - } - - getMonitorStatus(name, status, url) { - const translationKey = status === true ? "monitorStatusUp" : "monitorStatusDown"; - return this.translationService.getTranslation(translationKey).replace("{name}", name).replace("{url}", url); - } - - // Error Messages - get unknownError() { - return this.translationService.getTranslation("unknownError"); - } - - get friendlyError() { - return this.translationService.getTranslation("friendlyError"); - } - - get authIncorrectPassword() { - return this.translationService.getTranslation("authIncorrectPassword"); - } - - get unauthorized() { - return this.translationService.getTranslation("unauthorized"); - } - - get authAdminExists() { - return this.translationService.getTranslation("authAdminExists"); - } - - get authInviteNotFound() { - return this.translationService.getTranslation("authInviteNotFound"); - } - - get unknownService() { - return this.translationService.getTranslation("unknownService"); - } - - get noAuthToken() { - return this.translationService.getTranslation("noAuthToken"); - } - - get invalidAuthToken() { - return this.translationService.getTranslation("invalidAuthToken"); - } - - get expiredAuthToken() { - return this.translationService.getTranslation("expiredAuthToken"); - } - - // Queue Messages - get queueGetMetrics() { - return this.translationService.getTranslation("queueGetMetrics"); - } - - get queueGetJobs() { - return this.translationService.getTranslation("queueGetJobs"); - } - - get queueAddJob() { - return this.translationService.getTranslation("queueAddJob"); - } - - get queueObliterate() { - return this.translationService.getTranslation("queueObliterate"); - } - - // Job Queue Messages - get jobQueueDeleteJobSuccess() { - return this.translationService.getTranslation("jobQueueDeleteJobSuccess"); - } - - get jobQueuePauseJob() { - return this.translationService.getTranslation("jobQueuePauseJob"); - } - - get jobQueueResumeJob() { - return this.translationService.getTranslation("jobQueueResumeJob"); - } - - // Status Page Messages - get statusPageByUrl() { - return this.translationService.getTranslation("statusPageByUrl"); - } - - get statusPageCreate() { - return this.translationService.getTranslation("statusPageCreate"); - } - - get statusPageDelete() { - return this.translationService.getTranslation("statusPageDelete"); - } - - get statusPageUpdate() { - return this.translationService.getTranslation("statusPageUpdate"); - } - - get statusPageNotFound() { - return this.translationService.getTranslation("statusPageNotFound"); - } - - get statusPageByTeamId() { - return this.translationService.getTranslation("statusPageByTeamId"); - } - - get statusPageUrlNotUnique() { - return this.translationService.getTranslation("statusPageUrlNotUnique"); - } - - // Docker Messages - get dockerFail() { - return this.translationService.getTranslation("dockerFail"); - } - - get dockerNotFound() { - return this.translationService.getTranslation("dockerNotFound"); - } - - get dockerSuccess() { - return this.translationService.getTranslation("dockerSuccess"); - } - - // Port Messages - get portFail() { - return this.translationService.getTranslation("portFail"); - } - - get portSuccess() { - return this.translationService.getTranslation("portSuccess"); - } - - // Alert Messages - get alertCreate() { - return this.translationService.getTranslation("alertCreate"); - } - - get alertGetByUser() { - return this.translationService.getTranslation("alertGetByUser"); - } - - get alertGetByMonitor() { - return this.translationService.getTranslation("alertGetByMonitor"); - } - - get alertGetById() { - return this.translationService.getTranslation("alertGetById"); - } - - get alertEdit() { - return this.translationService.getTranslation("alertEdit"); - } - - get alertDelete() { - return this.translationService.getTranslation("alertDelete"); - } - - getDeletedCount(count) { - return this.translationService.getTranslation("deletedCount").replace("{count}", count); - } - - get pingSuccess() { - return this.translationService.getTranslation("pingSuccess"); - } - - get getAppSettings() { - return this.translationService.getTranslation("getAppSettings"); - } - - get httpNetworkError() { - return this.translationService.getTranslation("httpNetworkError"); - } - - get httpNotJson() { - return this.translationService.getTranslation("httpNotJson"); - } - - get httpJsonPathError() { - return this.translationService.getTranslation("httpJsonPathError"); - } - - get httpEmptyResult() { - return this.translationService.getTranslation("httpEmptyResult"); - } - - get httpMatchSuccess() { - return this.translationService.getTranslation("httpMatchSuccess"); - } - - get httpMatchFail() { - return this.translationService.getTranslation("httpMatchFail"); - } - - get updateAppSettings() { - return this.translationService.getTranslation("updateAppSettings"); - } - - get insufficientPermissions() { - return this.translationService.getTranslation("insufficientPermissions"); - } - - getDbFindMonitorById(monitorId) { - return this.translationService.getTranslation("dbFindMonitorById").replace("${monitorId}", monitorId); - } - - get dbUserExists() { - return this.translationService.getTranslation("dbUserExists"); - } - - get testEmailSubject() { - return this.translationService.getTranslation("testEmailSubject"); - } - - get verifyOwnerNotFound() { - return this.translationService.getTranslation("verifyOwnerNotFound"); - } - - get verifyOwnerUnauthorized() { - return this.translationService.getTranslation("verifyOwnerUnauthorized"); - } - - get dbUserNotFound() { - return this.translationService.getTranslation("dbUserNotFound"); - } -} - -export default StringService; diff --git a/server/src/service/v1/system/translationService.js b/server/src/service/v1/system/translationService.js deleted file mode 100755 index 0d3893852..000000000 --- a/server/src/service/v1/system/translationService.js +++ /dev/null @@ -1,92 +0,0 @@ -import fs from "fs"; -import path from "path"; - -class TranslationService { - static SERVICE_NAME = "TranslationService"; - constructor(logger) { - this.logger = logger; - this.translations = {}; - this._language = "en"; - this.localesDir = path.join(process.cwd(), "src/locales"); - } - - get serviceName() { - return TranslationService.SERVICE_NAME; - } - - setLanguage(language) { - this._language = language; - } - - get language() { - return this._language; - } - - async initialize() { - try { - await this.loadFromFiles(); - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "initialize", - stack: error.stack, - }); - } - } - - async loadFromFiles() { - try { - if (!fs.existsSync(this.localesDir)) { - return false; - } - - const files = fs.readdirSync(this.localesDir).filter((file) => file.endsWith(".json")); - - if (files.length === 0) { - return false; - } - - for (const file of files) { - const language = file.replace(".json", ""); - const filePath = path.join(this.localesDir, file); - const content = fs.readFileSync(filePath, "utf8"); - this.translations[language] = JSON.parse(content); - } - - this.logger.info({ - message: "Translations loaded from files successfully", - service: "TranslationService", - method: "loadFromFiles", - }); - - return true; - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "loadFromFiles", - stack: error.stack, - }); - return false; - } - } - - getTranslation(key) { - let language = this._language; - - try { - return this.translations[language]?.[key] || this.translations["en"]?.[key] || key; - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "getTranslation", - stack: error.stack, - }); - return key; - } - } -} - -export default TranslationService; diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts deleted file mode 100644 index 67ece226e..000000000 --- a/server/src/service/v2/business/AuthService.ts +++ /dev/null @@ -1,210 +0,0 @@ -import bcrypt from "bcryptjs"; -import { User, Role, ITokenizedUser, Monitor, Check, NotificationChannel } from "../../../db/v2/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; -import { Types } from "mongoose"; -import { IJobQueue } from "../infrastructure/JobQueue.js"; - -const SERVICE_NAME = "AuthServiceV2"; - -export const PERMISSIONS = { - users: { - all: "users.*", - create: "users.create", - view: "users.view", - update: "users.update", - delete: "users.delete", - }, - - monitors: { - all: "monitors.*", - create: "monitors.create", - view: "monitors.view", - update: "monitors.update", - delete: "monitors.delete", - }, - notifications: { - all: "notifications.*", - create: "notifications.create", - view: "notifications.view", - update: "notifications.update", - delete: "notifications.delete", - }, - checks: { - all: "checks.*", - create: "checks.create", - view: "checks.view", - update: "checks.update", - delete: "checks.delete", - }, - statusPages: { - all: "statusPages.*", - create: "statusPages.create", - view: "statusPages.view", - update: "statusPages.update", - delete: "statusPages.delete", - }, -}; - -const DEFAULT_ROLES = [ - { - name: "SuperAdmin", - description: "Super admin with all permissions", - permissions: ["*"], - isSystem: true, - }, - { - name: "Admin", - description: "Admin with full permissions", - permissions: [PERMISSIONS.monitors.all, PERMISSIONS.users.all], - isSystem: true, - }, - { - name: "Manager", - description: "Can manage users", - permissions: [PERMISSIONS.users.create, PERMISSIONS.users.update, PERMISSIONS.monitors.all], - isSystem: true, - }, - { - name: "Member", - description: "Basic team member", - permissions: [PERMISSIONS.users.update, PERMISSIONS.monitors.create, PERMISSIONS.monitors.view, PERMISSIONS.monitors.update], - isSystem: true, - }, -]; - -export type RegisterData = { - email: string; - firstName: string; - lastName: string; - password: string; - roles?: Types.ObjectId[]; // Optional roles for invite-based registration -}; - -export type LoginData = { - email: string; - password: string; -}; - -export type AuthResult = ITokenizedUser; - -export interface IAuthService { - register(signupData: RegisterData): Promise; - registerWithInvite(signupData: RegisterData): Promise; - login(loginData: LoginData): Promise; - cleanup(): Promise; - cleanMonitors(): Promise; -} - -class AuthService implements IAuthService { - static SERVICE_NAME = SERVICE_NAME; - - private jobQueue: IJobQueue; - constructor(jobQueue: IJobQueue) { - this.jobQueue = jobQueue; - } - - async register(signupData: RegisterData): Promise { - const userCount = await User.countDocuments(); - - if (userCount > 0) { - throw new Error("Registration is closed. Please request an invite."); - } - - const { email, firstName, lastName, password } = signupData; - - // Create all default roles - const rolePromises = DEFAULT_ROLES.map((roleData) => - new Role({ - ...roleData, - }).save() - ); - const roles = await Promise.all(rolePromises); - - // Hash password and create user - const saltRounds = 12; - const passwordHash = await bcrypt.hash(password, saltRounds); - - // Find admin role and assign to first user - const superAdminRole = roles.find((role) => role.name === "SuperAdmin"); - - const user = new User({ - email, - firstName, - lastName, - passwordHash, - roles: [superAdminRole!._id], - }); - - const savedUser = await user.save(); - return { - sub: savedUser._id.toString(), - roles: savedUser.roles.map((role) => role.toString()), - }; - } - - async registerWithInvite(signupData: RegisterData): Promise { - const { email, firstName, lastName, password, roles } = signupData; - - const saltRounds = 12; - const passwordHash = await bcrypt.hash(password, saltRounds); - const user = new User({ - email, - firstName, - lastName, - passwordHash, - roles: roles || [], - }); - try { - const savedUser = await user.save(); - return { - sub: savedUser._id.toString(), - roles: savedUser.roles.map((role) => role.toString()), - }; - } catch (error: any) { - if (error?.code === 11000) { - const dupError = new ApiError("Email already in use", 409); - dupError.stack = error?.stack; - throw dupError; - } - throw error; - } - } - - async login(loginData: LoginData): Promise { - const { email, password } = loginData; - - // Find user by email - const user = await User.findOne({ email }); - - if (!user) { - throw new Error("Invalid email or password"); - } - - // Check password - const isPasswordValid = await bcrypt.compare(password, user.passwordHash); - if (!isPasswordValid) { - throw new Error("Invalid email or password"); - } - - return { - sub: user._id.toString(), - roles: user.roles.map((role) => role.toString()), - }; - } - - async cleanup() { - await User.deleteMany({}); - await Role.deleteMany({}); - await Monitor.deleteMany({}); - await Check.deleteMany({}); - await NotificationChannel.deleteMany({}); - await this.jobQueue.flush(); - } - - async cleanMonitors() { - await Monitor.deleteMany({}); - await Check.deleteMany({}); - } -} - -export default AuthService; diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts deleted file mode 100644 index 54593ed2b..000000000 --- a/server/src/service/v2/business/CheckService.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { json } from "stream/consumers"; -import { ICheck, Check, Monitor } from "../../../db/v2/models/index.js"; -import type { ISystemInfo, ICaptureInfo } from "../../../db/v2/models/index.js"; -import { MonitorType } from "../../../db/v2/models/monitors/Monitor.js"; -import { StatusResponse } from "../infrastructure/NetworkService.js"; -import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js"; -import mongoose from "mongoose"; -import { stat } from "fs"; - -const SERVICE_NAME = "CheckServiceV2"; -export interface ICheckService { - buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise; - cleanupOrphanedChecks: () => Promise; -} - -class CheckService implements ICheckService { - static SERVICE_NAME = SERVICE_NAME; - - private isCapturePayload = (payload: any): payload is ICapturePayload => { - if (!payload || typeof payload !== "object") return false; - - if (!("data" in payload) || typeof payload.data !== "object") { - return false; - } - - const data = payload.data as Partial; - if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") { - return false; - } - - if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") { - return false; - } - - if (data.disk && !Array.isArray(data.disk)) { - return false; - } - if (data.net && !Array.isArray(data.net)) { - return false; - } - - if (!("capture" in payload) || typeof payload.capture !== "object") return false; - const capture = payload.capture as Record; - if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false; - - return true; - }; - - private isPagespeedPayload = (payload: any): payload is ILighthousePayload => { - if (!payload || typeof payload !== "object") return false; - - if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") { - return false; - } - return true; - }; - - private buildBaseCheck = (statusResponse: StatusResponse) => { - const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId); - const check = new Check({ - monitorId: monitorId, - type: statusResponse?.type, - status: statusResponse?.status, - httpStatusCode: statusResponse?.code, - message: statusResponse?.message, - responseTime: statusResponse?.responseTime, - timings: statusResponse?.timings, - }); - return check; - }; - - private buildInfrastructureCheck = (statusResponse: StatusResponse) => { - if (!this.isCapturePayload(statusResponse.payload)) { - throw new Error("Invalid payload for infrastructure monitor"); - } - const check = this.buildBaseCheck(statusResponse); - check.system = statusResponse.payload.data; - check.capture = statusResponse.payload.capture; - return check; - }; - - private buildPagespeedCheck = (statusResponse: StatusResponse) => { - if (!this.isPagespeedPayload(statusResponse.payload)) { - throw new Error("Invalid payload for pagespeed monitor"); - } - const check = this.buildBaseCheck(statusResponse); - const lighthouseResult = statusResponse?.payload?.lighthouseResult; - check.lighthouse = { - accessibility: lighthouseResult?.categories?.accessibility?.score || 0, - bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0, - seo: lighthouseResult?.categories?.seo?.score || 0, - performance: lighthouseResult?.categories?.performance?.score || 0, - audits: { - cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {}, - si: lighthouseResult?.audits?.["speed-index"] || {}, - fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {}, - lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {}, - tbt: lighthouseResult?.audits?.["total-blocking-time"] || {}, - }, - }; - return check; - }; - - buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise => { - switch (type) { - case "infrastructure": - return this.buildInfrastructureCheck(statusResponse as StatusResponse); - - case "pagespeed": - return this.buildPagespeedCheck(statusResponse as StatusResponse); - case "http": - case "https": - return this.buildBaseCheck(statusResponse); - - case "ping": - return this.buildBaseCheck(statusResponse); - default: - throw new Error(`Unsupported monitor type: ${type}`); - } - }; - - cleanupOrphanedChecks = async () => { - try { - const monitorIds = await Monitor.find().distinct("_id"); - const result = await Check.deleteMany({ - monitorId: { $nin: monitorIds }, - }); - console.log(`Deleted ${result.deletedCount} orphaned Checks.`); - return true; - } catch (error) { - console.error("Error cleaning up orphaned Checks:", error); - return false; - } - }; - - getChecks = async (monitorId: string, page: number, rowsPerPage: number) => { - const count = await Check.countDocuments({ monitorId: new mongoose.Types.ObjectId(monitorId) }); - const checks = await Check.find({ monitorId: new mongoose.Types.ObjectId(monitorId) }) - .sort({ createdAt: -1 }) - .skip(page * rowsPerPage) - .limit(rowsPerPage) - .exec(); - return { checks, count }; - }; -} - -export default CheckService; diff --git a/server/src/service/v2/business/InviteService.ts b/server/src/service/v2/business/InviteService.ts deleted file mode 100644 index b06b2a2c2..000000000 --- a/server/src/service/v2/business/InviteService.ts +++ /dev/null @@ -1,63 +0,0 @@ -import crypto from "node:crypto"; -import { ITokenizedUser, IInvite, Invite } from "../../../db/v2/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; - -const SERVICE_NAME = "InviteServiceV2"; -export interface IInviteService { - create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>; - getAll: () => Promise; - get: (tokenHash: string) => Promise; - delete: (id: string) => Promise; -} - -class InviteService implements IInviteService { - static SERVICE_NAME = SERVICE_NAME; - constructor() {} - - create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => { - const token = crypto.randomBytes(32).toString("hex"); - const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); - try { - const invite = await Invite.create({ - ...inviteData, - tokenHash, - createdBy: tokenizedUser.sub, - updatedBy: tokenizedUser.sub, - }); - if (!invite) { - throw new ApiError("Failed to create invite", 500); - } - return { token }; - } catch (error: any) { - if (error?.code === 11000) { - const dupError = new ApiError("Invite with this email already exists", 409); - dupError.stack = error?.stack; - throw dupError; - } - throw error; - } - }; - - get = async (token: string) => { - const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); - const invite = await Invite.findOne({ tokenHash }); - if (!invite) { - throw new ApiError("Invite not found", 404); - } - return invite; - }; - - getAll = async () => { - return Invite.find(); - }; - - delete = async (id: string) => { - const result = await Invite.deleteOne({ _id: id }); - if (!result.deletedCount) { - throw new ApiError("Invite not found", 404); - } - return result.deletedCount === 1; - }; -} - -export default InviteService; diff --git a/server/src/service/v2/business/MaintenanceService.ts b/server/src/service/v2/business/MaintenanceService.ts deleted file mode 100644 index 55b2d787b..000000000 --- a/server/src/service/v2/business/MaintenanceService.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { ITokenizedUser, IMaintenance, Maintenance } from "../../../db/v2/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; - -const SERVICE_NAME = "MaintenanceServiceV2"; - -export interface IMaintenanceService { - create: ( - tokenizedUser: ITokenizedUser, - - maintenance: IMaintenance - ) => Promise; - getAll: () => Promise; - get: (id: string) => Promise; - toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise; - update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => Promise; - delete: (id: string) => Promise; - isInMaintenance: (monitorId: string) => Promise; -} - -type MaintenanceCache = Map; - -class MaintenanceService implements IMaintenanceService { - static SERVICE_NAME = SERVICE_NAME; - private maintenanceCache: MaintenanceCache; - private lastRefresh: number; - private CACHE_TTL_MS = 60 * 1000; - - constructor() { - this.maintenanceCache = new Map(); - this.lastRefresh = 0; - } - - create = async (tokenizedUser: ITokenizedUser, maintenanceData: IMaintenance) => { - const maintenance = await Maintenance.create({ - ...maintenanceData, - createdBy: tokenizedUser.sub, - updatedBy: tokenizedUser.sub, - }); - return maintenance; - }; - - get = async (id: string) => { - const maintenance = await Maintenance.findById(id); - if (!maintenance) { - throw new ApiError("Maintenance not found", 404); - } - return maintenance; - }; - - getAll = async () => { - return Maintenance.find(); - }; - - toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => { - const updatedMaintenance = await Maintenance.findOneAndUpdate( - { _id: id }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - updatedBy: tokenizedUser.sub, - updatedAt: new Date(), - }, - }, - ], - { new: true } - ); - if (!updatedMaintenance) { - throw new ApiError("Maintenance not found", 404); - } - return updatedMaintenance; - }; - - update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => { - const allowedFields: (keyof IMaintenance)[] = ["name", "monitors", "startTime", "endTime", "isActive"]; - const safeUpdate: Partial = {}; - for (const field of allowedFields) { - if (updateData[field] !== undefined) { - (safeUpdate as any)[field] = updateData[field]; - } - } - - const updatedMaintenance = await Maintenance.findByIdAndUpdate( - id, - { - $set: { - ...safeUpdate, - updatedAt: new Date(), - updatedBy: tokenizedUser.sub, - }, - }, - { new: true, runValidators: true } - ); - - if (!updatedMaintenance) { - throw new ApiError("Failed to update maintenance", 500); - } - - return updatedMaintenance; - }; - - delete = async (id: string) => { - const result = await Maintenance.deleteOne({ _id: id }); - if (!result.deletedCount) { - throw new ApiError("Maintenance not found", 404); - } - return result.deletedCount === 1; - }; - - private refreshCache = async () => { - const now = new Date(); - - const activeMaintenances = await Maintenance.find({ - isActive: true, - startTime: { $lte: now }, - endTime: { $gte: now }, - }).lean(); - - // Reset cache - const newCache = new Map(); - - for (const m of activeMaintenances) { - for (const monitorId of m.monitors) { - const key = monitorId.toString(); - if (!newCache.has(key)) newCache.set(key, []); - newCache.get(key)!.push(m); - } - } - - this.maintenanceCache = newCache; - this.lastRefresh = Date.now(); - }; - - isInMaintenance = async (monitorId: string) => { - const now = Date.now(); - if (now - this.lastRefresh > this.CACHE_TTL_MS) { - await this.refreshCache(); - } - const maintenances = this.maintenanceCache.get(monitorId) || []; - return maintenances.length > 0; - }; -} - -export default MaintenanceService; diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts deleted file mode 100644 index fb338c348..000000000 --- a/server/src/service/v2/business/MonitorService.ts +++ /dev/null @@ -1,468 +0,0 @@ -import mongoose from "mongoose"; - -import { IMonitor, Monitor, ITokenizedUser, MonitorStats, Check } from "../../../db/v2/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; -import { IJobQueue } from "../infrastructure/JobQueue.js"; -import { MonitorWithChecksResponse } from "../../../types/index.js"; -import { MonitorStatus, MonitorType } from "../../../db/v2/models/monitors/Monitor.js"; - -const SERVICE_NAME = "MonitorServiceV2"; - -export interface IMonitorService { - create: (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => Promise; - getAll: () => Promise; - getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise; - get: (monitorId: string) => Promise; - getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise; - toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise; - update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) => Promise; - delete: (monitorId: string) => Promise; -} - -class MonitorService implements IMonitorService { - static SERVICE_NAME = SERVICE_NAME; - private jobQueue: IJobQueue; - constructor(jobQueue: IJobQueue) { - this.jobQueue = jobQueue; - } - - create = async (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => { - const monitor = await Monitor.create({ - ...monitorData, - createdBy: tokenizedUser.sub, - updatedBy: tokenizedUser.sub, - }); - await MonitorStats.create({ - monitorId: monitor._id, - currentStreakStartedAt: Date.now(), - }); - await this.jobQueue.addJob(monitor); - return monitor; - }; - - getAll = async () => { - return Monitor.find(); - }; - - getAllEmbedChecks = async (page: number, limit: number, type: MonitorType[] = []) => { - const skip = (page - 1) * limit; - let find = {}; - if (type.length > 0) find = { type: { $in: type } }; - const monitors = await Monitor.find(find).skip(skip).limit(limit); - return monitors; - }; - - get = async (monitorId: string) => { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - return monitor; - }; - - private getStartDate(range: string): Date { - const now = new Date(); - switch (range) { - case "2h": - return new Date(now.getTime() - 2 * 60 * 60 * 1000); - case "24h": - return new Date(now.getTime() - 24 * 60 * 60 * 1000); - case "7d": - return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - case "30d": - return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - default: - throw new ApiError("Invalid range parameter", 400); - } - } - - private getDateFormat(range: string): string { - switch (range) { - case "2h": - return "%Y-%m-%dT%H:%M:00Z"; - case "24h": - case "7d": - return "%Y-%m-%dT%H:00:00Z"; - case "30d": - return "%Y-%m-%d"; - default: - throw new ApiError("Invalid range parameter", 400); - } - } - - private getBaseGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - }; - }; - - private getBaseProjection = (): object => { - return { status: 1, responseTime: 1, createdAt: 1 }; - }; - - private getPageSpeedGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - accessibility: { $avg: "$lighthouse.accessibility" }, - bestPractices: { $avg: "$lighthouse.bestPractices" }, - seo: { $avg: "$lighthouse.seo" }, - performance: { $avg: "$lighthouse.performance" }, - cls: { $avg: "$lighthouse.audits.cls.score" }, - si: { $avg: "$lighthouse.audits.si.score" }, - fcp: { $avg: "$lighthouse.audits.fcp.score" }, - lcp: { $avg: "$lighthouse.audits.lcp.score" }, - tbt: { $avg: "$lighthouse.audits.tbt.score" }, - }; - }; - - private getPageSpeedProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["lighthouse.accessibility"] = 1; - projectStage["lighthouse.seo"] = 1; - projectStage["lighthouse.bestPractices"] = 1; - projectStage["lighthouse.performance"] = 1; - projectStage["lighthouse.audits.cls.score"] = 1; - projectStage["lighthouse.audits.si.score"] = 1; - projectStage["lighthouse.audits.fcp.score"] = 1; - projectStage["lighthouse.audits.lcp.score"] = 1; - projectStage["lighthouse.audits.tbt.score"] = 1; - return projectStage; - }; - - private getInfraGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - physicalCores: { $last: "$system.cpu.physical_core" }, - logicalCores: { $last: "$system.cpu.logical_core" }, - frequency: { $avg: "$system.cpu.frequency" }, - currentFrequency: { $last: "$system.cpu.current_frequency" }, - tempsArrays: { $push: "$system.cpu.temperature" }, - freePercent: { $avg: "$system.cpu.free_percent" }, - usedPercent: { $avg: "$system.cpu.usage_percent" }, - total_bytes: { $last: "$system.memory.total_bytes" }, - available_bytes: { $last: "$system.memory.available_bytes" }, - used_bytes: { $last: "$system.memory.used_bytes" }, - memory_usage_percent: { $avg: "$system.memory.usage_percent" }, - disksArray: { $push: "$system.disk" }, - os: { $last: "$system.host.os" }, - platform: { $last: "$system.host.platform" }, - kernel_version: { $last: "$system.host.kernel_version" }, - pretty_name: { $last: "$system.host.pretty_name" }, - netsArray: { $push: "$system.net" }, - }; - }; - - private getInfraProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["system.cpu.physical_core"] = 1; - projectStage["system.cpu.logical_core"] = 1; - projectStage["system.cpu.frequency"] = 1; - projectStage["system.cpu.current_frequency"] = 1; - projectStage["system.cpu.temperature"] = 1; - projectStage["system.cpu.free_percent"] = 1; - projectStage["system.cpu.usage_percent"] = 1; - projectStage["system.memory.total_bytes"] = 1; - projectStage["system.memory.available_bytes"] = 1; - projectStage["system.memory.used_bytes"] = 1; - projectStage["system.memory.usage_percent"] = 1; - projectStage["system.disk"] = 1; - projectStage["system.host.os"] = 1; - projectStage["system.host.platform"] = 1; - projectStage["system.host.kernel_version"] = 1; - projectStage["system.host.pretty_name"] = 1; - projectStage["system.net"] = 1; - return projectStage; - }; - - private getFinalProjection = (type: string): object => { - if (type === "pagespeed") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - accessibility: "$accessibility", - seo: "$seo", - bestPractices: "$bestPractices", - performance: "$performance", - cls: "$cls", - si: "$si", - fcp: "$fcp", - lcp: "$lcp", - tbt: "$tbt", - }; - } - - if (type === "infrastructure") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - cpu: { - physicalCores: "$physicalCores", - logicalCores: "$logicalCores", - frequency: "$frequency", - currentFrequency: "$currentFrequency", - temperatures: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }], - }, - as: "idx", - in: { - $avg: { - $map: { - input: "$tempsArrays", - as: "arr", - in: { $arrayElemAt: ["$$arr", "$$idx"] }, - }, - }, - }, - }, - }, - freePercent: "$freePercent", - usedPercent: "$usedPercent", - }, - memory: { - total_bytes: "$total_bytes", - available_bytes: "$available_bytes", - used_bytes: "$used_bytes", - usage_percent: "$memory_usage_percent", - }, - disks: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }], - }, - as: "idx", - in: { - $let: { - vars: { - diskGroup: { - $map: { - input: "$disksArray", - as: "diskArr", - in: { $arrayElemAt: ["$$diskArr", "$$idx"] }, - }, - }, - }, - in: { - device: { $arrayElemAt: ["$$diskGroup.device", 0] }, - total_bytes: { $avg: "$$diskGroup.total_bytes" }, - free_bytes: { $avg: "$$diskGroup.free_bytes" }, - used_bytes: { $avg: "$$diskGroup.used_bytes" }, - usage_percent: { $avg: "$$diskGroup.usage_percent" }, - total_inodes: { $avg: "$$diskGroup.total_inodes" }, - free_inodes: { $avg: "$$diskGroup.free_inodes" }, - used_inodes: { $avg: "$$diskGroup.used_inodes" }, - inodes_usage_percent: { - $avg: "$$diskGroup.inodes_usage_percent", - }, - read_bytes: { $avg: "$$diskGroup.read_bytes" }, - write_bytes: { $avg: "$$diskGroup.write_bytes" }, - read_time: { $avg: "$$diskGroup.read_time" }, - write_time: { $avg: "$$diskGroup.write_time" }, - }, - }, - }, - }, - }, - host: { - os: "$os", - platform: "$platform", - kernel_version: "$kernel_version", - pretty_name: "$pretty_name", - }, - net: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }], - }, - as: "idx", - in: { - $let: { - vars: { - netGroup: { - $map: { - input: "$netsArray", - as: "netArr", - in: { $arrayElemAt: ["$$netArr", "$$idx"] }, - }, - }, - }, - in: { - name: { $arrayElemAt: ["$$netGroup.name", 0] }, - bytes_sent: { $avg: "$$netGroup.bytes_sent" }, - bytes_recv: { $avg: "$$netGroup.bytes_recv" }, - packets_sent: { $avg: "$$netGroup.packets_sent" }, - packets_recv: { $avg: "$$netGroup.packets_recv" }, - err_in: { $avg: "$$netGroup.err_in" }, - err_out: { $avg: "$$netGroup.err_out" }, - drop_in: { $avg: "$$netGroup.drop_in" }, - drop_out: { $avg: "$$netGroup.drop_out" }, - fifo_in: { $avg: "$$netGroup.fifo_in" }, - fifo_out: { $avg: "$$netGroup.fifo_out" }, - }, - }, - }, - }, - }, - }; - } - return {}; - }; - - getEmbedChecks = async (monitorId: string, range: string, status: string | undefined): Promise => { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - const startDate = this.getStartDate(range); - const dateFormat = this.getDateFormat(range); - - // Build match stage - const matchStage: { - monitorId: mongoose.Types.ObjectId; - createdAt: { $gte: Date }; - status?: string; - } = { - monitorId: monitor._id, - createdAt: { $gte: startDate }, - }; - - if (status) { - matchStage.status = status; - } - - let groupClause; - - if (monitor.type === "pagespeed") { - groupClause = this.getPageSpeedGroup(dateFormat); - } else if (monitor.type === "infrastructure") { - groupClause = this.getInfraGroup(dateFormat); - } else { - groupClause = this.getBaseGroup(dateFormat); - } - - let projectStage; - if (monitor.type === "pagespeed") { - projectStage = this.getPageSpeedProjection(); - } else if (monitor.type === "infrastructure") { - projectStage = this.getInfraProjection(); - } else { - projectStage = this.getBaseProjection(); - } - - let finalProjection = {}; - if (monitor.type === "pagespeed" || monitor.type === "infrastructure") { - finalProjection = this.getFinalProjection(monitor.type); - } else { - finalProjection = { _id: 1, count: 1, avgResponseTime: 1 }; - } - - const checks = await Check.aggregate([ - { - $match: matchStage, - }, - { $sort: { createdAt: 1 } }, - { $project: projectStage }, - { $group: groupClause }, - { $sort: { _id: -1 } }, - { - $project: finalProjection, - }, - ]); - - // Get monitor stats - const monitorStats = await MonitorStats.findOne({ - monitorId: monitor._id, - }); - - if (!monitorStats) { - throw new ApiError("Monitor stats not found", 404); - } - - return { - monitor: monitor.toObject(), - checks, - stats: monitorStats, - }; - }; - - async toggleActive(id: string, tokenizedUser: ITokenizedUser) { - const pendingStatus: MonitorStatus = "initializing"; - const updatedMonitor = await Monitor.findOneAndUpdate( - { _id: id }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - status: pendingStatus, - updatedBy: tokenizedUser.sub, - updatedAt: new Date(), - }, - }, - ], - { new: true } - ); - - if (!updatedMonitor) { - throw new ApiError("Monitor not found", 404); - } - - await this.jobQueue.updateJob(updatedMonitor); - - if (updatedMonitor?.isActive) { - await this.jobQueue.resumeJob(updatedMonitor); - } else { - await this.jobQueue.pauseJob(updatedMonitor); - } - return updatedMonitor; - } - - async update(tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) { - const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "notificationChannels"]; - const safeUpdate: Partial = {}; - - for (const field of allowedFields) { - if (updateData[field] !== undefined) { - (safeUpdate as any)[field] = updateData[field]; - } - } - - const updatedMonitor = await Monitor.findByIdAndUpdate( - monitorId, - { - $set: { - ...safeUpdate, - updatedAt: new Date(), - updatedBy: tokenizedUser.sub, - }, - }, - { new: true, runValidators: true } - ); - - if (!updatedMonitor) { - throw new ApiError("Monitor not found", 404); - } - await this.jobQueue.updateJob(updatedMonitor); - return updatedMonitor; - } - - async delete(monitorId: string) { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - await monitor.deleteOne(); - await this.jobQueue.deleteJob(monitor); - return true; - } -} - -export default MonitorService; diff --git a/server/src/service/v2/business/MonitorStatsService.ts b/server/src/service/v2/business/MonitorStatsService.ts deleted file mode 100644 index 089b43ec4..000000000 --- a/server/src/service/v2/business/MonitorStatsService.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { MonitorStats } from "../../../db/v2/models/index.js"; -import { Monitor } from "../../../db/v2/models/index.js"; - -const SERVICE_NAME = "MonitorStatsServiceV2"; -export interface IMonitorStatsService { - cleanupOrphanedMonitorStats: () => Promise; -} - -class MonitorStatsService implements IMonitorStatsService { - static SERVICE_NAME = SERVICE_NAME; - constructor() {} - - async cleanupOrphanedMonitorStats() { - try { - const monitorIds = await Monitor.find().distinct("_id"); - const result = await MonitorStats.deleteMany({ - monitorId: { $nin: monitorIds }, - }); - console.log(`Deleted ${result.deletedCount} orphaned MonitorStats.`); - return true; - } catch (error) { - console.error("Error cleaning up orphaned MonitorStats:", error); - return false; - } - } -} - -export default MonitorStatsService; diff --git a/server/src/service/v2/business/NotificationChannelService.ts b/server/src/service/v2/business/NotificationChannelService.ts deleted file mode 100644 index 112efa691..000000000 --- a/server/src/service/v2/business/NotificationChannelService.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { ITokenizedUser, INotificationChannel, NotificationChannel, Monitor } from "../../../db/v2/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; - -const SERVICE_NAME = "NotificationChannelServiceV2"; - -export interface INotificationChannelService { - create: ( - tokenizedUser: ITokenizedUser, - - notificationChannel: INotificationChannel - ) => Promise; - getAll: () => Promise; - get: (id: string) => Promise; - toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise; - update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => Promise; - delete: (id: string) => Promise; -} - -class NotificationChannelService implements INotificationChannelService { - static SERVICE_NAME = SERVICE_NAME; - - constructor() {} - - create = async (tokenizedUser: ITokenizedUser, notificationChannelData: INotificationChannel) => { - const notificationChannel = await NotificationChannel.create({ - ...notificationChannelData, - createdBy: tokenizedUser.sub, - updatedBy: tokenizedUser.sub, - }); - return notificationChannel; - }; - - get = async (id: string) => { - const channel = await NotificationChannel.findById(id); - if (!channel) { - throw new ApiError("Notification channel not found", 404); - } - return channel; - }; - - getAll = async () => { - return NotificationChannel.find(); - }; - - toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => { - const updatedChannel = await NotificationChannel.findOneAndUpdate( - { _id: id }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - updatedBy: tokenizedUser.sub, - updatedAt: new Date(), - }, - }, - ], - { new: true } - ); - if (!updatedChannel) { - throw new ApiError("Notification channel not found", 404); - } - return updatedChannel; - }; - - update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => { - const allowedFields: (keyof INotificationChannel)[] = ["name", "config", "isActive"]; - const safeUpdate: Partial = {}; - for (const field of allowedFields) { - if (updateData[field] !== undefined) { - (safeUpdate as any)[field] = updateData[field]; - } - } - - const updatedChannel = await NotificationChannel.findByIdAndUpdate( - id, - { - $set: { - ...safeUpdate, - updatedAt: new Date(), - updatedBy: tokenizedUser.sub, - }, - }, - { new: true, runValidators: true } - ); - - if (!updatedChannel) { - throw new ApiError("Failed to update notification channel", 500); - } - - return updatedChannel; - }; - - delete = async (id: string) => { - const result = await NotificationChannel.deleteOne({ _id: id }); - if (!result.deletedCount) { - throw new ApiError("Notification channel not found", 404); - } - - await Monitor.updateMany({ notificationChannels: id }, { $pull: { notificationChannels: id } }); - return result.deletedCount === 1; - }; -} - -export default NotificationChannelService; diff --git a/server/src/service/v2/business/QueueService.ts b/server/src/service/v2/business/QueueService.ts deleted file mode 100644 index 620544ba7..000000000 --- a/server/src/service/v2/business/QueueService.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { IJobQueue } from "../infrastructure/JobQueue.js"; - -const SERVICE_NAME = "QueueServiceV2"; - -class QueueService { - static SERVICE_NAME = SERVICE_NAME; - private jobQueue: IJobQueue; - - constructor(jobQueue: IJobQueue) { - this.jobQueue = jobQueue; - } - - async getMetrics() { - return await this.jobQueue.getMetrics(); - } - - async getJobs() { - return await this.jobQueue.getJobs(); - } - - async flush() { - return await this.jobQueue.flush(); - } -} - -export default QueueService; diff --git a/server/src/service/v2/business/UserService.ts b/server/src/service/v2/business/UserService.ts deleted file mode 100644 index 09c50dcbc..000000000 --- a/server/src/service/v2/business/UserService.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IUser, User } from "../../../db/v2/models/index.js"; - -const SERVICE_NAME = "UserServiceV2"; -export interface IUserService { - getAllUsers(): Promise; -} - -class UserService implements IUserService { - static SERVICE_NAME = SERVICE_NAME; - async getAllUsers(): Promise { - return await User.find(); - } -} - -export default UserService; diff --git a/server/src/service/v2/infrastructure/JobGenerator.ts b/server/src/service/v2/infrastructure/JobGenerator.ts deleted file mode 100644 index 69b716920..000000000 --- a/server/src/service/v2/infrastructure/JobGenerator.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { IMonitor } from "../../../db/v2/models/index.js"; -import { INetworkService } from "./NetworkService.js"; -import { ICheckService } from "../business/CheckService.js"; -import { IMonitorStatsService } from "../business/MonitorStatsService.js"; -import { IStatusService } from "./StatusService.js"; -import { INotificationService } from "./NotificationService.js"; -import { IMaintenanceService } from "../business/MaintenanceService.js"; -import ApiError from "../../../utils/ApiError.js"; - -const SERVICE_NAME = "JobGeneratorV2"; -export interface IJobGenerator { - generateJob: () => (Monitor: IMonitor) => Promise; - generateCleanupJob: () => () => Promise; -} - -class JobGenerator implements IJobGenerator { - static SERVICE_NAME = SERVICE_NAME; - private networkService: INetworkService; - private checkService: ICheckService; - private monitorStatsService: IMonitorStatsService; - private statusService: IStatusService; - private notificationService: INotificationService; - private maintenanceService: IMaintenanceService; - - constructor( - networkService: INetworkService, - checkService: ICheckService, - monitorStatsService: IMonitorStatsService, - statusService: IStatusService, - notificationService: INotificationService, - maintenanceService: IMaintenanceService - ) { - this.networkService = networkService; - this.checkService = checkService; - this.monitorStatsService = monitorStatsService; - this.statusService = statusService; - this.notificationService = notificationService; - this.maintenanceService = maintenanceService; - } - - generateJob = () => { - return async (monitor: IMonitor) => { - try { - const monitorId = monitor._id.toString(); - if (!monitorId) { - throw new ApiError("No monitorID for creating job", 400); - } - - // Check for active maintenance window, if found, skip the check - const isInMaintenance = await this.maintenanceService.isInMaintenance(monitorId); - if (isInMaintenance) { - return; - } - - const status = await this.networkService.requestStatus(monitor); - const check = await this.checkService.buildCheck(status, monitor.type); - await check.save(); - const [updatedMonitor, statusChanged] = await this.statusService.updateMonitorStatus(monitor, status); - - if (statusChanged) { - await this.notificationService.handleNotifications(updatedMonitor); - } - await this.statusService.updateMonitorStats(updatedMonitor, status, statusChanged); - } catch (error) { - throw error; - } - }; - }; - - generateCleanupJob = () => { - return async () => { - try { - await this.checkService.cleanupOrphanedChecks(); - await this.monitorStatsService.cleanupOrphanedMonitorStats(); - } catch (error) { - throw error; - } - }; - }; -} - -export default JobGenerator; diff --git a/server/src/service/v2/infrastructure/JobQueue.ts b/server/src/service/v2/infrastructure/JobQueue.ts deleted file mode 100644 index 85479d8e5..000000000 --- a/server/src/service/v2/infrastructure/JobQueue.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { IJob } from "super-simple-scheduler/dist/job/job.js"; -import { Monitor, IMonitor } from "../../../db/v2/models/index.js"; -import Scheduler from "super-simple-scheduler"; -import { IJobGenerator } from "./JobGenerator.js"; - -const SERVICE_NAME = "JobQueueV2"; -export interface IJobMetrics { - jobs: number; - activeJobs: number; - failingJobs: number; - jobsWithFailures: Array<{ - monitorId: string | number; - monitorUrl: string | null; - monitorType: string | null; - failedAt: number | null; - failCount: number | null; - failReason: string | null; - }>; - totalRuns: number; - totalFailures: number; -} - -export interface IJobData extends IJob { - lastRunTook: number | null; -} - -export interface IJobQueue { - init: () => Promise; - addJob: (monitor: IMonitor) => Promise; - pauseJob: (monitor: IMonitor) => Promise; - resumeJob: (monitor: IMonitor) => Promise; - updateJob: (monitor: IMonitor) => Promise; - deleteJob: (monitor: IMonitor) => Promise; - getMetrics: () => Promise; - getJobs: () => Promise; - flush: () => Promise; - shutdown: () => Promise; -} - -export default class JobQueue implements IJobQueue { - static SERVICE_NAME = SERVICE_NAME; - - private scheduler: Scheduler; - private static instance: JobQueue | null = null; - private jobGenerator: any; - constructor() { - this.scheduler = new Scheduler({ - logLevel: "debug", - }); - } - - static async create(jobGenerator: IJobGenerator) { - if (!JobQueue.instance) { - const instance = new JobQueue(); - instance.jobGenerator = jobGenerator; - await instance.init(); - JobQueue.instance = instance; - } - return JobQueue.instance; - } - - static getInstance(): JobQueue | null { - return JobQueue.instance; - } - - init = async () => { - try { - this.scheduler.start(); - // Add template and jobs - this.scheduler.addTemplate("monitor-job", this.jobGenerator.generateJob()); - - // Add a cleanup job - this.scheduler.addTemplate("cleanup-job", this.jobGenerator.generateCleanupJob()); - await this.scheduler.addJob({ - id: "cleanup-orphaned-checks", - template: "cleanup-job", - repeat: 24 * 60 * 60 * 1000, // 24 hours - active: true, - }); - - const monitors = await Monitor.find(); - for (const monitor of monitors) { - this.addJob(monitor); - } - - return true; - } catch (error) { - console.error(error); - return false; - } - }; - - addJob = async (monitor: IMonitor) => { - try { - return await this.scheduler?.addJob({ - id: monitor._id.toString(), - template: "monitor-job", - repeat: monitor.interval, - active: monitor.isActive, - data: monitor, - }); - } catch (error) { - console.error(error); - return false; - } - }; - - pauseJob = async (monitor: IMonitor) => { - try { - return await this.scheduler?.pauseJob(monitor._id.toString()); - } catch (error) { - console.error(error); - return false; - } - }; - - resumeJob = async (monitor: IMonitor) => { - try { - return await this.scheduler.resumeJob(monitor._id.toString()); - } catch (error) { - console.error(error); - return false; - } - }; - - updateJob = async (monitor: IMonitor) => { - try { - return await this.scheduler.updateJob(monitor._id.toString(), { - repeat: monitor.interval, - data: monitor, - }); - } catch (error) { - console.error(error); - return false; - } - }; - - deleteJob = async (monitor: IMonitor) => { - try { - this.scheduler?.removeJob(monitor._id.toString()); - return true; - } catch (error) { - console.error(error); - return false; - } - }; - - getMetrics = async (): Promise => { - try { - const jobs = await this.scheduler.getJobs(); - const metrics: IJobMetrics = jobs.reduce( - (acc, job) => { - if (!job.data) return acc; - - acc.totalRuns += job.runCount || 0; - acc.totalFailures += job.failCount || 0; - acc.jobs++; - - // Check if job is currently failing (has recent failures) - const hasFailures = job.failCount && job.failCount > 0; - const isCurrentlyFailing = hasFailures && job.lastFailedAt && (!job.lastRunAt || job.lastFailedAt > job.lastRunAt); - - if (isCurrentlyFailing) { - acc.failingJobs++; - } - - if (job.lockedAt) { - acc.activeJobs++; - } - - if (hasFailures) { - acc.jobsWithFailures.push({ - monitorId: job.id, - monitorUrl: job.data?.url || null, - monitorType: job.data?.type || null, - failedAt: job.lastFailedAt || null, - failCount: job.failCount || null, - failReason: job.lastFailReason || null, - }); - } - return acc; - }, - { - jobs: 0, - activeJobs: 0, - failingJobs: 0, - jobsWithFailures: [], - totalRuns: 0, - totalFailures: 0, - } - ); - return metrics; - } catch (error) { - console.error(error); - return null; - } - }; - - getJobs = async (): Promise => { - try { - const jobs = await this.scheduler.getJobs(); - return jobs.map((job) => { - return { - ...job, - lastRunTook: job.lockedAt || !job.lastFinishedAt || !job.lastRunAt ? null : job.lastFinishedAt - job.lastRunAt, - }; - }); - } catch (error) { - console.error(error); - return null; - } - }; - - flush = async () => { - try { - return await this.scheduler.flushJobs(); - } catch (error) { - console.error(error); - return false; - } - }; - - shutdown = async () => { - try { - return await this.scheduler.stop(); - } catch (error) { - console.error(error); - return false; - } - }; -} diff --git a/server/src/service/v2/infrastructure/NetworkService.ts b/server/src/service/v2/infrastructure/NetworkService.ts deleted file mode 100644 index ab8427d84..000000000 --- a/server/src/service/v2/infrastructure/NetworkService.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { Got, HTTPError } from "got"; -import got from "got"; -import ping from "ping"; -import { IMonitor } from "../../../db/v2/models/index.js"; -import { GotTimings } from "../../../db/v2/models/checks/Check.js"; -import type { Response } from "got"; -import type { ISystemInfo, ICaptureInfo, ILighthouseResult } from "../../../db/v2/models/index.js"; -import { MonitorType, MonitorStatus } from "../../../db/v2/models/monitors/Monitor.js"; -import ApiError from "../../../utils/ApiError.js"; -import { config } from "../../../config/index.js"; - -const SERVICE_NAME = "NetworkServiceV2"; -export interface INetworkService { - requestHttp: (monitor: IMonitor) => Promise; - requestInfrastructure: (monitor: IMonitor) => Promise; - requestStatus: (monitor: IMonitor) => Promise; - requestPagespeed: (monitor: IMonitor) => Promise; - requestPing: (monitor: IMonitor) => Promise; -} - -export interface ICapturePayload { - data: ISystemInfo; - capture: ICaptureInfo; -} - -export interface ILighthousePayload { - lighthouseResult: ILighthouseResult; -} - -export interface StatusResponse { - monitorId: string; - type: MonitorType; - code?: number; - status: MonitorStatus; - message: string; - responseTime: number; - timings?: GotTimings; - payload?: TPayload; -} - -class NetworkService implements INetworkService { - static SERVICE_NAME = SERVICE_NAME; - private got: Got; - private NETWORK_ERROR: number; - constructor() { - this.got = got; - this.NETWORK_ERROR = 5000; - } - - private buildStatusResponse = (monitor: IMonitor, response: Response | null, error: any | null): StatusResponse => { - if (error) { - const statusResponse: StatusResponse = { - monitorId: monitor._id.toString(), - type: monitor.type, - status: "down" as MonitorStatus, - code: this.NETWORK_ERROR, - message: error.message || "Network error", - responseTime: 0, - timings: { phases: {} } as GotTimings, - }; - if (error instanceof HTTPError) { - statusResponse.code = error?.response?.statusCode || this.NETWORK_ERROR; - statusResponse.message = error.message || "HTTP error"; - statusResponse.responseTime = error.timings?.phases?.total || 0; - statusResponse.timings = error.timings; - } - return statusResponse; - } - - const statusResponse: StatusResponse = { - monitorId: monitor._id.toString(), - type: monitor.type, - code: response?.statusCode || this.NETWORK_ERROR, - status: response?.ok === true ? "up" : "down", - message: response?.statusMessage || "", - responseTime: response?.timings?.phases?.total || 0, - timings: response?.timings || ({ phases: {} } as GotTimings), - }; - - return statusResponse; - }; - - requestHttp = async (monitor: IMonitor) => { - try { - const url = monitor.url; - if (!url) { - throw new Error("No URL provided"); - } - - try { - const response: Response = await this.got(url); - return this.buildStatusResponse(monitor, response, null); - } catch (error) { - return this.buildStatusResponse(monitor, null, error); - } - } catch (error) { - throw error; - } - }; - - requestInfrastructure = async (monitor: IMonitor) => { - const url = monitor.url; - if (!url) { - throw new Error("No URL provided"); - } - const secret = monitor.secret; - if (!secret) { - throw new Error("No secret provided for infrastructure monitor"); - } - - let statusResponse: StatusResponse; - try { - const response: Response | null = await this.got(url, { - headers: { Authorization: `Bearer ${secret}` }, - responseType: "json", - }); - - statusResponse = this.buildStatusResponse(monitor, response, null); - if (!response?.body) { - throw new ApiError("No payload received from infrastructure monitor", 500); - } - statusResponse.payload = response?.body; - return statusResponse; - } catch (error) { - statusResponse = this.buildStatusResponse(monitor, null, error); - } - return statusResponse; - }; - - requestPagespeed = async (monitor: IMonitor) => { - const apiKey = config.PAGESPEED_API_KEY; - if (!apiKey) { - throw new Error("No API key provided for pagespeed monitor"); - } - const url = monitor.url; - if (!url) { - throw new Error("No URL provided"); - } - - let statusResponse: StatusResponse; - - try { - const response: Response = await this.got(url); - statusResponse = this.buildStatusResponse(monitor, response, null) as StatusResponse; - } catch (error) { - statusResponse = this.buildStatusResponse(monitor, null, error); - } - - const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance&key=${apiKey}`; - const pagespeedResponse = await this.got(pagespeedUrl, { - responseType: "json", - }); - const payload = pagespeedResponse.body; - if (payload) { - statusResponse.payload = payload; - return statusResponse; - } else { - throw new ApiError("No payload received from pagespeed monitor", 500); - } - }; - - requestPing = async (monitor: IMonitor) => { - const response = await ping.promise.probe(monitor.url); - const status = response?.alive === true ? "up" : "down"; - - const rawTime = typeof response?.time === "string" ? parseFloat(response.time) : Number(response?.time); - const responseTime = Number.isFinite(rawTime) ? rawTime : 0; - - return { - monitorId: monitor._id.toString(), - type: monitor.type, - status: status as MonitorStatus, - message: "Ping successful", - responseTime, - timings: { phases: {} } as GotTimings, - }; - }; - - requestStatus = async (monitor: IMonitor) => { - switch (monitor?.type) { - case "http": - return await this.requestHttp(monitor); // uses GOT - case "https": - return await this.requestHttp(monitor); // uses GOT - case "infrastructure": - return await this.requestInfrastructure(monitor); // uses GOT - case "pagespeed": - return await this.requestPagespeed(monitor); // uses GOT - case "ping": - return await this.requestPing(monitor); // uses PING - default: - throw new Error("Not implemented"); - } - }; -} - -export default NetworkService; diff --git a/server/src/service/v2/infrastructure/NotificationService.ts b/server/src/service/v2/infrastructure/NotificationService.ts deleted file mode 100644 index 2cc5e75d8..000000000 --- a/server/src/service/v2/infrastructure/NotificationService.ts +++ /dev/null @@ -1,61 +0,0 @@ -import UserService from "../business/UserService.js"; -import { IMonitor, NotificationChannel } from "../../../db/v2/models/index.js"; -import { EmailService, SlackService, DiscordService, WebhookService } from "./NotificationServices/index.js"; - -const SERVICE_NAME = "NotificationServiceV2"; -export interface INotificationService { - handleNotifications: (monitor: IMonitor) => Promise; -} - -class NotificationService implements INotificationService { - static SERVICE_NAME = SERVICE_NAME; - private emailService: EmailService; - private slackService: SlackService; - private discordService: DiscordService; - private webhookService: WebhookService; - private userService: UserService; - - constructor(userService: UserService) { - this.userService = userService; - this.emailService = new EmailService(userService); - this.slackService = new SlackService(); - this.discordService = new DiscordService(); - this.webhookService = new WebhookService(); - } - - handleNotifications = async (monitor: IMonitor) => { - const notificationIds = monitor.notificationChannels || []; - - if (notificationIds.length === 0) { - return; - } - - const notificationChannels = await NotificationChannel.find({ - _id: { $in: notificationIds }, - }); - - for (const channel of notificationChannels) { - // Implement sending logic based on channel.type and channel.config - let service; - switch (channel.type) { - case "email": - await this.emailService.sendMessage(this.emailService.buildAlert(monitor), channel); - break; - case "slack": - await this.slackService.sendMessage(this.slackService.buildAlert(monitor), channel); - break; - case "discord": - await this.discordService.sendMessage(this.discordService.buildAlert(monitor), channel); - break; - case "webhook": - await this.webhookService.sendMessage(this.webhookService.buildAlert(monitor), channel); - break; - default: - console.warn(`Unknown notification channel type: ${channel.type}`); - } - } - return; - }; -} - -export default NotificationService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Discord.ts b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts deleted file mode 100644 index c3a5d375a..000000000 --- a/server/src/service/v2/infrastructure/NotificationServices/Discord.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; -import { IAlert, IMessageService } from "./IMessageService.js"; -import got from "got"; -import ApiError from "../../../../utils/ApiError.js"; - -const SERVICE_NAME = "DiscordServiceV2"; -class DiscordService implements IMessageService { - static SERVICE_NAME = SERVICE_NAME; - constructor() {} - - private toDiscordEmbeds = (alert: IAlert) => { - return { - color: alert.status === "up" ? 65280 : 16711680, - title: `Monitor name: ${alert.name}`, - description: `Status: **${alert.status}**`, - fields: [ - { - name: "Url", - value: alert.url, - }, - { - name: "Checked at", - value: alert.checkTime ? alert.checkTime.toISOString() : "N/A", - }, - { name: "Alert time", value: alert.alertTime.toISOString() }, - ...(alert.details - ? Object.entries(alert.details).map(([key, value]) => ({ - name: key, - value, - })) - : []), - ], - }; - }; - - buildAlert = (monitor: IMonitor) => { - const name = monitor?.name || "Unnamed monitor"; - const monitorStatus = monitor?.status || "unknown status"; - const url = monitor?.url || "no URL"; - const checkTime = monitor?.lastCheckedAt || null; - const alertTime = new Date(); - return { - name, - url, - status: monitorStatus, - checkTime, - alertTime, - }; - }; - - sendMessage = async (alert: IAlert, channel: INotificationChannel) => { - const notificationUrl = channel?.config?.url; - if (!notificationUrl) { - throw new ApiError("Webhook URL not configured", 400); - } - - try { - const payload = { - content: "Status Alert", - embeds: [this.toDiscordEmbeds(alert)], - }; - await got.post(notificationUrl, { json: payload }); - } catch (error) { - console.warn("Failed to send Discord message", error); - return false; - } - - return true; - }; - - testMessage = async () => { - return true; - }; -} - -export default DiscordService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Email.ts b/server/src/service/v2/infrastructure/NotificationServices/Email.ts deleted file mode 100644 index 4bed9a156..000000000 --- a/server/src/service/v2/infrastructure/NotificationServices/Email.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; -import { IMessageService, IAlert } from "./IMessageService.js"; -import nodemailer, { Transporter } from "nodemailer"; -import { config } from "../../../../config/index.js"; -import UserService from "../../business/UserService.js"; -import ApiError from "../../../../utils/ApiError.js"; - -const SERVICE_NAME = "EmailServiceV2"; -class EmailService implements IMessageService { - static SERVICE_NAME = SERVICE_NAME; - private transporter: Transporter; - private userService: UserService; - - constructor(userService: UserService) { - this.userService = userService; - this.transporter = nodemailer.createTransport({ - host: config.SMTP_HOST, - port: config.SMTP_PORT, - secure: config.SMTP_PORT === 465, - auth: { - user: config.SMTP_USER, - pass: config.SMTP_PASS, - }, - }); - } - - buildAlert = (monitor: IMonitor) => { - const name = monitor?.name || "Unnamed monitor"; - const monitorStatus = monitor?.status || "unknown status"; - const url = monitor?.url || "no URL"; - const checkTime = monitor?.lastCheckedAt || null; - const alertTime = new Date(); - return { - name, - url, - status: monitorStatus, - checkTime, - alertTime, - }; - }; - - sendMessage = async (alert: string | IAlert, channel: INotificationChannel) => { - try { - const users = await this.userService.getAllUsers(); - const emails = users.map((u) => u.email).join(","); - - if (!emails || emails.length === 0) { - throw new ApiError("No user emails found", 500); - } - - await this.transporter.sendMail({ - from: `"Checkmate" <${config.SMTP_USER}>`, - to: emails, - subject: "Monitor Alert", - text: JSON.stringify(alert, null, 2), - }); - return true; - } catch (error) { - return false; - } - }; - - testMessage = async () => { - return true; - }; -} - -export default EmailService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts deleted file mode 100644 index f00a20549..000000000 --- a/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; - -export interface IAlert { - name: string; - url: string; - status: string; - details?: Record; - checkTime: Date | null; - alertTime: Date; -} -export interface IMessageService { - buildAlert: (monitor: IMonitor) => IAlert; - sendMessage: (alert: IAlert, channel: INotificationChannel) => Promise; - testMessage: (message: string, channel: INotificationChannel) => Promise; -} diff --git a/server/src/service/v2/infrastructure/NotificationServices/Slack.ts b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts deleted file mode 100644 index 0be56ca56..000000000 --- a/server/src/service/v2/infrastructure/NotificationServices/Slack.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; -import { IAlert, IMessageService } from "./IMessageService.js"; -import got from "got"; - -const SERVICE_NAME = "SlackServiceV2"; -class SlackService implements IMessageService { - static SERVICE_NAME = SERVICE_NAME; - constructor() {} - - private toSlackBlocks = (alert: IAlert) => { - return [ - { - type: "header", - text: { - type: "plain_text", - text: "Status Alert", - }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: `*Monitor name:* ${alert.name}`, - }, - }, - - { - type: "section", - text: { - type: "mrkdwn", - text: `*Status:* ${alert.status}`, - }, - }, - - { - type: "section", - text: { - type: "mrkdwn", - text: `*URL:* ${alert.url}`, - }, - }, - { - type: "section", - text: { - type: "mrkdwn", - text: `*Checked at:* ${alert?.checkTime?.toISOString() || "N/A"}`, - }, - }, - - { - type: "divider", - }, - - ...(alert.details - ? Object.entries(alert.details).map(([key, value]) => ({ - type: "section", - fields: [ - { - type: "mrkdwn", - text: `*${key}:* ${value}`, - }, - ], - })) - : []), - - { - type: "context", - elements: [ - { - type: "mrkdwn", - text: `*Alert generated at:* ${alert?.alertTime?.toISOString() || "N/A"}`, - }, - ], - }, - ]; - }; - - buildAlert = (monitor: IMonitor) => { - const name = monitor?.name || "Unnamed monitor"; - const monitorStatus = monitor?.status || "unknown status"; - const url = monitor?.url || "no URL"; - const checkTime = monitor?.lastCheckedAt || null; - const alertTime = new Date(); - return { - name, - url, - status: monitorStatus, - checkTime, - alertTime, - }; - }; - - sendMessage = async (alert: IAlert, channel: INotificationChannel) => { - const notificationUrl = channel?.config?.url; - if (!notificationUrl) { - throw new Error("Webhook URL not configured"); - } - - try { - const payload = { - text: "Status Alert", - blocks: this.toSlackBlocks(alert), - }; - await got.post(notificationUrl, { json: payload }); - } catch (error) { - console.warn("Error sending Slack message:", error); - return false; - } - - return true; - }; - - testMessage = async () => { - return true; - }; -} - -export default SlackService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts deleted file mode 100644 index 5dd89b16d..000000000 --- a/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; -import { IAlert, IMessageService } from "./IMessageService.js"; -import ApiError from "../../../../utils/ApiError.js"; -import got from "got"; - -const SERVICE_NAME = "WebhookServiceV2"; -class WebhookService implements IMessageService { - static SERVICE_NAME = SERVICE_NAME; - - constructor() {} - - buildAlert = (monitor: IMonitor) => { - const name = monitor?.name || "Unnamed monitor"; - const monitorStatus = monitor?.status || "unknown status"; - const url = monitor?.url || "no URL"; - const checkTime = monitor?.lastCheckedAt || null; - const alertTime = new Date(); - return { - name, - url, - status: monitorStatus, - checkTime, - alertTime, - }; - }; - - sendMessage = async (alert: IAlert, channel: INotificationChannel) => { - const notificationUrl = channel?.config?.url; - if (!notificationUrl) { - throw new ApiError("Webhook URL not configured", 400); - } - try { - await got.post(notificationUrl, { json: { ...alert } }); - } catch (error) { - console.warn("Failed to send webhook notification:", error); - return false; - } - - return true; - }; - - testMessage = async () => { - return true; - }; -} - -export default WebhookService; diff --git a/server/src/service/v2/infrastructure/StatusService.ts b/server/src/service/v2/infrastructure/StatusService.ts deleted file mode 100644 index f2881b160..000000000 --- a/server/src/service/v2/infrastructure/StatusService.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { IMonitor, IMonitorStats, MonitorStats } from "../../../db/v2/models/index.js"; -import { StatusResponse } from "./NetworkService.js"; -import ApiError from "../../../utils/ApiError.js"; - -const SERVICE_NAME = "StatusServiceV2"; -const MAX_LATEST_CHECKS = 25; -export interface IStatusService { - updateMonitorStatus: (monitor: IMonitor, status: StatusResponse) => Promise; - - calculateAvgResponseTime: (stats: IMonitorStats, statusResponse: StatusResponse) => number; - - updateMonitorStats: (monitor: IMonitor, status: StatusResponse, statusChanged: boolean) => Promise; -} - -export type StatusChangeResult = [updatedMonitor: IMonitor, statusChanged: boolean]; - -class StatusService implements IStatusService { - static SERVICE_NAME = SERVICE_NAME; - updateMonitorStatus = async (monitor: IMonitor, statusResponse: StatusResponse): Promise => { - const newStatus = statusResponse.status; - monitor.lastCheckedAt = new Date(); - - // Store latest checks for display - monitor.latestChecks = monitor.latestChecks || []; - monitor.latestChecks.push({ - status: newStatus, - responseTime: statusResponse.responseTime, - checkedAt: monitor.lastCheckedAt, - }); - while (monitor.latestChecks.length > MAX_LATEST_CHECKS) { - monitor.latestChecks.shift(); - } - - // Update monitor status - if (monitor.status === "initializing") { - monitor.status = newStatus; - return [await monitor.save(), true]; - } else { - const { n } = monitor; - const latestChecks = monitor.latestChecks.slice(-n); - // Return early if not enough statuses to evaluate - if (latestChecks.length < n) { - return [await monitor.save(), false]; - } - - // If all different than current status, update status - const allDifferent = latestChecks.every((check) => check.status !== monitor.status); - if (allDifferent && monitor.status !== newStatus) { - monitor.status = newStatus; - } - return [await monitor.save(), allDifferent]; - } - }; - - calculateAvgResponseTime = (stats: IMonitorStats, statusResponse: StatusResponse): number => { - let avgResponseTime = stats.avgResponseTime; - // Set initial - if (avgResponseTime === 0) { - avgResponseTime = statusResponse.responseTime; - } else { - avgResponseTime = (avgResponseTime * (stats.totalChecks - 1) + statusResponse.responseTime) / stats.totalChecks; - } - return avgResponseTime; - }; - - updateMonitorStats = async (monitor: IMonitor, statusResponse: StatusResponse, statusChanged: boolean) => { - const stats = await MonitorStats.findOne({ monitorId: monitor._id }); - if (!stats) { - throw new ApiError("MonitorStats not found", 500); - } - - // Update check counts - stats.totalChecks += 1; - stats.totalUpChecks += statusResponse.status === "up" ? 1 : 0; - stats.totalDownChecks += statusResponse.status === "down" ? 1 : 0; - - // Update streak - if (!statusChanged) { - stats.currentStreak += 1; - } else { - stats.currentStreak = 1; - stats.currentStreakStatus = statusResponse.status; - stats.currentStreakStartedAt = Date.now(); - } - - // Update time stamps - stats.lastCheckTimestamp = Date.now(); - stats.timeOfLastFailure = statusResponse.status === "down" ? Date.now() : stats.timeOfLastFailure; - - // Update stats that need updated check counts - stats.avgResponseTime = this.calculateAvgResponseTime(stats, statusResponse); - stats.uptimePercentage = stats.totalUpChecks / stats.totalChecks; - - // Other - stats.lastResponseTime = statusResponse.responseTime; - stats.maxResponseTime = Math.max(stats.maxResponseTime, statusResponse.responseTime); - - return await stats.save(); - }; -} - -export default StatusService; diff --git a/server/src/services/business/AuthService.ts b/server/src/services/business/AuthService.ts new file mode 100644 index 000000000..6f3a9d27d --- /dev/null +++ b/server/src/services/business/AuthService.ts @@ -0,0 +1,624 @@ +import bcrypt from "bcryptjs"; +import { + User, + Org, + OrgMembership, + Team, + ITeam, + IUserReturnable, + TeamMembership, + Role, + IRole, + IUser, + ITokenizedUser, + Monitor, + Check, + NotificationChannel, + IInvite, +} from "@/db/models/index.js"; +import { Types } from "mongoose"; +import ApiError from "@/utils/ApiError.js"; +import { IJobQueue } from "../infrastructure/JobQueue.js"; +import { hashPassword } from "@/utils/JWTUtils.js"; + +const SERVICE_NAME = "AuthService"; + +export const PERMISSIONS = { + users: { + all: "users.*", + write: "users.write", + read: "users.read", + delete: "users.delete", + }, + + monitors: { + all: "monitors.*", + write: "monitors.write", + read: "monitors.read", + update: "monitors.update", + delete: "monitors.delete", + }, + notifications: { + all: "notifications.*", + write: "notifications.write", + read: "notifications.read", + update: "notifications.update", + delete: "notifications.delete", + }, + maintenance: { + all: "maintenance.*", + write: "maintenance.write", + read: "maintenance.read", + update: "maintenance.update", + delete: "maintenance.delete", + }, + invite: { + all: "invite.*", + write: "invite.write", + read: "invite.read", + delete: "invite.delete", + }, + checks: { + all: "checks.*", + write: "checks.write", + read: "checks.read", + update: "checks.update", + delete: "checks.delete", + }, + statusPages: { + all: "statusPages.*", + write: "statusPages.write", + read: "statusPages.read", + update: "statusPages.update", + delete: "statusPages.delete", + }, + teams: { + all: "teams.*", + write: "teams.write", + read: "teams.read", + update: "teams.update", + delete: "teams.delete", + }, + roles: { + all: "roles.*", + write: "roles.write", + read: "roles.read", + update: "roles.update", + delete: "roles.delete", + }, + + diagnostic: { + all: "diagnostic.*", + write: "diagnostic.write", + read: "diagnostic.read", + update: "diagnostic.update", + delete: "diagnostic.delete", + }, + incidents: { + all: "incidents.*", + write: "incidents.write", + read: "incidents.read", + update: "incidents.update", + delete: "incidents.delete", + }, +}; + +export type RegisterData = { + firstName: string; + lastName: string; + email: string; + password: string; +}; + +export type LoginData = { + email: string; + password: string; +}; + +export type AuthResult = Partial; + +export interface IAuthService { + me(userId: string): Promise; + register(signupData: RegisterData): Promise<{ + tokenizedUser: ITokenizedUser; + returnableUser: IUserReturnable; + }>; + registerWithInvite( + invite: IInvite, + signupData: RegisterData + ): Promise<{ + tokenizedUser: ITokenizedUser; + returnableUser: IUserReturnable; + }>; + login(loginData: LoginData): Promise<{ + tokenizedUser: ITokenizedUser; + returnableUser: IUserReturnable; + }>; + getTeamIds(userId: string): Promise; + getTeams(teamIds: string[]): Promise; + cleanup(): Promise; + cleanMonitors(): Promise; + changePassword(userId: Types.ObjectId, newPassword: string): Promise; +} + +class AuthService implements IAuthService { + public SERVICE_NAME: string; + private jobQueue: IJobQueue; + constructor(jobQueue: IJobQueue) { + this.SERVICE_NAME = SERVICE_NAME; + this.jobQueue = jobQueue; + } + + me = async (userId: string) => { + // Need to get teamIds + const user = await User.findById(userId); + if (!user) { + throw new ApiError("User not found", 404); + } + + // Find OrgMembership + const orgMembership = await OrgMembership.findOne({ + userId: user._id, + }).lean(); + if (!orgMembership) { + throw new ApiError("User is not part of any organization"); + } + + // Find org and roles + const org = await Org.findById(orgMembership.orgId).lean(); + + if (!org) { + throw new ApiError("Organization not found"); + } + + const orgRoles = await Role.findById(orgMembership.roleId).lean(); + + // Get teams + const teamMembershipWithRoles = await TeamMembership.find({ + userId: user._id, + }) + .populate<{ roleId: IRole }>("roleId") + .lean(); + + const teamIds = teamMembershipWithRoles.map((tm) => tm.teamId.toString()); + + const teams = await this.getTeams(teamIds); + const teamMap = new Map(teams.map((t) => [t._id.toString(), t])); + const returnableTeams = teamMembershipWithRoles.map((tm) => { + const team = teamMap.get(tm.teamId.toString()); + if (!team) { + throw new ApiError("Team not found for membership"); + } + return { + id: team._id.toString(), + name: team.name, + permissions: tm.roleId?.permissions ?? [], + }; + }); + + const returnableUser: IUserReturnable = { + id: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + org: { + name: org.name, + permissions: orgRoles?.permissions || [], + }, + teams: returnableTeams, + }; + + return returnableUser; + }; + + async register(signupData: RegisterData) { + const passwordHash = await hashPassword(signupData.password); + + const created: Record = { + user: null, + org: null, + roles: [], + team: null, + teamMembership: null, + orgMembership: null, + }; + + try { + const newUserData: Partial = { + firstName: signupData.firstName, + lastName: signupData.lastName, + email: signupData.email, + passwordHash, + }; + + const user = await User.create(newUserData); + created.user = user._id; + + const org = await Org.create({ + name: `${user.firstName}'s Org`, + ownerId: user._id, + }); + created.org = org._id; + + const roles = await Role.insertMany([ + { + organizationId: org._id, + name: "Org Admin", + scope: "organization", + permissions: ["*"], + }, + { + organizationId: org._id, + name: "Org Member", + scope: "organization", + permissions: [ + PERMISSIONS.monitors.read, + PERMISSIONS.monitors.write, + PERMISSIONS.statusPages.read, + PERMISSIONS.statusPages.write, + PERMISSIONS.notifications.read, + PERMISSIONS.notifications.write, + PERMISSIONS.teams.read, + PERMISSIONS.teams.write, + PERMISSIONS.roles.read, + PERMISSIONS.roles.write, + PERMISSIONS.invite.read, + PERMISSIONS.invite.write, + PERMISSIONS.incidents.read, + PERMISSIONS.incidents.write, + ], + }, + { + organizationId: org._id, + name: "Team Admin", + permissions: [ + PERMISSIONS.monitors.all, + PERMISSIONS.statusPages.all, + PERMISSIONS.notifications.all, + PERMISSIONS.incidents.all, + ], + scope: "team", + }, + { + organizationId: org._id, + name: "Team Member", + permissions: [ + PERMISSIONS.monitors.read, + PERMISSIONS.statusPages.read, + PERMISSIONS.notifications.read, + PERMISSIONS.incidents.read, + ], + scope: "team", + }, + ]); + + created.roles = roles.map((r) => r._id); + + const membership = await OrgMembership.create({ + userId: user._id, + orgId: org._id, + roleId: roles[0]?._id, + }); + created.orgMembership = membership._id; + + const team = await Team.create({ + name: "Default Team", + orgId: org._id, + description: "This is your default team", + isSystem: true, + }); + created.team = team._id; + + const teamMembership = await TeamMembership.create({ + orgId: org._id, + userId: user._id, + teamId: team._id, + roleId: roles[2]?._id, + }); + created.teamMembership = teamMembership._id; + + const tokenizedUser: ITokenizedUser = { + sub: user._id.toString(), + email: user.email, + orgId: org._id.toString(), + }; + + const returnableTeam = { + id: team._id.toString(), + name: team.name, + permissions: roles[2]?.permissions || [], + }; + + const returnableUser: IUserReturnable = { + id: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + org: { + name: org.name, + permissions: roles[0]?.permissions || [], + }, + teams: [returnableTeam], + }; + + return { tokenizedUser, returnableUser }; + } catch (error) { + await TeamMembership.deleteOne({ _id: created.teamMembership }); + await OrgMembership.deleteOne({ _id: created.orgMembership }); + await Team.deleteOne({ _id: created.team }); + await Role.deleteMany({ _id: { $in: created.roles } }); + if (created.org) await Org.findByIdAndDelete(created.org); + if (created.user) await User.findByIdAndDelete(created.user); + throw error; + } + } + + registerWithInvite = async (invite: IInvite, signupData: RegisterData) => { + const created: Record = { + user: null, + orgMembership: null, + teamMembership: null, + }; + + try { + if (!invite) { + throw new ApiError("No token found", 404); + } + + const { orgId, orgRoleId, teamId, teamRoleId, email, expiry } = invite; + + if (expiry < new Date()) { + throw new ApiError("Invite token has expired", 400); + } + if (!orgId) { + throw new ApiError("No organization ID", 400); + } + // Check for org + const org = await Org.findById(orgId); + if (!org) { + throw new ApiError("Organization not found", 404); + } + + // Get or Create user + let user = await User.findOne({ email }); + if (!user) { + const passwordHash = await hashPassword(signupData.password); + user = await User.create({ + email, + firstName: signupData.firstName, + lastName: signupData.lastName, + passwordHash, + }); + created.user = user._id; + } + + const team = await Team.findById(teamId); + if (!team) { + throw new ApiError("Team not found", 404); + } + + // Get or create orgMembership + let orgMembership = await OrgMembership.findOne({ + userId: user._id, + orgId, + }); + if (orgMembership && !orgMembership.roleId && orgRoleId) { + orgMembership.roleId = orgRoleId; + await orgMembership.save(); + } + if (!orgMembership) { + orgMembership = await OrgMembership.create({ + orgId, + userId: user._id, + ...(orgRoleId ? { roleId: orgRoleId } : {}), + }); + created.orgMembership = orgMembership._id; + } + + // Create TeamMembership + let teamMembership = await TeamMembership.findOne({ + userId: user._id, + teamId, + }); + if (teamMembership) { + throw new ApiError("User is already a member of the team", 400); + } + teamMembership = await TeamMembership.create({ + orgId, + teamId, + userId: user._id, + roleId: teamRoleId, + }); + created.teamMembership = teamMembership._id; + + // Get data for return + const teamMembershipWithRoles = await TeamMembership.find({ + userId: user._id, + }) + .populate<{ roleId: IRole }>("roleId") + .lean(); + + const teamIds = teamMembershipWithRoles.map((tm) => tm.teamId.toString()); + + const teams = await this.getTeams(teamIds); + const teamMap = new Map(teams.map((t) => [t._id.toString(), t])); + + const tokenizedUser: ITokenizedUser = { + sub: user._id.toString(), + email: user.email, + orgId: orgId.toString(), + }; + + const returnableTeams = teamMembershipWithRoles.map((tm) => { + const team = teamMap.get(tm.teamId.toString()); + if (!team) { + throw new ApiError("Team not found for membership"); + } + return { + id: team._id.toString(), + name: team.name, + permissions: tm.roleId?.permissions ?? [], + }; + }); + + const orgRole = await Role.findById(orgMembership.roleId); + const orgPermissions = orgRole?.permissions || []; + + const returnableUser: IUserReturnable = { + id: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + org: { + name: org.name, + permissions: orgPermissions, + }, + teams: returnableTeams, + }; + + return { + tokenizedUser, + returnableUser, + }; + } catch (error) { + if (created.orgMembership) { + await OrgMembership.deleteOne({ _id: created.orgMembership }); + } + if (created.teamMembership) { + await TeamMembership.deleteOne({ _id: created.teamMembership }); + } + if (created.user) { + await User.findByIdAndDelete(created.user); + } + throw error; + } + }; + + async login(loginData: LoginData): Promise<{ + tokenizedUser: ITokenizedUser; + returnableUser: IUserReturnable; + }> { + const { email, password } = loginData; + + // Find user by email + const user = await User.findOne({ email }); + + if (!user) { + throw new ApiError("Invalid email or password"); + } + + // Check password + const isPasswordValid = await bcrypt.compare(password, user.passwordHash); + if (!isPasswordValid) { + throw new ApiError("Invalid email or password"); + } + + // Find OrgMembership + const orgMembership = await OrgMembership.findOne({ + userId: user._id, + }).lean(); + if (!orgMembership) { + throw new ApiError("User is not part of any organization"); + } + + // Find org and roles + const org = await Org.findById(orgMembership.orgId).lean(); + + if (!org) { + throw new ApiError("Organization not found"); + } + + const orgRoles = await Role.findById(orgMembership.roleId).lean(); + + // Get teams + const teamMemberships = await TeamMembership.find({ userId: user._id }) + .populate<{ roleId: IRole }>("roleId") + .lean(); + + const teamIds = teamMemberships.map((tm) => tm.teamId.toString()); + + const teams = await this.getTeams(teamIds); + const teamMap = new Map(teams.map((t) => [t._id.toString(), t])); + const returnableTeams = teamMemberships.map((tm) => { + const team = teamMap.get(tm.teamId.toString()); + if (!team) { + throw new ApiError("Team not found for membership"); + } + return { + id: team._id.toString(), + name: team.name, + permissions: tm.roleId?.permissions ?? [], + }; + }); + + const tokenizedUser: ITokenizedUser = { + sub: user._id.toString(), + email: user.email, + orgId: orgMembership.orgId.toString(), + }; + + const returnableUser: IUserReturnable = { + id: user._id.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + org: { + name: org.name, + permissions: orgRoles?.permissions || [], + }, + teams: returnableTeams, + }; + + return { + tokenizedUser, + returnableUser, + }; + } + + async getTeamIds(userId: string): Promise { + const teamMemberships = await TeamMembership.find({ userId }) + .select("teamId") + .lean(); + const teamIds = teamMemberships.map((t) => t.teamId.toString()); + return teamIds; + } + + async getTeams(teamIds: string[]): Promise { + const teams = await Team.find({ _id: { $in: teamIds } }).select( + "_id, name" + ); + return teams; + } + + async cleanup() { + await User.deleteMany({}); + await Role.deleteMany({}); + await Monitor.deleteMany({}); + await Check.deleteMany({}); + await NotificationChannel.deleteMany({}); + await this.jobQueue.flush(); + } + + async cleanMonitors() { + await Monitor.deleteMany({}); + await Check.deleteMany({}); + } + + changePassword = async ( + userId: Types.ObjectId, + newPassword: string + ): Promise => { + try { + const passwordHash = await hashPassword(newPassword); + const result = await User.updateOne( + { _id: userId }, + { $set: { passwordHash } } + ); + return result.modifiedCount > 0; + } catch (error) { + throw error; + } + }; +} + +export default AuthService; diff --git a/server/src/services/business/CheckService.ts b/server/src/services/business/CheckService.ts new file mode 100644 index 000000000..50f36fa8e --- /dev/null +++ b/server/src/services/business/CheckService.ts @@ -0,0 +1,273 @@ +import { ICheck, Check, Monitor, ISystemInfo } from "@/db/models/index.js"; +import { MonitorStatus } from "@/db/models/monitors/Monitor.js"; +import { MonitorType } from "@/db/models/monitors/Monitor.js"; +import { StatusResponse } from "../infrastructure/NetworkService.js"; +import type { + ICapturePayload, + ILighthousePayload, +} from "../infrastructure/NetworkService.js"; +import mongoose from "mongoose"; +import ApiError from "@/utils/ApiError.js"; +import { getChildLogger } from "@/logger/Logger.js"; + +const SERVICE_NAME = "CheckService"; +const logger = getChildLogger(SERVICE_NAME); + +export interface ICheckService { + buildCheck: ( + statusResponse: StatusResponse, + type: MonitorType + ) => Promise; + getMonitorChecks: ( + monitorId: string, + page: number, + rowsPerPage: number + ) => Promise<{ checks: ICheck[]; count: number }>; + getChecksByStatus: ( + status: MonitorStatus, + teamId: string, + monitorId: string, + page: number, + rowsPerPage: number, + range: string + ) => Promise<{ checks: ICheck[]; count: number }>; + + cleanupOrphanedChecks: () => Promise; +} + +class CheckService implements ICheckService { + public SERVICE_NAME: string; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + private getStartDate(range: string): Date { + const now = new Date(); + switch (range) { + case "2h": + return new Date(now.getTime() - 2 * 60 * 60 * 1000); + case "24h": + return new Date(now.getTime() - 24 * 60 * 60 * 1000); + case "7d": + return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case "30d": + return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + default: + throw new ApiError("Invalid range parameter", 400); + } + } + + private isCapturePayload = (payload: any): payload is ICapturePayload => { + if (!payload || typeof payload !== "object") return false; + + if (!("data" in payload) || typeof payload.data !== "object") { + return false; + } + + const data = payload.data as Partial; + if ( + !data.cpu || + typeof data.cpu !== "object" || + typeof data.cpu.usage_percent !== "number" + ) { + return false; + } + + if ( + !data.memory || + typeof data.memory !== "object" || + typeof data.memory.usage_percent !== "number" + ) { + return false; + } + + if (data.disk && !Array.isArray(data.disk)) { + return false; + } + if (data.net && !Array.isArray(data.net)) { + return false; + } + + if (!("capture" in payload) || typeof payload.capture !== "object") + return false; + const capture = payload.capture as Record; + if (typeof capture.version !== "string" || typeof capture.mode !== "string") + return false; + + return true; + }; + + private isPagespeedPayload = ( + payload: any + ): payload is ILighthousePayload => { + if (!payload || typeof payload !== "object") return false; + + if ( + !("lighthouseResult" in payload) || + typeof payload.lighthouseResult !== "object" + ) { + return false; + } + return true; + }; + + private buildBaseCheck = (statusResponse: StatusResponse) => { + const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId); + const teamId = new mongoose.Types.ObjectId(statusResponse.teamId); + const checkData: Partial = { + metadata: { + monitorId: monitorId, + teamId: teamId, + type: statusResponse?.type, + }, + status: statusResponse?.status, + httpStatusCode: statusResponse?.code, + message: statusResponse?.message, + responseTime: statusResponse?.responseTime, + timings: statusResponse?.timings, + }; + + const check: ICheck = new Check(checkData); + return check; + }; + + private buildInfrastructureCheck = ( + statusResponse: StatusResponse + ) => { + if (!this.isCapturePayload(statusResponse.payload)) { + throw new Error("Invalid payload for infrastructure monitor"); + } + const check = this.buildBaseCheck(statusResponse); + check.system = statusResponse.payload.data; + check.capture = statusResponse.payload.capture; + return check; + }; + + private buildPagespeedCheck = ( + statusResponse: StatusResponse + ) => { + if (!this.isPagespeedPayload(statusResponse.payload)) { + throw new Error("Invalid payload for pagespeed monitor"); + } + const check = this.buildBaseCheck(statusResponse); + const lighthouseResult = statusResponse?.payload?.lighthouseResult; + check.lighthouse = { + accessibility: lighthouseResult?.categories?.accessibility?.score || 0, + bestPractices: + lighthouseResult?.categories?.["best-practices"]?.score || 0, + seo: lighthouseResult?.categories?.seo?.score || 0, + performance: lighthouseResult?.categories?.performance?.score || 0, + audits: { + cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {}, + si: lighthouseResult?.audits?.["speed-index"] || {}, + fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {}, + lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {}, + tbt: lighthouseResult?.audits?.["total-blocking-time"] || {}, + }, + }; + return check; + }; + + buildCheck = async ( + statusResponse: StatusResponse, + type: MonitorType + ): Promise => { + switch (type) { + case "infrastructure": + return this.buildInfrastructureCheck( + statusResponse as StatusResponse + ); + + case "pagespeed": + return this.buildPagespeedCheck( + statusResponse as StatusResponse + ); + case "http": + case "https": + return this.buildBaseCheck(statusResponse); + + case "ping": + return this.buildBaseCheck(statusResponse); + default: + throw new Error(`Unsupported monitor type: ${type}`); + } + }; + + cleanupOrphanedChecks = async () => { + try { + const monitorIds = await Monitor.find().distinct("_id"); + const result = await Check.deleteMany({ + "metadata.monitorId": { $nin: monitorIds }, + }); + logger.info(`Deleted ${result.deletedCount} orphaned Checks.`); + return true; + } catch (error) { + logger.error("Error cleaning up orphaned Checks:", error); + return false; + } + }; + + getMonitorChecks = async ( + monitorId: string, + page: number, + rowsPerPage: number + ) => { + const count = await Check.countDocuments({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + }); + + const checks = await Check.find({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + }) + .sort({ createdAt: -1 }) + .skip(page * rowsPerPage) + .limit(rowsPerPage); + return { checks, count }; + }; + + getChecksByStatus = async ( + status: MonitorStatus, + teamId: string, + monitorId: string, + page: number, + rowsPerPage: number, + range: string + ) => { + let match; + const startDate = this.getStartDate(range); + + if (monitorId) { + const authorized = await Monitor.exists({ + _id: monitorId, + teamId, + }); + if (!authorized) { + throw new ApiError("Not authorized", 403); + } + + match = { + status, + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + createdAt: { $gte: startDate }, + }; + } else { + match = { + status, + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + createdAt: { $gte: startDate }, + }; + } + const [count, checks] = await Promise.all([ + Check.countDocuments(match), + Check.find(match) + .populate({ path: "metadata.monitorId", select: "name" }) + .sort({ createdAt: -1 }) + .skip(page * rowsPerPage) + .limit(rowsPerPage), + ]); + return { checks, count }; + }; +} + +export default CheckService; diff --git a/server/src/services/business/DiagnosticService.ts b/server/src/services/business/DiagnosticService.ts new file mode 100644 index 000000000..eed8c573c --- /dev/null +++ b/server/src/services/business/DiagnosticService.ts @@ -0,0 +1,37 @@ +import { IJobQueue } from "@/services/infrastructure/JobQueue.js"; +import type { + IJobData, + IJobMetrics, +} from "@/services/infrastructure/JobQueue.js"; +import { memoryTransport } from "@/logger/Logger.js"; + +export interface IDiagnosticService { + getLogs: () => any[]; + getJobs: () => Promise<{ + jobs: IJobData[] | null; + metrics: IJobMetrics | null; + }>; +} + +const SERVICE_NAME = "DiagnosticService"; +class DiagnosticService implements IDiagnosticService { + public SERVICE_NAME: string; + private jobQueue: IJobQueue; + + constructor(jobQueue: IJobQueue) { + this.SERVICE_NAME = SERVICE_NAME; + this.jobQueue = jobQueue; + } + + getLogs = () => { + return memoryTransport.getLogs(); + }; + + getJobs = async () => { + const jobs = await this.jobQueue.getJobs(); + const metrics = await this.jobQueue.getMetrics(); + return { jobs, metrics }; + }; +} + +export default DiagnosticService; diff --git a/server/src/services/business/IncidentService.ts b/server/src/services/business/IncidentService.ts new file mode 100644 index 000000000..5ad6653a9 --- /dev/null +++ b/server/src/services/business/IncidentService.ts @@ -0,0 +1,204 @@ +import { IIncident, Incident, IMonitor, ICheck } from "@/db/models/index.js"; +import type { ResolutionType } from "@/db/models/index.js"; +import mongoose from "mongoose"; +import { getChildLogger } from "@/logger/Logger.js"; +import ApiError from "@/utils/ApiError.js"; + +export interface IIncidentService { + handleStatusChange: ( + updatedMonitor: IMonitor, + lastCheck: ICheck + ) => Promise; + create: ( + teamId: mongoose.Types.ObjectId, + monitorId: mongoose.Types.ObjectId, + startCheckId: mongoose.Types.ObjectId + ) => Promise; + get: ( + teamId: mongoose.Types.ObjectId, + incidentId: mongoose.Types.ObjectId + ) => Promise; + getAll: ( + teamId: string, + monitorId: string, + page: number, + rowsPerPage: number, + range: string, + resolved?: boolean, + resolutionType?: ResolutionType + ) => Promise<{ incidents: IIncident[]; count: number }>; + resolve: ( + teamId: string, + incidentId: string, + resolutionType: ResolutionType, + lastCheck?: ICheck, + resolvedBy?: string, + resolutionNote?: string + ) => Promise; + delete: ( + teamId: mongoose.Types.ObjectId, + incidentId: mongoose.Types.ObjectId + ) => Promise; +} + +const SERVICE_NAME = "IncidentService"; + +class IncidentService implements IIncidentService { + public SERVICE_NAME: string; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + private getStartDate(range: string): Date { + const now = new Date(); + switch (range) { + case "2h": + return new Date(now.getTime() - 2 * 60 * 60 * 1000); + case "24h": + return new Date(now.getTime() - 24 * 60 * 60 * 1000); + case "7d": + return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case "30d": + return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + default: + throw new ApiError("Invalid range parameter", 400); + } + } + + handleStatusChange = async (updatedMonitor: IMonitor, lastCheck: ICheck) => { + if (updatedMonitor.status === "down") { + const incident = await this.create( + updatedMonitor.teamId, + updatedMonitor._id, + lastCheck._id + ); + return incident; + } else if (updatedMonitor.status === "up") { + const incident = await Incident.findOne({ + monitorId: updatedMonitor._id, + teamId: updatedMonitor.teamId, + resolved: false, + }); + + if (!incident) { + return null; + } + + const resolvedIncident = await this.resolve( + updatedMonitor.teamId.toString(), + incident._id.toString(), + "auto", + lastCheck + ); + return resolvedIncident; + } + return null; + }; + + create = async ( + teamId: mongoose.Types.ObjectId, + monitorId: mongoose.Types.ObjectId, + startCheckId: mongoose.Types.ObjectId + ) => { + const existing = await Incident.findOne({ + monitorId, + teamId, + resolved: false, + }); + + if (existing) { + return existing; + } + + let data: Partial = { + teamId: new mongoose.Types.ObjectId(teamId), + monitorId: new mongoose.Types.ObjectId(monitorId), + startedAt: new Date(), + startCheck: new mongoose.Types.ObjectId(startCheckId), + }; + const incident = await Incident.create(data); + return incident; + }; + + get = async ( + teamId: mongoose.Types.ObjectId, + incidentId: mongoose.Types.ObjectId + ) => { + const incident = await Incident.findOne({ + _id: new mongoose.Types.ObjectId(incidentId), + teamId: new mongoose.Types.ObjectId(teamId), + }) + .populate("monitorId") + .populate("resolvedBy"); + return incident; + }; + + getAll = async ( + teamId: string, + monitorId: string, + page: number, + rowsPerPage: number, + range: string, + resolved?: boolean, + resolutionType?: ResolutionType + ) => { + const startDate = this.getStartDate(range); + const match = { + teamId: teamId, + ...(monitorId && { monitorId }), + createdAt: { $gte: startDate }, + ...(resolved !== undefined && { resolved }), + ...(resolutionType !== undefined && { resolutionType }), + }; + + const [count, incidents] = await Promise.all([ + Incident.countDocuments(match), + Incident.find(match) + .populate("monitorId") + .populate("resolvedBy") + .sort({ createdAt: -1 }) + .skip(page * rowsPerPage) + .limit(rowsPerPage), + ]); + return { count, incidents }; + }; + + resolve = async ( + teamId: string, + incidentId: string, + resolutionType: ResolutionType, + lastCheck?: ICheck, + resolvedBy?: string, + resolutionNote?: string + ) => { + const incident = await Incident.findOneAndUpdate( + { _id: incidentId, teamId, resolved: false }, + { + $set: { + resolved: true, + endedAt: new Date(), + endCheck: lastCheck ? lastCheck._id : undefined, + resolvedBy: resolvedBy ? resolvedBy : undefined, + resolutionType, + resolutionNote: resolutionNote ? resolutionNote : undefined, + }, + }, + { new: true } + ); + + return incident; + }; + + delete = async ( + teamId: mongoose.Types.ObjectId, + incidentId: mongoose.Types.ObjectId + ) => { + const result = await Incident.deleteOne({ + _id: new mongoose.Types.ObjectId(incidentId), + teamId: new mongoose.Types.ObjectId(teamId), + }); + return result.deletedCount === 1; + }; +} + +export default IncidentService; diff --git a/server/src/services/business/InviteService.ts b/server/src/services/business/InviteService.ts new file mode 100644 index 000000000..77e0a6ab5 --- /dev/null +++ b/server/src/services/business/InviteService.ts @@ -0,0 +1,118 @@ +import crypto from "node:crypto"; +import { + IInvite, + Invite, + User, + IUser, + Role, + Team, + TeamMembership, +} from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; + +const SERVICE_NAME = "InviteService"; +export interface IInviteService { + create: ( + userId: string, + email: string, + orgId: string, + orgRoleId: string, + teamId: string, + teamRoleId: string + ) => Promise; + getAll: () => Promise; + get: (tokenHash: string) => Promise<{ user: IUser | null; invite: IInvite }>; + delete: (id: string) => Promise; +} + +class InviteService implements IInviteService { + public SERVICE_NAME: string; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + create = async ( + userId: string, + email: string, + orgId: string, + orgRoleId: string, + teamId: string, + teamRoleId: string + ) => { + try { + const role = await Role.findOne({ + _id: teamRoleId, + organizationId: orgId, + }); + + if (!role) { + throw new ApiError("Role not found", 404); + } + + const team = await Team.findOne({ + _id: teamId, + orgId, + }); + + if (!team) { + throw new ApiError("Team not found", 404); + } + + // Check if already a team member + const user = await User.findOne({ email }); + const existingMembership = await TeamMembership.findOne({ + userId: user?._id, + teamId, + }); + + if (existingMembership) { + throw new ApiError("User is already a team member", 409); + } + + const token = crypto.randomBytes(32).toString("hex"); + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + + const invite = await Invite.create({ + orgId, + ...(orgRoleId && { orgRoleId }), + teamId, + teamRoleId, + email, + tokenHash, + createdBy: userId, + updatedBy: userId, + }); + + if (!invite) { + throw new ApiError("Failed to create invite", 500); + } + return token; + } catch (error: any) { + throw error; + } + }; + + get = async (token: string) => { + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + const invite = await Invite.findOne({ tokenHash }); + if (!invite) { + throw new ApiError("Invite not found", 404); + } + const user = await User.findOne({ email: invite.email }); + return { user, invite }; + }; + + getAll = async () => { + return Invite.find(); + }; + + delete = async (id: string) => { + const result = await Invite.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Invite not found", 404); + } + return result.deletedCount === 1; + }; +} + +export default InviteService; diff --git a/server/src/services/business/MaintenanceService.ts b/server/src/services/business/MaintenanceService.ts new file mode 100644 index 000000000..f76865c22 --- /dev/null +++ b/server/src/services/business/MaintenanceService.ts @@ -0,0 +1,235 @@ +import { + Monitor, + IUserContext, + IMaintenance, + Maintenance, + MaintenanceRepeats, +} from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; + +const SERVICE_NAME = "MaintenanceService"; +const DAY_MS = 24 * 60 * 60 * 1000; +const WEEK_MS = 7 * DAY_MS; + +export interface IMaintenanceService { + create: ( + userContext: IUserContext, + maintenance: IMaintenance + ) => Promise; + getAll: (teamId: string) => Promise; + get: (teamId: string, id: string) => Promise; + toggleActive: ( + teamId: string, + userContext: IUserContext, + id: string + ) => Promise; + update: ( + teamId: string, + userContext: IUserContext, + id: string, + updateData: Partial + ) => Promise; + delete: (teamId: string, id: string) => Promise; + isInMaintenance: (monitorId: string) => Promise; +} + +type MaintenanceCache = Map; + +class MaintenanceService implements IMaintenanceService { + public SERVICE_NAME: string; + private maintenanceCache: MaintenanceCache; + private lastRefresh: number; + private CACHE_TTL_MS = 60 * 1000; + + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + this.maintenanceCache = new Map(); + this.lastRefresh = 0; + } + + create = async (userContext: IUserContext, maintenanceData: IMaintenance) => { + // Make sure monitors belong to current team + const monitorIds = maintenanceData.monitors || []; + const count = await Monitor.countDocuments({ + _id: { $in: monitorIds }, + teamId: userContext.currentTeamId, + }); + + if (count !== monitorIds.length) { + throw new ApiError( + "One or more monitors do not belong to the current team", + 403 + ); + } + + const maintenance = await Maintenance.create({ + ...maintenanceData, + orgId: userContext.orgId, + teamId: userContext.currentTeamId, + createdBy: userContext.sub, + updatedBy: userContext.sub, + }); + return maintenance; + }; + + get = async (teamId: string, id: string) => { + const maintenance = await Maintenance.findOne({ _id: id, teamId }); + if (!maintenance) { + throw new ApiError("Maintenance not found", 404); + } + return maintenance; + }; + + getAll = async (teamId: string) => { + return Maintenance.find({ teamId }); + }; + + toggleActive = async ( + teamId: string, + userContext: IUserContext, + id: string + ) => { + const updatedMaintenance = await Maintenance.findOneAndUpdate( + { _id: id, teamId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + updatedBy: userContext.sub, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + if (!updatedMaintenance) { + throw new ApiError("Maintenance not found", 404); + } + return updatedMaintenance; + }; + + update = async ( + teamId: string, + userContext: IUserContext, + id: string, + updateData: Partial + ) => { + // Make sure monitors belong to current team + const monitorIds = updateData.monitors || []; + const count = await Monitor.countDocuments({ + _id: { $in: monitorIds }, + teamId: userContext.currentTeamId, + }); + + if (count !== monitorIds.length) { + throw new ApiError( + "One or more monitors do not belong to the current team", + 403 + ); + } + + const allowedFields: (keyof IMaintenance)[] = [ + "name", + "monitors", + "repeat", + "startTime", + "endTime", + "isActive", + ]; + const safeUpdate: Partial = {}; + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + (safeUpdate as any)[field] = updateData[field]; + } + } + + const updatedMaintenance = await Maintenance.findOneAndUpdate( + { _id: id, teamId }, + { + $set: { + ...safeUpdate, + updatedAt: new Date(), + updatedBy: userContext.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedMaintenance) { + throw new ApiError("Failed to update maintenance", 500); + } + + return updatedMaintenance; + }; + + delete = async (teamId: string, id: string) => { + const result = await Maintenance.deleteOne({ _id: id, teamId }); + if (!result.deletedCount) { + throw new ApiError("Maintenance not found", 404); + } + return result.deletedCount === 1; + }; + + private refreshCache = async () => { + const now = new Date(); + + const activeMaintenances = await Maintenance.find({ + isActive: true, + $or: [ + { repeat: { $in: MaintenanceRepeats } }, + { startTime: { $lte: now }, endTime: { $gte: now } }, + ], + }).lean(); + + // Reset cache + const newCache = new Map(); + + for (const m of activeMaintenances) { + for (const monitorId of m.monitors) { + const key = monitorId.toString(); + if (!newCache.has(key)) newCache.set(key, []); + newCache.get(key)!.push(m); + } + } + + this.maintenanceCache = newCache; + this.lastRefresh = Date.now(); + }; + + isInMaintenance = async (monitorId: string) => { + const now = Date.now(); + + if (now - this.lastRefresh > this.CACHE_TTL_MS) { + await this.refreshCache(); + } + + const maintenances = this.maintenanceCache.get(monitorId) || []; + + for (const m of maintenances) { + const start = m.startTime.getTime(); + const end = m.endTime.getTime(); + const duration = end - start; + + let isActiveNow = false; + if (!m.repeat || m.repeat === "no repeat") { + isActiveNow = start <= now && now <= end; + } else { + const elapsed = now - start; + if (elapsed >= 0) { + const period = m.repeat === "daily" ? DAY_MS : WEEK_MS; + const offset = elapsed % period; + if (offset < duration) { + isActiveNow = true; + } + } + } + if (isActiveNow) { + return true; + } + } + + return false; + }; +} + +export default MaintenanceService; diff --git a/server/src/services/business/MonitorService.ts b/server/src/services/business/MonitorService.ts new file mode 100644 index 000000000..a080e3392 --- /dev/null +++ b/server/src/services/business/MonitorService.ts @@ -0,0 +1,603 @@ +import mongoose from "mongoose"; + +import { IMonitor, Monitor, MonitorStats, Check } from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; +import { IJobQueue } from "@/services/infrastructure/JobQueue.js"; +import { MonitorWithChecksResponse } from "@/types/index.js"; +import { MonitorStatus, MonitorType } from "@/db/models/monitors/Monitor.js"; + +const SERVICE_NAME = "MonitorService"; + +export interface IMonitorService { + create: ( + orgId: string, + userId: string, + currentTeamId: string, + monitorData: IMonitor + ) => Promise; + getAll: (teamId: string) => Promise; + getAllEmbedChecks: ( + teamId: string, + page: number, + limit: number, + type: MonitorType[] + ) => Promise<{ + count: number; + upCount: number; + downCount: number; + pausedCount: number; + monitors: any[]; + }>; + get: (teamId: string, monitorId: string) => Promise; + getEmbedChecks: ( + teamId: string, + monitorId: string, + range: string, + status?: string + ) => Promise; + toggleActive: ( + userId: string, + teamId: string, + monitorId: string + ) => Promise; + update: ( + userId: string, + teamId: string, + monitorId: string, + updateData: Partial + ) => Promise; + delete: (teamId: string, monitorId: string) => Promise; +} + +class MonitorService implements IMonitorService { + public SERVICE_NAME: string; + private jobQueue: IJobQueue; + constructor(jobQueue: IJobQueue) { + this.SERVICE_NAME = SERVICE_NAME; + this.jobQueue = jobQueue; + } + + create = async ( + orgId: string, + userId: string, + currentTeamId: string, + monitorData: IMonitor + ) => { + const monitorLiteral: Partial = { + ...monitorData, + orgId: new mongoose.Types.ObjectId(orgId), + teamId: new mongoose.Types.ObjectId(currentTeamId), + createdBy: new mongoose.Types.ObjectId(userId), + updatedBy: new mongoose.Types.ObjectId(userId), + }; + + const monitor = await Monitor.create(monitorLiteral); + await MonitorStats.create({ + monitorId: monitor._id, + currentStreakStartedAt: Date.now(), + }); + await this.jobQueue.addJob(monitor); + return monitor; + }; + + getAll = async (teamId: string) => { + return Monitor.find({ teamId }); + }; + + getAllEmbedChecks = async ( + teamId: string, + page: number, + rowsPerPage: number, + type: MonitorType[] = [] + ) => { + const countResult = await Monitor.aggregate([ + { + $match: { + teamId: new mongoose.Types.ObjectId(teamId), + type: { $in: type }, + }, + }, + { + $group: { + _id: null, + total: { $sum: 1 }, + upCount: { $sum: { $cond: [{ $eq: ["$status", "up"] }, 1, 0] } }, + downCount: { $sum: { $cond: [{ $eq: ["$status", "down"] }, 1, 0] } }, + pausedCount: { + $sum: { $cond: [{ $eq: ["$isActive", false] }, 1, 0] }, + }, + }, + }, + ]); + + const counts = countResult[0] || { + total: 0, + upCount: 0, + downCount: 0, + pausedCount: 0, + }; + + const skip = page * rowsPerPage; + let find = {}; + if (type.length > 0) find = { type: { $in: type } }; + find = { ...find, teamId }; + + const monitors = await Monitor.find(find) + .lean() + .skip(skip) + .limit(rowsPerPage); + + if ( + type.length === 1 && + (type[0] === "pagespeed" || type[0] === "infrastructure") + ) { + const monitorIds = monitors.map((m) => m._id); + + const checks = await Check.aggregate([ + { + $match: { + "metadata.monitorId": { $in: monitorIds }, + }, + }, + { $sort: { createdAt: -1 } }, + { + $group: { + _id: "$metadata.monitorId", + latestChecks: { $push: "$$ROOT" }, + }, + }, + { $project: { latestChecks: { $slice: ["$latestChecks", 25] } } }, + ]); + + const checksMap = new Map( + checks.map((c: any) => [c._id.toString(), c.latestChecks]) + ); + + monitors.forEach((monitor) => { + monitor.latestChecks = checksMap.get(monitor._id.toString()) || []; + }); + } + return { + count: counts.total, + upCount: counts.upCount, + downCount: counts.downCount, + pausedCount: counts.pausedCount, + monitors, + }; + }; + + get = async (teamId: string, monitorId: string) => { + const monitor = await Monitor.findOne({ _id: monitorId, teamId }); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + return monitor; + }; + + private getStartDate(range: string): Date { + const now = new Date(); + switch (range) { + case "2h": + return new Date(now.getTime() - 2 * 60 * 60 * 1000); + case "24h": + return new Date(now.getTime() - 24 * 60 * 60 * 1000); + case "7d": + return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case "30d": + return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + default: + throw new ApiError("Invalid range parameter", 400); + } + } + + private getDateFormat(range: string): string { + switch (range) { + case "2h": + return "%Y-%m-%dT%H:%M:00Z"; + case "24h": + case "7d": + return "%Y-%m-%dT%H:00:00Z"; + case "30d": + return "%Y-%m-%d"; + default: + throw new ApiError("Invalid range parameter", 400); + } + } + + private getBaseGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }; + }; + + private getBaseProjection = (): object => { + return { status: 1, responseTime: 1, createdAt: 1 }; + }; + + private getPageSpeedGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + accessibility: { $avg: "$lighthouse.accessibility" }, + bestPractices: { $avg: "$lighthouse.bestPractices" }, + seo: { $avg: "$lighthouse.seo" }, + performance: { $avg: "$lighthouse.performance" }, + cls: { $avg: "$lighthouse.audits.cls.score" }, + si: { $avg: "$lighthouse.audits.si.score" }, + fcp: { $avg: "$lighthouse.audits.fcp.score" }, + lcp: { $avg: "$lighthouse.audits.lcp.score" }, + tbt: { $avg: "$lighthouse.audits.tbt.score" }, + }; + }; + + private getPageSpeedProjection = (): object => { + const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; + projectStage["lighthouse.accessibility"] = 1; + projectStage["lighthouse.seo"] = 1; + projectStage["lighthouse.bestPractices"] = 1; + projectStage["lighthouse.performance"] = 1; + projectStage["lighthouse.audits.cls.score"] = 1; + projectStage["lighthouse.audits.si.score"] = 1; + projectStage["lighthouse.audits.fcp.score"] = 1; + projectStage["lighthouse.audits.lcp.score"] = 1; + projectStage["lighthouse.audits.tbt.score"] = 1; + return projectStage; + }; + + private getInfraGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + physicalCores: { $last: "$system.cpu.physical_core" }, + logicalCores: { $last: "$system.cpu.logical_core" }, + frequency: { $avg: "$system.cpu.frequency" }, + currentFrequency: { $last: "$system.cpu.current_frequency" }, + tempsArrays: { $push: "$system.cpu.temperature" }, + freePercent: { $avg: "$system.cpu.free_percent" }, + usedPercent: { $avg: "$system.cpu.usage_percent" }, + total_bytes: { $last: "$system.memory.total_bytes" }, + available_bytes: { $last: "$system.memory.available_bytes" }, + used_bytes: { $last: "$system.memory.used_bytes" }, + memory_usage_percent: { $avg: "$system.memory.usage_percent" }, + disksArray: { $push: "$system.disk" }, + os: { $last: "$system.host.os" }, + platform: { $last: "$system.host.platform" }, + kernel_version: { $last: "$system.host.kernel_version" }, + pretty_name: { $last: "$system.host.pretty_name" }, + netsArray: { $push: "$system.net" }, + }; + }; + + private getInfraProjection = (): object => { + const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; + projectStage["system.cpu.physical_core"] = 1; + projectStage["system.cpu.logical_core"] = 1; + projectStage["system.cpu.frequency"] = 1; + projectStage["system.cpu.current_frequency"] = 1; + projectStage["system.cpu.temperature"] = 1; + projectStage["system.cpu.free_percent"] = 1; + projectStage["system.cpu.usage_percent"] = 1; + projectStage["system.memory.total_bytes"] = 1; + projectStage["system.memory.available_bytes"] = 1; + projectStage["system.memory.used_bytes"] = 1; + projectStage["system.memory.usage_percent"] = 1; + projectStage["system.disk"] = 1; + projectStage["system.host.os"] = 1; + projectStage["system.host.platform"] = 1; + projectStage["system.host.kernel_version"] = 1; + projectStage["system.host.pretty_name"] = 1; + projectStage["system.net"] = 1; + return projectStage; + }; + + private getFinalProjection = (type: string): object => { + if (type === "pagespeed") { + return { + _id: 1, + count: 1, + avgResponseTime: 1, + accessibility: "$accessibility", + seo: "$seo", + bestPractices: "$bestPractices", + performance: "$performance", + cls: "$cls", + si: "$si", + fcp: "$fcp", + lcp: "$lcp", + tbt: "$tbt", + }; + } + + if (type === "infrastructure") { + return { + _id: 1, + count: 1, + avgResponseTime: 1, + cpu: { + physical_core: "$physicalCores", + logical_core: "$logicalCores", + frequency: "$frequency", + current_frequency: "$currentFrequency", + temperature: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }], + }, + as: "idx", + in: { + $avg: { + $map: { + input: "$tempsArrays", + as: "arr", + in: { $arrayElemAt: ["$$arr", "$$idx"] }, + }, + }, + }, + }, + }, + free_percent: "$freePercent", + used_percent: "$usedPercent", + }, + memory: { + total_bytes: "$total_bytes", + available_bytes: "$available_bytes", + used_bytes: "$used_bytes", + usage_percent: "$memory_usage_percent", + }, + disk: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }], + }, + as: "idx", + in: { + $let: { + vars: { + diskGroup: { + $map: { + input: "$disksArray", + as: "diskArr", + in: { $arrayElemAt: ["$$diskArr", "$$idx"] }, + }, + }, + }, + in: { + device: { $arrayElemAt: ["$$diskGroup.device", 0] }, + total_bytes: { $avg: "$$diskGroup.total_bytes" }, + free_bytes: { $avg: "$$diskGroup.free_bytes" }, + used_bytes: { $avg: "$$diskGroup.used_bytes" }, + usage_percent: { $avg: "$$diskGroup.usage_percent" }, + total_inodes: { $avg: "$$diskGroup.total_inodes" }, + free_inodes: { $avg: "$$diskGroup.free_inodes" }, + used_inodes: { $avg: "$$diskGroup.used_inodes" }, + inodes_usage_percent: { + $avg: "$$diskGroup.inodes_usage_percent", + }, + read_bytes: { $avg: "$$diskGroup.read_bytes" }, + write_bytes: { $avg: "$$diskGroup.write_bytes" }, + read_time: { $avg: "$$diskGroup.read_time" }, + write_time: { $avg: "$$diskGroup.write_time" }, + }, + }, + }, + }, + }, + host: { + os: "$os", + platform: "$platform", + kernel_version: "$kernel_version", + pretty_name: "$pretty_name", + }, + net: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }], + }, + as: "idx", + in: { + $let: { + vars: { + netGroup: { + $map: { + input: "$netsArray", + as: "netArr", + in: { $arrayElemAt: ["$$netArr", "$$idx"] }, + }, + }, + }, + in: { + name: { $arrayElemAt: ["$$netGroup.name", 0] }, + bytes_sent: { $avg: "$$netGroup.bytes_sent" }, + bytes_recv: { $avg: "$$netGroup.bytes_recv" }, + packets_sent: { $avg: "$$netGroup.packets_sent" }, + packets_recv: { $avg: "$$netGroup.packets_recv" }, + err_in: { $avg: "$$netGroup.err_in" }, + err_out: { $avg: "$$netGroup.err_out" }, + drop_in: { $avg: "$$netGroup.drop_in" }, + drop_out: { $avg: "$$netGroup.drop_out" }, + fifo_in: { $avg: "$$netGroup.fifo_in" }, + fifo_out: { $avg: "$$netGroup.fifo_out" }, + }, + }, + }, + }, + }, + }; + } + return {}; + }; + + getEmbedChecks = async ( + teamId: string, + monitorId: string, + range: string, + status: string | undefined + ): Promise => { + const monitor = await Monitor.findOne({ _id: monitorId, teamId }); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + + const startDate = this.getStartDate(range); + const dateFormat = this.getDateFormat(range); + + // Build match stage + const matchStage: { + "metadata.monitorId": mongoose.Types.ObjectId; + createdAt: { $gte: Date }; + status?: string; + } = { + "metadata.monitorId": monitor._id, + createdAt: { $gte: startDate }, + }; + + if (status) { + matchStage.status = status; + } + + let groupClause; + + if (monitor.type === "pagespeed") { + groupClause = this.getPageSpeedGroup(dateFormat); + } else if (monitor.type === "infrastructure") { + groupClause = this.getInfraGroup(dateFormat); + } else { + groupClause = this.getBaseGroup(dateFormat); + } + + let projectStage; + if (monitor.type === "pagespeed") { + projectStage = this.getPageSpeedProjection(); + } else if (monitor.type === "infrastructure") { + projectStage = this.getInfraProjection(); + } else { + projectStage = this.getBaseProjection(); + } + + let finalProjection = {}; + if (monitor.type === "pagespeed" || monitor.type === "infrastructure") { + finalProjection = this.getFinalProjection(monitor.type); + } else { + finalProjection = { _id: 1, count: 1, avgResponseTime: 1 }; + } + + const checks = await Check.aggregate([ + { + $match: matchStage, + }, + { $sort: { createdAt: 1 } }, + { $project: projectStage }, + { $group: groupClause }, + { $sort: { _id: -1 } }, + { + $project: finalProjection, + }, + ]); + + // Get monitor stats + const monitorStats = await MonitorStats.findOne({ + monitorId: monitor._id, + }); + + if (!monitorStats) { + throw new ApiError("Monitor stats not found", 404); + } + + return { + monitor: monitor.toObject(), + checks, + stats: monitorStats, + }; + }; + + async toggleActive(userId: string, teamId: string, id: string) { + const pendingStatus: MonitorStatus = "initializing"; + const updatedMonitor = await Monitor.findOneAndUpdate( + { _id: id, teamId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: pendingStatus, + updatedBy: userId, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + + if (!updatedMonitor) { + throw new ApiError("Monitor not found", 404); + } + + await this.jobQueue.updateJob(updatedMonitor); + + if (updatedMonitor?.isActive) { + await this.jobQueue.resumeJob(updatedMonitor); + } else { + await this.jobQueue.pauseJob(updatedMonitor); + } + return updatedMonitor; + } + + async update( + userId: string, + teamId: string, + monitorId: string, + updateData: Partial + ) { + const allowedFields: (keyof IMonitor)[] = [ + "name", + "secret", + "interval", + "isActive", + "n", + "notificationChannels", + ]; + const safeUpdate: Partial = {}; + + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + (safeUpdate as any)[field] = updateData[field]; + } + } + + const updatedMonitor = await Monitor.findOneAndUpdate( + { _id: monitorId, teamId }, + { + $set: { + ...safeUpdate, + updatedAt: new Date(), + updatedBy: userId, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedMonitor) { + throw new ApiError("Monitor not found", 404); + } + await this.jobQueue.updateJob(updatedMonitor); + return updatedMonitor; + } + + async delete(teamId: string, monitorId: string) { + const monitor = await Monitor.findOne({ _id: monitorId, teamId }); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + const deleted = await this.jobQueue.deleteJob(monitor); + if (!deleted) { + throw new ApiError("Failed to delete monitor job from queue", 500); + } + await monitor.deleteOne(); + return true; + } +} + +export default MonitorService; diff --git a/server/src/services/business/MonitorStatsService.ts b/server/src/services/business/MonitorStatsService.ts new file mode 100644 index 000000000..4672c2d1e --- /dev/null +++ b/server/src/services/business/MonitorStatsService.ts @@ -0,0 +1,31 @@ +import { Monitor, MonitorStats } from "@/db/models/index.js"; +import { getChildLogger } from "@/logger/Logger.js"; + +const SERVICE_NAME = "MonitorStatsService"; +const logger = getChildLogger(SERVICE_NAME); +export interface IMonitorStatsService { + cleanupOrphanedMonitorStats: () => Promise; +} + +class MonitorStatsService implements IMonitorStatsService { + public SERVICE_NAME: string; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + async cleanupOrphanedMonitorStats() { + try { + const monitorIds = await Monitor.find().distinct("_id"); + const result = await MonitorStats.deleteMany({ + monitorId: { $nin: monitorIds }, + }); + logger.info(`Deleted ${result.deletedCount} orphaned MonitorStats.`); + return true; + } catch (error) { + logger.error("Error cleaning up orphaned MonitorStats:", error); + return false; + } + } +} + +export default MonitorStatsService; diff --git a/server/src/services/business/NotificationChannelService.ts b/server/src/services/business/NotificationChannelService.ts new file mode 100644 index 000000000..702083f31 --- /dev/null +++ b/server/src/services/business/NotificationChannelService.ts @@ -0,0 +1,132 @@ +import mongoose from "mongoose"; + +import { + IUserContext, + INotificationChannel, + NotificationChannel, + Monitor, +} from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; + +const SERVICE_NAME = "NotificationChannelService"; + +export interface INotificationChannelService { + create: ( + tokenizedUser: IUserContext, + notificationChannel: INotificationChannel + ) => Promise; + getAll: (teamId: string) => Promise; + get: (teamId: string, id: string) => Promise; + toggleActive: ( + teamId: string, + tokenizedUser: IUserContext, + id: string + ) => Promise; + update: ( + teamId: string, + tokenizedUser: IUserContext, + id: string, + updateData: Partial + ) => Promise; + delete: (teamId: string, id: string) => Promise; +} + +class NotificationChannelService implements INotificationChannelService { + public SERVICE_NAME: string; + + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + create = async ( + userContext: IUserContext, + notificationChannelData: INotificationChannel + ) => { + const data: INotificationChannel = { + ...notificationChannelData, + orgId: new mongoose.Types.ObjectId(userContext.orgId), + teamId: new mongoose.Types.ObjectId(userContext.currentTeamId), + createdBy: new mongoose.Types.ObjectId(userContext.sub), + updatedBy: new mongoose.Types.ObjectId(userContext.sub), + }; + + const notificationChannel = await NotificationChannel.create(data); + return notificationChannel; + }; + + get = async (teamId: string, id: string) => { + const channel = await NotificationChannel.findOne({ _id: id, teamId }); + if (!channel) { + throw new ApiError("Notification channel not found", 404); + } + return channel; + }; + + getAll = async (teamId: string) => { + return NotificationChannel.find({ teamId }); + }; + + toggleActive = async ( + teamId: string, + userContext: IUserContext, + id: string + ) => { + const updatedChannel = await NotificationChannel.findOneAndUpdate( + { _id: id, teamId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + updatedBy: userContext.sub, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + if (!updatedChannel) { + throw new ApiError("Notification channel not found", 404); + } + return updatedChannel; + }; + + update = async ( + teamId: string, + userContext: IUserContext, + id: string, + updateData: Partial + ) => { + const updatedChannel = await NotificationChannel.findOneAndUpdate( + { _id: id, teamId }, + { + $set: { + ...updateData, + updatedAt: new Date(), + updatedBy: userContext.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedChannel) { + throw new ApiError("Failed to update notification channel", 500); + } + + return updatedChannel; + }; + + delete = async (teamId: string, id: string) => { + const result = await NotificationChannel.deleteOne({ _id: id, teamId }); + if (!result.deletedCount) { + throw new ApiError("Notification channel not found", 404); + } + + await Monitor.updateMany( + { notificationChannels: id }, + { $pull: { notificationChannels: id } } + ); + return result.deletedCount === 1; + }; +} + +export default NotificationChannelService; diff --git a/server/src/services/business/QueueService.ts b/server/src/services/business/QueueService.ts new file mode 100644 index 000000000..f1d09a4af --- /dev/null +++ b/server/src/services/business/QueueService.ts @@ -0,0 +1,27 @@ +import { IJobQueue } from "@/services/infrastructure/JobQueue.js"; + +const SERVICE_NAME = "QueueService"; + +class QueueService { + public SERVICE_NAME: string; + private jobQueue: IJobQueue; + + constructor(jobQueue: IJobQueue) { + this.jobQueue = jobQueue; + this.SERVICE_NAME = SERVICE_NAME; + } + + async getMetrics() { + return await this.jobQueue.getMetrics(); + } + + async getJobs() { + return await this.jobQueue.getJobs(); + } + + async flush() { + return await this.jobQueue.flush(); + } +} + +export default QueueService; diff --git a/server/src/services/business/RecoveryService.ts b/server/src/services/business/RecoveryService.ts new file mode 100644 index 000000000..bdfe78536 --- /dev/null +++ b/server/src/services/business/RecoveryService.ts @@ -0,0 +1,54 @@ +import crypto from "node:crypto"; +import { RecoveryToken, IRecoveryToken } from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; +import { Types } from "mongoose"; +const SERVICE_NAME = "RecoveryService"; + +export interface IRecoveryService { + create: (userId: Types.ObjectId) => Promise; + get: (token: string) => Promise; + delete: (id: string) => Promise; +} + +class RecoveryService implements IRecoveryService { + public SERVICE_NAME: string; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + create = async (userId: Types.ObjectId) => { + const token = crypto.randomBytes(32).toString("hex"); + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + + await RecoveryToken.create({ + userId, + tokenHash, + }); + return token; + }; + + get = async (token: string) => { + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + + const recoveryToken = await RecoveryToken.findOne({ tokenHash }); + if (!recoveryToken) { + throw new ApiError("Recovery token not found", 404); + } + + if (recoveryToken.expiry.getTime() <= Date.now()) { + throw new ApiError("Recovery token expired", 400); + } + + return recoveryToken; + }; + + delete = async (id: string) => { + const result = await RecoveryToken.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Recovery token not found", 404); + } + return result.deletedCount === 1; + }; +} + +export default RecoveryService; diff --git a/server/src/services/business/RoleService.ts b/server/src/services/business/RoleService.ts new file mode 100644 index 000000000..48097da5d --- /dev/null +++ b/server/src/services/business/RoleService.ts @@ -0,0 +1,45 @@ +import { Role, IRole } from "@/db/models/index.js"; +import type { IJobQueue } from "../infrastructure/JobQueue.js"; +import ApiError from "@/utils/ApiError.js"; + +const SERVICE_NAME = "RoleService"; +export interface IRoleService { + getAll: (orgId: string, type: string) => Promise>; + getTeam: (orgId: string, teamId: string) => Promise>; + get: (roleId: string) => Promise; +} + +class RoleService implements IRoleService { + public SERVICE_NAME: string; + + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + getAll = async (orgId: string, type: string) => { + if (!type) { + return await Role.find({ organizationId: orgId }); + } + const roles = await Role.find({ organizationId: orgId, scope: type }); + return roles; + }; + + getTeam = async (orgId: string, teamId: string) => { + const roles = await Role.find({ + organizationId: orgId, + teamId, + scope: "team", + }); + return roles; + }; + + get = async (roleId: string) => { + const role = await Role.findById(roleId); + if (!role) { + throw new ApiError("Role not found", 404); + } + return role; + }; +} + +export default RoleService; diff --git a/server/src/services/business/StatusPageService.ts b/server/src/services/business/StatusPageService.ts new file mode 100644 index 000000000..f3886b17d --- /dev/null +++ b/server/src/services/business/StatusPageService.ts @@ -0,0 +1,136 @@ +import mongoose from "mongoose"; + +import { + IUserContext, + IStatusPage, + StatusPage, + Monitor, +} from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; + +const SERVICE_NAME = "StatusPageService"; + +export interface IStatusPageService { + create: ( + tokenizedUser: IUserContext, + statusPage: IStatusPage + ) => Promise; + getAll: (teamId: string) => Promise; + get: (teamId: string, id: string) => Promise; + getPublic: (url: string) => Promise; + update: ( + teamId: string, + tokenizedUser: IUserContext, + id: string, + updateData: Partial + ) => Promise; + delete: (teamId: string, id: string) => Promise; +} + +class StatusPageService implements IStatusPageService { + public SERVICE_NAME: string; + + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + create = async (userContext: IUserContext, statusPageData: IStatusPage) => { + const monitorIds = statusPageData.monitors || []; + const count = await Monitor.countDocuments({ + _id: { $in: monitorIds }, + teamId: userContext.currentTeamId, + }); + + if (count !== monitorIds.length) { + throw new ApiError( + "One or more monitors do not belong to the current team", + 403 + ); + } + + const data: IStatusPage = { + ...statusPageData, + orgId: new mongoose.Types.ObjectId(userContext.orgId), + teamId: new mongoose.Types.ObjectId(userContext.currentTeamId), + createdBy: new mongoose.Types.ObjectId(userContext.sub), + updatedBy: new mongoose.Types.ObjectId(userContext.sub), + }; + + const statusPage = await StatusPage.create(data); + return statusPage; + }; + + get = async (teamId: string, id: string) => { + const statusPage = await StatusPage.findOne({ _id: id, teamId }).populate( + "monitors" + ); + if (!statusPage) { + throw new ApiError("Status page not found", 404); + } + return statusPage; + }; + + getPublic = async (url: string) => { + const statusPage = await StatusPage.findOne({ + url, + isPublished: true, + }).populate("monitors"); + if (!statusPage) { + throw new ApiError("Public status page not found", 404); + } + return statusPage; + }; + + getAll = async (teamId: string) => { + return StatusPage.find({ teamId }); + }; + + update = async ( + teamId: string, + userContext: IUserContext, + id: string, + updateData: Partial + ) => { + const monitorIds = updateData.monitors || []; + const count = await Monitor.countDocuments({ + _id: { $in: monitorIds }, + teamId: userContext.currentTeamId, + }); + + if (count !== monitorIds.length) { + throw new ApiError( + "One or more monitors do not belong to the current team", + 403 + ); + } + + const updatedStatusPage = await StatusPage.findOneAndUpdate( + { _id: id, teamId }, + { + $set: { + ...updateData, + updatedAt: new Date(), + updatedBy: userContext.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedStatusPage) { + throw new ApiError("Failed to update status page", 500); + } + + return updatedStatusPage; + }; + + delete = async (teamId: string, id: string) => { + const result = await StatusPage.deleteOne({ _id: id, teamId }); + if (!result.deletedCount) { + throw new ApiError("Status page not found", 404); + } + + return result.deletedCount === 1; + }; +} + +export default StatusPageService; diff --git a/server/src/services/business/TeamMemberServices.ts b/server/src/services/business/TeamMemberServices.ts new file mode 100644 index 000000000..44840f181 --- /dev/null +++ b/server/src/services/business/TeamMemberServices.ts @@ -0,0 +1,148 @@ +import mongoose from "mongoose"; +import { + ITeamMembership, + TeamMembership, + IUser, + IRole, + Role, + Team, + OrgMembership, + IOrgMembership, +} from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; + +const SERVICE_NAME = "TeamMemberService"; +export interface ITeamMemberService { + create: ( + orgId: string, + data: Partial + ) => Promise; + getAll: ( + orgId: string, + teamId: string | undefined, + type: string + ) => Promise>; + get: (orgId: string, teamMemberId: string) => Promise; + update: ( + orgId: string, + teamMembershipId: string, + roleId: string + ) => Promise; + delete: (orgId: string, teamMemberId: string) => Promise; +} + +class TeamMemberService implements ITeamMemberService { + public SERVICE_NAME: string; + + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + create = async (orgId: string, data: Partial) => { + const userId = data.userId?.toString(); + if (!userId) { + throw new ApiError("User ID is required", 400); + } + + const existing = await TeamMembership.findOne({ + teamId: data.teamId, + userId: data.userId, + orgId: orgId, + }); + + if (existing) { + throw new ApiError("User is already a member of the team", 400); + } + + const role = await Role.findOne({ + _id: data.roleId, + organizationId: orgId, + }); + if (!role) { + throw new ApiError("Role not found", 404); + } + + const tm = await TeamMembership.create({ + ...data, + orgId: new mongoose.Types.ObjectId(orgId), + }); + + return tm; + }; + + getAll = async (orgId: string, teamId: string | undefined, type: string) => { + if (type === "org") { + return await OrgMembership.find({ orgId }).populate( + "userId", + "firstName lastName email" + ); + } + + if (!teamId) { + return await TeamMembership.find({ orgId }) + .populate("userId", "firstName lastName email") + .populate("roleId", "name scope permissions"); + } + + const team = await Team.findOne({ _id: teamId, orgId: orgId }); + if (!team) { + throw new ApiError("Team not found", 404); + } + + const teamMembers = await TeamMembership.find({ orgId, teamId }) + .populate("userId", "firstName lastName email") + .populate("roleId", "name scope permissions"); + + return teamMembers; + }; + + get = async (orgId: string, teamMemberId: string) => { + const teamMember = await TeamMembership.findOne({ + _id: teamMemberId, + orgId: orgId, + }) + .populate("userId", "firstName lastName email") + .populate("roleId", "name scope permissions"); + + if (!teamMember) { + throw new ApiError("Team member not found", 404); + } + + return teamMember; + }; + + update = async (orgId: string, teamMembershipId: string, roleId: string) => { + const role = await Role.findOne({ _id: roleId, organizationId: orgId }); + if (!role) { + throw new ApiError("Role not found", 404); + } + + const teamMembership = await TeamMembership.findOneAndUpdate( + { orgId: orgId, _id: teamMembershipId }, + { roleId: roleId }, + { new: true } + ) + .populate("userId", "firstName lastName email") + .populate("roleId", "name scope permissions"); + + if (!teamMembership) { + throw new ApiError("Team membership not found", 404); + } + + return teamMembership; + }; + + delete = async (orgId: string, teamMemberId: string) => { + const tm = await TeamMembership.findOne({ + _id: teamMemberId, + orgId: orgId, + }); + if (!tm) { + throw new ApiError("Team member not found", 404); + } + await TeamMembership.findOneAndDelete({ _id: teamMemberId, orgId: orgId }); + return true; + }; +} + +export default TeamMemberService; diff --git a/server/src/services/business/TeamService.ts b/server/src/services/business/TeamService.ts new file mode 100644 index 000000000..bd36b042b --- /dev/null +++ b/server/src/services/business/TeamService.ts @@ -0,0 +1,163 @@ +import mongoose from "mongoose"; +import { + Monitor, + Role, + IRole, + ITeam, + Team, + TeamMembership, + OrgMembership, + IUserContext, +} from "@/db/models/index.js"; +import type { IJobQueue } from "../infrastructure/JobQueue.js"; +import ApiError from "@/utils/ApiError.js"; +import { PERMISSIONS } from "@/services/business/AuthService.js"; + +const SERVICE_NAME = "TeamService"; +export interface ITeamService { + create: ( + orgId: string, + userId: string, + teamData: ITeam, + roleId: string + ) => Promise; + getOrg: (orgId: string) => Promise>; + getJoined: (teamId: string, orgId: string) => Promise>; + get: (teamId: string, orgId: string) => Promise; + update: ( + user: IUserContext, + teamId: string, + teamData: Partial + ) => Promise; + delete: (orgId: string, teamId: string) => Promise; +} + +class TeamService implements ITeamService { + public SERVICE_NAME: string; + private jobQueue: IJobQueue; + + constructor(jobQueue: IJobQueue) { + this.SERVICE_NAME = SERVICE_NAME; + this.jobQueue = jobQueue; + } + + create = async ( + orgId: string, + userId: string, + teamData: ITeam, + roleId: string + ) => { + const created: Record = { + team: null, + memberships: null, + }; + + try { + const teamLiteral: Partial = { + name: teamData.name, + description: teamData.description, + orgId: new mongoose.Types.ObjectId(orgId), + }; + + // Check for roles + const roles = await Role.find({ organizationId: orgId }); + + if (!roles.some((r) => r._id.toString() === roleId)) { + throw new Error("Role does not exist"); + } + + const team = await Team.create(teamLiteral); + created.teamId = team._id; + const membership = await TeamMembership.create({ + teamId: team._id, + orgId: new mongoose.Types.ObjectId(orgId), + userId: new mongoose.Types.ObjectId(userId), + roleId: new mongoose.Types.ObjectId(roleId), + }); + created.teamMembershipId = membership._id; + + return true; + } catch (error) { + await Team.deleteOne({ _id: created.teamId }); + await TeamMembership.deleteOne({ _id: created.teamMembershipId }); + throw error; + } + }; + + getOrg = async (orgId: string) => { + const teams = await Team.find({ orgId }).select("_id name description"); + return teams; + }; + + getJoined = async (orgId: string, userId: string) => { + const teamMemberships = await TeamMembership.find({ + orgId, + userId, + }).select("teamId"); + + const teamIds = teamMemberships.map((tm) => tm.teamId); + + const teams = await Team.find({ _id: { $in: teamIds }, orgId }).select( + "_id name description" + ); + return teams; + }; + + get = async (teamId: string, orgId: string) => { + const team = await Team.findOne({ _id: teamId, orgId }); + if (!team) { + throw new ApiError("Team not found"); + } + return team; + }; + + update = async ( + user: IUserContext, + teamId: string, + teamData: Partial + ) => { + const updatedTeam = await Team.findOneAndUpdate( + { _id: teamId, orgId: user.orgId }, + { + $set: { + ...teamData, + updatedAt: new Date(), + updatedBy: user.sub, + }, + }, + { new: true, runValidators: true } + ); + if (!updatedTeam) { + throw new ApiError("Team not found"); + } + + return updatedTeam; + }; + + delete = async (orgId: string, teamId: string) => { + const team = await Team.findOne({ _id: teamId, orgId }).lean(); + if (!team) { + throw new ApiError("Team not found"); + } + + if (team.isSystem) { + throw new ApiError("Cannot delete your default team"); + } + + const memberships = await TeamMembership.find({ teamId }); + for (const membership of memberships) { + await membership.deleteOne(); + } + + const monitors = await Monitor.find({ teamId }); + await Promise.all( + monitors.map((monitor) => this.jobQueue.deleteJob(monitor)) + ); + + await Monitor.deleteMany({ teamId }); + await Team.deleteOne({ _id: teamId }); + return true; + }; +} + +export default TeamService; diff --git a/server/src/services/business/UserService.ts b/server/src/services/business/UserService.ts new file mode 100644 index 000000000..89458e654 --- /dev/null +++ b/server/src/services/business/UserService.ts @@ -0,0 +1,63 @@ +import { IUser, IUserProfile, User } from "@/db/models/index.js"; +import ApiError from "@/utils/ApiError.js"; +import { hashPassword } from "@/utils/JWTUtils.js"; +import { object } from "joi"; + +const SERVICE_NAME = "UserService"; +export interface IUserService { + get(email: string): Promise; + getAllUsers(): Promise; +} + +class UserService implements IUserService { + public SERVICE_NAME: string; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + async get(email: string): Promise { + const user = await User.findOne({ email }); + if (!user) { + throw new ApiError("User not found", 404); + } + return user; + } + + async update(userId: string, data: Partial): Promise { + let passwordHash = null; + + const updateData: Record = {}; + if (data.firstName !== undefined) updateData.firstName = data.firstName; + if (data.lastName !== undefined) updateData.lastName = data.lastName; + if (data.password !== undefined) { + updateData.passwordHash = await hashPassword(data.password); + } + + if (Object.keys(updateData).length === 0) { + throw new ApiError("No valid fields to update", 400); + } + + const updatedUser = await User.findOneAndUpdate( + { + _id: userId, + }, + { + $set: updateData, + }, + { + new: true, + } + ); + + if (!updatedUser) { + throw new ApiError("User not found", 404); + } + return updatedUser; + } + + async getAllUsers(): Promise { + return await User.find(); + } +} + +export default UserService; diff --git a/server/src/services/index.ts b/server/src/services/index.ts new file mode 100644 index 000000000..01bc1eb08 --- /dev/null +++ b/server/src/services/index.ts @@ -0,0 +1,26 @@ +export { default as AuthService } from "@/services/business/AuthService.js"; +export { default as CheckService } from "@/services/business/CheckService.js"; +export { default as InviteService } from "@/services/business/InviteService.js"; +export { default as MaintenanceService } from "@/services/business/MaintenanceService.js"; +export { default as MonitorService } from "@/services/business/MonitorService.js"; +export { default as MonitorStatsService } from "@/services/business/MonitorStatsService.js"; +export { default as NotificationChannelService } from "@/services/business/NotificationChannelService.js"; +export { default as QueueService } from "@/services/business/QueueService.js"; +export { default as UserService } from "@/services/business/UserService.js"; +export { default as TeamService } from "@/services/business/TeamService.js"; +export { default as RoleService } from "@/services/business/RoleService.js"; +export { default as TeamMemberService } from "@/services/business/TeamMemberServices.js"; +export { default as StatusPageService } from "@/services/business/StatusPageService.js"; +export { default as DiagnosticService } from "@/services/business/DiagnosticService.js"; +export { default as RecoveryService } from "@/services/business/RecoveryService.js"; +export { default as IncidentService } from "@/services/business/IncidentService.js"; + +export { default as NetworkService } from "@/services/infrastructure/NetworkService.js"; +export { default as NotificationService } from "@/services/infrastructure/NotificationService.js"; +export { default as StatusService } from "@/services/infrastructure/StatusService.js"; +export { default as JobQueue } from "@/services/infrastructure/JobQueue.js"; +export { default as JobGenerator } from "@/services/infrastructure/JobGenerator.js"; + +export { default as ServiceRegistry } from "@/services/system/ServiceRegistry.js"; + +export * from "@/services/infrastructure/NotificationServices/index.js"; diff --git a/server/src/services/infrastructure/JobGenerator.ts b/server/src/services/infrastructure/JobGenerator.ts new file mode 100644 index 000000000..6324046a6 --- /dev/null +++ b/server/src/services/infrastructure/JobGenerator.ts @@ -0,0 +1,108 @@ +import { IMonitor } from "@/db/models/index.js"; +import { INetworkService } from "./NetworkService.js"; +import { ICheckService } from "../business/CheckService.js"; +import { IMonitorStatsService } from "../business/MonitorStatsService.js"; +import { IStatusService } from "./StatusService.js"; +import { INotificationService } from "./NotificationService.js"; +import { IMaintenanceService } from "../business/MaintenanceService.js"; +import { IIncidentService } from "../business/IncidentService.js"; +import ApiError from "@/utils/ApiError.js"; +import { getChildLogger } from "@/logger/Logger.js"; + +const SERVICE_NAME = "JobGenerator"; +const logger = getChildLogger(SERVICE_NAME); +export interface IJobGenerator { + generateJob: () => (Monitor: IMonitor) => Promise; + generateCleanupJob: () => () => Promise; +} + +class JobGenerator implements IJobGenerator { + public SERVICE_NAME: string; + private networkService: INetworkService; + private checkService: ICheckService; + private monitorStatsService: IMonitorStatsService; + private statusService: IStatusService; + private notificationService: INotificationService; + private maintenanceService: IMaintenanceService; + private incidentService: IIncidentService; + + constructor( + networkService: INetworkService, + checkService: ICheckService, + monitorStatsService: IMonitorStatsService, + statusService: IStatusService, + notificationService: INotificationService, + incidentService: IIncidentService, + maintenanceService: IMaintenanceService + ) { + this.SERVICE_NAME = SERVICE_NAME; + this.networkService = networkService; + this.checkService = checkService; + this.monitorStatsService = monitorStatsService; + this.statusService = statusService; + this.notificationService = notificationService; + this.incidentService = incidentService; + this.maintenanceService = maintenanceService; + } + + generateJob = () => { + return async (monitor: IMonitor) => { + try { + const monitorId = monitor._id.toString(); + if (!monitorId) { + throw new ApiError("No monitorID for creating job", 400); + } + + // Check for active maintenance window, if found, skip the check + const isInMaintenance = await this.maintenanceService.isInMaintenance( + monitorId + ); + if (isInMaintenance) { + return; + } + + const status = await this.networkService.requestStatus(monitor); + const check = await this.checkService.buildCheck(status, monitor.type); + await check.save(); + const [updatedMonitor, statusChanged] = + await this.statusService.updateMonitorStatus(monitor, status); + + if (statusChanged) { + const incident = await this.incidentService.handleStatusChange( + updatedMonitor, + check + ); + + // Best effort, don't wait, don't fail + if (incident) { + this.notificationService + .handleNotifications(updatedMonitor, incident) + .catch((error) => { + logger.warn(error); + }); + } + } + await this.statusService.updateMonitorStats( + updatedMonitor, + status, + statusChanged + ); + } catch (error) { + throw error; + } + }; + }; + + generateCleanupJob = () => { + return async () => { + try { + await this.checkService.cleanupOrphanedChecks(); + await this.monitorStatsService.cleanupOrphanedMonitorStats(); + } catch (error) { + throw error; + } + }; + }; +} + +export default JobGenerator; diff --git a/server/src/services/infrastructure/JobQueue.ts b/server/src/services/infrastructure/JobQueue.ts new file mode 100644 index 000000000..038c46081 --- /dev/null +++ b/server/src/services/infrastructure/JobQueue.ts @@ -0,0 +1,252 @@ +import { IJob } from "super-simple-scheduler/dist/job/job.js"; +import { Monitor, IMonitor } from "@/db/models/index.js"; +import Scheduler from "super-simple-scheduler"; +import { IJobGenerator } from "./JobGenerator.js"; +import { getChildLogger } from "@/logger/Logger.js"; + +const SERVICE_NAME = "JobQueue"; +const logger = getChildLogger(SERVICE_NAME); +export interface IJobMetrics { + jobs: number; + activeJobs: number; + failingJobs: number; + jobsWithFailures: Array<{ + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + failedAt: number | null; + failCount: number | null; + failReason: string | null; + }>; + totalRuns: number; + totalFailures: number; +} + +export interface IJobData extends IJob { + lastRunTook: number | null; +} + +export interface IJobQueue { + init: () => Promise; + addJob: (monitor: IMonitor) => Promise; + pauseJob: (monitor: IMonitor) => Promise; + resumeJob: (monitor: IMonitor) => Promise; + updateJob: (monitor: IMonitor) => Promise; + deleteJob: (monitor: IMonitor) => Promise; + getMetrics: () => Promise; + getJobs: () => Promise; + flush: () => Promise; + shutdown: () => Promise; +} + +export default class JobQueue implements IJobQueue { + public SERVICE_NAME: string; + private scheduler: Scheduler; + private static instance: JobQueue | null = null; + private jobGenerator: any; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + this.scheduler = new Scheduler({ + logLevel: "debug", + }); + } + + static async create(jobGenerator: IJobGenerator) { + if (!JobQueue.instance) { + const instance = new JobQueue(); + instance.jobGenerator = jobGenerator; + await instance.init(); + JobQueue.instance = instance; + } + return JobQueue.instance; + } + + static getInstance(): JobQueue | null { + return JobQueue.instance; + } + + init = async () => { + try { + this.scheduler.start(); + // Add template and jobs + this.scheduler.addTemplate( + "monitor-job", + this.jobGenerator.generateJob() + ); + + // Add a cleanup job + this.scheduler.addTemplate( + "cleanup-job", + this.jobGenerator.generateCleanupJob() + ); + await this.scheduler.addJob({ + id: "cleanup-orphaned-checks", + template: "cleanup-job", + repeat: 24 * 60 * 60 * 1000, // 24 hours + active: true, + }); + + const monitors = await Monitor.find(); + for (const monitor of monitors) { + const randomOffset = 1000 + Math.random() * 1000; + + setTimeout(async () => { + const exists = await Monitor.exists({ _id: monitor._id }); + if (exists) { + this.addJob(monitor); + } + }, randomOffset); + } + + return true; + } catch (error) { + logger.error(error); + return false; + } + }; + + addJob = async (monitor: IMonitor) => { + try { + return await this.scheduler?.addJob({ + id: monitor._id.toString(), + template: "monitor-job", + repeat: monitor.interval, + active: monitor.isActive, + data: monitor, + }); + } catch (error) { + logger.error(error); + return false; + } + }; + + pauseJob = async (monitor: IMonitor) => { + try { + return await this.scheduler?.pauseJob(monitor._id.toString()); + } catch (error) { + logger.error(error); + return false; + } + }; + + resumeJob = async (monitor: IMonitor) => { + try { + return await this.scheduler.resumeJob(monitor._id.toString()); + } catch (error) { + logger.error(error); + return false; + } + }; + + updateJob = async (monitor: IMonitor) => { + try { + return await this.scheduler.updateJob(monitor._id.toString(), { + repeat: monitor.interval, + data: monitor, + }); + } catch (error) { + logger.error(error); + return false; + } + }; + + deleteJob = async (monitor: IMonitor) => { + try { + this.scheduler?.removeJob(monitor._id.toString()); + return true; + } catch (error) { + logger.error(error); + return false; + } + }; + + getMetrics = async (): Promise => { + try { + const jobs = await this.scheduler.getJobs(); + const metrics: IJobMetrics = jobs.reduce( + (acc, job) => { + if (!job.data) return acc; + + acc.totalRuns += job.runCount || 0; + acc.totalFailures += job.failCount || 0; + acc.jobs++; + + // Check if job is currently failing (has recent failures) + const hasFailures = job.failCount && job.failCount > 0; + const isCurrentlyFailing = + hasFailures && + job.lastFailedAt && + (!job.lastRunAt || job.lastFailedAt > job.lastRunAt); + + if (isCurrentlyFailing) { + acc.failingJobs++; + } + + if (job.lockedAt) { + acc.activeJobs++; + } + + if (hasFailures) { + acc.jobsWithFailures.push({ + monitorId: job.id, + monitorUrl: job.data?.url || null, + monitorType: job.data?.type || null, + failedAt: job.lastFailedAt || null, + failCount: job.failCount || null, + failReason: job.lastFailReason || null, + }); + } + return acc; + }, + { + jobs: 0, + activeJobs: 0, + failingJobs: 0, + jobsWithFailures: [], + totalRuns: 0, + totalFailures: 0, + } + ); + return metrics; + } catch (error) { + logger.error(error); + return null; + } + }; + + getJobs = async (): Promise => { + try { + const jobs = await this.scheduler.getJobs(); + return jobs.map((job) => { + return { + ...job, + lastRunTook: + job.lockedAt || !job.lastFinishedAt || !job.lastRunAt + ? null + : job.lastFinishedAt - job.lastRunAt, + }; + }); + } catch (error) { + logger.error(error); + return null; + } + }; + + flush = async () => { + try { + return await this.scheduler.flushJobs(); + } catch (error) { + logger.error(error); + return false; + } + }; + + shutdown = async () => { + try { + return await this.scheduler.stop(); + } catch (error) { + logger.error(error); + return false; + } + }; +} diff --git a/server/src/services/infrastructure/NetworkService.ts b/server/src/services/infrastructure/NetworkService.ts new file mode 100644 index 000000000..bd9d4c58e --- /dev/null +++ b/server/src/services/infrastructure/NetworkService.ts @@ -0,0 +1,232 @@ +import { Got, HTTPError } from "got"; +import got from "got"; +import ping from "ping"; +import { IMonitor } from "@/db/models/index.js"; +import { GotTimings } from "@/db/models/checks/Check.js"; +import type { Response } from "got"; +import type { + ISystemInfo, + ICaptureInfo, + ILighthouseResult, +} from "@/db/models/index.js"; +import { MonitorType, MonitorStatus } from "@/db/models/monitors/Monitor.js"; +import ApiError from "@/utils/ApiError.js"; +import { config } from "@/config/index.js"; +import CacheableLookup from "cacheable-lookup"; +import http from "http"; +import https from "https"; + +const SERVICE_NAME = "NetworkService"; +export interface INetworkService { + requestHttp: (monitor: IMonitor) => Promise; + requestInfrastructure: (monitor: IMonitor) => Promise; + requestStatus: (monitor: IMonitor) => Promise; + requestPagespeed: (monitor: IMonitor) => Promise; + requestPing: (monitor: IMonitor) => Promise; +} + +export interface ICapturePayload { + data: ISystemInfo; + capture: ICaptureInfo; +} + +export interface ILighthousePayload { + lighthouseResult: ILighthouseResult; +} + +export interface StatusResponse { + monitorId: string; + teamId: string; + type: MonitorType; + code?: number; + status: MonitorStatus; + message: string; + responseTime: number; + timings?: GotTimings; + payload?: TPayload; +} + +class NetworkService implements INetworkService { + public SERVICE_NAME = SERVICE_NAME; + private got: Got; + private NETWORK_ERROR: number; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + this.NETWORK_ERROR = 5000; + + const cacheable = new CacheableLookup(); + + this.got = got.extend({ + dnsCache: cacheable, + timeout: { + request: 30000, + }, + retry: { limit: 1 }, + }); + } + + private buildStatusResponse = ( + monitor: IMonitor, + response: Response | null, + error: any | null + ): StatusResponse => { + if (error) { + const statusResponse: StatusResponse = { + monitorId: monitor._id.toString(), + teamId: monitor.teamId.toString(), + type: monitor.type, + status: "down" as MonitorStatus, + code: this.NETWORK_ERROR, + message: error.message || "Network error", + responseTime: 0, + timings: { phases: {} } as GotTimings, + }; + if (error instanceof HTTPError) { + statusResponse.code = error?.response?.statusCode || this.NETWORK_ERROR; + statusResponse.message = error.message || "HTTP error"; + statusResponse.responseTime = error.timings?.phases?.total || 0; + statusResponse.timings = error.timings; + } + return statusResponse; + } + + const statusResponse: StatusResponse = { + monitorId: monitor._id.toString(), + teamId: monitor.teamId.toString(), + type: monitor.type, + code: response?.statusCode || this.NETWORK_ERROR, + status: response?.ok === true ? "up" : "down", + message: response?.statusMessage || "", + responseTime: response?.timings?.phases?.total || 0, + timings: response?.timings || ({ phases: {} } as GotTimings), + }; + + return statusResponse; + }; + + requestHttp = async (monitor: IMonitor) => { + try { + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + + try { + const response: Response = await this.got(url); + return this.buildStatusResponse(monitor, response, null); + } catch (error) { + return this.buildStatusResponse(monitor, null, error); + } + } catch (error) { + throw error; + } + }; + + requestInfrastructure = async (monitor: IMonitor) => { + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + const secret = monitor.secret; + if (!secret) { + throw new Error("No secret provided for infrastructure monitor"); + } + + let statusResponse: StatusResponse; + try { + const response: Response | null = await this.got(url, { + headers: { Authorization: `Bearer ${secret}` }, + responseType: "json", + }); + + statusResponse = this.buildStatusResponse(monitor, response, null); + if (!response?.body) { + throw new ApiError( + "No payload received from infrastructure monitor", + 500 + ); + } + statusResponse.payload = response?.body; + return statusResponse; + } catch (error) { + statusResponse = this.buildStatusResponse(monitor, null, error); + } + return statusResponse; + }; + + requestPagespeed = async (monitor: IMonitor) => { + const apiKey = config.PAGESPEED_API_KEY; + if (!apiKey) { + throw new Error("No API key provided for pagespeed monitor"); + } + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + + let statusResponse: StatusResponse; + + try { + const response: Response = await this.got(url); + statusResponse = this.buildStatusResponse( + monitor, + response, + null + ) as StatusResponse; + } catch (error) { + statusResponse = this.buildStatusResponse(monitor, null, error); + } + + const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance&key=${apiKey}`; + const pagespeedResponse = await this.got(pagespeedUrl, { + responseType: "json", + }); + const payload = pagespeedResponse.body; + if (payload) { + statusResponse.payload = payload; + return statusResponse; + } else { + throw new ApiError("No payload received from pagespeed monitor", 500); + } + }; + + requestPing = async (monitor: IMonitor) => { + const response = await ping.promise.probe(monitor.url); + const status = response?.alive === true ? "up" : "down"; + + const rawTime = + typeof response?.time === "string" + ? parseFloat(response.time) + : Number(response?.time); + const responseTime = Number.isFinite(rawTime) ? rawTime : 0; + + return { + monitorId: monitor._id.toString(), + teamId: monitor.teamId.toString(), + type: monitor.type, + status: status as MonitorStatus, + message: "Ping successful", + responseTime, + timings: { phases: {} } as GotTimings, + }; + }; + + requestStatus = async (monitor: IMonitor) => { + switch (monitor?.type) { + case "http": + return await this.requestHttp(monitor); // uses GOT + case "https": + return await this.requestHttp(monitor); // uses GOT + case "infrastructure": + return await this.requestInfrastructure(monitor); // uses GOT + case "pagespeed": + return await this.requestPagespeed(monitor); // uses GOT + case "ping": + return await this.requestPing(monitor); // uses PING + default: + throw new Error("Not implemented"); + } + }; +} + +export default NetworkService; diff --git a/server/src/services/infrastructure/NotificationService.ts b/server/src/services/infrastructure/NotificationService.ts new file mode 100644 index 000000000..58842a1d7 --- /dev/null +++ b/server/src/services/infrastructure/NotificationService.ts @@ -0,0 +1,186 @@ +import UserService from "../business/UserService.js"; +import { + IMonitor, + Monitor, + NotificationChannel, + INotificationChannel, + IIncident, +} from "@/db/models/index.js"; +import { + EmailService, + SlackService, + DiscordService, + WebhookService, +} from "./NotificationServices/index.js"; +import ApiError from "@/utils/ApiError.js"; +import { getChildLogger } from "@/logger/Logger.js"; + +const SERVICE_NAME = "NotificationService"; +const logger = getChildLogger(SERVICE_NAME); + +export interface ITestResult { + channelName: string; + channelUrl: string; + channelType: string; + sent: boolean; +} +export interface INotificationService { + handleNotifications: ( + monitor: IMonitor, + incident: IIncident + ) => Promise; + testNotificationChannels: ( + monitorId: string, + teamId: string + ) => Promise; + testNotificationChannel: ( + notificationChannel: INotificationChannel + ) => Promise; +} + +class NotificationService implements INotificationService { + public SERVICE_NAME: string; + private emailService: EmailService; + private slackService: SlackService; + private discordService: DiscordService; + private webhookService: WebhookService; + private userService: UserService; + + constructor(userService: UserService) { + this.SERVICE_NAME = SERVICE_NAME; + this.userService = userService; + this.emailService = new EmailService(userService); + this.slackService = new SlackService(); + this.discordService = new DiscordService(); + this.webhookService = new WebhookService(); + } + + handleNotifications = async (monitor: IMonitor, incident: IIncident) => { + const notificationIds = monitor.notificationChannels || []; + + if (notificationIds.length === 0) { + return; + } + + const notificationChannels = await NotificationChannel.find({ + _id: { $in: notificationIds }, + }); + + for (const channel of notificationChannels) { + // Implement sending logic based on channel.type and channel.config + let service; + switch (channel.type) { + case "email": + await this.emailService.sendMessage( + this.emailService.buildAlert(monitor, incident), + channel + ); + break; + case "slack": + await this.slackService.sendMessage( + this.slackService.buildAlert(monitor, incident), + channel + ); + break; + case "discord": + await this.discordService.sendMessage( + this.discordService.buildAlert(monitor, incident), + channel + ); + break; + case "webhook": + await this.webhookService.sendMessage( + this.webhookService.buildAlert(monitor, incident), + channel + ); + break; + default: + logger.warn(`Unknown notification channel type: ${channel.type}`); + } + } + return; + }; + + private testNotification = async ( + channel: INotificationChannel, + results: any[] + ) => { + switch (channel.type) { + case "email": + const sentEmail = await this.emailService.testMessage(channel); + results.push({ + channelName: channel.name, + channelType: channel.type, + channelUrl: channel.config?.emailAddress || "N/A", + sent: sentEmail, + }); + break; + case "slack": + const sentSlack = await this.slackService.testMessage(channel); + results.push({ + channelName: channel.name, + channelType: channel.type, + channelUrl: channel.config?.url || "N/A", + sent: sentSlack, + }); + break; + case "discord": + const sentDiscord = await this.discordService.testMessage(channel); + results.push({ + channelName: channel.name, + channelType: channel.type, + channelUrl: channel.config?.url || "N/A", + sent: sentDiscord, + }); + break; + case "webhook": + const sentWebhook = await this.webhookService.testMessage(channel); + results.push({ + channelName: channel.name, + channelType: channel.type, + channelUrl: channel.config?.url || "N/A", + sent: sentWebhook, + }); + break; + default: + logger.warn(`Unknown notification channel type: ${channel.type}`); + } + return results; + }; + + testNotificationChannels = async (monitorId: string, teamId: string) => { + const monitor = await Monitor.findOne({ + _id: monitorId, + teamId: teamId, + }); + + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + + const notificationIds = monitor.notificationChannels || []; + const notificationChannels = await NotificationChannel.find({ + _id: { $in: notificationIds }, + }).lean(); + + const results: any[] = []; + + for (const channel of notificationChannels) { + await this.testNotification(channel, results); + } + return results; + }; + + testNotificationChannel = async ( + notificationChannel: INotificationChannel + ) => { + const result: any[] = []; + await this.testNotification(notificationChannel, result); + if (result.length > 0) { + return true; + } + return false; + }; +} + +export default NotificationService; diff --git a/server/src/services/infrastructure/NotificationServices/Discord.ts b/server/src/services/infrastructure/NotificationServices/Discord.ts new file mode 100644 index 000000000..e52ba60dc --- /dev/null +++ b/server/src/services/infrastructure/NotificationServices/Discord.ts @@ -0,0 +1,112 @@ +import { + IMonitor, + INotificationChannel, + IIncident, +} from "@/db/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import got from "got"; +import ApiError from "@/utils/ApiError.js"; +import { getChildLogger } from "@/logger/Logger.js"; +const SERVICE_NAME = "DiscordService"; +const logger = getChildLogger(SERVICE_NAME); +class DiscordService implements IMessageService { + public SERVICE_NAME: string; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + private toDiscordEmbeds = (alert: IAlert) => { + return { + color: alert.status === "up" ? 65280 : 16711680, + title: `Monitor name: ${alert.name}`, + description: `Status: **${alert.status}**`, + fields: [ + { + name: "Url", + value: alert.url, + }, + { + name: "Checked at", + value: alert.checkTime ? alert.checkTime.toISOString() : "N/A", + }, + { + name: "Resolved", + value: alert.resolved ? "Yes" : "No", + }, + { + name: "Resolution type", + value: alert.resolutionType || "N/A", + }, + { + name: "Resolution note", + value: alert.resolutionNote || "N/A", + }, + { name: "Alert time", value: alert.alertTime.toISOString() }, + ...(alert.details + ? Object.entries(alert.details).map(([key, value]) => ({ + name: key, + value, + })) + : []), + ], + }; + }; + + buildAlert = (monitor: IMonitor, incident: IIncident) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + resolved: incident.resolved, + resolutionType: incident.resolutionType, + resolvedBy: incident.resolvedBy?.toString(), + resolutionNote: incident.resolutionNote, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new ApiError("Webhook URL not configured", 400); + } + + try { + const payload = { + content: "Status Alert", + embeds: [this.toDiscordEmbeds(alert)], + }; + await got.post(notificationUrl, { json: payload }); + } catch (error) { + logger.warn("Failed to send Discord message", error); + return false; + } + + return true; + }; + + testMessage = async (channel: INotificationChannel) => { + return this.sendMessage( + { + name: "This is a test", + url: "Test URL", + status: "Test status", + checkTime: new Date(), + alertTime: new Date(), + resolved: true, + resolutionType: "auto", + resolvedBy: "system", + resolutionNote: "This is a test message", + }, + channel + ); + }; +} + +export default DiscordService; diff --git a/server/src/services/infrastructure/NotificationServices/Email.ts b/server/src/services/infrastructure/NotificationServices/Email.ts new file mode 100644 index 000000000..b4ded0a0c --- /dev/null +++ b/server/src/services/infrastructure/NotificationServices/Email.ts @@ -0,0 +1,113 @@ +import { + IMonitor, + INotificationChannel, + IIncident, +} from "@/db/models/index.js"; +import { IMessageService, IAlert } from "./IMessageService.js"; +import nodemailer, { Transporter } from "nodemailer"; +import { config } from "@/config/index.js"; +import UserService from "../../business/UserService.js"; +import ApiError from "@/utils/ApiError.js"; + +const SERVICE_NAME = "EmailService"; + +export interface IEmailService extends IMessageService { + sendGeneric: ( + to: string, + subject: string, + content: string + ) => Promise; +} + +class EmailService implements IEmailService { + public SERVICE_NAME = SERVICE_NAME; + private transporter: Transporter; + private userService: UserService; + + constructor(userService: UserService) { + this.SERVICE_NAME = SERVICE_NAME; + this.userService = userService; + this.transporter = nodemailer.createTransport({ + host: config.SMTP_HOST, + port: config.SMTP_PORT, + secure: config.SMTP_PORT === 465, + auth: { + user: config.SMTP_USER, + pass: config.SMTP_PASS, + }, + }); + } + + buildAlert = (monitor: IMonitor, incident: IIncident) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + resolved: incident.resolved, + resolutionType: incident.resolutionType, + resolvedBy: incident.resolvedBy?.toString(), + resolutionNote: incident.resolutionNote, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + try { + const users = await this.userService.getAllUsers(); + const emails = users.map((u) => u.email).join(","); + + if (!emails || emails.length === 0) { + throw new ApiError("No user emails found", 500); + } + + await this.transporter.sendMail({ + from: `"Checkmate" <${config.SMTP_USER}>`, + to: emails, + subject: "Monitor Alert", + text: JSON.stringify(alert, null, 2), + }); + return true; + } catch (error) { + return false; + } + }; + + testMessage = async (channel: INotificationChannel) => { + return this.sendMessage( + { + name: "This is a test", + url: "Test URL", + status: "Test status", + checkTime: new Date(), + alertTime: new Date(), + resolved: true, + resolutionType: "auto", + resolvedBy: "system", + resolutionNote: "This is a test message", + }, + channel + ); + }; + + sendGeneric = async (to: string, subject: string, content: string) => { + try { + await this.transporter.sendMail({ + from: `"Checkmate" <${config.SMTP_USER}>`, + to: to, + subject: subject, + text: content, + }); + return true; + } catch (error) { + throw error; + } + }; +} + +export default EmailService; diff --git a/server/src/services/infrastructure/NotificationServices/IMessageService.ts b/server/src/services/infrastructure/NotificationServices/IMessageService.ts new file mode 100644 index 000000000..e9c294b89 --- /dev/null +++ b/server/src/services/infrastructure/NotificationServices/IMessageService.ts @@ -0,0 +1,27 @@ +import { + IMonitor, + INotificationChannel, + IIncident, + ResolutionType, +} from "@/db/models/index.js"; + +export interface IAlert { + name: string; + url: string; + status: string; + resolved: boolean; + resolutionType: ResolutionType | undefined; + resolvedBy?: string | undefined; + resolutionNote?: string | undefined; + details?: Record; + checkTime: Date | null; + alertTime: Date; +} +export interface IMessageService { + buildAlert: (monitor: IMonitor, incident: IIncident) => IAlert; + sendMessage: ( + alert: IAlert, + channel: INotificationChannel + ) => Promise; + testMessage: (channel: INotificationChannel) => Promise; +} diff --git a/server/src/services/infrastructure/NotificationServices/Slack.ts b/server/src/services/infrastructure/NotificationServices/Slack.ts new file mode 100644 index 000000000..861bfef28 --- /dev/null +++ b/server/src/services/infrastructure/NotificationServices/Slack.ts @@ -0,0 +1,173 @@ +import { + IMonitor, + INotificationChannel, + IIncident, +} from "@/db/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import got from "got"; + +import { getChildLogger } from "@/logger/Logger.js"; +const SERVICE_NAME = "SlackService"; +const logger = getChildLogger(SERVICE_NAME); +class SlackService implements IMessageService { + public SERVICE_NAME = SERVICE_NAME; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + private toSlackBlocks = (alert: IAlert) => { + return [ + { + type: "header", + text: { + type: "plain_text", + text: "Status Alert", + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Monitor name:* ${alert.name}`, + }, + }, + + { + type: "section", + text: { + type: "mrkdwn", + text: `*Status:* ${alert.status}`, + }, + }, + + { + type: "section", + text: { + type: "mrkdwn", + text: `*URL:* ${alert.url}`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `resolved: ${alert.resolved ? "Yes" : "No"}`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `Resolution Type: ${alert.resolutionType || "N/A"}`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `Resolution note: ${alert.resolutionNote || "N/A"}`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Checked at:* ${alert?.checkTime?.toISOString() || "N/A"}`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Checked at:* ${alert?.checkTime?.toISOString() || "N/A"}`, + }, + }, + + { + type: "divider", + }, + + ...(alert.details + ? Object.entries(alert.details).map(([key, value]) => ({ + type: "section", + fields: [ + { + type: "mrkdwn", + text: `*${key}:* ${value}`, + }, + ], + })) + : []), + + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `*Alert generated at:* ${ + alert?.alertTime?.toISOString() || "N/A" + }`, + }, + ], + }, + ]; + }; + + buildAlert = (monitor: IMonitor, incident: IIncident) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + resolved: incident.resolved, + resolutionType: incident.resolutionType, + resolvedBy: incident.resolvedBy?.toString(), + resolutionNote: incident.resolutionNote, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new Error("Webhook URL not configured"); + } + + try { + const payload = { + text: "Status Alert", + blocks: this.toSlackBlocks(alert), + }; + await got.post(notificationUrl, { json: payload }); + } catch (error) { + logger.warn("Error sending Slack message:", error); + return false; + } + + return true; + }; + + testMessage = async (channel: INotificationChannel) => { + return this.sendMessage( + { + name: "This is a test", + url: "Test URL", + status: "Test status", + checkTime: new Date(), + alertTime: new Date(), + resolved: true, + resolutionType: "auto", + resolvedBy: "system", + resolutionNote: "This is a test message", + }, + channel + ); + }; +} + +export default SlackService; diff --git a/server/src/services/infrastructure/NotificationServices/Webhook.ts b/server/src/services/infrastructure/NotificationServices/Webhook.ts new file mode 100644 index 000000000..09f81882f --- /dev/null +++ b/server/src/services/infrastructure/NotificationServices/Webhook.ts @@ -0,0 +1,60 @@ +import { IMonitor, INotificationChannel } from "@/db/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import ApiError from "@/utils/ApiError.js"; +import got from "got"; +import { getChildLogger } from "@/logger/Logger.js"; + +const SERVICE_NAME = "WebhookService"; +const logger = getChildLogger(SERVICE_NAME); +class WebhookService implements IMessageService { + public SERVICE_NAME: string; + + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new ApiError("Webhook URL not configured", 400); + } + try { + await got.post(notificationUrl, { json: { ...alert } }); + } catch (error) { + logger.warn("Failed to send webhook notification:", error); + return false; + } + + return true; + }; + + testMessage = async (channel: INotificationChannel) => { + return this.sendMessage( + { + name: "This is a test", + url: "Test URL", + status: "Test status", + checkTime: new Date(), + alertTime: new Date(), + }, + channel + ); + }; +} + +export default WebhookService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/index.ts b/server/src/services/infrastructure/NotificationServices/index.ts similarity index 100% rename from server/src/service/v2/infrastructure/NotificationServices/index.ts rename to server/src/services/infrastructure/NotificationServices/index.ts diff --git a/server/src/services/infrastructure/StatusService.ts b/server/src/services/infrastructure/StatusService.ts new file mode 100644 index 000000000..8fecb9614 --- /dev/null +++ b/server/src/services/infrastructure/StatusService.ts @@ -0,0 +1,141 @@ +import { IMonitor, IMonitorStats, MonitorStats } from "@/db/models/index.js"; +import { StatusResponse } from "./NetworkService.js"; +import ApiError from "@/utils/ApiError.js"; + +const SERVICE_NAME = "StatusService"; +const MAX_LATEST_CHECKS = 25; +export interface IStatusService { + updateMonitorStatus: ( + monitor: IMonitor, + status: StatusResponse + ) => Promise; + + calculateAvgResponseTime: ( + stats: IMonitorStats, + statusResponse: StatusResponse + ) => number; + + updateMonitorStats: ( + monitor: IMonitor, + status: StatusResponse, + statusChanged: boolean + ) => Promise; +} + +export type StatusChangeResult = [ + updatedMonitor: IMonitor, + statusChanged: boolean +]; + +class StatusService implements IStatusService { + public SERVICE_NAME: string; + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + } + + updateMonitorStatus = async ( + monitor: IMonitor, + statusResponse: StatusResponse + ): Promise => { + const newStatus = statusResponse.status; + monitor.lastCheckedAt = new Date(); + + // Store latest checks for display + monitor.latestChecks = monitor.latestChecks || []; + monitor.latestChecks.push({ + status: newStatus, + responseTime: statusResponse.responseTime, + checkedAt: monitor.lastCheckedAt, + }); + while (monitor.latestChecks.length > MAX_LATEST_CHECKS) { + monitor.latestChecks.shift(); + } + + // Update monitor status + if (monitor.status === "initializing") { + monitor.status = newStatus; + return [await monitor.save(), monitor.latestChecks.length === 0]; + } else { + const { n } = monitor; + const latestChecks = monitor.latestChecks.slice(-n); + // Return early if not enough statuses to evaluate + if (latestChecks.length < n) { + return [await monitor.save(), false]; + } + + // If all different than current status, update status + const allDifferent = latestChecks.every( + (check) => check.status !== monitor.status + ); + if (allDifferent && monitor.status !== newStatus) { + monitor.status = newStatus; + } + return [await monitor.save(), allDifferent]; + } + }; + + calculateAvgResponseTime = ( + stats: IMonitorStats, + statusResponse: StatusResponse + ): number => { + let avgResponseTime = stats.avgResponseTime; + // Set initial + if (avgResponseTime === 0) { + avgResponseTime = statusResponse.responseTime; + } else { + avgResponseTime = + (avgResponseTime * (stats.totalChecks - 1) + + statusResponse.responseTime) / + stats.totalChecks; + } + return avgResponseTime; + }; + + updateMonitorStats = async ( + monitor: IMonitor, + statusResponse: StatusResponse, + statusChanged: boolean + ) => { + const stats = await MonitorStats.findOne({ monitorId: monitor._id }); + if (!stats) { + throw new ApiError("MonitorStats not found", 500); + } + + // Update check counts + stats.totalChecks += 1; + stats.totalUpChecks += statusResponse.status === "up" ? 1 : 0; + stats.totalDownChecks += statusResponse.status === "down" ? 1 : 0; + + // Update streak + if (!statusChanged) { + stats.currentStreak += 1; + } else { + stats.currentStreak = 1; + stats.currentStreakStatus = statusResponse.status; + stats.currentStreakStartedAt = Date.now(); + } + + // Update time stamps + stats.lastCheckTimestamp = Date.now(); + stats.timeOfLastFailure = + statusResponse.status === "down" ? Date.now() : stats.timeOfLastFailure; + + // Update stats that need updated check counts + stats.avgResponseTime = this.calculateAvgResponseTime( + stats, + statusResponse + ); + stats.uptimePercentage = stats.totalUpChecks / stats.totalChecks; + + // Other + stats.lastResponseTime = statusResponse.responseTime; + stats.maxResponseTime = Math.max( + stats.maxResponseTime, + statusResponse.responseTime + ); + + return await stats.save(); + }; +} + +export default StatusService; diff --git a/server/src/services/system/ServiceRegistry.ts b/server/src/services/system/ServiceRegistry.ts new file mode 100644 index 000000000..2b8883b56 --- /dev/null +++ b/server/src/services/system/ServiceRegistry.ts @@ -0,0 +1,69 @@ +const SERVICE_NAME = "ServiceRegistry"; + +export interface IServiceRegistry { + register: (name: string, service: any) => void; + get: (name: string) => any; + listServices: () => string[]; +} + +class ServiceRegistry implements IServiceRegistry { + public SERVICE_NAME: string; + private static _instance: ServiceRegistry; + + private services: { [key: string]: any }; + + private constructor() { + this.SERVICE_NAME = SERVICE_NAME; + this.services = {}; + } + + static get instance(): ServiceRegistry { + if (!this._instance) { + this._instance = new ServiceRegistry(); + } + return this._instance; + } + + get serviceName() { + return this.SERVICE_NAME; + } + + // Instance methods + register(name: string, service: any) { + this.services[name] = service; + } + + get(name: string) { + if (!this.services[name]) { + throw new Error(`Service ${name} is not registered`); + } + return this.services[name]; + } + + listServices() { + return Object.keys(this.services); + } + + static get(name: string) { + if (!ServiceRegistry.instance) { + throw new Error("ServiceRegistry not initialized"); + } + return ServiceRegistry.instance.get(name); + } + + static register(name: string, service: any) { + if (!ServiceRegistry.instance) { + throw new Error("ServiceRegistry not initialized"); + } + return ServiceRegistry.instance.register(name, service); + } + + static listServices() { + if (!ServiceRegistry.instance) { + throw new Error("ServiceRegistry not initialized"); + } + return ServiceRegistry.instance.listServices(); + } +} + +export default ServiceRegistry; diff --git a/server/src/shutdown.js b/server/src/shutdown.js deleted file mode 100644 index 27eb0f21d..000000000 --- a/server/src/shutdown.js +++ /dev/null @@ -1,34 +0,0 @@ -import { logger } from "./utils/logger.js"; - -export const initShutdownListener = (server, services) => { - const SERVICE_NAME = "Server"; - - let isShuttingDown = false; - - const shutdown = async () => { - if (isShuttingDown) { - return; - } - isShuttingDown = true; - logger.info({ message: "Attempting graceful shutdown" }); - - try { - server.close(); - await services.jobQueue.shutdown(); - await services.db.disconnect(); - logger.info({ message: "Graceful shutdown complete" }); - process.exit(0); - } catch (error) { - logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "shutdown", - stack: error.stack, - }); - process.exit(1); - } - }; - process.on("SIGUSR2", shutdown); - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); -}; diff --git a/server/src/templates/addReview.mjml b/server/src/templates/addReview.mjml deleted file mode 100755 index e193ac1ba..000000000 --- a/server/src/templates/addReview.mjml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - Message from Checkmate Service - - - - - - -

Hello {{ name }}!

-

- We hope you’re finding Checkmate helpful in monitoring your infrastructure. Your support means a lot to us, and we - truly appreciate having you as part of our community. -

-

- If you’re happy with Checkmate, we’d love to hear about your experience! Leaving a review on G2 helps others discover Checkmate and - supports our ongoing improvements. -

- G2 Link: TBD -

Thank you for taking the time to share your thoughts - we greatly appreciate it!

- Checkmate Team -
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/templates/employeeActivation.mjml b/server/src/templates/employeeActivation.mjml deleted file mode 100755 index 317832c1f..000000000 --- a/server/src/templates/employeeActivation.mjml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - Message from Checkmate Service - - - - - -

Hello {{ name }}!

-

One of the admins created an account for you on the Checkmate server.

-

You can go ahead and create your account using this link.

-

- {{ link }} -

-

Thank you.

-
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/templates/hardwareIncident.mjml b/server/src/templates/hardwareIncident.mjml deleted file mode 100755 index 3f50a3d58..000000000 --- a/server/src/templates/hardwareIncident.mjml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - - - - - - Message from BlueWave Infrastructure Monitoring - - - - - Infrastructure Alerts - - - - - - - -

Hello {{ name }}!

-

{{ monitor }} at {{ url }} has the following infrastructure alerts:

- {{#each alerts}} -

• {{ this }}

- {{/each}} -
-
- - - View Infrastructure Details - -

This email was sent by BlueWave Infrastructure Monitoring.

-
-
-
-
-
diff --git a/server/src/templates/noIncidentsThisWeek.mjml b/server/src/templates/noIncidentsThisWeek.mjml deleted file mode 100755 index d160e44de..000000000 --- a/server/src/templates/noIncidentsThisWeek.mjml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - Message from Checkmate Service - - - No incidents this week! - - - - - - -

Hello {{ name }}!

-

There were no incidents this week. Good job!

-

Current monitors:

-

Google: 100% availability

-

Canada.ca:100% availability

-
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/templates/passwordReset.mjml b/server/src/templates/passwordReset.mjml deleted file mode 100755 index 568b363ee..000000000 --- a/server/src/templates/passwordReset.mjml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - Message from Checkmate Service - - - - - - -

Hello {{ name }}!

-

- You are receiving this email because a password reset request has been made for {{ email }}. Please use the link below on the site to - reset your password. -

- Reset Password -

If you didn't request this, please ignore this email.

- -

Thank you.

-
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/templates/serverIsDown.mjml b/server/src/templates/serverIsDown.mjml deleted file mode 100755 index bf6424a6b..000000000 --- a/server/src/templates/serverIsDown.mjml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - Message from Checkmate Service - - - - - {{ monitor }} is down - - - - - - - -

Hello {{ name }}!

-

We detected an incident on one of your monitors. Your service is currently down. We'll send a message to you once it is up again.

-

Monitor name: {{ monitor }}

-

URL: {{ url }}

-
-
- - - View incident details - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/templates/serverIsUp.mjml b/server/src/templates/serverIsUp.mjml deleted file mode 100755 index 074c5e3dd..000000000 --- a/server/src/templates/serverIsUp.mjml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - Message from Checkmate Service - - - - - {{ monitor }} is up - - - - - - - -

Hello {{ name }}!

-

Your latest incident is resolved and your monitored service is up again.

-

Monitor name: {{ monitor }}

-

URL: {{ url }}

-
-
- - - View incident details - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/templates/testEmailTemplate.mjml b/server/src/templates/testEmailTemplate.mjml deleted file mode 100644 index 4f8fd7644..000000000 --- a/server/src/templates/testEmailTemplate.mjml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - Hello! - - - This is a test email from the Monitoring System. - - - If you're receiving this, your email configuration is working correctly. - - - - - diff --git a/server/src/templates/welcomeEmail.mjml b/server/src/templates/welcomeEmail.mjml deleted file mode 100755 index e9f569e3f..000000000 --- a/server/src/templates/welcomeEmail.mjml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - Message from Checkmate Service - - - - - - -

Hello {{ name }}!

-

Thank you for trying out Checkmate! We developed it with great care to meet our own needs, and we're excited to share it with you.

-

Checkmate is an automated way of checking whether a service such as a website or an application is available or not.

-

We hope you find our service as valuable as we do.

-

Thank you.

-
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts index 7c78a053c..42b616791 100644 --- a/server/src/types/express.d.ts +++ b/server/src/types/express.d.ts @@ -1,10 +1,11 @@ -import { ITokenizedUser } from "../db/models/index.ts"; +import { IUserContext } from "../db/models/index.ts"; declare global { - namespace Express { - interface Request { - user?: ITokenizedUser; - resource?: any; - } - } + namespace Express { + interface Request { + user?: IUserContext; + resource?: any; + validatedQuery: Record; + } + } } diff --git a/server/src/types/monitor-response-with-checks.ts b/server/src/types/monitor-response-with-checks.ts index 660ce0035..b547459e5 100644 --- a/server/src/types/monitor-response-with-checks.ts +++ b/server/src/types/monitor-response-with-checks.ts @@ -1,11 +1,11 @@ -import { IMonitor, IMonitorStats } from "../db/v2/models/index.js"; +import { IMonitor, IMonitorStats } from "@/db/models/index.js"; export interface MonitorWithChecksResponse { - monitor: IMonitor; - checks: Array<{ - _id: string; - count: number; - avgResponseTime: number; - }>; - stats: IMonitorStats; + monitor: IMonitor; + checks: Array<{ + _id: string; + count: number; + avgResponseTime: number; + }>; + stats: IMonitorStats; } diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts index 4eebb78a0..bc9cc1965 100644 --- a/server/src/utils/JWTUtils.ts +++ b/server/src/utils/JWTUtils.ts @@ -1,22 +1,35 @@ import jwt from "jsonwebtoken"; -import { AuthResult } from "../service/v2/business/AuthService.js"; +import { AuthResult } from "@/services/business/AuthService.js"; +import { IUserContext } from "@/db/models/index.js"; +import bcrypt from "bcryptjs"; -const encode = (data: AuthResult): string => { - const secret = process.env.JWT_SECRET; - if (!secret) { - throw new Error("JWT_SECRET is not defined"); - } - const token = jwt.sign(data, secret, { expiresIn: "99d" }); - return token; +export const encode = (data: AuthResult): string => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("JWT_SECRET is not defined"); + } + const token = jwt.sign(data, secret, { expiresIn: "99d" }); + return token; }; -const decode = (token: string): AuthResult => { - const secret = process.env.JWT_SECRET; - if (!secret) { - throw new Error("JWT_SECRET is not defined"); - } - const decoded = jwt.verify(token, secret) as AuthResult; - return decoded; +export const decode = (token: string): IUserContext => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("JWT_SECRET is not defined"); + } + const decoded = jwt.verify(token, secret) as IUserContext; + return decoded; }; -export { encode, decode }; +export const hashPassword = async (password: string): Promise => { + const saltRounds = 10; + const hash = await bcrypt.hash(password, saltRounds); + return hash; +}; + +export const compareHash = async ( + password: string, + hash: string +): Promise => { + return await bcrypt.compare(password, hash); +}; diff --git a/server/src/utils/cookieHelpers.js b/server/src/utils/cookieHelpers.js deleted file mode 100644 index 7005b66ca..000000000 --- a/server/src/utils/cookieHelpers.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Get standardized cookie options for authentication tokens - * @param {Object} options - Additional cookie options - * @returns {Object} Cookie options object - */ -export const getAuthCookieOptions = (options = {}) => { - return { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 2 * 60 * 60 * 1000, // 2 hours (matches JWT TTL) - ...options, - }; -}; - -/** - * Clear cookie options for authentication tokens - * @returns {Object} Cookie clear options object - */ -export const getClearAuthCookieOptions = () => { - return { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - }; -}; diff --git a/server/src/utils/dataUtils.js b/server/src/utils/dataUtils.js deleted file mode 100755 index e7fd334dd..000000000 --- a/server/src/utils/dataUtils.js +++ /dev/null @@ -1,100 +0,0 @@ -const calculatePercentile = (arr, percentile) => { - const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime); - const index = (percentile / 100) * (sorted.length - 1); - const lower = Math.floor(index); - const upper = lower + 1; - const weight = index % 1; - if (upper >= sorted.length) return sorted[lower].responseTime; - return sorted[lower].responseTime * (1 - weight) + sorted[upper].responseTime * weight; -}; - -const calculatePercentileUptimeDetails = (arr, percentile) => { - const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime); - const index = (percentile / 100) * (sorted.length - 1); - const lower = Math.floor(index); - const upper = lower + 1; - const weight = index % 1; - if (upper >= sorted.length) return sorted[lower].avgResponseTime; - return sorted[lower].avgResponseTime * (1 - weight) + sorted[upper].avgResponseTime * weight; -}; - -const NormalizeData = (checks, rangeMin, rangeMax) => { - if (checks.length > 1) { - // Get the 5th and 95th percentile - const min = calculatePercentile(checks, 0); - const max = calculatePercentile(checks, 95); - const normalizedChecks = checks.map((check) => { - const originalResponseTime = check.responseTime; - // Normalize the response time between 1 and 100 - let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min); - - // Put a floor on the response times so we don't have extreme outliers - // Better visuals - normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); - return { - ...check, - responseTime: normalizedResponseTime, - originalResponseTime: originalResponseTime, - }; - }); - - return normalizedChecks; - } else { - return checks.map((check) => { - return { ...check, originalResponseTime: check.responseTime }; - }); - } -}; -const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => { - if (checks.length > 1) { - // Get the 5th and 95th percentile - const min = calculatePercentileUptimeDetails(checks, 0); - const max = calculatePercentileUptimeDetails(checks, 95); - - const normalizedChecks = checks.map((check) => { - const originalResponseTime = check.avgResponseTime; - // Normalize the response time between 1 and 100 - let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min); - - // Put a floor on the response times so we don't have extreme outliers - // Better visuals - normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); - return { - ...check, - avgResponseTime: normalizedResponseTime, - originalAvgResponseTime: originalResponseTime, - }; - }); - - return normalizedChecks; - } else { - return checks.map((check) => { - return { ...check, originalResponseTime: check.responseTime }; - }); - } -}; - -const safelyParseFloat = (value, defaultValue = 0) => { - if (value === null || typeof value === "undefined") { - return defaultValue; - } - const stringValue = String(value).trim(); - - if (typeof value === "number" && !isNaN(value)) { - return value; - } - - if (stringValue === "") { - return defaultValue; - } - - const parsedValue = parseFloat(stringValue); - - if (isNaN(parsedValue) || !isFinite(parsedValue)) { - return defaultValue; - } - - return parsedValue; -}; - -export { safelyParseFloat, calculatePercentile, NormalizeData, calculatePercentileUptimeDetails, NormalizeDataUptimeDetails }; diff --git a/server/src/utils/demoMonitors.json b/server/src/utils/demoMonitors.json deleted file mode 100755 index 3efea8d0e..000000000 --- a/server/src/utils/demoMonitors.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "name": "Google", - "url": "https://www.google.com" - }, - { - "name": "Facebook", - "url": "https://www.facebook.com" - }, - { - "name": "Yahoo", - "url": "https://www.yahoo.com" - }, - { - "name": "Amazon", - "url": "https://www.amazon.com" - }, - { - "name": "Apple", - "url": "https://www.apple.com" - } -] diff --git a/server/src/utils/demoMonitorsOld.json b/server/src/utils/demoMonitorsOld.json deleted file mode 100755 index ffce7d226..000000000 --- a/server/src/utils/demoMonitorsOld.json +++ /dev/null @@ -1,1271 +0,0 @@ -[ - { - "name": "0to255", - "url": "https://0to255.com" - }, - { - "name": "10015.io", - "url": "https://10015.io" - }, - { - "name": "3DIcons", - "url": "https://3dicons.co" - }, - { - "name": "About.me", - "url": "https://about.me" - }, - { - "name": "Alias", - "url": "https://alias.co" - }, - { - "name": "All About Berlin", - "url": "https://allaboutberlin.com" - }, - { - "name": "All Acronyms", - "url": "https://allacronyms.com" - }, - { - "name": "All You Can Read ", - "url": "https://allyoucanread.com" - }, - { - "name": "AllTrails", - "url": "https://alltrails.com" - }, - { - "name": "Anotepad", - "url": "https://anotepad.com" - }, - { - "name": "AnswerSocrates", - "url": "https://answersocrates.com" - }, - { - "name": "AnswerThePublic ", - "url": "https://answerthepublic.com" - }, - { - "name": "Apollo ", - "url": "https://apollo.io" - }, - { - "name": "ArrayList", - "url": "https://arraylist.org" - }, - { - "name": "Ask Difference", - "url": "https://askdifference.com" - }, - { - "name": "Audd.io", - "url": "https://audd.io" - }, - { - "name": "Audiocheck", - "url": "https://audiocheck.net" - }, - { - "name": "Audionautix", - "url": "https://audionautix.com" - }, - { - "name": "Authentic Jobs", - "url": "https://authenticjobs.com" - }, - { - "name": "Behind the Name", - "url": "https://behindthename.com" - }, - { - "name": "Bilim Terimleri", - "url": "https://terimler.org" - }, - { - "name": "BitBof", - "url": "https://bitbof.com" - }, - { - "name": "Blank Page", - "url": "https://blank.page" - }, - { - "name": "Bonanza", - "url": "https://bonanza.com" - }, - { - "name": "BookCrossing", - "url": "https://bookcrossing.com" - }, - { - "name": "Browse AI", - "url": "https://browse.ai" - }, - { - "name": "Bubbl.us", - "url": "https://bubbl.us" - }, - { - "name": "Business Model Toolbox", - "url": "https://bmtoolbox.net" - }, - { - "name": "ByClickDownloader", - "url": "https://byclickdownloader.com" - }, - { - "name": "Calligraphr", - "url": "https://calligraphr.com" - }, - { - "name": "CertificateClaim", - "url": "https://certificateclaim.com" - }, - { - "name": "Chosic", - "url": "https://chosic.com" - }, - { - "name": "ClipDrop", - "url": "https://clipdrop.co" - }, - { - "name": "CloudConvert", - "url": "https://cloudconvert.com" - }, - { - "name": "CodingFont", - "url": "https://codingfont.com" - }, - { - "name": "Color Hunt", - "url": "https://colorhunt.co" - }, - { - "name": "ColorHexa", - "url": "https://colorhexa.com" - }, - { - "name": "Conversion-Tool", - "url": "https://conversion-tool.com" - }, - { - "name": "Cool Startup Jobs", - "url": "https://coolstartupjobs.com" - }, - { - "name": "Coroflot", - "url": "https://coroflot.com" - }, - { - "name": "Corrupt-a-File", - "url": "https://corrupt-a-file.net" - }, - { - "name": "Couchsurfing", - "url": "https://couchsurfing.com" - }, - { - "name": "Countries Been", - "url": "https://countriesbeen.com" - }, - { - "name": "Country Code", - "url": "https://countrycode.org" - }, - { - "name": "Creately", - "url": "https://creately.com" - }, - { - "name": "Creately ", - "url": "https://creately.com" - }, - { - "name": "Crossfade.io", - "url": "https://crossfade.io" - }, - { - "name": "Crunchbase", - "url": "https://crunchbase.com" - }, - { - "name": "CVmkr", - "url": "https://cvwizard.com" - }, - { - "name": "Daily Remote", - "url": "https://dailyremote.com" - }, - { - "name": "David Li", - "url": "https://david.li" - }, - { - "name": "DemandHunt", - "url": "https://demandhunt.com" - }, - { - "name": "Designify", - "url": "https://designify.com" - }, - { - "name": "Diff Checker", - "url": "https://diffchecker.com" - }, - { - "name": "DifferenceBetween.info", - "url": "https://differencebetween.info" - }, - { - "name": "Digital Glossary", - "url": "https://digital-glossary.com" - }, - { - "name": "Dimensions", - "url": "https://dimensions.com" - }, - { - "name": "Discoverify Music", - "url": "https://discoverifymusic.com" - }, - { - "name": "discu.eu", - "url": "https://discu.eu" - }, - { - "name": "Do It Yourself", - "url": "https://doityourself.com" - }, - { - "name": "draw.io", - "url": "https://drawio.com" - }, - { - "name": "Drumeo", - "url": "https://drumeo.com" - }, - { - "name": "Dummies", - "url": "https://dummies.com" - }, - { - "name": "Easel.ly", - "url": "https://easel.ly" - }, - { - "name": "Educalingo", - "url": "https://educalingo.com" - }, - { - "name": "Emoji Combos", - "url": "https://emojicombos.com" - }, - { - "name": "EquityBee", - "url": "https://equitybee.com" - }, - { - "name": "EquityZen", - "url": "https://equityzen.com" - }, - { - "name": "Escape Room Tips", - "url": "https://escaperoomtips.com" - }, - { - "name": "Every Noise", - "url": "https://everynoise.com" - }, - { - "name": "Every Time Zone", - "url": "https://everytimezone.com" - }, - { - "name": "Excalideck", - "url": "https://excalideck.com" - }, - { - "name": "Excalidraw", - "url": "https://excalidraw.com" - }, - { - "name": "Extract pics", - "url": "https://extract.pics" - }, - { - "name": "EZGIF", - "url": "https://ezgif.com" - }, - { - "name": "FactSlides", - "url": "https://factslides.com" - }, - { - "name": "FIGR ", - "url": "https://figr.app" - }, - { - "name": "Fine Dictionary", - "url": "https://finedictionary.com" - }, - { - "name": "Fiverr", - "url": "https://fiverr.com" - }, - { - "name": "Fix It Club", - "url": "https://fixitclub.com" - }, - { - "name": "Flightradar24", - "url": "https://flightradar24.com" - }, - { - "name": "FlowCV ", - "url": "https://flowcv.com" - }, - { - "name": "Font Squirrel", - "url": "https://fontsquirrel.com" - }, - { - "name": "FontAwesome", - "url": "https://fontawesome.com" - }, - { - "name": "Fontello ", - "url": "https://fontello.com" - }, - { - "name": "Form to Chatbot", - "url": "https://formtochatbot.com" - }, - { - "name": "Founder Resources", - "url": "https://founderresources.io" - }, - { - "name": "Franz", - "url": "https://meetfranz.com" - }, - { - "name": "Fraze It", - "url": "https://fraze.it" - }, - { - "name": "Freecycle", - "url": "https://freecycle.org" - }, - { - "name": "FreeType", - "url": "https://freetype.org" - }, - { - "name": "FutureM", - "url": "https://futureme.org" - }, - { - "name": "Generated.Photos", - "url": "https://generated.photos" - }, - { - "name": "Get Human", - "url": "https://gethuman.com" - }, - { - "name": "Go Bento", - "url": "https://gobento.com" - }, - { - "name": "Good CV", - "url": "https://goodcv.com" - }, - { - "name": "Grammar Monster", - "url": "https://grammar-monster.com" - }, - { - "name": "Grammar Book", - "url": "https://grammarbook.com" - }, - { - "name": "Gummy Search", - "url": "https://gummysearch.com" - }, - { - "name": "Gumroad", - "url": "https://gumroad.com" - }, - { - "name": "HealthIcons", - "url": "https://healthicons.org" - }, - { - "name": "HexColor", - "url": "https://hexcolor.co" - }, - { - "name": "Hidden Life Radio", - "url": "https://hiddenliferadio.com" - }, - { - "name": "Hired", - "url": "https://lhh.com" - }, - { - "name": "Honey", - "url": "https://joinhoney.com" - }, - { - "name": "HowStuffWorks", - "url": "https://howstuffworks.com" - }, - { - "name": "HugeIcons Pro", - "url": "https://hugeicons.com" - }, - { - "name": "Humble Bundle", - "url": "https://humblebundle.com" - }, - { - "name": "I Have No TV", - "url": "https://ihavenotv.com" - }, - { - "name": "I Miss the Office", - "url": "https://imisstheoffice.eu" - }, - { - "name": "IcoMoon", - "url": "https://icomoon.io" - }, - { - "name": "Iconfinder", - "url": "https://iconfinder.com" - }, - { - "name": "Icon Packs", - "url": "https://iconpacks.net" - }, - { - "name": "Iconshock", - "url": "https://iconshock.com" - }, - { - "name": "Iconz Design", - "url": "https://iconz.design" - }, - { - "name": "iFixit", - "url": "https://ifixit.com" - }, - { - "name": "IFTTT", - "url": "https://ifttt.com" - }, - { - "name": "Illlustrations", - "url": "https://illlustrations.co" - }, - { - "name": "Illustration Kit", - "url": "https://illustrationkit.com" - }, - { - "name": "IMSDB", - "url": "https://imsdb.com" - }, - { - "name": "Incompetech", - "url": "https://incompetech.com" - }, - { - "name": "Incredibox", - "url": "https://incredibox.com" - }, - { - "name": "InnerBod", - "url": "https://innerbody.com" - }, - { - "name": "Instructables", - "url": "https://instructables.com" - }, - { - "name": "Integromat", - "url": "https://make.com" - }, - { - "name": "Investopedia", - "url": "https://investopedia.com" - }, - { - "name": "Japanese Wiki Corpus", - "url": "https://japanesewiki.com" - }, - { - "name": "Jitter.Video", - "url": "https://jitter.video" - }, - { - "name": "Jobspresso", - "url": "https://jobspresso.co" - }, - { - "name": "JPEG-Optimizer", - "url": "https://jpeg-optimizer.com" - }, - { - "name": "JS Remotely", - "url": "https://jsremotely.com" - }, - { - "name": "JScreenFix", - "url": "https://jscreenfix.com" - }, - { - "name": "JSON Resume", - "url": "https://jsonresume.io" - }, - { - "name": "Just Join", - "url": "https://justjoin.it" - }, - { - "name": "Just the Recipe", - "url": "https://justtherecipe.com" - }, - { - "name": "JustRemote", - "url": "https://justremote.co" - }, - { - "name": "JustWatch", - "url": "https://justwatch.com" - }, - { - "name": "Kanopy", - "url": "https://kanopy.com" - }, - { - "name": "Kassellabs", - "url": "https://kassellabs.io" - }, - { - "name": "Key Differences", - "url": "https://keydifferences.com" - }, - { - "name": "Keybase", - "url": "https://keybase.io" - }, - { - "name": "KeyValues", - "url": "https://keyvalues.com" - }, - { - "name": "KHInsider", - "url": "https://khinsider.com" - }, - { - "name": "Killed by Google", - "url": "https://killedbygoogle.com" - }, - { - "name": "Kimovil", - "url": "https://kimovil.com" - }, - { - "name": "Lalal.ai", - "url": "https://www.lalal.ai" - }, - { - "name": "Learn Anything", - "url": "https://learn-anything.xyz" - }, - { - "name": "LendingTree", - "url": "https://lendingtree.com" - }, - { - "name": "Lightyear.fm", - "url": "https://lightyear.fm" - }, - { - "name": "LittleSis", - "url": "https://littlesis.org" - }, - { - "name": "Looria", - "url": "https://looria.com" - }, - { - "name": "Lucidchart", - "url": "https://lucidchart.com" - }, - { - "name": "Lunar", - "url": "https://lunar.fyi" - }, - { - "name": "Manuals Lib", - "url": "https://manualslib.com" - }, - { - "name": "Map Crunch", - "url": "https://mapcrunch.com" - }, - { - "name": "Masterworks", - "url": "https://masterworks.com" - }, - { - "name": "MediaFire", - "url": "https://mediafire.com" - }, - { - "name": "Mixlr", - "url": "https://mixlr.com" - }, - { - "name": "Moises AI", - "url": "https://moises.ai" - }, - { - "name": "Money", - "url": "https://money.com" - }, - { - "name": "Mountain Project", - "url": "https://mountainproject.com" - }, - { - "name": "Movie Map", - "url": "https://movie-map.com" - }, - { - "name": "Movie Sounds", - "url": "https://movie-sounds.org" - }, - { - "name": "MP3Cut", - "url": "https://mp3cut.net" - }, - { - "name": "Murmel", - "url": "https://murmel.social" - }, - { - "name": "Muscle Wiki", - "url": "https://musclewiki.com" - }, - { - "name": "Music-Map", - "url": "https://music-map.com" - }, - { - "name": "MusicTheory.net", - "url": "https://musictheory.net" - }, - { - "name": "MyFonts", - "url": "https://myfonts.com" - }, - { - "name": "MyFridgeFood", - "url": "https://myfridgefood.com" - }, - { - "name": "Nameberry", - "url": "https://nameberry.com" - }, - { - "name": "Namechk", - "url": "https://namechk.com" - }, - { - "name": "Ncase", - "url": "https://ncase.me" - }, - { - "name": "News in Levels", - "url": "https://newsinlevels.com" - }, - { - "name": "Noisli", - "url": "https://noisli.com" - }, - { - "name": "Notes.io", - "url": "https://notes.io" - }, - { - "name": "Novoresume", - "url": "https://novoresume.com" - }, - { - "name": "Ocoya", - "url": "https://ocoya.com" - }, - { - "name": "Old Computers Museum", - "url": "https://oldcomputers.net" - }, - { - "name": "Online Tone Generator", - "url": "https://onlinetonegenerator.com" - }, - { - "name": "Online-Convert", - "url": "https://online-convert.com" - }, - { - "name": "OnlineConversion", - "url": "https://onlineconversion.com" - }, - { - "name": "Online OCR", - "url": "https://onlineocr.net" - }, - { - "name": "OpenWeatherMap", - "url": "https://openweathermap.org" - }, - { - "name": "OrgPad", - "url": "https://orgpad.com" - }, - { - "name": "Passport Index", - "url": "https://passportindex.org" - }, - { - "name": "PDF Candy", - "url": "https://pdfcandy.com" - }, - { - "name": "PDF2DOC", - "url": "https://pdf2doc.com" - }, - { - "name": "PDFescape", - "url": "https://pdfescape.com" - }, - { - "name": "PfpMaker", - "url": "https://pfpmaker.com" - }, - { - "name": "PIDGI Wiki ", - "url": "https://pidgi.net" - }, - { - "name": "PimEyes", - "url": "https://pimeyes.com" - }, - { - "name": "Pipl ", - "url": "https://pipl.com" - }, - { - "name": "PixelBazaar", - "url": "https://pixelbazaar.com" - }, - { - "name": "PixelPaper", - "url": "https://pixelpaper.io" - }, - { - "name": "Ponly", - "url": "https://ponly.com" - }, - { - "name": "PowerToFly", - "url": "https://powertofly.com" - }, - { - "name": "Pretzel Rocks", - "url": "https://pretzel.rocks" - }, - { - "name": "PrintIt", - "url": "https://printit.work" - }, - { - "name": "Prismatext", - "url": "https://prismatext.com" - }, - { - "name": "Puffin Maps", - "url": "https://puffinmaps.com" - }, - { - "name": "Puzzle Loop ", - "url": "https://puzzle-loop.com" - }, - { - "name": "QuoteMaster", - "url": "https://quotemaster.org" - }, - { - "name": "Radio Garden", - "url": "https://radio.garden" - }, - { - "name": "Radiooooo", - "url": "https://radiooooo.com" - }, - { - "name": "Radiosondy", - "url": "https://radiosondy.info" - }, - { - "name": "Rainy Mood", - "url": "https://rainymood.com" - }, - { - "name": "Random Street View", - "url": "https://randomstreetview.com" - }, - { - "name": "Rap4Ever", - "url": "https://rap4all.com" - }, - { - "name": "RareFilm", - "url": "https://rarefilm.net" - }, - { - "name": "Rattibha", - "url": "https://rattibha.com" - }, - { - "name": "Reddit List ", - "url": "https://redditlist.com" - }, - { - "name": "RedditSearch.io", - "url": "https://redditsearch.io" - }, - { - "name": "Reelgood", - "url": "https://reelgood.com" - }, - { - "name": "Reface", - "url": "https://reface.ai" - }, - { - "name": "Rejected.us", - "url": "https://rejected.us" - }, - { - "name": "Relanote", - "url": "https://relanote.com" - }, - { - "name": "Remote Leaf", - "url": "https://remoteleaf.com" - }, - { - "name": "Remote OK", - "url": "https://remoteok.com" - }, - { - "name": "Remote Starter Kit ", - "url": "https://remotestarterkit.com" - }, - { - "name": "Remote.co", - "url": "https://remote.co" - }, - { - "name": "Remote Base ", - "url": "https://remotebase.com" - }, - { - "name": "Remote Bear", - "url": "https://remotebear.io" - }, - { - "name": "Remove.bg", - "url": "https://remove.bg" - }, - { - "name": "Respresso", - "url": "https://respresso.io" - }, - { - "name": "Reveddit", - "url": "https://reveddit.com" - }, - { - "name": "Rhymer", - "url": "https://rhymer.com" - }, - { - "name": "RhymeZone", - "url": "https://rhymezone.com" - }, - { - "name": "Ribbet", - "url": "https://ribbet.com" - }, - { - "name": "Roadmap.sh", - "url": "https://roadmap.sh" - }, - { - "name": "Roadtrippers", - "url": "https://roadtrippers.com" - }, - { - "name": "RxResu.me", - "url": "https://rxresu.me" - }, - { - "name": "SchemeColor", - "url": "https://schemecolor.com" - }, - { - "name": "Screenshot.Guru", - "url": "https://screenshot.guru" - }, - { - "name": "SeatGuru", - "url": "https://seatguru.com" - }, - { - "name": "Sessions", - "url": "https://sessions.us" - }, - { - "name": "Shottr", - "url": "https://shottr.cc" - }, - { - "name": "Signature Maker", - "url": "https://signature-maker.net" - }, - { - "name": "Skip The Drive", - "url": "https://skipthedrive.com" - }, - { - "name": "Slowby", - "url": "https://slowby.travel" - }, - { - "name": "Small World", - "url": "https://smallworld.kiwi" - }, - { - "name": "SmallPDF", - "url": "https://smallpdf.com" - }, - { - "name": "Social Image Maker", - "url": "https://socialimagemaker.io" - }, - { - "name": "Social Sizes", - "url": "https://socialsizes.io" - }, - { - "name": "SoundLove", - "url": "https://soundlove.se" - }, - { - "name": "Spline", - "url": "https://spline.design" - }, - { - "name": "Starkey Comics", - "url": "https://starkeycomics.com" - }, - - { - "name": "Statista", - "url": "https://statista.com" - }, - { - "name": "Stolen Camera Finder", - "url": "https://stolencamerafinder.com" - }, - { - "name": "Strobe.Cool", - "url": "https://strobe.cool" - }, - { - "name": "Sumo", - "url": "https://sumo.app" - }, - { - "name": "SuperMeme AI", - "url": "https://supermeme.ai" - }, - { - "name": "Synthesia", - "url": "https://synthesia.io" - }, - { - "name": "TablerIcons", - "url": "https://tablericons.com" - }, - { - "name": "Tango", - "url": "https://tango.us" - }, - { - "name": "TasteDive", - "url": "https://tastedive.com" - }, - { - "name": "TechSpecs", - "url": "https://techspecs.io" - }, - { - "name": "Teoria", - "url": "https://teoria.com" - }, - { - "name": "Text Faces", - "url": "https://textfac.es" - }, - { - "name": "The Balance Money", - "url": "https://thebalancemoney.com" - }, - { - "name": "The Punctuation Guide", - "url": "https://thepunctuationguide.com" - }, - { - "name": "This to That", - "url": "https://thistothat.com" - }, - { - "name": "This vs That", - "url": "https://thisvsthat.io" - }, - { - "name": "ThreadReaderApp ", - "url": "https://threadreaderapp.com" - }, - { - "name": "Thumbly", - "url": "https://tokee.ai" - }, - { - "name": "Tiii.me", - "url": "https://tiii.me" - }, - { - "name": "TikTok Video Downloader", - "url": "https://ttvdl.com" - }, - { - "name": "Time and Date", - "url": "https://timeanddate.com" - }, - { - "name": "Time.is", - "url": "https://time.is" - }, - { - "name": "Title Case", - "url": "https://titlecase.com" - }, - { - "name": "Toaster Central", - "url": "https://toastercentral.com" - }, - { - "name": "Tongue-Twister ", - "url": "https://tongue-twister.net" - }, - { - "name": "TradingView", - "url": "https://tradingview.com" - }, - { - "name": "Transparent Textures", - "url": "https://transparenttextures.com" - }, - { - "name": "Tubi TV", - "url": "https://tubitv.com" - }, - { - "name": "Tunefind", - "url": "https://tunefind.com" - }, - { - "name": "TuneMyMusic", - "url": "https://tunemymusic.com" - }, - { - "name": "Tweepsmap", - "url": "https://fedica.com" - }, - { - "name": "Two Peas and Their Pod", - "url": "https://twopeasandtheirpod.com" - }, - { - "name": "Typatone", - "url": "https://typatone.com" - }, - { - "name": "Under Glass", - "url": "https://underglass.io" - }, - { - "name": "UniCorner", - "url": "https://unicorner.news" - }, - { - "name": "Unita", - "url": "https://unita.co" - }, - { - "name": "UnitConverters", - "url": "https://unitconverters.net" - }, - { - "name": "Unreadit", - "url": "https://unreadit.com" - }, - { - "name": "Unscreen", - "url": "https://unscreen.com" - }, - { - "name": "UnTools ", - "url": "https://untools.co" - }, - { - "name": "Upwork", - "url": "https://upwork.com" - }, - { - "name": "UTF8 Icons", - "url": "https://utf8icons.com" - }, - { - "name": "Vector Magic", - "url": "https://vectormagic.com" - }, - { - "name": "Virtual Vacation", - "url": "https://virtualvacation.us" - }, - { - "name": "Virtual Vocations", - "url": "https://virtualvocations.com" - }, - { - "name": "Visiwig", - "url": "https://visiwig.com" - }, - { - "name": "Visual CV", - "url": "https://visualcv.com" - }, - { - "name": "Vocus.io", - "url": "https://vocus.io" - }, - { - "name": "Voscreen", - "url": "https://voscreen.com" - }, - { - "name": "Wanderprep", - "url": "https://wanderprep.com" - }, - { - "name": "Warmshowers", - "url": "https:/warmshowers.org" - }, - { - "name": "Watch Documentaries", - "url": "https://watchdocumentaries.com" - }, - { - "name": "We Work Remotely", - "url": "https://weworkremotely.com" - }, - { - "name": "Web2PDFConvert", - "url": "https://web2pdfconvert.com" - }, - { - "name": "Welcome to My Garden", - "url": "https://welcometomygarden.org" - }, - { - "name": "When2meet ", - "url": "https://when2meet.com" - }, - { - "name": "Where's George", - "url": "https://wheresgeorge.com" - }, - { - "name": "Where's Willy", - "url": "https://whereswilly.com" - }, - { - "name": "WikiHow", - "url": "https://wikihow.com" - }, - { - "name": "Windy", - "url": "https://www.windy.com" - }, - { - "name": "WonderHowTo", - "url": "https://wonderhowto.com" - }, - { - "name": "Working Nomads", - "url": "https://workingnomads.com" - }, - { - "name": "Wormhole", - "url": "https://wormhole.app" - }, - { - "name": "Y Combinator Jobs", - "url": "https://ycombinator.com" - }, - { - "name": "Yes Promo", - "url": "https://yespromo.me" - }, - { - "name": "YouGlish", - "url": "https://youglish.com" - }, - { - "name": "Zamzar", - "url": "https://zamzar.com" - }, - { - "name": "Zippyshare", - "url": "https://zippyshare.com" - }, - { - "name": "Zoom Earth", - "url": "https://zoom.earth" - }, - { - "name": "Zoom.it", - "url": "https://zoom.it" - } -] diff --git a/server/src/utils/imageProcessing.js b/server/src/utils/imageProcessing.js deleted file mode 100755 index 470a5c27e..000000000 --- a/server/src/utils/imageProcessing.js +++ /dev/null @@ -1,25 +0,0 @@ -import sharp from "sharp"; -/** - * Generates a 64 * 64 pixel image from a given image - * @param {} file - */ -const GenerateAvatarImage = async (file) => { - try { - // Resize to target 64 * 64 - let resizedImageBuffer = await sharp(file.buffer) - .resize({ - width: 64, - height: 64, - fit: "cover", - }) - .toBuffer(); - - //Get b64 string - const base64Image = resizedImageBuffer.toString("base64"); - return base64Image; - } catch (error) { - throw error; - } -}; - -export { GenerateAvatarImage }; diff --git a/server/src/utils/logger.js b/server/src/utils/logger.js deleted file mode 100755 index 0917862fa..000000000 --- a/server/src/utils/logger.js +++ /dev/null @@ -1,153 +0,0 @@ -import { createLogger, format, transports } from "winston"; -import dotenv from "dotenv"; -dotenv.config(); - -const SERVICE_NAME = "Logger"; - -class Logger { - static SERVICE_NAME = SERVICE_NAME; - constructor({ envSettings }) { - this.envSettings = envSettings; - this.logCache = []; - this.maxCacheSize = 1000; - const consoleFormat = format.printf(({ level, message, service, method, details, timestamp, stack }) => { - if (message instanceof Object) { - message = JSON.stringify(message, null, 2); - } - - if (details instanceof Object) { - details = JSON.stringify(details, null, 2); - } - let msg = `${timestamp} ${level}:`; - service && (msg += ` [${service}]`); - method && (msg += `(${method})`); - message && (msg += ` ${message}`); - details && (msg += ` (details: ${details})`); - - if (typeof stack !== "undefined") { - const stackTrace = stack - ?.split("\n") - .slice(1) // Remove first line (error message) - .map((line) => { - const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/); - if (match) { - return { - function: match[1], - file: match[2], - line: parseInt(match[3]), - column: parseInt(match[4]), - }; - } - return line.trim(); - }); - stack && (msg += ` (stack: ${JSON.stringify(stackTrace, null, 2)})`); - } - - return msg; - }); - - const logLevel = this.envSettings.logLevel || "info"; - - this.logger = createLogger({ - level: logLevel, - format: format.combine(format.timestamp()), - transports: [ - new transports.Console({ - format: format.combine(format.colorize(), format.prettyPrint(), format.json(), consoleFormat), - }), - new transports.File({ - format: format.combine(format.json()), - filename: "app.log", - }), - ], - }); - } - - get serviceName() { - return Logger.SERVICE_NAME; - } - - /** - * Logs an informational message. - * @param {Object} config - The configuration object. - * @param {string} config.message - The message to log. - * @param {string} config.service - The service name. - * @param {string} config.method - The method name. - * @param {Object} config.details - Additional details. - */ - info(config) { - const logEntry = this.buildLogEntry("info", config); - this.cacheLog(logEntry); - this.logger.info(logEntry); - } - - /** - * Logs a warning message. - * @param {Object} config - The configuration object. - * @param {string} config.message - The message to log. - * @param {string} config.service - The service name. - * @param {string} config.method - The method name. - * @param {Object} config.details - Additional details. - */ - warn(config) { - const logEntry = this.buildLogEntry("warn", config); - this.cacheLog(logEntry); - this.logger.warn(logEntry); - } - - /** - * Logs an error message. - * @param {Object} config - The configuration object. - * @param {string} config.message - The message to log. - * @param {string} config.service - The service name. - * @param {string} config.method - The method name. - * @param {Object} config.details - Additional details. - */ - error(config) { - const logEntry = this.buildLogEntry("error", config); - this.cacheLog(logEntry); - this.logger.error(logEntry); - } - /** - * Logs a debug message. - * @param {Object} config - The configuration object. - * @param {string} config.message - The message to log. - * @param {string} config.service - The service name. - * @param {string} config.method - The method name. - * @param {Object} config.details - Additional details. - */ - debug(config) { - const logEntry = this.buildLogEntry("debug", config); - this.cacheLog(logEntry); - this.logger.debug(logEntry); - } - - cacheLog(entry) { - this.logCache.push(entry); - if (this.logCache.length > this.maxCacheSize) { - this.logCache.shift(); - } - } - - getLogs() { - return this.logCache; - } - - buildLogEntry(level, config) { - return { - level, - message: config.message, - service: config.service, - method: config.method, - details: config.details, - stack: config.stack, - timestamp: new Date().toISOString(), - }; - } -} - -export default Logger; - -// Legacy logger -const logger = new Logger({ envSettings: { logLevel: "debug" } }); -export { logger }; diff --git a/server/src/utils/roleUtils.js b/server/src/utils/roleUtils.js deleted file mode 100644 index f359246b2..000000000 --- a/server/src/utils/roleUtils.js +++ /dev/null @@ -1,13 +0,0 @@ -export const ROLES = { - SUPERADMIN: "superadmin", - ADMIN: "admin", - USER: "user", - DEMO: "demo", -}; - -export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; - -export const EDITABLE_ROLES = [ - { role: ROLES.ADMIN, _id: ROLES.ADMIN }, - { role: ROLES.USER, _id: ROLES.USER }, -]; diff --git a/server/src/utils/utils.js b/server/src/utils/utils.js deleted file mode 100755 index bcb4d8e5e..000000000 --- a/server/src/utils/utils.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Converts a request body parameter to a boolean. - * @param {string | boolean} value - * @returns {boolean} - */ -const ParseBoolean = (value) => { - if (value === true || value === "true") { - return true; - } else if (value === false || value === "false" || value === null || value === undefined) { - return false; - } -}; - -const getTokenFromHeaders = (headers) => { - const authorizationHeader = headers.authorization; - if (!authorizationHeader) throw new Error("No auth headers"); - - const parts = authorizationHeader.split(" "); - if (parts.length !== 2 || parts[0] !== "Bearer") throw new Error("Invalid auth headers"); - - return parts[1]; -}; - -export { ParseBoolean, getTokenFromHeaders }; diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts new file mode 100644 index 000000000..deb21b6cf --- /dev/null +++ b/server/src/validation/index.ts @@ -0,0 +1,527 @@ +import { + ChannelTypes, + MonitorStatuses, + ResolutionTypes, +} from "@/db/models/index.js"; +import { z } from "zod"; +import mongoose from "mongoose"; + +export const registerSchema = z.object({ + email: z.email({ message: "Invalid email address" }), + firstName: z.string().min(1, { message: "First Name is required" }), + lastName: z.string().min(1, { message: "Last Name is required" }), + password: z + .string() + .min(6, { message: "Password must be at least 6 characters" }), +}); + +export const registerWithInviteSchema = z.object({ + firstName: z + .string() + .min(1, { message: "First Name is required" }) + .optional(), + lastName: z.string().min(1, { message: "Last Name is required" }).optional(), + password: z + .string() + .min(6, { message: "Password must be at least 6 characters" }) + .optional(), + confirmPassword: z + .string() + .min(6, { message: "Password must be at least 6 characters" }) + .optional(), +}); + +export const loginSchema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), +}); + +export const inviteSchema = z.object({ + email: z.email("Invalid email address"), + teamId: z.string().min(1, "Team is required"), + teamRoleId: z.string().min(1, "Role is required"), + orgRoleId: z.string().optional(), +}); + +const urlRegex = + /^(https?:\/\/)?(localhost|[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}|(\d{1,3}\.){3}\d{1,3})(:\d{1,5})?(\/.*)?$/; + +export const monitorSchema = z + .object({ + type: z.string().min(1, "You must select an option"), + url: z.string().min(1, "URL is required").regex(urlRegex, "Invalid URL"), + name: z.string().min(1, "Display name is required"), + n: z + .number({ message: "Number required" }) + .min(1, "Minimum value is 1") + .max(25, "Maximum value is 25"), + notificationChannels: z.array(z.string()).optional().default([]), + secret: z.string().optional(), + interval: z.number({ message: "Interval required" }), + }) + .superRefine((data, ctx) => { + const minIntervals: Record = { + http: 10000, + ping: 10000, + pagespeed: 100000, + }; + + const minInterval = minIntervals[data.type]; + if (minInterval && data.interval < minInterval) { + ctx.addIssue({ + code: "custom", + message: `Minimum interval for ${data.type} monitors is ${minInterval} ms`, + }); + } + + if (data.type === "infrastructure" && !data.secret) { + ctx.addIssue({ + code: "custom", + message: `Secret is required for infrastructure monitors`, + }); + } + }); + +export const monitorPatchSchema = monitorSchema + .omit({ + url: true, + }) + .partial() + .superRefine((data, ctx) => { + const minIntervals: Record = { + http: 10000, + https: 10000, + ping: 10000, + pagespeed: 180000, + infrastructure: 60000, + }; + + if (!data.type || !data.interval) return; + + const minInterval = minIntervals[data.type]; + if (minInterval && data.interval < minInterval) { + ctx.addIssue({ + code: "custom", + message: `Minimum interval for ${data.type} monitors is ${minInterval} ms`, + }); + } + }); + +export const monitorAllEmbedChecksQuerySchema = z.object({ + embedChecks: z + .string() + .optional() + .transform((val, ctx) => { + if (val === undefined) return undefined; + if (val === "true") return true; + if (val === "false") return false; + ctx.addIssue({ + code: "custom", + message: "embedChecks must be 'true' or 'false'", + }); + return z.NEVER; + }), + type: z + .union([z.string().optional(), z.string().optional().array()]) + .optional() + .transform((val) => { + if (!val) return []; + return Array.isArray(val) ? val : [val]; + }), + page: z + .string() + .optional() + .transform((val, ctx) => { + const num = Number(val); + if (!val) return; + if (Number.isNaN(num)) { + ctx.addIssue({ + code: "custom", + message: "page must be a number", + }); + return z.NEVER; + } + if (num < 0) { + ctx.addIssue({ + code: "custom", + message: "page must greater than 0", + }); + return z.NEVER; + } + return num; + }), + rowsPerPage: z + .string() + .optional() + .transform((val, ctx) => { + if (!val) return; + const num = Number(val); + if (Number.isNaN(num)) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be a number", + }); + return z.NEVER; + } + if (num < 0) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be greater than 0", + }); + return z.NEVER; + } + if (num > 100) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be less than or equal to 100", + }); + return z.NEVER; + } + return num; + }), +}); + +export const monitorIdChecksQuerySchema = z.object({ + page: z.string().transform((val, ctx) => { + const num = Number(val); + if (Number.isNaN(num)) { + ctx.addIssue({ + code: "custom", + message: "page must be a number", + }); + return z.NEVER; + } + if (num < 0) { + ctx.addIssue({ + code: "custom", + message: "page must greater than 0", + }); + return z.NEVER; + } + return num; + }), + rowsPerPage: z.string().transform((val, ctx) => { + const num = Number(val); + if (Number.isNaN(num)) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be a number", + }); + return z.NEVER; + } + if (num < 0) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be greater than 0", + }); + return z.NEVER; + } + if (num > 100) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be less than or equal to 100", + }); + return z.NEVER; + } + return num; + }), +}); + +export const monitorIdQuerySchema = z.object({ + embedChecks: z + .string() + .transform((val) => val === "true") + .optional(), + range: z.string().optional(), + status: z.string().optional(), +}); + +export const notificationChannelSchema = z + .object({ + name: z.string().min(1, "Channel name is required"), + type: z.enum(ChannelTypes, { error: "Invalid channel type" }), + config: z.object({ + url: z + .string() + .regex(urlRegex, "Invalid URL") + .or(z.literal("")) + .optional(), + emailAddress: z + .email("Invalid email address") + .or(z.literal("")) + .optional(), + }), + }) + .superRefine((data, ctx) => { + const { type, config } = data; + if (!config.url && !config.emailAddress) { + ctx.addIssue({ + code: "custom", + message: "Either a URL or an email address must be provided.", + path: ["config"], + }); + } + + if (type === "email" && !config.emailAddress) { + ctx.addIssue({ + code: "custom", + message: "Email address is required for email-type channels.", + path: ["config", "emailAddress"], + }); + } else if (type !== "email" && !config.url) { + ctx.addIssue({ + code: "custom", + message: "URL is required for non-email-type channels.", + path: ["config", "url"], + }); + } + }); +export const notificationPatchSchema = z + .object({ + name: z.string().min(1).optional(), + type: z.enum(ChannelTypes).optional(), + config: z + .object({ + url: z + .string() + .regex(urlRegex, "Invalid URL") + .or(z.literal("")) + .optional(), + emailAddress: z + .string() + .email("Invalid email address") + .or(z.literal("")) + .optional(), + }) + .optional(), + }) + .superRefine((data, ctx) => { + const { type, config } = data; + + if (!config) return; + + const hasUrl = config.url !== undefined && config.url !== ""; + const hasEmail = + config.emailAddress !== undefined && config.emailAddress !== ""; + + if (!hasUrl && !hasEmail) { + ctx.addIssue({ + code: "custom", + message: "Either a URL or an email address must be provided.", + path: ["config"], + }); + } + + if (hasUrl && hasEmail) { + ctx.addIssue({ + code: "custom", + message: "Cannot provide both URL and email address at the same time.", + path: ["config"], + }); + } + + if (type === "email" && !hasEmail) { + ctx.addIssue({ + code: "custom", + message: "Email address is required for email-type channels.", + path: ["config", "emailAddress"], + }); + } else if (type && type !== "email" && !hasUrl) { + ctx.addIssue({ + code: "custom", + message: "URL is required for non-email-type channels.", + path: ["config", "url"], + }); + } + }); + +export const checksStatusIdQuerySchema = z.object({ + status: z.enum(MonitorStatuses, { error: "Invalid status" }), + monitorId: z + .string() + .refine((val) => mongoose.Types.ObjectId.isValid(val), { + message: "Invalid MongoDB ObjectId", + }) + .optional(), + page: z.string().transform((val, ctx) => { + const num = Number(val); + if (Number.isNaN(num)) { + ctx.addIssue({ + code: "custom", + message: "page must be a number", + }); + return z.NEVER; + } + if (num < 0) { + ctx.addIssue({ + code: "custom", + message: "page must greater than 0", + }); + return z.NEVER; + } + return num; + }), + rowsPerPage: z.string().transform((val, ctx) => { + const num = Number(val); + if (Number.isNaN(num)) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be a number", + }); + return z.NEVER; + } + if (num < 0) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be greater than 0", + }); + return z.NEVER; + } + if (num > 100) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be less than or equal to 100", + }); + return z.NEVER; + } + return num; + }), + range: z.string().min(1), +}); + +export const statusPageSchema = z.object({ + name: z.string().min(1, "Status page name is required"), + url: z.string().min(1, "Status page URL is required"), + description: z.string().optional(), + isPublished: z.boolean(), + monitors: z.array(z.string()).optional().default([]), +}); + +export const recoverySchema = z.object({ + email: z.email({ message: "Invalid email address" }), +}); + +export const resetPasswordSchema = z.object({ + token: z.string().min(1, { message: "Token is required" }), + password: z + .string() + .min(6, { message: "Password must be at least 6 characters" }), +}); + +export const profileSchema = z + .object({ + firstName: z + .string() + .min(1, { message: "First Name must be at least one char" }) + .optional(), + lastName: z + .string() + .min(1, { message: "Last Name must be at least one char" }) + .optional(), + password: z.preprocess( + (val) => (val === "" ? undefined : val), + z + .string() + .min(6, { message: "Password must be at least 6 characters" }) + .optional() + ), + confirmPassword: z.preprocess( + (val) => (val === "" ? undefined : val), + z + .string() + .min(6, { message: "Confirm Password must be at least 6 characters" }) + .optional() + ), + }) + .refine( + (data) => { + if (!data.password && !data.confirmPassword) return true; + + if (!data.password || !data.confirmPassword) return false; + + return data.password === data.confirmPassword; + }, + { + message: "Passwords must match", + path: ["confirmPassword"], + } + ); + +export const getIncidentsQuerySchema = z.object({ + resolutionType: z.enum(ResolutionTypes).optional(), + resolved: z + .string() + .optional() + .transform((val, ctx) => { + if (val === undefined) return undefined; + if (val === "true") return true; + if (val === "false") return false; + ctx.addIssue({ + code: "custom", + message: "resolved must be 'true' or 'false'", + }); + return z.NEVER; + }), + page: z + .string() + .optional() + .transform((val, ctx) => { + const num = Number(val); + if (!val) return; + if (Number.isNaN(num)) { + ctx.addIssue({ + code: "custom", + message: "page must be a number", + }); + return z.NEVER; + } + if (num < 0) { + ctx.addIssue({ + code: "custom", + message: "page must greater than 0", + }); + return z.NEVER; + } + return num; + }), + rowsPerPage: z + .string() + .optional() + .transform((val, ctx) => { + if (!val) return; + const num = Number(val); + if (Number.isNaN(num)) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be a number", + }); + return z.NEVER; + } + if (num < 0) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be greater than 0", + }); + return z.NEVER; + } + if (num > 100) { + ctx.addIssue({ + code: "custom", + message: "rowsPerPage must be less than or equal to 100", + }); + return z.NEVER; + } + return num; + }), + range: z.string().optional(), + monitorId: z + .string() + .refine((val) => mongoose.Types.ObjectId.isValid(val), { + message: "Invalid MongoDB ObjectId", + }) + .optional(), +}); + +export const patchIncidentsBodySchema = z.object({ + resolutionNote: z.string().max(200).optional(), +}); diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js deleted file mode 100755 index bc88d4567..000000000 --- a/server/src/validation/joi.js +++ /dev/null @@ -1,781 +0,0 @@ -import joi from "joi"; -import { ROLES, VALID_ROLES } from "../utils/roleUtils.js"; - -//**************************************** -// Custom Validators -//**************************************** - -const roleValidatior = (role) => (value, helpers) => { - const hasRole = role.some((role) => value.includes(role)); - if (!hasRole) { - throw new joi.ValidationError(`You do not have the required authorization. Required roles: ${role.join(", ")}`); - } - return value; -}; - -//**************************************** -// Auth -//**************************************** - -const passwordPattern = - /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]+$/; - -const loginValidation = joi.object({ - email: joi.string().email().required().lowercase(), - password: joi.string().required(), -}); -const nameValidation = joi - .string() - .trim() - .max(50) - .pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s'\-().]+$/u) - .messages({ - "string.empty": "Name is required", - "string.max": "Name must be less than 50 characters", - "string.pattern.base": - "Names must contain at least 1 letter and may only include letters, currency symbols, spaces, apostrophes, hyphens (-), periods (.), and parentheses ().", - }); - -const registrationBodyValidation = joi.object({ - firstName: nameValidation.required(), - lastName: nameValidation.required(), - email: joi - .string() - .email() - .required() - .custom((value, helpers) => { - const lowercasedValue = value.toLowerCase(); - if (value !== lowercasedValue) { - return helpers.message("Email must be in lowercase"); - } - return lowercasedValue; - }), - password: joi.string().min(8).required().pattern(passwordPattern), - profileImage: joi.any(), - role: joi.array().items(joi.string().valid("superadmin", "admin", "user", "demo")), - teamId: joi.string().allow("").required(), - inviteToken: joi.string().allow("").required(), -}); - -const editUserBodyValidation = joi.object({ - firstName: nameValidation.optional(), - lastName: nameValidation.optional(), - profileImage: joi.any(), - newPassword: joi.string().min(8).pattern(passwordPattern), - password: joi.string().min(8).pattern(passwordPattern), - deleteProfileImage: joi.boolean(), -}); - -const recoveryValidation = joi.object({ - email: joi - .string() - .email({ tlds: { allow: false } }) - .required(), -}); - -const recoveryTokenBodyValidation = joi.object({ - recoveryToken: joi.string().required(), -}); - -const newPasswordValidation = joi.object({ - recoveryToken: joi.string().required(), - password: joi.string().min(8).required().pattern(passwordPattern), - confirm: joi.string(), -}); - -const deleteUserParamValidation = joi.object({ - email: joi.string().email().required(), -}); - -const inviteBodyValidation = joi.object({ - email: joi.string().trim().email().required().messages({ - "string.empty": "Email is required", - "string.email": "Must be a valid email address", - }), - role: joi.array().required(), - teamId: joi.string().required(), -}); - -const inviteVerificationBodyValidation = joi.object({ - token: joi.string().required(), -}); - -//**************************************** -// Monitors -//**************************************** - -const getMonitorByIdParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const getMonitorByIdQueryValidation = joi.object({ - status: joi.boolean(), - sortOrder: joi.string().valid("asc", "desc"), - limit: joi.number(), - dateRange: joi.string().valid("hour", "day", "week", "month", "all"), - numToDisplay: joi.number(), - normalize: joi.boolean(), -}); - -const getMonitorsByTeamIdParamValidation = joi.object({}); - -const getMonitorsByTeamIdQueryValidation = joi.object({ - limit: joi.number(), - type: joi - .alternatives() - .try( - joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"), - joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game")) - ), - page: joi.number(), - rowsPerPage: joi.number(), - filter: joi.string(), - field: joi.string(), - order: joi.string().valid("asc", "desc"), -}); - -const getMonitorStatsByIdParamValidation = joi.object({ - monitorId: joi.string().required(), -}); -const getMonitorStatsByIdQueryValidation = joi.object({ - status: joi.string(), - limit: joi.number(), - sortOrder: joi.string().valid("asc", "desc"), - dateRange: joi.string().valid("hour", "day", "week", "month", "all"), - numToDisplay: joi.number(), - normalize: joi.boolean(), -}); - -const getCertificateParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const createMonitorBodyValidation = joi.object({ - _id: joi.string(), - name: joi.string().required(), - description: joi.string().required(), - type: joi.string().required(), - statusWindowSize: joi.number().min(1).max(20).default(5), - statusWindowThreshold: joi.number().min(1).max(100).default(60), - url: joi.string().required(), - ignoreTlsErrors: joi.boolean().default(false), - port: joi.number(), - isActive: joi.boolean(), - interval: joi.number(), - thresholds: joi.object().keys({ - usage_cpu: joi.number(), - usage_memory: joi.number(), - usage_disk: joi.number(), - usage_temperature: joi.number(), - }), - notifications: joi.array().items(joi.string()), - secret: joi.string(), - jsonPath: joi.string().allow(""), - expectedValue: joi.string().allow(""), - matchMethod: joi.string(), - gameId: joi.string().allow(""), - group: joi.string().max(50).trim().allow(null, "").optional(), -}); - -const createMonitorsBodyValidation = joi.array().items( - createMonitorBodyValidation.keys({ - userId: joi.string().required(), - teamId: joi.string().required(), - }) -); - -const editMonitorBodyValidation = joi.object({ - name: joi.string(), - statusWindowSize: joi.number().min(1).max(20).default(5), - statusWindowThreshold: joi.number().min(1).max(100).default(60), - description: joi.string(), - interval: joi.number(), - notifications: joi.array().items(joi.string()), - secret: joi.string(), - ignoreTlsErrors: joi.boolean(), - jsonPath: joi.string().allow(""), - expectedValue: joi.string().allow(""), - matchMethod: joi.string().allow(null, ""), - port: joi.number().min(1).max(65535), - thresholds: joi.object().keys({ - usage_cpu: joi.number(), - usage_memory: joi.number(), - usage_disk: joi.number(), - usage_temperature: joi.number(), - }), - gameId: joi.string(), - group: joi.string().max(50).trim().allow(null, "").optional(), -}); - -const pauseMonitorParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const getMonitorURLByQueryValidation = joi.object({ - monitorURL: joi.string().uri().required(), -}); - -const getHardwareDetailsByIdParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const getHardwareDetailsByIdQueryValidation = joi.object({ - dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"), -}); - -//**************************************** -// Alerts -//**************************************** - -const createAlertParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const createAlertBodyValidation = joi.object({ - checkId: joi.string().required(), - monitorId: joi.string().required(), - userId: joi.string().required(), - status: joi.boolean(), - message: joi.string(), - notifiedStatus: joi.boolean(), - acknowledgeStatus: joi.boolean(), -}); - -const getAlertsByUserIdParamValidation = joi.object({ - userId: joi.string().required(), -}); - -const getAlertsByMonitorIdParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const getAlertByIdParamValidation = joi.object({ - alertId: joi.string().required(), -}); - -const editAlertParamValidation = joi.object({ - alertId: joi.string().required(), -}); - -const editAlertBodyValidation = joi.object({ - status: joi.boolean(), - message: joi.string(), - notifiedStatus: joi.boolean(), - acknowledgeStatus: joi.boolean(), -}); - -const deleteAlertParamValidation = joi.object({ - alertId: joi.string().required(), -}); - -//**************************************** -// Checks -//**************************************** - -const createCheckParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const createCheckBodyValidation = joi.object({ - monitorId: joi.string().required(), - status: joi.boolean().required(), - responseTime: joi.number().required(), - statusCode: joi.number().required(), - message: joi.string().required(), -}); - -const ackCheckBodyValidation = joi.object({ - ack: joi.boolean(), -}); - -const ackAllChecksParamValidation = joi.object({ - monitorId: joi.string().optional(), - path: joi.string().valid("monitor", "team").required(), -}); - -const ackAllChecksBodyValidation = joi.object({ - ack: joi.boolean(), -}); - -const getChecksParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const getChecksQueryValidation = joi.object({ - type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port", "game"), - sortOrder: joi.string().valid("asc", "desc"), - limit: joi.number(), - dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"), - filter: joi.string().valid("all", "down", "resolve"), - ack: joi.boolean(), - page: joi.number(), - rowsPerPage: joi.number(), - status: joi.boolean(), -}); - -const getTeamChecksQueryValidation = joi.object({ - sortOrder: joi.string().valid("asc", "desc"), - limit: joi.number(), - dateRange: joi.string().valid("hour", "day", "week", "month", "all"), - filter: joi.string().valid("all", "down", "resolve"), - ack: joi.boolean(), - page: joi.number(), - rowsPerPage: joi.number(), -}); - -const deleteChecksParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const deleteChecksByTeamIdParamValidation = joi.object({}); - -const updateChecksTTLBodyValidation = joi.object({ - ttl: joi.number().required(), -}); - -//**************************************** -// PageSpeedCheckValidation -//**************************************** - -const getPageSpeedCheckParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -//Validation schema for the monitorId parameter -const createPageSpeedCheckParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -//Validation schema for the monitorId body -const createPageSpeedCheckBodyValidation = joi.object({ - url: joi.string().required(), -}); - -const deletePageSpeedCheckParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -//**************************************** -// MaintenanceWindowValidation -//**************************************** - -const createMaintenanceWindowBodyValidation = joi.object({ - monitors: joi.array().items(joi.string()).required(), - name: joi.string().required(), - active: joi.boolean(), - start: joi.date().required(), - end: joi.date().required(), - repeat: joi.number().required(), - expiry: joi.date(), -}); - -const getMaintenanceWindowByIdParamValidation = joi.object({ - id: joi.string().required(), -}); - -const getMaintenanceWindowsByTeamIdQueryValidation = joi.object({ - active: joi.boolean(), - page: joi.number(), - rowsPerPage: joi.number(), - field: joi.string(), - order: joi.string().valid("asc", "desc"), -}); - -const getMaintenanceWindowsByMonitorIdParamValidation = joi.object({ - monitorId: joi.string().required(), -}); - -const deleteMaintenanceWindowByIdParamValidation = joi.object({ - id: joi.string().required(), -}); - -const editMaintenanceWindowByIdParamValidation = joi.object({ - id: joi.string().required(), -}); - -const editMaintenanceByIdWindowBodyValidation = joi.object({ - active: joi.boolean(), - name: joi.string(), - repeat: joi.number(), - start: joi.date(), - end: joi.date(), - expiry: joi.date(), - monitors: joi.array(), -}); - -//**************************************** -// SettingsValidation -//**************************************** -const updateAppSettingsBodyValidation = joi.object({ - checkTTL: joi.number().allow(""), - pagespeedApiKey: joi.string().allow(""), - language: joi.string().allow(""), - timezone: joi.string().allow(""), - // showURL: joi.bool().required(), - systemEmailHost: joi.string().allow(""), - systemEmailPort: joi.number().allow(""), - systemEmailAddress: joi.string().allow(""), - systemEmailPassword: joi.string().allow(""), - systemEmailUser: joi.string().allow(""), - systemEmailConnectionHost: joi.string().allow("").optional(), - systemEmailTLSServername: joi.string().allow("").optional(), - systemEmailSecure: joi.boolean(), - systemEmailPool: joi.boolean(), - systemEmailIgnoreTLS: joi.boolean(), - systemEmailRequireTLS: joi.boolean(), - systemEmailRejectUnauthorized: joi.boolean(), - - globalThresholds: joi - .object({ - cpu: joi.number().min(1).max(100).allow("").optional(), - memory: joi.number().min(1).max(100).allow("").optional(), - disk: joi.number().min(1).max(100).allow("").optional(), - temperature: joi.number().min(1).max(150).allow("").optional(), - }) - .optional(), -}); - -//**************************************** -// Status Page Validation -//**************************************** - -const getStatusPageParamValidation = joi.object({ - url: joi.string().required(), -}); - -const getStatusPageQueryValidation = joi.object({ - type: joi.string().valid("uptime").required(), - timeFrame: joi.number().optional(), -}); - -const createStatusPageBodyValidation = joi.object({ - type: joi.string().valid("uptime").required(), - companyName: joi.string().required(), - url: joi - .string() - .pattern(/^[a-zA-Z0-9_-]+$/) // Only allow alphanumeric, underscore, and hyphen - .required() - .messages({ - "string.pattern.base": "URL can only contain letters, numbers, underscores, and hyphens", - }), - timezone: joi.string().optional(), - color: joi.string().optional(), - monitors: joi - .array() - .items(joi.string().pattern(/^[0-9a-fA-F]{24}$/)) - .required() - .messages({ - "string.pattern.base": "Must be a valid monitor ID", - "array.base": "Monitors must be an array", - "array.empty": "At least one monitor is required", - "any.required": "Monitors are required", - }), - subMonitors: joi - .array() - .items(joi.string().pattern(/^[0-9a-fA-F]{24}$/)) - .optional(), - deleteSubmonitors: joi.boolean().optional(), - isPublished: joi.boolean(), - showCharts: joi.boolean().optional(), - showUptimePercentage: joi.boolean(), - showAdminLoginLink: joi.boolean().optional(), -}); - -const imageValidation = joi - .object({ - fieldname: joi.string().required(), - originalname: joi.string().required(), - encoding: joi.string().required(), - mimetype: joi.string().valid("image/jpeg", "image/png", "image/jpg").required().messages({ - "string.valid": "File must be a valid image (jpeg, jpg, or png)", - }), - size: joi.number().max(3145728).required().messages({ - "number.max": "File size must be less than 3MB", - }), - buffer: joi.binary().required(), - destination: joi.string(), - filename: joi.string(), - path: joi.string(), - }) - .messages({ - "any.required": "Image file is required", - }); - -const webhookConfigValidation = joi - .object({ - webhookUrl: joi - .string() - .uri() - .when("$platform", { - switch: [ - { - is: "telegram", - then: joi.optional(), - }, - { - is: "discord", - then: joi.required().messages({ - "string.empty": "Discord webhook URL is required", - "string.uri": "Discord webhook URL must be a valid URL", - "any.required": "Discord webhook URL is required", - }), - }, - { - is: "slack", - then: joi.required().messages({ - "string.empty": "Slack webhook URL is required", - "string.uri": "Slack webhook URL must be a valid URL", - "any.required": "Slack webhook URL is required", - }), - }, - ], - }), - botToken: joi.string().when("$platform", { - is: "telegram", - then: joi.required().messages({ - "string.empty": "Telegram bot token is required", - "any.required": "Telegram bot token is required", - }), - otherwise: joi.optional(), - }), - chatId: joi.string().when("$platform", { - is: "telegram", - then: joi.required().messages({ - "string.empty": "Telegram chat ID is required", - "any.required": "Telegram chat ID is required", - }), - otherwise: joi.optional(), - }), - }) - .required(); - -const triggerNotificationBodyValidation = joi.object({ - monitorId: joi.string().required().messages({ - "string.empty": "Monitor ID is required", - "any.required": "Monitor ID is required", - }), - type: joi.string().valid("webhook").required().messages({ - "string.empty": "Notification type is required", - "any.required": "Notification type is required", - "any.only": "Notification type must be webhook", - }), - platform: joi.string().valid("telegram", "discord", "slack").required().messages({ - "string.empty": "Platform type is required", - "any.required": "Platform type is required", - "any.only": "Platform must be telegram, discord, or slack", - }), - config: webhookConfigValidation.required().messages({ - "any.required": "Webhook configuration is required", - }), -}); - -const createNotificationBodyValidation = joi.object({ - notificationName: joi.string().required().messages({ - "string.empty": "Notification name is required", - "any.required": "Notification name is required", - }), - - type: joi.string().valid("email", "webhook", "slack", "discord", "pager_duty", "matrix").required().messages({ - "string.empty": "Notification type is required", - "any.required": "Notification type is required", - "any.only": "Notification type must be email, webhook, slack, discord, pager_duty, or matrix", - }), - - address: joi.when("type", { - switch: [ - { - is: "email", - then: joi.string().email().required().messages({ - "string.empty": "E-mail address cannot be empty", - "any.required": "E-mail address is required", - "string.email": "Please enter a valid e-mail address", - }), - }, - { - is: "pager_duty", - then: joi.string().required().messages({ - "string.empty": "PagerDuty integration key cannot be empty", - "any.required": "PagerDuty integration key is required", - }), - }, - { - is: joi.string().valid("webhook", "slack", "discord"), - then: joi.string().uri().required().messages({ - "string.empty": "Webhook URL cannot be empty", - "any.required": "Webhook URL is required", - "string.uri": "Please enter a valid Webhook URL", - }), - }, - { - is: "matrix", - then: joi.string().allow("").optional(), - }, - ], - }), - - homeserverUrl: joi.when("type", { - is: "matrix", - then: joi.string().uri().required().messages({ - "string.empty": "Homeserver URL cannot be empty", - "any.required": "Homeserver URL is required", - "string.uri": "Please enter a valid Homeserver URL", - }), - otherwise: joi.string().allow("").optional(), - }), - - roomId: joi.when("type", { - is: "matrix", - then: joi.string().required().messages({ - "string.empty": "Room ID cannot be empty", - "any.required": "Room ID is required", - }), - otherwise: joi.string().allow("").optional(), - }), - - accessToken: joi.when("type", { - is: "matrix", - then: joi.string().required().messages({ - "string.empty": "Access Token cannot be empty", - "any.required": "Access Token is required", - }), - otherwise: joi.string().allow("").optional(), - }), -}); - -//**************************************** -// Announcetment Page Validation -//**************************************** - -const createAnnouncementValidation = joi.object({ - title: joi.string().required().messages({ - "string.empty": "Title cannot be empty", - "any.required": "Title is required", - }), - message: joi.string().required().messages({ - "string.empty": "Message cannot be empty", - "any.required": "Message is required", - }), - userId: joi.string().required(), -}); - -const sendTestEmailBodyValidation = joi.object({ - to: joi.string().required(), - systemEmailHost: joi.string(), - systemEmailPort: joi.number(), - systemEmailSecure: joi.boolean(), - systemEmailPool: joi.boolean(), - systemEmailAddress: joi.string(), - systemEmailPassword: joi.string(), - systemEmailUser: joi.string(), - systemEmailConnectionHost: joi.string().allow("").optional(), - systemEmailIgnoreTLS: joi.boolean(), - systemEmailRequireTLS: joi.boolean(), - systemEmailRejectUnauthorized: joi.boolean(), - systemEmailTLSServername: joi.string().allow("").optional(), -}); - -const getUserByIdParamValidation = joi.object({ - userId: joi.string().required(), -}); - -const editUserByIdParamValidation = joi.object({ - userId: joi.string().required(), -}); - -const editUserByIdBodyValidation = joi.object({ - firstName: nameValidation.required(), - lastName: nameValidation.required(), - email: joi.string().email().required(), - role: joi - .array() - .items(joi.string().valid(...VALID_ROLES)) - .min(1) - .required(), -}); - -const editSuperadminUserByIdBodyValidation = joi.object({ - firstName: nameValidation.required(), - lastName: nameValidation.required(), - email: joi.string().email().required(), - role: joi - .array() - .items(joi.string().valid(...VALID_ROLES, ROLES.SUPERADMIN)) - .min(1) - .required(), -}); - -const editUserPasswordByIdBodyValidation = joi.object({ - password: joi.string().min(8).required().pattern(passwordPattern), -}); - -export { - roleValidatior, - loginValidation, - registrationBodyValidation, - recoveryValidation, - recoveryTokenBodyValidation, - newPasswordValidation, - inviteBodyValidation, - inviteVerificationBodyValidation, - createMonitorBodyValidation, - createMonitorsBodyValidation, - getMonitorByIdParamValidation, - getMonitorByIdQueryValidation, - getMonitorsByTeamIdParamValidation, - getMonitorsByTeamIdQueryValidation, - getMonitorStatsByIdParamValidation, - getMonitorStatsByIdQueryValidation, - getHardwareDetailsByIdParamValidation, - getHardwareDetailsByIdQueryValidation, - getCertificateParamValidation, - editMonitorBodyValidation, - pauseMonitorParamValidation, - getMonitorURLByQueryValidation, - editUserBodyValidation, - createAlertParamValidation, - createAlertBodyValidation, - getAlertsByUserIdParamValidation, - getAlertsByMonitorIdParamValidation, - getAlertByIdParamValidation, - editAlertParamValidation, - editAlertBodyValidation, - deleteAlertParamValidation, - createCheckParamValidation, - createCheckBodyValidation, - getChecksParamValidation, - getChecksQueryValidation, - getTeamChecksQueryValidation, - ackCheckBodyValidation, - ackAllChecksParamValidation, - ackAllChecksBodyValidation, - deleteChecksParamValidation, - deleteChecksByTeamIdParamValidation, - updateChecksTTLBodyValidation, - deleteUserParamValidation, - getPageSpeedCheckParamValidation, - createPageSpeedCheckParamValidation, - deletePageSpeedCheckParamValidation, - createPageSpeedCheckBodyValidation, - createMaintenanceWindowBodyValidation, - getMaintenanceWindowByIdParamValidation, - getMaintenanceWindowsByTeamIdQueryValidation, - getMaintenanceWindowsByMonitorIdParamValidation, - deleteMaintenanceWindowByIdParamValidation, - editMaintenanceWindowByIdParamValidation, - editMaintenanceByIdWindowBodyValidation, - updateAppSettingsBodyValidation, - createStatusPageBodyValidation, - getStatusPageParamValidation, - getStatusPageQueryValidation, - imageValidation, - triggerNotificationBodyValidation, - createNotificationBodyValidation, - webhookConfigValidation, - createAnnouncementValidation, - sendTestEmailBodyValidation, - getUserByIdParamValidation, - editUserByIdParamValidation, - editUserByIdBodyValidation, - editSuperadminUserByIdBodyValidation, - editUserPasswordByIdBodyValidation, -}; diff --git a/server/tsconfig.json b/server/tsconfig.json index 260ccb964..83d3c70cb 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,26 +1,15 @@ { - "compilerOptions": { - "rootDir": "./src", - "outDir": "./dist", - "module": "nodenext", - "target": "esnext", - "types": ["node"], - "sourceMap": true, - "declaration": false, - "declarationMap": false, - "noUncheckedIndexedAccess": true, - "exactOptionalPropertyTypes": true, - "strict": true, - "isolatedModules": true, - "noUncheckedSideEffectImports": true, - "moduleDetection": "force", - "skipLibCheck": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "forceConsistentCasingInFileNames": true, - "allowJs": true, - "checkJs": false - }, - "exclude": ["node_modules", "dist/", "**/*.config.js"] + "compilerOptions": { + "rootDir": "./src", + "paths": { + "@/*": ["./src/*"] // allows "@/db" -> "./src/db" + }, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "skipLibCheck": true, + "noUncheckedIndexedAccess": true + } }