diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml new file mode 100644 index 000000000..4d2de7041 --- /dev/null +++ b/.github/workflows/check-build.yml @@ -0,0 +1,40 @@ +name: Build Check (Client & Server) +on: + pull_request: + workflow_dispatch: + +jobs: + build-client: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install client dependencies + working-directory: client + run: npm install + + - name: Check client build + working-directory: client + run: npm run build + build-server: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install server dependencies + working-directory: server + run: npm install + + - name: Check server build + working-directory: server + run: npm run build diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index 27b7e8690..84be4e483 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -88,3 +88,4 @@ jobs: docker compose down docker compose pull docker compose up -d + docker system prune -af diff --git a/.gitignore b/.gitignore index 600d2d33b..74015c392 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.vscode \ No newline at end of file +.idea +.vscode +.VSCodeCounter +*.sh +mongo diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1535e84cf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature). + +## Development Commands + +### Client (React/Vite) +```bash +cd client +npm install +npm run dev # Start dev server at http://localhost:5173 +npm run build # TypeScript check + production build +npm run lint # ESLint (strict, max-warnings 0) +npm run format # Prettier formatting +npm run format-check # Check formatting +``` + +### Server (Node.js/Express) +```bash +cd server +npm install +npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345 +npm run build # TypeScript compile + path alias resolution +npm run test # Run Mocha tests with c8 coverage +npm run lint # ESLint v9 +npm run lint-fix # Auto-fix lint issues +npm run format # Prettier formatting +``` + +### Docker Development +```bash +cd docker/dev +./build_images.sh +docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0 +``` + +## Environment Setup + +### Server `.env` (minimum required) +```env +CLIENT_HOST="http://localhost:5173" +JWT_SECRET="my_secret_key_change_this" +DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db" +TOKEN_TTL="99d" +ORIGIN="localhost" +LOG_LEVEL="debug" +``` + +### Client `.env` +```env +VITE_APP_API_BASE_URL="http://localhost:52345/api/v1" +VITE_APP_LOG_LEVEL="debug" +``` + +## Architecture + +### Monorepo Structure +- `/client` - React 18 + TypeScript + Vite + MUI frontend +- `/server` - Node.js 20+ + Express + TypeScript backend +- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono) + +### Backend Layers +``` +server/src/ +├── controllers/ # Route handlers (authController, monitorController, etc.) +├── service/ # Business logic +│ ├── business/ # Core monitoring logic +│ ├── infrastructure/ # Server/system utilities +│ └── system/ # App-level settings +├── db/ +│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.) +│ ├── migration/ # Database migrations (run on startup) +│ └── modules/ # Database-specific modules +├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler +├── routes/v1/ # API route definitions +├── validation/ # Joi input validation schemas +└── repositories/ # Data access layer +``` + +### Frontend Structure +``` +client/src/ +├── Components/ # Reusable UI components +├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.) +├── Features/ # Redux slices (Auth, UI) +├── Hooks/ # Custom React hooks +├── Utils/ # Utilities (NetworkService.js is main API client) +├── Validation/ # Input validation +└── locales/ # i18n translations +``` + +### API +- Base URL: `/api/v1` +- Documentation: `http://localhost:52345/api-docs` (Swagger UI) +- OpenAPI spec: `/server/openapi.json` + +### Key Technologies +- **State Management**: Redux Toolkit + Redux-Persist +- **Data Fetching**: SWR + Axios +- **Database**: MongoDB with Mongoose ODM +- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling) +- **i18n**: i18next + react-i18next (translations via PoEditor) + +## Code Conventions + +### Internationalization +All user-facing strings must use the translation function: +```javascript +t('your.key') // Never hardcode UI strings +``` + +### Branching +- Always branch from `develop` (not master) +- Use descriptive names: `feat/add-alerts`, `fix/login-error` +- PRs target `develop` branch + +### Formatting +- **Client**: Prettier with `printWidth: 90`, tabs, double quotes +- **Server**: Prettier with `printWidth: 150`, tabs, double quotes +- Both use ESLint with strict settings + +### Testing +Server tests use Mocha + Chai + Sinon: +```bash +npm test # Run all tests with coverage +npm test -- --grep "pattern" # Run specific tests +``` +Test files: `server/tests/**/*.test.js` + +## Database Models + +Key Mongoose models in `/server/src/db/models/`: +- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.) +- **Check** - Individual monitoring check results +- **Incident** - Downtime incidents +- **User** - User accounts +- **Team** - Team/workspace management +- **StatusPage** - Public status pages +- **Notification** - Alert configuration (email, Discord, Slack, webhooks) +- **MaintenanceWindow** - Scheduled maintenance periods +- **AppSettings** - Global application settings diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 702436b38..656523f88 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,10 +6,35 @@ We truly appreciate all kinds of contributions — code, ideas, translations or Before you start, please take a moment to read the relevant section. It helps us review and accept contributions faster, and makes the whole process smoother for everyone. 💚 -PS: **We work closely with contributors on our [Discord channel](https://discord.com/invite/NAb6H3UTjK)**. You’ll find community members, core maintainers, and first-timers helping each other out. +PS: **We work closely with contributors on our [Discord channel](https://discord.com/invite/NAb6H3UTjK)**. You'll find community members, core maintainers, and first-timers helping each other out. --- + +## 🚀 Quick Setup Checklist + +Before you dive in, make sure you have these installed: + +```bash +# Check Node.js (v16+ required) +node --version + +# Check npm +npm --version + +# Check Docker +docker --version + +# Check Git +git --version +``` + +**New to contributing?** Start here: +1. Pick a [`good-first-issue`](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) +2. Comment that you'd like to work on it +3. Follow the [setup guide](#set-up-checkmate-locally) below +4. Join our [Discord](https://discord.com/invite/NAb6H3UTjK) if you get stuck + ## Table of contents - [How do I...?](#how-do-i) @@ -32,12 +57,12 @@ PS: **We work closely with contributors on our [Discord channel](https://discord ### Get help or ask a question? -Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) — we’re friendly and happy to help. [Our core contributors](https://github.com/bluewave-labs/checkmate?tab=readme-ov-file#-contributing) are active and ready to support you. You can also use [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) section to ask your questions. +Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) — we're friendly and happy to help. [Our core contributors](https://github.com/bluewave-labs/checkmate?tab=readme-ov-file#-contributing) are active and ready to support you. You can also use [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) section to ask your questions. ### Report a bug? 1. Search [existing issues](https://github.com/bluewave-labs/checkmate/issues). -2. If it’s not listed, open a **new issue**. +2. If it's not listed, open a **new issue**. 3. Include as much detail as possible: what happened, what you expected, and steps to reproduce. Logs and screenshots help. ### Suggest a new feature? @@ -48,19 +73,175 @@ Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) — ### Set up Checkmate locally? -Frontend & backend: +#### Prerequisites + +- Node.js (with npm) +- Docker +- Git + +#### Step 1: Clone the Repository + +```bash +git clone https://github.com/bluewave-labs/Checkmate.git +cd Checkmate +``` + +#### Step 2: Set Up Docker Containers (MongoDB) + +Navigate to the Docker dev directory: + +```bash +cd docker/dev +``` + +Build the Docker images: + +```bash +./build_images.sh +``` + +Run MongoDB container: + +```bash +docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0 +``` + +Navigate back to the root directory: + +```bash +cd ../.. +``` + +#### Step 3: Set Up the Backend (Server) + +Navigate to the server directory: + +```bash +cd server +``` + +Install dependencies: + ```bash npm install +``` + +Create a `.env` file in the `server` directory with the following minimum required configuration: + +```env +CLIENT_HOST="http://localhost:5173" +JWT_SECRET="my_secret_key_change_this" +DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db" +TOKEN_TTL="99d" +ORIGIN="localhost" +LOG_LEVEL="debug" +``` + +**Environment Variables Explained:** + +- `CLIENT_HOST`: Frontend URL (default: http://localhost:5173) +- `JWT_SECRET`: Secret key for JWT tokens (change to something secure) +- `DB_CONNECTION_STRING`: MongoDB connection URL +- `ORIGIN`: Origin for CORS purposes +- `TOKEN_TTL`: Token time to live (in vercel/ms format) +- `LOG_LEVEL`: Debug level (debug, info, warn, error) + +Start the backend server: + +```bash npm run dev ``` -By default, the frontend expects the backend on `http://localhost:3001`. Update configs if needed. +The server will run at `http://localhost:52345`. + +#### Step 4: Set Up the Frontend (Client) + +Open a new terminal window and navigate to the client directory from the root: + +```bash +cd client +``` + +Install dependencies: + +```bash +npm install +``` + +Create a `.env` file in the `client` directory: + +```env +VITE_APP_API_BASE_URL="http://localhost:52345/api/v1" +VITE_APP_LOG_LEVEL="debug" +``` + +**Environment Variables Explained:** + +- `VITE_APP_API_BASE_URL`: Backend API URL +- `VITE_APP_LOG_LEVEL`: Log level (none, error, warn, debug, info) + +Start the frontend: + +```bash +npm run dev +``` + +The client will run at `http://localhost:5173`. + +#### Step 5: Access the Application + +Open your browser and navigate to: + +- **Frontend**: http://localhost:5173 +- **Backend API**: http://localhost:52345 +- **API Documentation**: http://localhost:52345/api-docs + +#### Managing Docker Containers + +Stop containers: + +```bash +docker stop uptime_database_mongo +``` + +Start containers: + +```bash +docker start uptime_database_mongo +``` + +Remove containers (if needed): + +```bash +docker rm uptime_database_mongo +``` + +#### Troubleshooting + +**Port already in use:** + +- Check if another service is using ports 5173, 52345, 27017, or 6379 +- Stop the conflicting service or change the port in `.env` files + +**MongoDB connection issues:** + +- Verify container is running: `docker ps` +- Check container logs: `docker logs uptime_database_mongo` + +**Module not found errors:** + +- Ensure you ran `npm install` in both `client` and `server` directories + +**Need more help?** + +- Check the [full documentation](https://docs.checkmate.so) +- Ask on [Discord](https://discord.com/invite/NAb6H3UTjK) ### Start contributing code? 1. Pick or open an issue (check `good-first-issue`s first) 2. (optional but highly suggested) Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. -3. Ask to be assigned. If there is alrady someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well. +3. Ask to be assigned. If there is already someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well. 4. Create a branch from `develop`. 5. Write your code. 6. Run and test locally. @@ -75,6 +256,7 @@ Docs live in [checkmate-documentation](https://github.com/bluewave-labs/checkmat ### Help with translations? We use [PoEditor](https://poeditor.com) for translations. You can: + - [Sign up and join your language team](https://poeditor.com/join/project/lRUoGZFCsJ). - Translate UI strings. - Ask questions on Discord in the relevant #translations channel. @@ -84,6 +266,7 @@ Make sure all new UI strings in code use `t('key')`. ### Submit a pull request? Follow the [pull request checklist](#pull-request-checklist). Your PR should: + - Be focused on one issue. - Be tested locally. - Use our linting and translation rules. @@ -123,7 +306,6 @@ If one or more of these are missing, we may ask you to update your pull request ## Branching model - - Code contributions should go to the `develop` branch. - `master` is used for stable releases. - Use descriptive branch names, like `fix/login-error` or `feat/add-alerts`. @@ -137,13 +319,10 @@ If one or more of these are missing, we may ask you to update your pull request ## Thank you -Thanks for making Checkmate better. We mean it. Whether it’s your first pull request or your 50th, we’re excited to build with you. +Thanks for making Checkmate better. We mean it. Whether it's your first pull request or your 50th, we're excited to build with you. - -PS: feel free to introduce yourself on [Discord](https://discord.gg/YOUR-DISCORD-LINK) and say hi. +PS: feel free to introduce yourself on [Discord](https://discord.gg/NAb6H3UTjK) and say hi. -- Checkmate team Also make sure you read the [document about how to make a good pull request](/PULLREQUESTS.md). - - diff --git a/Checkmate.CodeCanvas b/Checkmate.CodeCanvas new file mode 100644 index 000000000..623ea35bb --- /dev/null +++ b/Checkmate.CodeCanvas @@ -0,0 +1,11325 @@ +{ + "drawioXML": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "fileName": "Checkmate.CodeCanvas", + "fileURL": "github", + "diagramTemplateVersion": 0.2, + "filePath": "Checkmate.CodeCanvas", + "repoData": { + "client": { + "path": "client", + "fileName": "client", + "cellName": "client", + "cellId": "0b23ea66-3e37-4643-b3b9-acb87ea02bae", + "visible": true, + "children": [ + "client/src" + ] + }, + "client/src": { + "path": "client/src", + "fileName": "src", + "cellName": "src", + "cellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "visible": true, + "parentCellId": "0b23ea66-3e37-4643-b3b9-acb87ea02bae", + "children": [ + "client/src/Pages", + "client/src/Hooks", + "client/src/Utils", + "client/src/Routes" + ] + }, + "client/src/Hooks": { + "path": "client/src/Hooks", + "fileName": "Hooks", + "cellName": "Hooks", + "cellId": "4295a047-076f-4c07-95dd-b3123b6614f7", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "children": [ + "client/src/Hooks/v1" + ] + }, + "client/src/Hooks/v1": { + "path": "client/src/Hooks/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "visible": true, + "parentCellId": "4295a047-076f-4c07-95dd-b3123b6614f7", + "children": [ + "client/src/Hooks/v1/monitorHooks.js", + "client/src/Hooks/v1/useNotifications.js", + "client/src/Hooks/v1/checkHooks.js", + "client/src/Hooks/v1/settingsHooks.js" + ] + }, + "client/src/Hooks/v1/checkHooks.js": { + "path": "client/src/Hooks/v1/checkHooks.js", + "fileName": "checkHooks.js", + "cellName": "checkHooks.js", + "cellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "children": [ + "client/src/Hooks/v1/checkHooks.js-simstep-8dc4347c-3e9e-40d7-ae6f-54684f2e6956", + "client/src/Hooks/v1/checkHooks.js-simstep-faba07f3-7460-47ae-924d-f92f5b2e821d" + ] + }, + "client/src/Hooks/v1/monitorHooks.js": { + "path": "client/src/Hooks/v1/monitorHooks.js", + "fileName": "monitorHooks.js", + "cellName": "monitorHooks.js", + "cellId": "595cf0e8-2f86-4543-b51c-b2aeac307f4b", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "client/src/Hooks/v1/settingsHooks.js": { + "path": "client/src/Hooks/v1/settingsHooks.js", + "fileName": "settingsHooks.js", + "cellName": "settingsHooks.js", + "cellId": "8ee5d524-e61d-4e39-a729-babf54929def", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "children": [ + "client/src/Hooks/v1/settingsHooks.js-simstep-d7098072-9a83-4369-8176-9646ba34e186", + "client/src/Hooks/v1/settingsHooks.js-simstep-a157bd07-b969-44f8-914c-3d8393c1605a" + ] + }, + "client/src/Hooks/v1/useNotifications.js": { + "path": "client/src/Hooks/v1/useNotifications.js", + "fileName": "useNotifications.js", + "cellName": "useNotifications.js", + "cellId": "cd2428fd-6545-48dd-b685-5261311b7532", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "children": [ + "client/src/Hooks/v1/useNotifications.js-simstep-383c6be8-aa51-4398-9ae3-21035413f73a" + ] + }, + "client/src/Pages": { + "path": "client/src/Pages", + "fileName": "Pages", + "cellName": "Pages", + "cellId": "f0feab85-316a-41b5-a2f1-629220018846", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "children": [ + "client/src/Pages/v1" + ] + }, + "client/src/Pages/v1": { + "path": "client/src/Pages/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "visible": true, + "parentCellId": "f0feab85-316a-41b5-a2f1-629220018846", + "children": [ + "client/src/Pages/v1/Uptime", + "client/src/Pages/v1/Infrastructure", + "client/src/Pages/v1/Notifications", + "client/src/Pages/v1/StatusPage", + "client/src/Pages/v1/PageSpeed", + "client/src/Pages/v1/Maintenance", + "client/src/Pages/v1/Account", + "client/src/Pages/v1/Settings", + "client/src/Pages/v1/Logs" + ] + }, + "client/src/Pages/v1/Account": { + "path": "client/src/Pages/v1/Account", + "fileName": "Account", + "cellName": "Account", + "cellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Account/index.jsx", + "client/src/Pages/v1/Account/components" + ] + }, + "client/src/Pages/v1/Account/components": { + "path": "client/src/Pages/v1/Account/components", + "fileName": "components", + "cellName": "components", + "cellId": "425a4c15-043f-4d05-8832-da00e18f97b9", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "children": [ + "client/src/Pages/v1/Account/components/TeamPanel.jsx" + ] + }, + "client/src/Pages/v1/Account/components/TeamPanel.jsx": { + "path": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "fileName": "TeamPanel.jsx", + "cellName": "TeamPanel.jsx", + "cellId": "1145f351-2257-411d-86f7-30bf8c26ad68", + "visible": true, + "parentCellId": "425a4c15-043f-4d05-8832-da00e18f97b9", + "children": [ + "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-eebaca25-d588-49c4-a35c-9abc82fde580", + "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-700f35e0-b39d-417c-b0a8-0612feade977" + ] + }, + "client/src/Pages/v1/Account/index.jsx": { + "path": "client/src/Pages/v1/Account/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "33960806-1b9e-4d61-a163-e4bebf82d87a", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "children": [ + "client/src/Pages/v1/Account/index.jsx-simstep-2d5a952a-ce62-4948-9309-96b28ad74e59" + ] + }, + "client/src/Pages/v1/Infrastructure": { + "path": "client/src/Pages/v1/Infrastructure", + "fileName": "Infrastructure", + "cellName": "Infrastructure", + "cellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Infrastructure/Create", + "client/src/Pages/v1/Infrastructure/Details" + ] + }, + "client/src/Pages/v1/Infrastructure/Create": { + "path": "client/src/Pages/v1/Infrastructure/Create", + "fileName": "Create", + "cellName": "Create", + "cellId": "74019e8a-3148-4a32-aae2-5afd16f3eb9b", + "visible": true, + "parentCellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "children": [ + "client/src/Pages/v1/Infrastructure/Create/Components" + ] + }, + "client/src/Pages/v1/Infrastructure/Create/Components": { + "path": "client/src/Pages/v1/Infrastructure/Create/Components", + "fileName": "Components", + "cellName": "Components", + "cellId": "ceded1fc-87a9-43c8-a756-746be3d4ad8d", + "visible": true, + "parentCellId": "74019e8a-3148-4a32-aae2-5afd16f3eb9b", + "children": [ + "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx" + ] + }, + "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx": { + "path": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "fileName": "CustomAlertsSection.jsx", + "cellName": "CustomAlertsSection.jsx", + "cellId": "83800be3-2a0e-4419-b379-3f995f4050e7", + "visible": true, + "parentCellId": "ceded1fc-87a9-43c8-a756-746be3d4ad8d", + "children": [ + "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx-simstep-80dd889a-1fc9-4d56-b9cf-5b45bf1110b1" + ] + }, + "client/src/Pages/v1/Infrastructure/Details": { + "path": "client/src/Pages/v1/Infrastructure/Details", + "fileName": "Details", + "cellName": "Details", + "cellId": "591128f9-0994-4e01-a1ab-f23357f97aff", + "visible": true, + "parentCellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "client/src/Pages/v1/Infrastructure/Details/Components" + ] + }, + "client/src/Pages/v1/Infrastructure/Details/Components": { + "path": "client/src/Pages/v1/Infrastructure/Details/Components", + "fileName": "Components", + "cellName": "Components", + "cellId": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449", + "visible": true, + "parentCellId": "591128f9-0994-4e01-a1ab-f23357f97aff", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes" + ] + }, + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes": { + "path": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes", + "fileName": "GaugeBoxes", + "cellName": "GaugeBoxes", + "cellId": "6a7c7720-a399-4d4d-8225-36e0b2f59731", + "visible": true, + "parentCellId": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx" + ] + }, + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx": { + "path": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "8fe3d890-3605-407d-b06c-1d890a354e46", + "visible": true, + "parentCellId": "6a7c7720-a399-4d4d-8225-36e0b2f59731", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx-simstep-6eedce82-b9c8-4022-95e5-3a6e00628105" + ] + }, + "client/src/Pages/v1/Infrastructure/Details/index.jsx": { + "path": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "e292c552-b3df-4bac-bd9a-8f44c260a5aa", + "visible": true, + "parentCellId": "591128f9-0994-4e01-a1ab-f23357f97aff", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/index.jsx-simstep-c27365db-9db6-4cc9-96f0-4f046fd38562" + ] + }, + "client/src/Pages/v1/Logs": { + "path": "client/src/Pages/v1/Logs", + "fileName": "Logs", + "cellName": "Logs", + "cellId": "1716ec41-8b42-4386-9850-55b8a607638e", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Logs/Diagnostics" + ] + }, + "client/src/Pages/v1/Logs/Diagnostics": { + "path": "client/src/Pages/v1/Logs/Diagnostics", + "fileName": "Diagnostics", + "cellName": "Diagnostics", + "cellId": "e0cb598c-e300-4fd3-8554-db7ffd6201f1", + "visible": true, + "parentCellId": "1716ec41-8b42-4386-9850-55b8a607638e", + "children": [ + "client/src/Pages/v1/Logs/Diagnostics/index.jsx" + ] + }, + "client/src/Pages/v1/Logs/Diagnostics/index.jsx": { + "path": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "visible": true, + "parentCellId": "e0cb598c-e300-4fd3-8554-db7ffd6201f1", + "children": [ + "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-a9b24b5f-534b-4baf-a185-bac7c32a1c42", + "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-e6c94f52-a599-4568-a06f-2db18f963817" + ] + }, + "client/src/Pages/v1/Maintenance": { + "path": "client/src/Pages/v1/Maintenance", + "fileName": "Maintenance", + "cellName": "Maintenance", + "cellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance", + "client/src/Pages/v1/Maintenance/index.jsx", + "client/src/Pages/v1/Maintenance/MaintenanceTable" + ] + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance", + "fileName": "CreateMaintenance", + "cellName": "CreateMaintenance", + "cellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks" + ] + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks", + "fileName": "hooks", + "cellName": "hooks", + "cellId": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx" + ] + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "fileName": "useMaintenanceActions.jsx", + "cellName": "useMaintenanceActions.jsx", + "cellId": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4", + "visible": true, + "parentCellId": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx-simstep-5e0d7085-099b-478a-afaa-47ad4dd07043" + ] + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-2401ef7a-8d8e-4056-868a-547ab213daf1", + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc" + ] + }, + "client/src/Pages/v1/Maintenance/MaintenanceTable": { + "path": "client/src/Pages/v1/Maintenance/MaintenanceTable", + "fileName": "MaintenanceTable", + "cellName": "MaintenanceTable", + "cellId": "5ad4d33f-0932-479c-bb57-22ce6405758f", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "children": [ + "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx" + ] + }, + "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx": { + "path": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "e7ec5ca9-6d25-4bf2-9695-639556f7d141", + "visible": true, + "parentCellId": "5ad4d33f-0932-479c-bb57-22ce6405758f", + "children": [ + "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx-simstep-acf97639-e291-4ca6-81a6-d0487397f42a" + ] + }, + "client/src/Pages/v1/Maintenance/index.jsx": { + "path": "client/src/Pages/v1/Maintenance/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "1eae6263-aaa2-4206-b92e-1e67f529c1c9", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "children": [ + "client/src/Pages/v1/Maintenance/index.jsx-simstep-82b5ea14-bb00-4511-aba4-76196029fd81" + ] + }, + "client/src/Pages/v1/Notifications": { + "path": "client/src/Pages/v1/Notifications", + "fileName": "Notifications", + "cellName": "Notifications", + "cellId": "b3575a92-df07-4cf7-810f-8e5e0672d666", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Notifications/create" + ] + }, + "client/src/Pages/v1/Notifications/create": { + "path": "client/src/Pages/v1/Notifications/create", + "fileName": "create", + "cellName": "create", + "cellId": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb", + "visible": true, + "parentCellId": "b3575a92-df07-4cf7-810f-8e5e0672d666", + "children": [ + "client/src/Pages/v1/Notifications/create/index.jsx" + ] + }, + "client/src/Pages/v1/Notifications/create/index.jsx": { + "path": "client/src/Pages/v1/Notifications/create/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2", + "visible": true, + "parentCellId": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb", + "children": [ + "client/src/Pages/v1/Notifications/create/index.jsx-simstep-e924ec15-1bea-4e3b-befb-a8bf30cbf740" + ] + }, + "client/src/Pages/v1/PageSpeed": { + "path": "client/src/Pages/v1/PageSpeed", + "fileName": "PageSpeed", + "cellName": "PageSpeed", + "cellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/PageSpeed/Create", + "client/src/Pages/v1/PageSpeed/Details" + ] + }, + "client/src/Pages/v1/PageSpeed/Create": { + "path": "client/src/Pages/v1/PageSpeed/Create", + "fileName": "Create", + "cellName": "Create", + "cellId": "414c500b-7b68-42ba-8110-e855efbee750", + "visible": true, + "parentCellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "children": [ + "client/src/Pages/v1/PageSpeed/Create/index.jsx" + ] + }, + "client/src/Pages/v1/PageSpeed/Create/index.jsx": { + "path": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "5ffe0baa-054a-4e5b-8b29-1254f07ea975", + "visible": true, + "parentCellId": "414c500b-7b68-42ba-8110-e855efbee750", + "children": [ + "client/src/Pages/v1/PageSpeed/Create/index.jsx-simstep-b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12" + ] + }, + "client/src/Pages/v1/PageSpeed/Details": { + "path": "client/src/Pages/v1/PageSpeed/Details", + "fileName": "Details", + "cellName": "Details", + "cellId": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b", + "visible": true, + "parentCellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "children": [ + "client/src/Pages/v1/PageSpeed/Details/index.jsx" + ] + }, + "client/src/Pages/v1/PageSpeed/Details/index.jsx": { + "path": "client/src/Pages/v1/PageSpeed/Details/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702", + "visible": true, + "parentCellId": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b", + "children": [ + "client/src/Pages/v1/PageSpeed/Details/index.jsx-simstep-0ec0eb06-05eb-4204-b077-9e47eb2cc316" + ] + }, + "client/src/Pages/v1/Settings": { + "path": "client/src/Pages/v1/Settings", + "fileName": "Settings", + "cellName": "Settings", + "cellId": "50f77798-f41b-4f5b-b096-7afc921a0419", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Settings/index.jsx", + "client/src/Pages/v1/Settings/SettingsEmail.jsx" + ] + }, + "client/src/Pages/v1/Settings/SettingsEmail.jsx": { + "path": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "fileName": "SettingsEmail.jsx", + "cellName": "SettingsEmail.jsx", + "cellId": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee", + "visible": true, + "parentCellId": "50f77798-f41b-4f5b-b096-7afc921a0419", + "children": [ + "client/src/Pages/v1/Settings/SettingsEmail.jsx-simstep-ae5c5146-4dbb-4612-9d23-086e57f72474" + ] + }, + "client/src/Pages/v1/Settings/index.jsx": { + "path": "client/src/Pages/v1/Settings/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "d4b34195-7e1e-47a0-9303-603987c81f95", + "visible": true, + "parentCellId": "50f77798-f41b-4f5b-b096-7afc921a0419", + "children": [ + "client/src/Pages/v1/Settings/index.jsx-simstep-64e9f1af-13b7-4361-a2a9-3f45760e316d" + ] + }, + "client/src/Pages/v1/StatusPage": { + "path": "client/src/Pages/v1/StatusPage", + "fileName": "StatusPage", + "cellName": "StatusPage", + "cellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/StatusPage/Create", + "client/src/Pages/v1/StatusPage/Status" + ] + }, + "client/src/Pages/v1/StatusPage/Create": { + "path": "client/src/Pages/v1/StatusPage/Create", + "fileName": "Create", + "cellName": "Create", + "cellId": "08602a28-f073-484e-9403-7eb798126dd4", + "visible": true, + "parentCellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "children": [ + "client/src/Pages/v1/StatusPage/Create/index.jsx", + "client/src/Pages/v1/StatusPage/Create/Hooks" + ] + }, + "client/src/Pages/v1/StatusPage/Create/Hooks": { + "path": "client/src/Pages/v1/StatusPage/Create/Hooks", + "fileName": "Hooks", + "cellName": "Hooks", + "cellId": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4", + "children": [ + "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx" + ] + }, + "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx": { + "path": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "fileName": "useCreateStatusPage.jsx", + "cellName": "useCreateStatusPage.jsx", + "cellId": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a", + "visible": true, + "parentCellId": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9", + "children": [ + "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx-simstep-3c99ae6a-b671-4f47-bed0-ddccd8ff5d35" + ] + }, + "client/src/Pages/v1/StatusPage/Create/index.jsx": { + "path": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4", + "children": [ + "client/src/Pages/v1/StatusPage/Create/index.jsx-simstep-de7b5d1f-b370-42a4-a410-4178f1e2d3a6" + ] + }, + "client/src/Pages/v1/StatusPage/Status": { + "path": "client/src/Pages/v1/StatusPage/Status", + "fileName": "Status", + "cellName": "Status", + "cellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "visible": true, + "parentCellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "children": [ + "client/src/Pages/v1/StatusPage/Status/index.jsx", + "client/src/Pages/v1/StatusPage/Status/Hooks" + ] + }, + "client/src/Pages/v1/StatusPage/Status/Hooks": { + "path": "client/src/Pages/v1/StatusPage/Status/Hooks", + "fileName": "Hooks", + "cellName": "Hooks", + "cellId": "79da0721-d810-4f7f-9cf9-3ea462bf0bff", + "visible": true, + "parentCellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "children": [ + "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx" + ] + }, + "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx": { + "path": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "fileName": "useStatusPageFetch.jsx", + "cellName": "useStatusPageFetch.jsx", + "cellId": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae", + "visible": true, + "parentCellId": "79da0721-d810-4f7f-9cf9-3ea462bf0bff", + "children": [ + "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx-simstep-b57bb249-9066-454e-aa8c-89108fdfcc2e" + ] + }, + "client/src/Pages/v1/StatusPage/Status/index.jsx": { + "path": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3", + "visible": true, + "parentCellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "children": [ + "client/src/Pages/v1/StatusPage/Status/index.jsx-simstep-3d51e811-a659-442e-97e0-00798cd15647" + ] + }, + "client/src/Pages/v1/Uptime": { + "path": "client/src/Pages/v1/Uptime", + "fileName": "Uptime", + "cellName": "Uptime", + "cellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Uptime/Create", + "client/src/Pages/v1/Uptime/Details", + "client/src/Pages/v1/Uptime/BulkImport" + ] + }, + "client/src/Pages/v1/Uptime/BulkImport": { + "path": "client/src/Pages/v1/Uptime/BulkImport", + "fileName": "BulkImport", + "cellName": "BulkImport", + "cellId": "56dae13e-5735-4089-a9a4-e18be315aff7", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "children": [ + "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx" + ] + }, + "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx": { + "path": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "fileName": "Upload.jsx", + "cellName": "Upload.jsx", + "cellId": "cd84476a-1c8b-4781-811f-58fb9944365b", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7", + "children": [ + "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx-simstep-bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb" + ] + }, + "client/src/Pages/v1/Uptime/BulkImport/index.jsx": { + "path": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7", + "children": [ + "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c", + "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-72b3f16d-290d-4a61-8748-2ad90d63ec68" + ] + }, + "client/src/Pages/v1/Uptime/Create": { + "path": "client/src/Pages/v1/Uptime/Create", + "fileName": "Create", + "cellName": "Create", + "cellId": "3dba5acc-cf25-40b7-ad21-9cd715454cd5", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "children": [ + "client/src/Pages/v1/Uptime/Create/index.jsx" + ] + }, + "client/src/Pages/v1/Uptime/Create/index.jsx": { + "path": "client/src/Pages/v1/Uptime/Create/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "visible": true, + "parentCellId": "3dba5acc-cf25-40b7-ad21-9cd715454cd5", + "children": [ + "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-ab5de0b9-c70d-492c-98d9-8ddee66284ad", + "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-e02ddf57-270e-4f71-b4c4-d5183bb7879a" + ] + }, + "client/src/Pages/v1/Uptime/Details": { + "path": "client/src/Pages/v1/Uptime/Details", + "fileName": "Details", + "cellName": "Details", + "cellId": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "children": [ + "client/src/Pages/v1/Uptime/Details/index.jsx", + "client/src/Pages/v1/Uptime/Details/Components" + ] + }, + "client/src/Pages/v1/Uptime/Details/Components": { + "path": "client/src/Pages/v1/Uptime/Details/Components", + "fileName": "Components", + "cellName": "Components", + "cellId": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404", + "visible": true, + "parentCellId": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "children": [ + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable" + ] + }, + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable": { + "path": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable", + "fileName": "ResponseTable", + "cellName": "ResponseTable", + "cellId": "5ac0878f-d238-4763-8464-a85b4885a40f", + "visible": true, + "parentCellId": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404", + "children": [ + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx" + ] + }, + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx": { + "path": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27", + "visible": true, + "parentCellId": "5ac0878f-d238-4763-8464-a85b4885a40f", + "children": [ + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx-simstep-2b1651cd-2e70-4cc6-b2bb-23f94fa73d66" + ] + }, + "client/src/Pages/v1/Uptime/Details/index.jsx": { + "path": "client/src/Pages/v1/Uptime/Details/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "1807d5b9-54be-430b-8997-cada4dd9b521", + "visible": true, + "parentCellId": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "children": [ + "client/src/Pages/v1/Uptime/Details/index.jsx-simstep-35195328-9847-4678-b014-9331553f0d19" + ] + }, + "client/src/Routes": { + "path": "client/src/Routes", + "fileName": "Routes", + "cellName": "Routes", + "cellId": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "children": [ + "client/src/Routes/index.jsx" + ] + }, + "client/src/Routes/index.jsx": { + "path": "client/src/Routes/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "visible": true, + "parentCellId": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc", + "children": [ + "client/src/Routes/index.jsx-simstep-132354d7-df7a-4e03-9d00-16d54f836a1b", + "client/src/Routes/index.jsx-simstep-a29da01e-8c5a-4e6d-8d3c-e49af528e4fa" + ] + }, + "client/src/Utils": { + "path": "client/src/Utils", + "fileName": "Utils", + "cellName": "Utils", + "cellId": "02b07589-ec21-4366-b7d2-9071e8f3bad8", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "children": [ + "client/src/Utils/NetworkService.js" + ] + }, + "client/src/Utils/NetworkService.js": { + "path": "client/src/Utils/NetworkService.js", + "fileName": "NetworkService.js", + "cellName": "NetworkService.js", + "cellId": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "visible": true, + "parentCellId": "02b07589-ec21-4366-b7d2-9071e8f3bad8", + "children": [ + "client/src/Utils/NetworkService.js-simstep-14e9986b-c6ce-475e-97ed-da1fa2b6968d", + "client/src/Utils/NetworkService.js-simstep-2fc087b3-033d-4cf5-81f2-e35b99a14bce" + ] + }, + "server": { + "path": "server", + "fileName": "server", + "cellName": "server", + "cellId": "98af25c7-efc2-43d2-a273-c8da4e1038ad", + "visible": true, + "children": [ + "server/src" + ] + }, + "server/src": { + "path": "server/src", + "fileName": "src", + "cellName": "src", + "cellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "visible": true, + "parentCellId": "98af25c7-efc2-43d2-a273-c8da4e1038ad", + "children": [ + "server/src/routes", + "server/src/controllers", + "server/src/service", + "server/src/db", + "server/src/config", + "server/src/middleware" + ] + }, + "server/src/config": { + "path": "server/src/config", + "fileName": "config", + "cellName": "config", + "cellId": "fe47576c-c186-4950-82a5-b2fd24eb2561", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/config/routes.js" + ] + }, + "server/src/config/routes.js": { + "path": "server/src/config/routes.js", + "fileName": "routes.js", + "cellName": "routes.js", + "cellId": "cb53d612-9b89-47da-911c-165d0382a7d8", + "visible": true, + "parentCellId": "fe47576c-c186-4950-82a5-b2fd24eb2561", + "children": [ + "server/src/config/routes.js-simstep-3e93ef65-8e2c-405b-8dc1-56d8e93937b3" + ] + }, + "server/src/controllers": { + "path": "server/src/controllers", + "fileName": "controllers", + "cellName": "controllers", + "cellId": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/controllers/v1" + ] + }, + "server/src/controllers/v1": { + "path": "server/src/controllers/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "visible": true, + "parentCellId": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1", + "children": [ + "server/src/controllers/v1/monitorController.js", + "server/src/controllers/v1/notificationController.js", + "server/src/controllers/v1/statusPageController.js", + "server/src/controllers/v1/checkController.js", + "server/src/controllers/v1/maintenanceWindowController.js", + "server/src/controllers/v1/inviteController.js", + "server/src/controllers/v1/settingsController.js", + "server/src/controllers/v1/diagnosticController.js" + ] + }, + "server/src/controllers/v1/checkController.js": { + "path": "server/src/controllers/v1/checkController.js", + "fileName": "checkController.js", + "cellName": "checkController.js", + "cellId": "d983e54f-2fbe-496a-932d-a827e1124804", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/checkController.js-simstep-5768fdc5-045c-48ef-b0c9-79769403648f" + ] + }, + "server/src/controllers/v1/diagnosticController.js": { + "path": "server/src/controllers/v1/diagnosticController.js", + "fileName": "diagnosticController.js", + "cellName": "diagnosticController.js", + "cellId": "5882419e-8a92-4edd-a71a-1252983f9af7", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "server/src/controllers/v1/inviteController.js": { + "path": "server/src/controllers/v1/inviteController.js", + "fileName": "inviteController.js", + "cellName": "inviteController.js", + "cellId": "6a186805-9d50-4bc3-9d56-e627a7ec2eb8", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "server/src/controllers/v1/maintenanceWindowController.js": { + "path": "server/src/controllers/v1/maintenanceWindowController.js", + "fileName": "maintenanceWindowController.js", + "cellName": "maintenanceWindowController.js", + "cellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/maintenanceWindowController.js-simstep-95fc1919-339d-4659-98cb-3dc50f662075", + "server/src/controllers/v1/maintenanceWindowController.js-simstep-1251da83-5949-4e24-a1d2-fc74ff785aa9" + ] + }, + "server/src/controllers/v1/monitorController.js": { + "path": "server/src/controllers/v1/monitorController.js", + "fileName": "monitorController.js", + "cellName": "monitorController.js", + "cellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/monitorController.js-simstep-38fd069d-9c44-4078-954b-3a0899e9acf4", + "server/src/controllers/v1/monitorController.js-simstep-a3799570-64bf-481e-9ffe-31dbad8a36e4", + "server/src/controllers/v1/monitorController.js-simstep-f5377571-156f-4c5c-8bab-b6dacdf0fea6", + "server/src/controllers/v1/monitorController.js-simstep-d9db3f40-8add-4551-9323-274348b9800f", + "server/src/controllers/v1/monitorController.js-simstep-256095b8-5e3a-4c20-abd9-be9e6a9d0bad", + "server/src/controllers/v1/monitorController.js-simstep-60abdb27-4078-4678-a010-0f6938d4be48" + ] + }, + "server/src/controllers/v1/notificationController.js": { + "path": "server/src/controllers/v1/notificationController.js", + "fileName": "notificationController.js", + "cellName": "notificationController.js", + "cellId": "8e5941e1-e577-455f-bbf2-83a418ad5908", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "server/src/controllers/v1/settingsController.js": { + "path": "server/src/controllers/v1/settingsController.js", + "fileName": "settingsController.js", + "cellName": "settingsController.js", + "cellId": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/settingsController.js-simstep-441b0c45-5f27-478d-8ca8-58fbd5f460ea", + "server/src/controllers/v1/settingsController.js-simstep-7632e8d2-4086-4169-ae3f-bfbf8d657987" + ] + }, + "server/src/controllers/v1/statusPageController.js": { + "path": "server/src/controllers/v1/statusPageController.js", + "fileName": "statusPageController.js", + "cellName": "statusPageController.js", + "cellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/statusPageController.js-simstep-01694730-8a75-4949-8815-bb4d6e3e2b42", + "server/src/controllers/v1/statusPageController.js-simstep-2a115421-5175-4c8f-8cdd-62f3fe148af5", + "server/src/controllers/v1/statusPageController.js-simstep-5bdbd179-ce8f-4171-a073-af72a99b4468", + "server/src/controllers/v1/statusPageController.js-simstep-83224406-2f1d-4030-9beb-d65c2baf544d" + ] + }, + "server/src/db": { + "path": "server/src/db", + "fileName": "db", + "cellName": "db", + "cellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/db/v1", + "server/src/db/v2" + ] + }, + "server/src/db/v1": { + "path": "server/src/db/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf", + "visible": true, + "parentCellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "children": [ + "server/src/db/v1/modules" + ] + }, + "server/src/db/v1/modules": { + "path": "server/src/db/v1/modules", + "fileName": "modules", + "cellName": "modules", + "cellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "visible": true, + "parentCellId": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf", + "children": [ + "server/src/db/v1/modules/monitorModule.js", + "server/src/db/v1/modules/monitorModuleQueries.js", + "server/src/db/v1/modules/notificationModule.js", + "server/src/db/v1/modules/statusPageModule.js", + "server/src/db/v1/modules/checkModule.js", + "server/src/db/v1/modules/maintenanceWindowModule.js", + "server/src/db/v1/modules/inviteModule.js" + ] + }, + "server/src/db/v1/modules/checkModule.js": { + "path": "server/src/db/v1/modules/checkModule.js", + "fileName": "checkModule.js", + "cellName": "checkModule.js", + "cellId": "860b5eec-a1ce-4e1f-a316-e1586f03af96", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "server/src/db/v1/modules/inviteModule.js": { + "path": "server/src/db/v1/modules/inviteModule.js", + "fileName": "inviteModule.js", + "cellName": "inviteModule.js", + "cellId": "08853f8a-f596-4df0-9190-dad04f54caef", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "server/src/db/v1/modules/maintenanceWindowModule.js": { + "path": "server/src/db/v1/modules/maintenanceWindowModule.js", + "fileName": "maintenanceWindowModule.js", + "cellName": "maintenanceWindowModule.js", + "cellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "children": [ + "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-a3c47fd4-c701-46c2-9aff-94a667b3e368", + "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-9bfbae5a-f487-40ef-86f5-04a1cc3c7662" + ] + }, + "server/src/db/v1/modules/monitorModule.js": { + "path": "server/src/db/v1/modules/monitorModule.js", + "fileName": "monitorModule.js", + "cellName": "monitorModule.js", + "cellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "children": [ + "server/src/db/v1/modules/monitorModule.js-simstep-ca3ad309-323a-418d-a171-c16f56fae23c", + "server/src/db/v1/modules/monitorModule.js-simstep-8502ae1f-f029-446e-9dde-59c2aba87a8a", + "server/src/db/v1/modules/monitorModule.js-simstep-70f18c21-2918-402e-9c35-1bdce8bc82d4" + ] + }, + "server/src/db/v1/modules/monitorModuleQueries.js": { + "path": "server/src/db/v1/modules/monitorModuleQueries.js", + "fileName": "monitorModuleQueries.js", + "cellName": "monitorModuleQueries.js", + "cellId": "a0b22a5a-59e1-47e3-8218-a4ad083b1b71", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "server/src/db/v1/modules/notificationModule.js": { + "path": "server/src/db/v1/modules/notificationModule.js", + "fileName": "notificationModule.js", + "cellName": "notificationModule.js", + "cellId": "62daeac4-9402-448a-972c-cb8cb760671c", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "children": [ + "server/src/db/v1/modules/notificationModule.js-simstep-d29ab4ab-be50-4f17-af6e-2c61cec9d0ab" + ] + }, + "server/src/db/v1/modules/statusPageModule.js": { + "path": "server/src/db/v1/modules/statusPageModule.js", + "fileName": "statusPageModule.js", + "cellName": "statusPageModule.js", + "cellId": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "children": [ + "server/src/db/v1/modules/statusPageModule.js-simstep-b6fa93cd-6fca-4d9e-969a-5400e2437ee8", + "server/src/db/v1/modules/statusPageModule.js-simstep-ceb044e9-6545-4d04-bf79-ec8146eb8ec9" + ] + }, + "server/src/db/v2": { + "path": "server/src/db/v2", + "fileName": "v2", + "cellName": "v2", + "cellId": "e2f99143-54b7-44dc-b566-1c2fb901bd16", + "visible": true, + "parentCellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "children": [ + "server/src/db/v2/models" + ] + }, + "server/src/db/v2/models": { + "path": "server/src/db/v2/models", + "fileName": "models", + "cellName": "models", + "cellId": "b53384d7-b989-4ac7-bd68-1c3621174ef4", + "visible": true, + "parentCellId": "e2f99143-54b7-44dc-b566-1c2fb901bd16", + "children": [ + "server/src/db/v2/models/checks" + ] + }, + "server/src/db/v2/models/checks": { + "path": "server/src/db/v2/models/checks", + "fileName": "checks", + "cellName": "checks", + "cellId": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc", + "visible": true, + "parentCellId": "b53384d7-b989-4ac7-bd68-1c3621174ef4", + "children": [ + "server/src/db/v2/models/checks/Check.ts" + ] + }, + "server/src/db/v2/models/checks/Check.ts": { + "path": "server/src/db/v2/models/checks/Check.ts", + "fileName": "Check.ts", + "cellName": "Check.ts", + "cellId": "1425ec43-f0f4-4c8b-a140-176c079219d6", + "visible": true, + "parentCellId": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc" + }, + "server/src/middleware": { + "path": "server/src/middleware", + "fileName": "middleware", + "cellName": "middleware", + "cellId": "a968db5d-8f6d-4fd7-9df0-8b332b521703", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/middleware/v1" + ] + }, + "server/src/middleware/v1": { + "path": "server/src/middleware/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "09082f73-e215-465f-9206-0a963fb4c7cb", + "visible": true, + "parentCellId": "a968db5d-8f6d-4fd7-9df0-8b332b521703", + "children": [ + "server/src/middleware/v1/isAllowed.js" + ] + }, + "server/src/middleware/v1/isAllowed.js": { + "path": "server/src/middleware/v1/isAllowed.js", + "fileName": "isAllowed.js", + "cellName": "isAllowed.js", + "cellId": "001fa2e4-eadd-4474-b2ec-79d7ea43465f", + "visible": true, + "parentCellId": "09082f73-e215-465f-9206-0a963fb4c7cb", + "children": [ + "server/src/middleware/v1/isAllowed.js-simstep-50dd7316-7d01-43f7-9036-d97d51a9c178" + ] + }, + "server/src/routes": { + "path": "server/src/routes", + "fileName": "routes", + "cellName": "routes", + "cellId": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/routes/v1" + ] + }, + "server/src/routes/v1": { + "path": "server/src/routes/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "visible": true, + "parentCellId": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b", + "children": [ + "server/src/routes/v1/monitorRoute.js", + "server/src/routes/v1/notificationRoute.js", + "server/src/routes/v1/statusPageRoute.js", + "server/src/routes/v1/checkRoute.js", + "server/src/routes/v1/maintenanceWindowRoute.js", + "server/src/routes/v1/inviteRoute.js" + ] + }, + "server/src/routes/v1/checkRoute.js": { + "path": "server/src/routes/v1/checkRoute.js", + "fileName": "checkRoute.js", + "cellName": "checkRoute.js", + "cellId": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "children": [ + "server/src/routes/v1/checkRoute.js-simstep-c5fd192f-ea28-48a0-b3e7-5bc04f521192" + ] + }, + "server/src/routes/v1/inviteRoute.js": { + "path": "server/src/routes/v1/inviteRoute.js", + "fileName": "inviteRoute.js", + "cellName": "inviteRoute.js", + "cellId": "9edafb8a-3661-4a7e-8510-a47f3cbc7564", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "server/src/routes/v1/maintenanceWindowRoute.js": { + "path": "server/src/routes/v1/maintenanceWindowRoute.js", + "fileName": "maintenanceWindowRoute.js", + "cellName": "maintenanceWindowRoute.js", + "cellId": "5f21f109-1967-416f-a649-9b48e78b90f6", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "server/src/routes/v1/monitorRoute.js": { + "path": "server/src/routes/v1/monitorRoute.js", + "fileName": "monitorRoute.js", + "cellName": "monitorRoute.js", + "cellId": "10611636-ec73-4a26-87a5-654ae64a4843", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "children": [ + "server/src/routes/v1/monitorRoute.js-simstep-07bf3d83-047d-4e51-9f96-8c9c64848500", + "server/src/routes/v1/monitorRoute.js-simstep-63d8d81f-f436-4594-a6eb-8c0fd2110bc1" + ] + }, + "server/src/routes/v1/notificationRoute.js": { + "path": "server/src/routes/v1/notificationRoute.js", + "fileName": "notificationRoute.js", + "cellName": "notificationRoute.js", + "cellId": "96250197-23af-4059-9904-fd6751f70ad5", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "children": [ + "server/src/routes/v1/notificationRoute.js-simstep-2f9266c9-4cbc-4b50-a280-cf8b5800d540", + "server/src/routes/v1/notificationRoute.js-simstep-c1b17b64-90e9-48bc-8927-25d7ebfc3036" + ] + }, + "server/src/routes/v1/statusPageRoute.js": { + "path": "server/src/routes/v1/statusPageRoute.js", + "fileName": "statusPageRoute.js", + "cellName": "statusPageRoute.js", + "cellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "children": [ + "server/src/routes/v1/statusPageRoute.js-simstep-84f11567-b3c4-42a2-aa7f-d78c1412099a", + "server/src/routes/v1/statusPageRoute.js-simstep-477c6821-b29b-4b38-8930-ced21b8a75e5" + ] + }, + "server/src/service": { + "path": "server/src/service", + "fileName": "service", + "cellName": "service", + "cellId": "99e9b746-7809-41f4-a433-1de7cc90574b", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/service/v1", + "server/src/service/v2" + ] + }, + "server/src/service/v1": { + "path": "server/src/service/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "visible": true, + "parentCellId": "99e9b746-7809-41f4-a433-1de7cc90574b", + "children": [ + "server/src/service/v1/business", + "server/src/service/v1/infrastructure" + ] + }, + "server/src/service/v1/business": { + "path": "server/src/service/v1/business", + "fileName": "business", + "cellName": "business", + "cellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "visible": true, + "parentCellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "children": [ + "server/src/service/v1/business/monitorService.js", + "server/src/service/v1/business/checkService.js", + "server/src/service/v1/business/maintenanceWindowService.js", + "server/src/service/v1/business/inviteService.js", + "server/src/service/v1/business/diagnosticService.js" + ] + }, + "server/src/service/v1/business/checkService.js": { + "path": "server/src/service/v1/business/checkService.js", + "fileName": "checkService.js", + "cellName": "checkService.js", + "cellId": "f90db867-e1e7-4554-b446-fa78fc6a8578", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/checkService.js-simstep-83fea05d-8306-4a73-a43f-882cdd14f50f" + ] + }, + "server/src/service/v1/business/diagnosticService.js": { + "path": "server/src/service/v1/business/diagnosticService.js", + "fileName": "diagnosticService.js", + "cellName": "diagnosticService.js", + "cellId": "643b26c1-3bfb-4c41-8179-96dfc237d99e", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/diagnosticService.js-simstep-8fb704ca-67d9-437c-90d8-01302634d568" + ] + }, + "server/src/service/v1/business/inviteService.js": { + "path": "server/src/service/v1/business/inviteService.js", + "fileName": "inviteService.js", + "cellName": "inviteService.js", + "cellId": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/inviteService.js-simstep-55fe9963-30d2-47f8-859f-b485a77064c4", + "server/src/service/v1/business/inviteService.js-simstep-7d1f86f2-c8d5-48fb-a8c1-b337dc56271f" + ] + }, + "server/src/service/v1/business/maintenanceWindowService.js": { + "path": "server/src/service/v1/business/maintenanceWindowService.js", + "fileName": "maintenanceWindowService.js", + "cellName": "maintenanceWindowService.js", + "cellId": "26b8daff-aa48-4baa-9d6c-05f771bf0990", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/maintenanceWindowService.js-simstep-6e2cf739-1514-4c37-9874-10b68a47010e" + ] + }, + "server/src/service/v1/business/monitorService.js": { + "path": "server/src/service/v1/business/monitorService.js", + "fileName": "monitorService.js", + "cellName": "monitorService.js", + "cellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/monitorService.js-simstep-2c7c3082-e4b6-4525-860f-2ffcd9e86db1", + "server/src/service/v1/business/monitorService.js-simstep-66fbf913-e662-4bd9-ada6-077be9e37f07", + "server/src/service/v1/business/monitorService.js-simstep-85cd1d5d-7ffb-49c5-aacb-907048003fc5" + ] + }, + "server/src/service/v1/infrastructure": { + "path": "server/src/service/v1/infrastructure", + "fileName": "infrastructure", + "cellName": "infrastructure", + "cellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "visible": true, + "parentCellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "children": [ + "server/src/service/v1/infrastructure/SuperSimpleQueue", + "server/src/service/v1/infrastructure/networkService.js", + "server/src/service/v1/infrastructure/statusService.js", + "server/src/service/v1/infrastructure/notificationService.js", + "server/src/service/v1/infrastructure/notificationUtils.js" + ] + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue", + "fileName": "SuperSimpleQueue", + "cellName": "SuperSimpleQueue", + "cellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js" + ] + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "fileName": "SuperSimpleQueue.js", + "cellName": "SuperSimpleQueue.js", + "cellId": "d9c0127e-31aa-4509-b5c9-f7017d10d69a", + "visible": true, + "parentCellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "children": [ + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js-simstep-2702bfca-7611-4562-8d3e-a05af8257871" + ] + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "SuperSimpleQueueHelper.js", + "cellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "visible": true, + "parentCellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "children": [ + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e22a708a-a348-4775-8ee3-f32a35446e24", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-a47affaf-b952-46d6-9709-6120a77690e1", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-2127e749-80b4-499a-ae14-8dc2dcc9db9b", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-6ba6b9d1-5fc3-454e-8744-2022b5192ed9", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e59fe64e-f06b-4855-b631-923bb5a68540" + ] + }, + "server/src/service/v1/infrastructure/networkService.js": { + "path": "server/src/service/v1/infrastructure/networkService.js", + "fileName": "networkService.js", + "cellName": "networkService.js", + "cellId": "00406786-9663-440a-96b6-919d544732c0", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/networkService.js-simstep-84f82fea-e306-4e0b-8222-e9705803d51a", + "server/src/service/v1/infrastructure/networkService.js-simstep-ed6e7faf-69dd-4d0b-bda8-137739ed288b" + ] + }, + "server/src/service/v1/infrastructure/notificationService.js": { + "path": "server/src/service/v1/infrastructure/notificationService.js", + "fileName": "notificationService.js", + "cellName": "notificationService.js", + "cellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/notificationService.js-simstep-1a260a75-cf50-4f7a-90b0-c280b512a5cb", + "server/src/service/v1/infrastructure/notificationService.js-simstep-f29f9be1-a0cd-43a3-b259-e5105a25ce41" + ] + }, + "server/src/service/v1/infrastructure/notificationUtils.js": { + "path": "server/src/service/v1/infrastructure/notificationUtils.js", + "fileName": "notificationUtils.js", + "cellName": "notificationUtils.js", + "cellId": "4940977f-933d-4bbc-b19e-749647b44d25", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/notificationUtils.js-simstep-07fe8489-1d94-4afb-83f8-7cebb0dff49a" + ] + }, + "server/src/service/v1/infrastructure/statusService.js": { + "path": "server/src/service/v1/infrastructure/statusService.js", + "fileName": "statusService.js", + "cellName": "statusService.js", + "cellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/statusService.js-simstep-c66a04c6-113e-4391-ab71-93fc612279ef", + "server/src/service/v1/infrastructure/statusService.js-simstep-e5b90cf2-9369-4741-9803-c835a81c009b" + ] + }, + "server/src/service/v2": { + "path": "server/src/service/v2", + "fileName": "v2", + "cellName": "v2", + "cellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "visible": true, + "parentCellId": "99e9b746-7809-41f4-a433-1de7cc90574b", + "children": [ + "server/src/service/v2/infrastructure", + "server/src/service/v2/business" + ] + }, + "server/src/service/v2/business": { + "path": "server/src/service/v2/business", + "fileName": "business", + "cellName": "business", + "cellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "children": [ + "server/src/service/v2/business/CheckService.ts", + "server/src/service/v2/business/MonitorService.ts" + ] + }, + "server/src/service/v2/business/CheckService.ts": { + "path": "server/src/service/v2/business/CheckService.ts", + "fileName": "CheckService.ts", + "cellName": "CheckService.ts", + "cellId": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "children": [ + "server/src/service/v2/business/CheckService.ts-simstep-01206383-2db8-4b18-84d0-be2a0289c196" + ] + }, + "server/src/service/v2/business/MonitorService.ts": { + "path": "server/src/service/v2/business/MonitorService.ts", + "fileName": "MonitorService.ts", + "cellName": "MonitorService.ts", + "cellId": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "children": [ + "server/src/service/v2/business/MonitorService.ts-simstep-5616ac6d-c281-4879-82f1-6590c721c8f7" + ] + }, + "server/src/service/v2/infrastructure": { + "path": "server/src/service/v2/infrastructure", + "fileName": "infrastructure", + "cellName": "infrastructure", + "cellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "children": [ + "server/src/service/v2/infrastructure/JobGenerator.ts", + "server/src/service/v2/infrastructure/NotificationService.ts", + "server/src/service/v2/infrastructure/NetworkService.ts" + ] + }, + "server/src/service/v2/infrastructure/JobGenerator.ts": { + "path": "server/src/service/v2/infrastructure/JobGenerator.ts", + "fileName": "JobGenerator.ts", + "cellName": "JobGenerator.ts", + "cellId": "159e569d-31d8-4883-b443-4ec08b21a044", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "children": [ + "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-157cec87-9b76-4a1c-9d85-0a6d3decbaa3", + "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-55ad645b-118a-478e-a00c-09ef82357008" + ] + }, + "server/src/service/v2/infrastructure/NetworkService.ts": { + "path": "server/src/service/v2/infrastructure/NetworkService.ts", + "fileName": "NetworkService.ts", + "cellName": "NetworkService.ts", + "cellId": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "children": [ + "server/src/service/v2/infrastructure/NetworkService.ts-simstep-53968267-0c6e-42f4-88b3-cd90f44532ae" + ] + }, + "server/src/service/v2/infrastructure/NotificationService.ts": { + "path": "server/src/service/v2/infrastructure/NotificationService.ts", + "fileName": "NotificationService.ts", + "cellName": "NotificationService.ts", + "cellId": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "children": [ + "server/src/service/v2/infrastructure/NotificationService.ts-simstep-8d1176f3-c755-401d-9ca8-234689b4b837" + ] + }, + "0b23ea66-3e37-4643-b3b9-acb87ea02bae": { + "path": "0b23ea66-3e37-4643-b3b9-acb87ea02bae", + "cellName": "client", + "cellId": "0b23ea66-3e37-4643-b3b9-acb87ea02bae", + "visible": true + }, + "98af25c7-efc2-43d2-a273-c8da4e1038ad": { + "path": "98af25c7-efc2-43d2-a273-c8da4e1038ad", + "cellName": "server", + "cellId": "98af25c7-efc2-43d2-a273-c8da4e1038ad", + "visible": true + }, + "d3264577-10f0-42fe-9411-7e9df0754d1e": { + "path": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "cellName": "src", + "cellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "visible": true, + "parentCellId": "0b23ea66-3e37-4643-b3b9-acb87ea02bae" + }, + "35864a2c-7bd0-4428-84fc-c0aa90c7f1be": { + "path": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "cellName": "src", + "cellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "visible": true, + "parentCellId": "98af25c7-efc2-43d2-a273-c8da4e1038ad" + }, + "f0feab85-316a-41b5-a2f1-629220018846": { + "path": "f0feab85-316a-41b5-a2f1-629220018846", + "cellName": "Pages", + "cellId": "f0feab85-316a-41b5-a2f1-629220018846", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "4295a047-076f-4c07-95dd-b3123b6614f7": { + "path": "4295a047-076f-4c07-95dd-b3123b6614f7", + "cellName": "Hooks", + "cellId": "4295a047-076f-4c07-95dd-b3123b6614f7", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "02b07589-ec21-4366-b7d2-9071e8f3bad8": { + "path": "02b07589-ec21-4366-b7d2-9071e8f3bad8", + "cellName": "Utils", + "cellId": "02b07589-ec21-4366-b7d2-9071e8f3bad8", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b": { + "path": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b", + "cellName": "routes", + "cellId": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "478e649c-1d1b-41d2-9b7b-6927e1dec7e1": { + "path": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1", + "cellName": "controllers", + "cellId": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "99e9b746-7809-41f4-a433-1de7cc90574b": { + "path": "99e9b746-7809-41f4-a433-1de7cc90574b", + "cellName": "service", + "cellId": "99e9b746-7809-41f4-a433-1de7cc90574b", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "15ee5d81-a0cf-4614-80b8-7a1123ab20b8": { + "path": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "cellName": "db", + "cellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "1a083b4a-787c-4eb4-8029-6700de987679": { + "path": "1a083b4a-787c-4eb4-8029-6700de987679", + "cellName": "v1", + "cellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "visible": true, + "parentCellId": "f0feab85-316a-41b5-a2f1-629220018846" + }, + "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961": { + "path": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "cellName": "v1", + "cellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "visible": true, + "parentCellId": "4295a047-076f-4c07-95dd-b3123b6614f7" + }, + "16731e9d-3f73-4055-9d79-5ca9be679f68": { + "path": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "cellName": "NetworkService.js", + "cellId": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "visible": true, + "parentCellId": "02b07589-ec21-4366-b7d2-9071e8f3bad8" + }, + "d3375a20-2776-4771-b806-649bbeb541bd": { + "path": "d3375a20-2776-4771-b806-649bbeb541bd", + "cellName": "v1", + "cellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "visible": true, + "parentCellId": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b" + }, + "fc591f7a-d4ac-4565-949d-a87a58a79bbc": { + "path": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "cellName": "v1", + "cellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "visible": true, + "parentCellId": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1" + }, + "bbbe53d6-f595-498a-ae45-e08611f39c3d": { + "path": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "cellName": "v1", + "cellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "visible": true, + "parentCellId": "99e9b746-7809-41f4-a433-1de7cc90574b" + }, + "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf": { + "path": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf", + "cellName": "v1", + "cellId": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf", + "visible": true, + "parentCellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8" + }, + "7090fcdb-09be-4c85-86fd-e73553bbb2ff": { + "path": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "cellName": "Uptime", + "cellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "595cf0e8-2f86-4543-b51c-b2aeac307f4b": { + "path": "595cf0e8-2f86-4543-b51c-b2aeac307f4b", + "cellName": "monitorHooks.js", + "cellId": "595cf0e8-2f86-4543-b51c-b2aeac307f4b", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "10611636-ec73-4a26-87a5-654ae64a4843": { + "path": "10611636-ec73-4a26-87a5-654ae64a4843", + "cellName": "monitorRoute.js", + "cellId": "10611636-ec73-4a26-87a5-654ae64a4843", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "341f2784-67ee-4477-9445-a8c240ee6261": { + "path": "341f2784-67ee-4477-9445-a8c240ee6261", + "cellName": "monitorController.js", + "cellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "87d3b101-9443-4ed4-9c4f-f759d41677eb": { + "path": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "cellName": "business", + "cellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "visible": true, + "parentCellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d" + }, + "1670815f-860d-4ea4-92e1-b238a799d3dc": { + "path": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "cellName": "infrastructure", + "cellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "visible": true, + "parentCellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d" + }, + "82cb3dbe-c922-438a-a912-255376f4b380": { + "path": "82cb3dbe-c922-438a-a912-255376f4b380", + "cellName": "modules", + "cellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "visible": true, + "parentCellId": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf" + }, + "3dba5acc-cf25-40b7-ad21-9cd715454cd5": { + "path": "3dba5acc-cf25-40b7-ad21-9cd715454cd5", + "cellName": "Create", + "cellId": "3dba5acc-cf25-40b7-ad21-9cd715454cd5", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff" + }, + "b4e479df-9b21-4430-9383-84831f503c2e": { + "path": "b4e479df-9b21-4430-9383-84831f503c2e", + "cellName": "monitorService.js", + "cellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b": { + "path": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "cellName": "SuperSimpleQueue", + "cellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "00406786-9663-440a-96b6-919d544732c0": { + "path": "00406786-9663-440a-96b6-919d544732c0", + "cellName": "networkService.js", + "cellId": "00406786-9663-440a-96b6-919d544732c0", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "fbd7d244-2f37-453c-8c83-a23a260b26cc": { + "path": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "cellName": "statusService.js", + "cellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "2ff1f030-2080-48da-bbbe-90302fdfc2f7": { + "path": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "cellName": "notificationService.js", + "cellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "630b3ece-2648-48d5-99b5-ef9611de2ad5": { + "path": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "cellName": "monitorModule.js", + "cellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "391ad99e-3114-4f54-a6ca-fd89b33c7e06": { + "path": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "cellName": "index.jsx", + "cellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "visible": true, + "parentCellId": "3dba5acc-cf25-40b7-ad21-9cd715454cd5" + }, + "d9c0127e-31aa-4509-b5c9-f7017d10d69a": { + "path": "d9c0127e-31aa-4509-b5c9-f7017d10d69a", + "cellName": "SuperSimpleQueue.js", + "cellId": "d9c0127e-31aa-4509-b5c9-f7017d10d69a", + "visible": true, + "parentCellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b" + }, + "04c12e9a-e025-457c-a634-5f7472a42985": { + "path": "04c12e9a-e025-457c-a634-5f7472a42985", + "cellName": "SuperSimpleQueueHelper.js", + "cellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "visible": true, + "parentCellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b" + }, + "7a2da928-a65c-49d8-b0a3-c159a6df2bc5": { + "path": "7a2da928-a65c-49d8-b0a3-c159a6df2bc5", + "cellName": "Uptime Monitor Creation: Display Form - index.jsx:L315-802", + "cellId": "7a2da928-a65c-49d8-b0a3-c159a6df2bc5", + "visible": true, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06" + }, + "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-ab5de0b9-c70d-492c-98d9-8ddee66284ad": { + "path": "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-ab5de0b9-c70d-492c-98d9-8ddee66284ad", + "fileName": "index.jsx", + "wiki": "The user navigates to the 'Create Uptime Monitor' page. The `UptimeCreate` React component renders the form, allowing the user to select the monitor type (HTTP, Ping, Docker, etc.) and input details like URL, name, and check interval.", + "cellName": "Uptime Monitor Creation: Display Form - index.jsx:L315-802", + "cellId": "7a2da928-a65c-49d8-b0a3-c159a6df2bc5", + "visible": true, + "startLine": 315, + "endLine": 802, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "parentPath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "ab5de0b9-c70d-492c-98d9-8ddee66284ad" + } + ] + }, + "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c": { + "path": "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c", + "cellName": "Uptime Monitor Creation: Handle Form Submission and Validation - index.jsx:L170-248", + "cellId": "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c", + "visible": true, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06" + }, + "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-e02ddf57-270e-4f71-b4c4-d5183bb7879a": { + "path": "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-e02ddf57-270e-4f71-b4c4-d5183bb7879a", + "fileName": "index.jsx", + "wiki": "The `onSubmit` function in `UptimeCreate.jsx` is executed. It prevents the default form submission, constructs a `form` object with monitor data, validates it using `monitorValidation`, and then calls the `createMonitor` function from the `useCreateMonitor` hook.", + "cellName": "Uptime Monitor Creation: Handle Form Submission and Validation - index.jsx:L170-248", + "cellId": "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c", + "visible": true, + "startLine": 170, + "endLine": 248, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "parentPath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "e02ddf57-270e-4f71-b4c4-d5183bb7879a" + } + ] + }, + "a30ac765-c546-4993-988e-8c8cfdb7532f": { + "path": "a30ac765-c546-4993-988e-8c8cfdb7532f", + "cellName": "Uptime Monitor Creation: Send API Request - NetworkService.js:L119-121", + "cellId": "a30ac765-c546-4993-988e-8c8cfdb7532f", + "visible": true, + "parentCellId": "16731e9d-3f73-4055-9d79-5ca9be679f68" + }, + "client/src/Utils/NetworkService.js-simstep-14e9986b-c6ce-475e-97ed-da1fa2b6968d": { + "path": "client/src/Utils/NetworkService.js-simstep-14e9986b-c6ce-475e-97ed-da1fa2b6968d", + "fileName": "NetworkService.js", + "wiki": "The `createMonitor` method in `NetworkService.js` makes an HTTP POST request to the `/monitors` endpoint on the server, sending the new monitor's data in the request body.", + "cellName": "Uptime Monitor Creation: Send API Request - NetworkService.js:L119-121", + "cellId": "a30ac765-c546-4993-988e-8c8cfdb7532f", + "visible": true, + "startLine": 119, + "endLine": 121, + "parentCellId": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "parentPath": "client/src/Utils/NetworkService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "14e9986b-c6ce-475e-97ed-da1fa2b6968d" + } + ] + }, + "f0409948-75dc-457f-a02d-6b9774258e48": { + "path": "f0409948-75dc-457f-a02d-6b9774258e48", + "cellName": "Uptime Monitor Creation: Controller to Service - monitorController.js:L195", + "cellId": "f0409948-75dc-457f-a02d-6b9774258e48", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-38fd069d-9c44-4078-954b-3a0899e9acf4": { + "path": "server/src/controllers/v1/monitorController.js-simstep-38fd069d-9c44-4078-954b-3a0899e9acf4", + "fileName": "monitorController.js", + "wiki": "The `MonitorController` extracts the user and team ID from the request and calls the `createMonitor` method in the `MonitorService`, passing along the monitor data from the request body.", + "cellName": "Uptime Monitor Creation: Controller to Service - monitorController.js:L195", + "cellId": "f0409948-75dc-457f-a02d-6b9774258e48", + "visible": true, + "startLine": 195, + "endLine": 195, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "38fd069d-9c44-4078-954b-3a0899e9acf4" + } + ] + }, + "2d407bfd-e4b0-46b8-99e7-121c507d525f": { + "path": "2d407bfd-e4b0-46b8-99e7-121c507d525f", + "cellName": "Uptime Monitor Creation: Save Monitor to Database - monitorModule.js:L454-455", + "cellId": "2d407bfd-e4b0-46b8-99e7-121c507d525f", + "visible": true, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5" + }, + "server/src/db/v1/modules/monitorModule.js-simstep-ca3ad309-323a-418d-a171-c16f56fae23c": { + "path": "server/src/db/v1/modules/monitorModule.js-simstep-ca3ad309-323a-418d-a171-c16f56fae23c", + "fileName": "monitorModule.js", + "wiki": "The `monitorModule` creates a new `Monitor` model instance with the provided data and saves it to the MongoDB database. The newly created document, including its generated `_id`, is returned.", + "cellName": "Uptime Monitor Creation: Save Monitor to Database - monitorModule.js:L454-455", + "cellId": "2d407bfd-e4b0-46b8-99e7-121c507d525f", + "visible": true, + "startLine": 454, + "endLine": 455, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "ca3ad309-323a-418d-a171-c16f56fae23c" + } + ] + }, + "51e8db5f-2fdd-4ddc-9605-43981db9d940": { + "path": "51e8db5f-2fdd-4ddc-9605-43981db9d940", + "cellName": "Uptime Monitor Creation: Schedule Job - SuperSimpleQueue.js:L50-57", + "cellId": "51e8db5f-2fdd-4ddc-9605-43981db9d940", + "visible": true, + "parentCellId": "d9c0127e-31aa-4509-b5c9-f7017d10d69a" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js-simstep-2702bfca-7611-4562-8d3e-a05af8257871": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js-simstep-2702bfca-7611-4562-8d3e-a05af8257871", + "fileName": "SuperSimpleQueue.js", + "wiki": "The `SuperSimpleQueue` receives the new monitor and uses its `scheduler.addJob` method to create a recurring job based on the monitor's interval. The monitor object itself is stored as data for the job.", + "cellName": "Uptime Monitor Creation: Schedule Job - SuperSimpleQueue.js:L50-57", + "cellId": "51e8db5f-2fdd-4ddc-9605-43981db9d940", + "visible": true, + "startLine": 50, + "endLine": 57, + "parentCellId": "d9c0127e-31aa-4509-b5c9-f7017d10d69a", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "2702bfca-7611-4562-8d3e-a05af8257871" + } + ] + }, + "18c13391-0cf7-4303-8180-0a40cfd64b58": { + "path": "18c13391-0cf7-4303-8180-0a40cfd64b58", + "cellName": "Service Monitoring: Job Execution - SuperSimpleQueueHelper.js:L27-73", + "cellId": "18c13391-0cf7-4303-8180-0a40cfd64b58", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e22a708a-a348-4775-8ee3-f32a35446e24": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e22a708a-a348-4775-8ee3-f32a35446e24", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "The scheduler triggers a monitor job at the specified interval. The `getMonitorJob` function in `SuperSimpleQueueHelper` is executed, which first checks if the monitor is in a maintenance window.", + "cellName": "Service Monitoring: Job Execution - SuperSimpleQueueHelper.js:L27-73", + "cellId": "18c13391-0cf7-4303-8180-0a40cfd64b58", + "visible": true, + "startLine": 27, + "endLine": 73, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "e22a708a-a348-4775-8ee3-f32a35446e24" + } + ] + }, + "5c900373-e6ba-44d5-b341-676a32d659c8": { + "path": "5c900373-e6ba-44d5-b341-676a32d659c8", + "cellName": "Service Monitoring: Perform Network Check - networkService.js:L44-59", + "cellId": "5c900373-e6ba-44d5-b341-676a32d659c8", + "visible": true, + "parentCellId": "00406786-9663-440a-96b6-919d544732c0" + }, + "server/src/service/v1/infrastructure/networkService.js-simstep-84f82fea-e306-4e0b-8222-e9705803d51a": { + "path": "server/src/service/v1/infrastructure/networkService.js-simstep-84f82fea-e306-4e0b-8222-e9705803d51a", + "fileName": "networkService.js", + "wiki": "The `requestStatus` method in the network service acts as a router. Based on the monitor's `type` (e.g., 'http', 'ping', 'docker'), it calls the corresponding specialized request function (e.g., `requestHttp`) to perform the check and constructs a standardized response object.", + "cellName": "Service Monitoring: Perform Network Check - networkService.js:L44-59", + "cellId": "5c900373-e6ba-44d5-b341-676a32d659c8", + "visible": true, + "startLine": 44, + "endLine": 59, + "parentCellId": "00406786-9663-440a-96b6-919d544732c0", + "parentPath": "server/src/service/v1/infrastructure/networkService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "84f82fea-e306-4e0b-8222-e9705803d51a" + } + ] + }, + "45e38f3a-a7c3-4de7-b3dc-589361d793fd": { + "path": "45e38f3a-a7c3-4de7-b3dc-589361d793fd", + "cellName": "Service Monitoring: Update Monitor Status - statusService.js:L108-183", + "cellId": "45e38f3a-a7c3-4de7-b3dc-589361d793fd", + "visible": true, + "parentCellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc" + }, + "server/src/service/v1/infrastructure/statusService.js-simstep-c66a04c6-113e-4391-ab71-93fc612279ef": { + "path": "server/src/service/v1/infrastructure/statusService.js-simstep-c66a04c6-113e-4391-ab71-93fc612279ef", + "fileName": "statusService.js", + "wiki": "The job handler passes the network response to `statusService.updateStatus`. This service creates a new `Check` document in the database, updates the monitor's status based on a sliding window and threshold, and calculates running statistics.", + "cellName": "Service Monitoring: Update Monitor Status - statusService.js:L108-183", + "cellId": "45e38f3a-a7c3-4de7-b3dc-589361d793fd", + "visible": true, + "startLine": 108, + "endLine": 183, + "parentCellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "parentPath": "server/src/service/v1/infrastructure/statusService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "c66a04c6-113e-4391-ab71-93fc612279ef" + } + ] + }, + "9f3ea7fc-16d3-4d36-97eb-66f8808425cc": { + "path": "9f3ea7fc-16d3-4d36-97eb-66f8808425cc", + "cellName": "Service Monitoring: Handle Notifications (Conditional) - notificationService.js:L76-92", + "cellId": "9f3ea7fc-16d3-4d36-97eb-66f8808425cc", + "visible": true, + "parentCellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7" + }, + "server/src/service/v1/infrastructure/notificationService.js-simstep-1a260a75-cf50-4f7a-90b0-c280b512a5cb": { + "path": "server/src/service/v1/infrastructure/notificationService.js-simstep-1a260a75-cf50-4f7a-90b0-c280b512a5cb", + "fileName": "notificationService.js", + "wiki": "If `statusChanged` is true, the job handler calls `notificationService.handleNotifications`. The service identifies the notification channels associated with the monitor and sends out alerts (e.g., email, webhook, Discord) with details about the status change.", + "cellName": "Service Monitoring: Handle Notifications (Conditional) - notificationService.js:L76-92", + "cellId": "9f3ea7fc-16d3-4d36-97eb-66f8808425cc", + "visible": true, + "startLine": 76, + "endLine": 92, + "parentCellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "parentPath": "server/src/service/v1/infrastructure/notificationService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "1a260a75-cf50-4f7a-90b0-c280b512a5cb" + } + ] + }, + "0e09aa33-d7c7-4c89-9da8-f0937658f4d3": { + "path": "0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "cellName": "Uptime Monitor\nCreation: Form\nSubmission", + "cellId": "0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "visible": true, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06" + }, + "generated-edge-simstep-888df086-41bb-469d-9c0a-daf74bdecb9f-0e09aa33-d7c7-4c89-9da8-f0937658f4d3": { + "path": "generated-edge-simstep-888df086-41bb-469d-9c0a-daf74bdecb9f-0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "fileName": "index.jsx", + "cellName": "Uptime Monitor Creation: Form Submission", + "cellId": "0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "visible": true, + "startLine": 435, + "endLine": 443, + "parentPath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "888df086-41bb-469d-9c0a-daf74bdecb9f" + } + ] + }, + "1f6f04a9-0123-4729-b06a-8f3da6d1b58c": { + "path": "1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "cellName": "Uptime Monitor\nCreation: Call\nNetwork Service", + "cellId": "1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-3fe0afaa-c673-4b52-a0c4-02a3ffd3a075-1f6f04a9-0123-4729-b06a-8f3da6d1b58c": { + "path": "generated-edge-simstep-3fe0afaa-c673-4b52-a0c4-02a3ffd3a075-1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "fileName": "index.jsx", + "cellName": "Uptime Monitor Creation: Call Network Service", + "cellId": "1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "visible": true, + "startLine": 306, + "endLine": 306, + "parentPath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "3fe0afaa-c673-4b52-a0c4-02a3ffd3a075" + } + ] + }, + "b6d963d5-c613-407a-9ce9-60a04048dea7": { + "path": "b6d963d5-c613-407a-9ce9-60a04048dea7", + "cellName": "Uptime Monitor\nCreation: API\nRoute to\nController", + "cellId": "b6d963d5-c613-407a-9ce9-60a04048dea7", + "visible": true + }, + "generated-edge-simstep-13dd0564-44a5-43ea-a163-ce1148cc48e7-b6d963d5-c613-407a-9ce9-60a04048dea7": { + "path": "generated-edge-simstep-13dd0564-44a5-43ea-a163-ce1148cc48e7-b6d963d5-c613-407a-9ce9-60a04048dea7", + "fileName": "NetworkService.js", + "cellName": "Uptime Monitor Creation: API Route to Controller", + "cellId": "b6d963d5-c613-407a-9ce9-60a04048dea7", + "visible": true, + "startLine": 35, + "endLine": 35, + "parentPath": "client/src/Utils/NetworkService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "13dd0564-44a5-43ea-a163-ce1148cc48e7" + } + ] + }, + "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7": { + "path": "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "cellName": "Uptime Monitor\nCreation: Service\nto DB\nModule", + "cellId": "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-a6f4c967-b1a8-4804-bc2e-67f47a7b68f9-a6bccd1d-4272-4df7-a7c8-b3979e37b8d7": { + "path": "generated-edge-simstep-a6f4c967-b1a8-4804-bc2e-67f47a7b68f9-a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "fileName": "monitorController.js", + "cellName": "Uptime Monitor Creation: Service to DB Module", + "cellId": "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "visible": true, + "startLine": 74, + "endLine": 78, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "a6f4c967-b1a8-4804-bc2e-67f47a7b68f9" + } + ] + }, + "de320ac1-0949-4297-be7c-b9d7a14e0fec": { + "path": "de320ac1-0949-4297-be7c-b9d7a14e0fec", + "cellName": "Uptime Monitor\nCreation: Service\nto Job\nQueue", + "cellId": "de320ac1-0949-4297-be7c-b9d7a14e0fec", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-71ec03b6-1166-4c5d-9ada-0875a0866e30-de320ac1-0949-4297-be7c-b9d7a14e0fec": { + "path": "generated-edge-simstep-71ec03b6-1166-4c5d-9ada-0875a0866e30-de320ac1-0949-4297-be7c-b9d7a14e0fec", + "fileName": "monitorModule.js", + "cellName": "Uptime Monitor Creation: Service to Job Queue", + "cellId": "de320ac1-0949-4297-be7c-b9d7a14e0fec", + "visible": true, + "startLine": 80, + "endLine": 80, + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "71ec03b6-1166-4c5d-9ada-0875a0866e30" + } + ] + }, + "38ba9b22-5b26-432d-a856-0eceb792c737": { + "path": "38ba9b22-5b26-432d-a856-0eceb792c737", + "cellName": "Service Monitoring:\nRequest Service\nStatus", + "cellId": "38ba9b22-5b26-432d-a856-0eceb792c737", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-1d7580f2-f107-4008-a207-b8ec07998721-38ba9b22-5b26-432d-a856-0eceb792c737": { + "path": "generated-edge-simstep-1d7580f2-f107-4008-a207-b8ec07998721-38ba9b22-5b26-432d-a856-0eceb792c737", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "Service Monitoring: Request Service Status", + "cellId": "38ba9b22-5b26-432d-a856-0eceb792c737", + "visible": true, + "startLine": 45, + "endLine": 45, + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "1d7580f2-f107-4008-a207-b8ec07998721" + } + ] + }, + "5e80734c-7b72-41be-9068-30cda3ffb425": { + "path": "5e80734c-7b72-41be-9068-30cda3ffb425", + "cellName": "Service Monitoring:\nPropagate Network\nResponse", + "cellId": "5e80734c-7b72-41be-9068-30cda3ffb425", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-9b0ada30-d3e6-4b2c-b3ff-f50615139588-5e80734c-7b72-41be-9068-30cda3ffb425": { + "path": "generated-edge-simstep-9b0ada30-d3e6-4b2c-b3ff-f50615139588-5e80734c-7b72-41be-9068-30cda3ffb425", + "fileName": "networkService.js", + "cellName": "Service Monitoring: Propagate Network Response", + "cellId": "5e80734c-7b72-41be-9068-30cda3ffb425", + "visible": true, + "startLine": 45, + "endLine": 45, + "parentPath": "server/src/service/v1/infrastructure/networkService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "9b0ada30-d3e6-4b2c-b3ff-f50615139588" + } + ] + }, + "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c": { + "path": "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "cellName": "Service Monitoring:\nCheck for\nStatus Change", + "cellId": "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-9896c387-4fca-47ad-82eb-7301286decb7-0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c": { + "path": "generated-edge-simstep-9896c387-4fca-47ad-82eb-7301286decb7-0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "fileName": "statusService.js", + "cellName": "Service Monitoring: Check for Status Change", + "cellId": "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "visible": true, + "startLine": 51, + "endLine": 51, + "parentPath": "server/src/service/v1/infrastructure/statusService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "9896c387-4fca-47ad-82eb-7301286decb7" + } + ] + }, + "3f26b33e-477f-41df-a78a-3e3e4f4209ae": { + "path": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "cellName": "Infrastructure", + "cellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "74019e8a-3148-4a32-aae2-5afd16f3eb9b": { + "path": "74019e8a-3148-4a32-aae2-5afd16f3eb9b", + "cellName": "Create", + "cellId": "74019e8a-3148-4a32-aae2-5afd16f3eb9b", + "visible": true, + "parentCellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae" + }, + "591128f9-0994-4e01-a1ab-f23357f97aff": { + "path": "591128f9-0994-4e01-a1ab-f23357f97aff", + "cellName": "Details", + "cellId": "591128f9-0994-4e01-a1ab-f23357f97aff", + "visible": true, + "parentCellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae" + }, + "4940977f-933d-4bbc-b19e-749647b44d25": { + "path": "4940977f-933d-4bbc-b19e-749647b44d25", + "cellName": "notificationUtils.js", + "cellId": "4940977f-933d-4bbc-b19e-749647b44d25", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "a0b22a5a-59e1-47e3-8218-a4ad083b1b71": { + "path": "a0b22a5a-59e1-47e3-8218-a4ad083b1b71", + "cellName": "monitorModuleQueries.js", + "cellId": "a0b22a5a-59e1-47e3-8218-a4ad083b1b71", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "ceded1fc-87a9-43c8-a756-746be3d4ad8d": { + "path": "ceded1fc-87a9-43c8-a756-746be3d4ad8d", + "cellName": "Components", + "cellId": "ceded1fc-87a9-43c8-a756-746be3d4ad8d", + "visible": true, + "parentCellId": "74019e8a-3148-4a32-aae2-5afd16f3eb9b" + }, + "e292c552-b3df-4bac-bd9a-8f44c260a5aa": { + "path": "e292c552-b3df-4bac-bd9a-8f44c260a5aa", + "cellName": "index.jsx", + "cellId": "e292c552-b3df-4bac-bd9a-8f44c260a5aa", + "visible": true, + "parentCellId": "591128f9-0994-4e01-a1ab-f23357f97aff" + }, + "e97d4e61-483e-4ac5-85a1-0ff72fcdd449": { + "path": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449", + "cellName": "Components", + "cellId": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449", + "visible": true, + "parentCellId": "591128f9-0994-4e01-a1ab-f23357f97aff" + }, + "83800be3-2a0e-4419-b379-3f995f4050e7": { + "path": "83800be3-2a0e-4419-b379-3f995f4050e7", + "cellName": "CustomAlertsSection.jsx", + "cellId": "83800be3-2a0e-4419-b379-3f995f4050e7", + "visible": true, + "parentCellId": "ceded1fc-87a9-43c8-a756-746be3d4ad8d" + }, + "6a7c7720-a399-4d4d-8225-36e0b2f59731": { + "path": "6a7c7720-a399-4d4d-8225-36e0b2f59731", + "cellName": "GaugeBoxes", + "cellId": "6a7c7720-a399-4d4d-8225-36e0b2f59731", + "visible": true, + "parentCellId": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449" + }, + "8fe3d890-3605-407d-b06c-1d890a354e46": { + "path": "8fe3d890-3605-407d-b06c-1d890a354e46", + "cellName": "index.jsx", + "cellId": "8fe3d890-3605-407d-b06c-1d890a354e46", + "visible": true, + "parentCellId": "6a7c7720-a399-4d4d-8225-36e0b2f59731" + }, + "9651d084-5c16-4417-89c1-f52ae6572a95": { + "path": "9651d084-5c16-4417-89c1-f52ae6572a95", + "cellName": "Configuration: User Sets Alert Thresholds - CustomAlertsSection.jsx:L45-53", + "cellId": "9651d084-5c16-4417-89c1-f52ae6572a95", + "visible": true, + "parentCellId": "83800be3-2a0e-4419-b379-3f995f4050e7" + }, + "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx-simstep-80dd889a-1fc9-4d56-b9cf-5b45bf1110b1": { + "path": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx-simstep-80dd889a-1fc9-4d56-b9cf-5b45bf1110b1", + "fileName": "CustomAlertsSection.jsx", + "wiki": "A user navigates to the 'Create Infrastructure Monitor' page to set up monitoring for a new server. They enable alerts for CPU, memory, and disk usage, and define specific thresholds for each metric. For example, an alert is configured to trigger if CPU usage exceeds 80%.", + "cellName": "Configuration: User Sets Alert Thresholds - CustomAlertsSection.jsx:L45-53", + "cellId": "9651d084-5c16-4417-89c1-f52ae6572a95", + "visible": true, + "startLine": 45, + "endLine": 53, + "parentCellId": "83800be3-2a0e-4419-b379-3f995f4050e7", + "parentPath": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "80dd889a-1fc9-4d56-b9cf-5b45bf1110b1" + } + ] + }, + "4c47b5f2-5363-4816-9745-30e61d184c7a": { + "path": "4c47b5f2-5363-4816-9745-30e61d184c7a", + "cellName": "Automated Check: Backend Fetches Hardware Data - networkService.js:L7-14", + "cellId": "4c47b5f2-5363-4816-9745-30e61d184c7a", + "visible": true, + "parentCellId": "00406786-9663-440a-96b6-919d544732c0" + }, + "server/src/service/v1/infrastructure/networkService.js-simstep-ed6e7faf-69dd-4d0b-bda8-137739ed288b": { + "path": "server/src/service/v1/infrastructure/networkService.js-simstep-ed6e7faf-69dd-4d0b-bda8-137739ed288b", + "fileName": "networkService.js", + "wiki": "The Checkmate server's job queue periodically initiates a check for the newly created hardware monitor. It makes an HTTP request to the monitor's URL, which points to the external 'Capture' agent running on the target server.", + "cellName": "Automated Check: Backend Fetches Hardware Data - networkService.js:L7-14", + "cellId": "4c47b5f2-5363-4816-9745-30e61d184c7a", + "visible": true, + "startLine": 7, + "endLine": 14, + "parentCellId": "00406786-9663-440a-96b6-919d544732c0", + "parentPath": "server/src/service/v1/infrastructure/networkService.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "ed6e7faf-69dd-4d0b-bda8-137739ed288b" + } + ] + }, + "c1bf10c9-8c1a-44f4-8e23-39708f0301e8": { + "path": "c1bf10c9-8c1a-44f4-8e23-39708f0301e8", + "cellName": "Automated Check: Process and Store Hardware Data - statusService.js:L265-274", + "cellId": "c1bf10c9-8c1a-44f4-8e23-39708f0301e8", + "visible": true, + "parentCellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc" + }, + "server/src/service/v1/infrastructure/statusService.js-simstep-e5b90cf2-9369-4741-9803-c835a81c009b": { + "path": "server/src/service/v1/infrastructure/statusService.js-simstep-e5b90cf2-9369-4741-9803-c835a81c009b", + "fileName": "statusService.js", + "wiki": "The backend's StatusService receives the payload from the Capture agent. It parses the data and constructs a 'check' document. This document, containing the detailed hardware metrics, is then saved to the database for historical tracking and visualization.", + "cellName": "Automated Check: Process and Store Hardware Data - statusService.js:L265-274", + "cellId": "c1bf10c9-8c1a-44f4-8e23-39708f0301e8", + "visible": true, + "startLine": 265, + "endLine": 274, + "parentCellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "parentPath": "server/src/service/v1/infrastructure/statusService.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "e5b90cf2-9369-4741-9803-c835a81c009b" + } + ] + }, + "d4f48f1a-61de-4cde-87d2-4b5215464899": { + "path": "d4f48f1a-61de-4cde-87d2-4b5215464899", + "cellName": "Automated Check: Evaluate Data Against Thresholds - notificationUtils.js:L95-109", + "cellId": "d4f48f1a-61de-4cde-87d2-4b5215464899", + "visible": true, + "parentCellId": "4940977f-933d-4bbc-b19e-749647b44d25" + }, + "server/src/service/v1/infrastructure/notificationUtils.js-simstep-07fe8489-1d94-4afb-83f8-7cebb0dff49a": { + "path": "server/src/service/v1/infrastructure/notificationUtils.js-simstep-07fe8489-1d94-4afb-83f8-7cebb0dff49a", + "fileName": "notificationUtils.js", + "wiki": "The notification utility function compares the received hardware metrics against the user-defined thresholds stored in the monitor's configuration. In this case, the CPU usage of 85% is greater than the 80% threshold, so an alert condition is met.", + "cellName": "Automated Check: Evaluate Data Against Thresholds - notificationUtils.js:L95-109", + "cellId": "d4f48f1a-61de-4cde-87d2-4b5215464899", + "visible": true, + "startLine": 95, + "endLine": 109, + "parentCellId": "4940977f-933d-4bbc-b19e-749647b44d25", + "parentPath": "server/src/service/v1/infrastructure/notificationUtils.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "07fe8489-1d94-4afb-83f8-7cebb0dff49a" + } + ] + }, + "801fe70a-1a19-406c-8f0d-770b0cb74409": { + "path": "801fe70a-1a19-406c-8f0d-770b0cb74409", + "cellName": "Visualization: User Views Monitor Details - index.jsx:L23-31", + "cellId": "801fe70a-1a19-406c-8f0d-770b0cb74409", + "visible": true, + "parentCellId": "e292c552-b3df-4bac-bd9a-8f44c260a5aa" + }, + "client/src/Pages/v1/Infrastructure/Details/index.jsx-simstep-c27365db-9db6-4cc9-96f0-4f046fd38562": { + "path": "client/src/Pages/v1/Infrastructure/Details/index.jsx-simstep-c27365db-9db6-4cc9-96f0-4f046fd38562", + "fileName": "index.jsx", + "wiki": "A user navigates to the details page for their 'Production Web Server' monitor in the Checkmate UI to view its performance. The React component for the details page mounts and prepares to fetch data.", + "cellName": "Visualization: User Views Monitor Details - index.jsx:L23-31", + "cellId": "801fe70a-1a19-406c-8f0d-770b0cb74409", + "visible": true, + "startLine": 23, + "endLine": 31, + "parentCellId": "e292c552-b3df-4bac-bd9a-8f44c260a5aa", + "parentPath": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "c27365db-9db6-4cc9-96f0-4f046fd38562" + } + ] + }, + "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03": { + "path": "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03", + "cellName": "Backend: Aggregate Historical Data - monitorModule.js:L312-322", + "cellId": "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03", + "visible": true, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5" + }, + "server/src/db/v1/modules/monitorModule.js-simstep-8502ae1f-f029-446e-9dde-59c2aba87a8a": { + "path": "server/src/db/v1/modules/monitorModule.js-simstep-8502ae1f-f029-446e-9dde-59c2aba87a8a", + "fileName": "monitorModule.js", + "wiki": "The backend receives the request and executes a database aggregation pipeline. This query fetches all 'hardware' checks for the monitor within the given date range and calculates metrics like average CPU usage, average memory usage, and average temperatures, grouped by time intervals.", + "cellName": "Backend: Aggregate Historical Data - monitorModule.js:L312-322", + "cellId": "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03", + "visible": true, + "startLine": 312, + "endLine": 322, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "8502ae1f-f029-446e-9dde-59c2aba87a8a" + } + ] + }, + "9ea2e32d-1ea8-42f2-9a17-613844723ae6": { + "path": "9ea2e32d-1ea8-42f2-9a17-613844723ae6", + "cellName": "Visualization: Frontend Renders Charts and Gauges - index.jsx:L22-51", + "cellId": "9ea2e32d-1ea8-42f2-9a17-613844723ae6", + "visible": true, + "parentCellId": "8fe3d890-3605-407d-b06c-1d890a354e46" + }, + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx-simstep-6eedce82-b9c8-4022-95e5-3a6e00628105": { + "path": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx-simstep-6eedce82-b9c8-4022-95e5-3a6e00628105", + "fileName": "index.jsx", + "wiki": "The frontend components on the Infrastructure Details page receive the aggregated data. The 'GaugeBoxes' component uses the 'latestCheck' data to render real-time gauges for current CPU and memory usage, while the 'AreaChartBoxes' component uses the historical 'checks' data to render time-series charts.", + "cellName": "Visualization: Frontend Renders Charts and Gauges - index.jsx:L22-51", + "cellId": "9ea2e32d-1ea8-42f2-9a17-613844723ae6", + "visible": true, + "startLine": 22, + "endLine": 51, + "parentCellId": "8fe3d890-3605-407d-b06c-1d890a354e46", + "parentPath": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "6eedce82-b9c8-4022-95e5-3a6e00628105" + } + ] + }, + "1c5c9857-bf32-40e4-921b-957c252dac2a": { + "path": "1c5c9857-bf32-40e4-921b-957c252dac2a", + "cellName": "API Call:\nSubmit Monitor\nConfiguration", + "cellId": "1c5c9857-bf32-40e4-921b-957c252dac2a", + "visible": true + }, + "generated-edge-simstep-815f96af-c28e-4f80-b4ab-18a6fb23eee0-1c5c9857-bf32-40e4-921b-957c252dac2a": { + "path": "generated-edge-simstep-815f96af-c28e-4f80-b4ab-18a6fb23eee0-1c5c9857-bf32-40e4-921b-957c252dac2a", + "fileName": "CustomAlertsSection.jsx", + "cellName": "API Call: Submit Monitor Configuration", + "cellId": "1c5c9857-bf32-40e4-921b-957c252dac2a", + "visible": true, + "startLine": 386, + "endLine": 389, + "parentPath": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "815f96af-c28e-4f80-b4ab-18a6fb23eee0" + } + ] + }, + "a64547c9-d9e6-4941-8e5e-f49add06516b": { + "path": "a64547c9-d9e6-4941-8e5e-f49add06516b", + "cellName": "Automated Check:\nData Received\nfrom Capture\nAgent", + "cellId": "a64547c9-d9e6-4941-8e5e-f49add06516b", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-4434dca1-2048-4458-a70b-b70d569e378d-a64547c9-d9e6-4941-8e5e-f49add06516b": { + "path": "generated-edge-simstep-4434dca1-2048-4458-a70b-b70d569e378d-a64547c9-d9e6-4941-8e5e-f49add06516b", + "fileName": "networkService.js", + "cellName": "Automated Check: Data Received from Capture Agent", + "cellId": "a64547c9-d9e6-4941-8e5e-f49add06516b", + "visible": true, + "startLine": 266, + "endLine": 273, + "parentPath": "server/src/service/v1/infrastructure/networkService.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "4434dca1-2048-4458-a70b-b70d569e378d" + } + ] + }, + "7a250d09-50c6-4935-b10c-626b0ebe1fba": { + "path": "7a250d09-50c6-4935-b10c-626b0ebe1fba", + "cellName": "Automated Check:\nPass Data\nfor Alert\nEvaluation", + "cellId": "7a250d09-50c6-4935-b10c-626b0ebe1fba", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-b019a6d0-a23a-4a1d-bd21-644f01996f14-7a250d09-50c6-4935-b10c-626b0ebe1fba": { + "path": "generated-edge-simstep-b019a6d0-a23a-4a1d-bd21-644f01996f14-7a250d09-50c6-4935-b10c-626b0ebe1fba", + "fileName": "statusService.js", + "cellName": "Automated Check: Pass Data for Alert Evaluation", + "cellId": "7a250d09-50c6-4935-b10c-626b0ebe1fba", + "visible": true, + "startLine": 60, + "endLine": 79, + "parentPath": "server/src/service/v1/infrastructure/statusService.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "b019a6d0-a23a-4a1d-bd21-644f01996f14" + } + ] + }, + "3d357de8-9fd2-4d0b-9623-f36fce4cf1de": { + "path": "3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "cellName": "Automated Check:\nGenerate Alert\nNotification Content", + "cellId": "3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "visible": true + }, + "generated-edge-simstep-c691b3b9-88b3-4c88-a399-ff9fd5866194-3d357de8-9fd2-4d0b-9623-f36fce4cf1de": { + "path": "generated-edge-simstep-c691b3b9-88b3-4c88-a399-ff9fd5866194-3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "fileName": "notificationUtils.js", + "cellName": "Automated Check: Generate Alert Notification Content", + "cellId": "3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "visible": true, + "startLine": 177, + "endLine": 183, + "parentPath": "server/src/service/v1/infrastructure/notificationUtils.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "c691b3b9-88b3-4c88-a399-ff9fd5866194" + } + ] + }, + "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4": { + "path": "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "cellName": "API Call:\nRequest for\nHistorical Hardware\nData", + "cellId": "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "visible": true + }, + "generated-edge-simstep-a5d20172-1c95-4c9e-af39-f86a498734bb-00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4": { + "path": "generated-edge-simstep-a5d20172-1c95-4c9e-af39-f86a498734bb-00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "fileName": "index.jsx", + "cellName": "API Call: Request for Historical Hardware Data", + "cellId": "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "visible": true, + "startLine": 198, + "endLine": 206, + "parentPath": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "a5d20172-1c95-4c9e-af39-f86a498734bb" + } + ] + }, + "60308696-6f8a-43c8-b9a1-da199ef500cd": { + "path": "60308696-6f8a-43c8-b9a1-da199ef500cd", + "cellName": "API Response:\nSend Aggregated\nData to\nFrontend", + "cellId": "60308696-6f8a-43c8-b9a1-da199ef500cd", + "visible": true + }, + "generated-edge-simstep-627fcb0c-0282-4b4d-8c55-aceb636e4da0-60308696-6f8a-43c8-b9a1-da199ef500cd": { + "path": "generated-edge-simstep-627fcb0c-0282-4b4d-8c55-aceb636e4da0-60308696-6f8a-43c8-b9a1-da199ef500cd", + "fileName": "monitorModule.js", + "cellName": "API Response: Send Aggregated Data to Frontend", + "cellId": "60308696-6f8a-43c8-b9a1-da199ef500cd", + "visible": true, + "startLine": 247, + "endLine": 262, + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "627fcb0c-0282-4b4d-8c55-aceb636e4da0" + } + ] + }, + "feefae96-61b8-4ac5-a37c-592ed4b2330e": { + "path": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "cellName": "v2", + "cellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "visible": true, + "parentCellId": "99e9b746-7809-41f4-a433-1de7cc90574b" + }, + "b3575a92-df07-4cf7-810f-8e5e0672d666": { + "path": "b3575a92-df07-4cf7-810f-8e5e0672d666", + "cellName": "Notifications", + "cellId": "b3575a92-df07-4cf7-810f-8e5e0672d666", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "cd2428fd-6545-48dd-b685-5261311b7532": { + "path": "cd2428fd-6545-48dd-b685-5261311b7532", + "cellName": "useNotifications.js", + "cellId": "cd2428fd-6545-48dd-b685-5261311b7532", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "96250197-23af-4059-9904-fd6751f70ad5": { + "path": "96250197-23af-4059-9904-fd6751f70ad5", + "cellName": "notificationRoute.js", + "cellId": "96250197-23af-4059-9904-fd6751f70ad5", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "8e5941e1-e577-455f-bbf2-83a418ad5908": { + "path": "8e5941e1-e577-455f-bbf2-83a418ad5908", + "cellName": "notificationController.js", + "cellId": "8e5941e1-e577-455f-bbf2-83a418ad5908", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f": { + "path": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "cellName": "infrastructure", + "cellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e" + }, + "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb": { + "path": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb", + "cellName": "create", + "cellId": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb", + "visible": true, + "parentCellId": "b3575a92-df07-4cf7-810f-8e5e0672d666" + }, + "62daeac4-9402-448a-972c-cb8cb760671c": { + "path": "62daeac4-9402-448a-972c-cb8cb760671c", + "cellName": "notificationModule.js", + "cellId": "62daeac4-9402-448a-972c-cb8cb760671c", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "159e569d-31d8-4883-b443-4ec08b21a044": { + "path": "159e569d-31d8-4883-b443-4ec08b21a044", + "cellName": "JobGenerator.ts", + "cellId": "159e569d-31d8-4883-b443-4ec08b21a044", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f" + }, + "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa": { + "path": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa", + "cellName": "NotificationService.ts", + "cellId": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f" + }, + "b71b02fb-d08e-4fcf-b959-de1b9547d9c2": { + "path": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2", + "cellName": "index.jsx", + "cellId": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2", + "visible": true, + "parentCellId": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb" + }, + "15486629-84b3-463a-962b-06083f69e4bf": { + "path": "15486629-84b3-463a-962b-06083f69e4bf", + "cellName": "Flow 1: Render Notification Creation Form - index.jsx:L157-321", + "cellId": "15486629-84b3-463a-962b-06083f69e4bf", + "visible": true, + "parentCellId": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2" + }, + "client/src/Pages/v1/Notifications/create/index.jsx-simstep-e924ec15-1bea-4e3b-befb-a8bf30cbf740": { + "path": "client/src/Pages/v1/Notifications/create/index.jsx-simstep-e924ec15-1bea-4e3b-befb-a8bf30cbf740", + "fileName": "index.jsx", + "wiki": "User accesses the `/notifications/create` route, which renders the `CreateNotifications` component. This component displays a form for setting up a new notification channel, including fields for name, type, and provider-specific details.", + "cellName": "Flow 1: Render Notification Creation Form - index.jsx:L157-321", + "cellId": "15486629-84b3-463a-962b-06083f69e4bf", + "visible": true, + "startLine": 157, + "endLine": 321, + "parentCellId": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2", + "parentPath": "client/src/Pages/v1/Notifications/create/index.jsx", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "e924ec15-1bea-4e3b-befb-a8bf30cbf740" + } + ] + }, + "a07b805e-e55f-46cb-b54b-765b7813b0ca": { + "path": "a07b805e-e55f-46cb-b54b-765b7813b0ca", + "cellName": "Flow 1: Frontend API Call - useNotifications.js:L14-17", + "cellId": "a07b805e-e55f-46cb-b54b-765b7813b0ca", + "visible": true, + "parentCellId": "cd2428fd-6545-48dd-b685-5261311b7532" + }, + "client/src/Hooks/v1/useNotifications.js-simstep-383c6be8-aa51-4398-9ae3-21035413f73a": { + "path": "client/src/Hooks/v1/useNotifications.js-simstep-383c6be8-aa51-4398-9ae3-21035413f73a", + "fileName": "useNotifications.js", + "wiki": "The `createNotification` hook executes an asynchronous function that calls `networkService.createNotification` to send the new notification data to the backend API.", + "cellName": "Flow 1: Frontend API Call - useNotifications.js:L14-17", + "cellId": "a07b805e-e55f-46cb-b54b-765b7813b0ca", + "visible": true, + "startLine": 14, + "endLine": 17, + "parentCellId": "cd2428fd-6545-48dd-b685-5261311b7532", + "parentPath": "client/src/Hooks/v1/useNotifications.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "383c6be8-aa51-4398-9ae3-21035413f73a" + } + ] + }, + "d0a4737f-715f-4f50-b6d5-5e4b31f86f93": { + "path": "d0a4737f-715f-4f50-b6d5-5e4b31f86f93", + "cellName": "Flow 1: Backend Route Handling - notificationRoute.js:L10", + "cellId": "d0a4737f-715f-4f50-b6d5-5e4b31f86f93", + "visible": true, + "parentCellId": "96250197-23af-4059-9904-fd6751f70ad5" + }, + "server/src/routes/v1/notificationRoute.js-simstep-2f9266c9-4cbc-4b50-a280-cf8b5800d540": { + "path": "server/src/routes/v1/notificationRoute.js-simstep-2f9266c9-4cbc-4b50-a280-cf8b5800d540", + "fileName": "notificationRoute.js", + "wiki": "The backend server receives the POST request. The Express router matches the `/notifications` path and forwards the request to the `createNotification` method of the `NotificationController`.", + "cellName": "Flow 1: Backend Route Handling - notificationRoute.js:L10", + "cellId": "d0a4737f-715f-4f50-b6d5-5e4b31f86f93", + "visible": true, + "startLine": 10, + "endLine": 10, + "parentCellId": "96250197-23af-4059-9904-fd6751f70ad5", + "parentPath": "server/src/routes/v1/notificationRoute.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "2f9266c9-4cbc-4b50-a280-cf8b5800d540" + } + ] + }, + "4a796ccf-40a9-4446-a109-6a6eed52e898": { + "path": "4a796ccf-40a9-4446-a109-6a6eed52e898", + "cellName": "Flow 1: Database Insertion - notificationModule.js:L10-13", + "cellId": "4a796ccf-40a9-4446-a109-6a6eed52e898", + "visible": true, + "parentCellId": "62daeac4-9402-448a-972c-cb8cb760671c" + }, + "server/src/db/v1/modules/notificationModule.js-simstep-d29ab4ab-be50-4f17-af6e-2c61cec9d0ab": { + "path": "server/src/db/v1/modules/notificationModule.js-simstep-d29ab4ab-be50-4f17-af6e-2c61cec9d0ab", + "fileName": "notificationModule.js", + "wiki": "The `notificationModule` creates a new instance of the `Notification` model with the provided data and saves it to the MongoDB database.", + "cellName": "Flow 1: Database Insertion - notificationModule.js:L10-13", + "cellId": "4a796ccf-40a9-4446-a109-6a6eed52e898", + "visible": true, + "startLine": 10, + "endLine": 13, + "parentCellId": "62daeac4-9402-448a-972c-cb8cb760671c", + "parentPath": "server/src/db/v1/modules/notificationModule.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "d29ab4ab-be50-4f17-af6e-2c61cec9d0ab" + } + ] + }, + "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39": { + "path": "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39", + "cellName": "Flow 1: Backend Receives Test Request - notificationRoute.js:L13", + "cellId": "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39", + "visible": true, + "parentCellId": "96250197-23af-4059-9904-fd6751f70ad5" + }, + "server/src/routes/v1/notificationRoute.js-simstep-c1b17b64-90e9-48bc-8927-25d7ebfc3036": { + "path": "server/src/routes/v1/notificationRoute.js-simstep-c1b17b64-90e9-48bc-8927-25d7ebfc3036", + "fileName": "notificationRoute.js", + "wiki": "The client sends a POST request to `/notifications/test`. The router directs this request to the `testNotification` method in the `NotificationController`.", + "cellName": "Flow 1: Backend Receives Test Request - notificationRoute.js:L13", + "cellId": "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39", + "visible": true, + "startLine": 13, + "endLine": 13, + "parentCellId": "96250197-23af-4059-9904-fd6751f70ad5", + "parentPath": "server/src/routes/v1/notificationRoute.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "c1b17b64-90e9-48bc-8927-25d7ebfc3036" + } + ] + }, + "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709": { + "path": "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709", + "cellName": "Flow 1: Send Test Notification - notificationService.js:L120-123", + "cellId": "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709", + "visible": true, + "parentCellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7" + }, + "server/src/service/v1/infrastructure/notificationService.js-simstep-f29f9be1-a0cd-43a3-b259-e5105a25ce41": { + "path": "server/src/service/v1/infrastructure/notificationService.js-simstep-f29f9be1-a0cd-43a3-b259-e5105a25ce41", + "fileName": "notificationService.js", + "wiki": "The `NotificationService` prepares a standard test message and calls its internal `sendNotification` method. This method identifies the notification type (e.g., email, Slack) and uses the corresponding service (e.g., EmailService, NetworkService for webhooks) to dispatch the test message.", + "cellName": "Flow 1: Send Test Notification - notificationService.js:L120-123", + "cellId": "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709", + "visible": true, + "startLine": 120, + "endLine": 123, + "parentCellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "parentPath": "server/src/service/v1/infrastructure/notificationService.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "f29f9be1-a0cd-43a3-b259-e5105a25ce41" + } + ] + }, + "28f1eabb-4f24-4944-8a0a-04f41ceb8589": { + "path": "28f1eabb-4f24-4944-8a0a-04f41ceb8589", + "cellName": "Flow 2: Job Execution and Status Update - JobGenerator.ts:L43-66", + "cellId": "28f1eabb-4f24-4944-8a0a-04f41ceb8589", + "visible": true, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044" + }, + "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-157cec87-9b76-4a1c-9d85-0a6d3decbaa3": { + "path": "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-157cec87-9b76-4a1c-9d85-0a6d3decbaa3", + "fileName": "JobGenerator.ts", + "wiki": "A background job is generated for a monitor. When executed, it performs a network check, saves the result, and updates the monitor's status via `statusService.updateMonitorStatus`.", + "cellName": "Flow 2: Job Execution and Status Update - JobGenerator.ts:L43-66", + "cellId": "28f1eabb-4f24-4944-8a0a-04f41ceb8589", + "visible": true, + "startLine": 43, + "endLine": 66, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044", + "parentPath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "157cec87-9b76-4a1c-9d85-0a6d3decbaa3" + } + ] + }, + "31fe5a28-0640-4b52-841b-dd02e2ae646e": { + "path": "31fe5a28-0640-4b52-841b-dd02e2ae646e", + "cellName": "Flow 2: Initiate Notification Handling - JobGenerator.ts:L61", + "cellId": "31fe5a28-0640-4b52-841b-dd02e2ae646e", + "visible": true, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044" + }, + "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-55ad645b-118a-478e-a00c-09ef82357008": { + "path": "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-55ad645b-118a-478e-a00c-09ef82357008", + "fileName": "JobGenerator.ts", + "wiki": "If the monitor's status has changed, the `handleNotifications` method of the `NotificationService` is invoked with the updated monitor's data.", + "cellName": "Flow 2: Initiate Notification Handling - JobGenerator.ts:L61", + "cellId": "31fe5a28-0640-4b52-841b-dd02e2ae646e", + "visible": true, + "startLine": 61, + "endLine": 61, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044", + "parentPath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "55ad645b-118a-478e-a00c-09ef82357008" + } + ] + }, + "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1": { + "path": "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1", + "cellName": "Flow 2: Dispatch Alerts - NotificationService.ts:L28-57", + "cellId": "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1", + "visible": true, + "parentCellId": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa" + }, + "server/src/service/v2/infrastructure/NotificationService.ts-simstep-8d1176f3-c755-401d-9ca8-234689b4b837": { + "path": "server/src/service/v2/infrastructure/NotificationService.ts-simstep-8d1176f3-c755-401d-9ca8-234689b4b837", + "fileName": "NotificationService.ts", + "wiki": "The `handleNotifications` method retrieves the full details for each associated notification channel. It then iterates through them, using specific provider services (e.g., `SlackService`, `EmailService`) to format and send a tailored alert message for each channel, notifying users that the monitor is down.", + "cellName": "Flow 2: Dispatch Alerts - NotificationService.ts:L28-57", + "cellId": "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1", + "visible": true, + "startLine": 28, + "endLine": 57, + "parentCellId": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa", + "parentPath": "server/src/service/v2/infrastructure/NotificationService.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "8d1176f3-c755-401d-9ca8-234689b4b837" + } + ] + }, + "54497b9a-4910-4df4-b016-480b9ac250a7": { + "path": "54497b9a-4910-4df4-b016-480b9ac250a7", + "cellName": "Flow 1:\nSubmit Form\nData for\nCreation", + "cellId": "54497b9a-4910-4df4-b016-480b9ac250a7", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f-54497b9a-4910-4df4-b016-480b9ac250a7": { + "path": "generated-edge-simstep-71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f-54497b9a-4910-4df4-b016-480b9ac250a7", + "fileName": "index.jsx", + "cellName": "Flow 1: Submit Form Data for Creation", + "cellId": "54497b9a-4910-4df4-b016-480b9ac250a7", + "visible": true, + "startLine": 96, + "endLine": 101, + "parentPath": "client/src/Pages/v1/Notifications/create/index.jsx", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f" + } + ] + }, + "dc135d8d-0515-403d-ae58-f3c04647b2ae": { + "path": "dc135d8d-0515-403d-ae58-f3c04647b2ae", + "cellName": "Flow 1:\nHTTP POST\nRequest", + "cellId": "dc135d8d-0515-403d-ae58-f3c04647b2ae", + "visible": true + }, + "generated-edge-simstep-162d8d2f-6cd1-45fa-8711-1ee85c5f86ef-dc135d8d-0515-403d-ae58-f3c04647b2ae": { + "path": "generated-edge-simstep-162d8d2f-6cd1-45fa-8711-1ee85c5f86ef-dc135d8d-0515-403d-ae58-f3c04647b2ae", + "fileName": "useNotifications.js", + "cellName": "Flow 1: HTTP POST Request", + "cellId": "dc135d8d-0515-403d-ae58-f3c04647b2ae", + "visible": true, + "startLine": 1064, + "endLine": 1066, + "parentPath": "client/src/Hooks/v1/useNotifications.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "162d8d2f-6cd1-45fa-8711-1ee85c5f86ef" + } + ] + }, + "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc": { + "path": "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "cellName": "Flow 1:\nController to\nDatabase Module", + "cellId": "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-73e63d2f-2b43-488d-9e86-170f511673f5-7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc": { + "path": "generated-edge-simstep-73e63d2f-2b43-488d-9e86-170f511673f5-7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "fileName": "notificationRoute.js", + "cellName": "Flow 1: Controller to Database Module", + "cellId": "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "visible": true, + "startLine": 55, + "endLine": 55, + "parentPath": "server/src/routes/v1/notificationRoute.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "73e63d2f-2b43-488d-9e86-170f511673f5" + } + ] + }, + "279155cd-ff31-41a2-8f5f-a538f01bfa66": { + "path": "279155cd-ff31-41a2-8f5f-a538f01bfa66", + "cellName": "Flow 1:\nUser Clicks\nTest Notification\nButton", + "cellId": "279155cd-ff31-41a2-8f5f-a538f01bfa66", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-d390ef45-f2fe-4806-a45a-cc9c84b48a5a-279155cd-ff31-41a2-8f5f-a538f01bfa66": { + "path": "generated-edge-simstep-d390ef45-f2fe-4806-a45a-cc9c84b48a5a-279155cd-ff31-41a2-8f5f-a538f01bfa66", + "fileName": "notificationModule.js", + "cellName": "Flow 1: User Clicks Test Notification Button", + "cellId": "279155cd-ff31-41a2-8f5f-a538f01bfa66", + "visible": true, + "startLine": 281, + "endLine": 285, + "parentPath": "server/src/db/v1/modules/notificationModule.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "d390ef45-f2fe-4806-a45a-cc9c84b48a5a" + } + ] + }, + "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f": { + "path": "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "cellName": "Flow 1:\nController to\nNotification Service", + "cellId": "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-6f2bf027-50d9-472e-9569-808a64fce7cf-57018b30-e2f4-4eb2-a1da-fe75afbb8c2f": { + "path": "generated-edge-simstep-6f2bf027-50d9-472e-9569-808a64fce7cf-57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "fileName": "notificationRoute.js", + "cellName": "Flow 1: Controller to Notification Service", + "cellId": "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "visible": true, + "startLine": 22, + "endLine": 22, + "parentPath": "server/src/routes/v1/notificationRoute.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "6f2bf027-50d9-472e-9569-808a64fce7cf" + } + ] + }, + "50797935-1447-47a4-891b-9e886e5601fa": { + "path": "50797935-1447-47a4-891b-9e886e5601fa", + "cellName": "Flow 2:\nStatus Change\nDetected", + "cellId": "50797935-1447-47a4-891b-9e886e5601fa", + "visible": true, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044" + }, + "generated-edge-simstep-b7ec7415-fcae-4077-8c19-302a015b00b5-50797935-1447-47a4-891b-9e886e5601fa": { + "path": "generated-edge-simstep-b7ec7415-fcae-4077-8c19-302a015b00b5-50797935-1447-47a4-891b-9e886e5601fa", + "fileName": "JobGenerator.ts", + "cellName": "Flow 2: Status Change Detected", + "cellId": "50797935-1447-47a4-891b-9e886e5601fa", + "visible": true, + "startLine": 60, + "endLine": 60, + "parentPath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "b7ec7415-fcae-4077-8c19-302a015b00b5" + } + ] + }, + "c778c6b9-c12e-45c0-8682-2c9e892867b0": { + "path": "c778c6b9-c12e-45c0-8682-2c9e892867b0", + "cellName": "Flow 2:\nMonitor Data\nPassed to\nNotification Service", + "cellId": "c778c6b9-c12e-45c0-8682-2c9e892867b0", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f" + }, + "generated-edge-simstep-b7259d77-c8df-4060-ac03-58728ae3eb54-c778c6b9-c12e-45c0-8682-2c9e892867b0": { + "path": "generated-edge-simstep-b7259d77-c8df-4060-ac03-58728ae3eb54-c778c6b9-c12e-45c0-8682-2c9e892867b0", + "fileName": "JobGenerator.ts", + "cellName": "Flow 2: Monitor Data Passed to Notification Service", + "cellId": "c778c6b9-c12e-45c0-8682-2c9e892867b0", + "visible": true, + "startLine": 7, + "endLine": 7, + "parentPath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "b7259d77-c8df-4060-ac03-58728ae3eb54" + } + ] + }, + "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc": { + "path": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc", + "cellName": "Routes", + "cellId": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562": { + "path": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "cellName": "index.jsx", + "cellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "visible": true, + "parentCellId": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc" + }, + "9d662180-8b65-4426-ad37-cfed4bc01fb4": { + "path": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "cellName": "StatusPage", + "cellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "773e1065-b0a5-46a9-b0cc-6b963f69f5cc": { + "path": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "cellName": "statusPageRoute.js", + "cellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "17a4f435-5a8e-4433-84a6-9ac17362709a": { + "path": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "cellName": "statusPageController.js", + "cellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "08602a28-f073-484e-9403-7eb798126dd4": { + "path": "08602a28-f073-484e-9403-7eb798126dd4", + "cellName": "Create", + "cellId": "08602a28-f073-484e-9403-7eb798126dd4", + "visible": true, + "parentCellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4" + }, + "8ef70566-37d9-48fb-b5e8-37e5b68768a2": { + "path": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "cellName": "Status", + "cellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "visible": true, + "parentCellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4" + }, + "f8541fe3-4bcf-4833-a226-66b5301adf58": { + "path": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "cellName": "statusPageModule.js", + "cellId": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745": { + "path": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745", + "cellName": "index.jsx", + "cellId": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4" + }, + "56f6775e-9843-4ca2-bc8c-1c263b6c35e9": { + "path": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9", + "cellName": "Hooks", + "cellId": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4" + }, + "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3": { + "path": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3", + "cellName": "index.jsx", + "cellId": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3", + "visible": true, + "parentCellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2" + }, + "79da0721-d810-4f7f-9cf9-3ea462bf0bff": { + "path": "79da0721-d810-4f7f-9cf9-3ea462bf0bff", + "cellName": "Hooks", + "cellId": "79da0721-d810-4f7f-9cf9-3ea462bf0bff", + "visible": true, + "parentCellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2" + }, + "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a": { + "path": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a", + "cellName": "useCreateStatusPage.jsx", + "cellId": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a", + "visible": true, + "parentCellId": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9" + }, + "5b2cdab8-66f1-40a1-9621-0efaf54e04ae": { + "path": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae", + "cellName": "useStatusPageFetch.jsx", + "cellId": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae", + "visible": true, + "parentCellId": "79da0721-d810-4f7f-9cf9-3ea462bf0bff" + }, + "00289d11-0015-4b2a-9c5b-dc9fd0f32b80": { + "path": "00289d11-0015-4b2a-9c5b-dc9fd0f32b80", + "cellName": "Create Status Page: User Initiates Creation - index.jsx:L26-313", + "cellId": "00289d11-0015-4b2a-9c5b-dc9fd0f32b80", + "visible": true, + "parentCellId": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745" + }, + "client/src/Pages/v1/StatusPage/Create/index.jsx-simstep-de7b5d1f-b370-42a4-a410-4178f1e2d3a6": { + "path": "client/src/Pages/v1/StatusPage/Create/index.jsx-simstep-de7b5d1f-b370-42a4-a410-4178f1e2d3a6", + "fileName": "index.jsx", + "wiki": "The user navigates to the 'Create Status Page' screen. The `CreateStatusPage` component renders the UI, including tabs for configuration, and initializes the form state.", + "cellName": "Create Status Page: User Initiates Creation - index.jsx:L26-313", + "cellId": "00289d11-0015-4b2a-9c5b-dc9fd0f32b80", + "visible": true, + "startLine": 26, + "endLine": 313, + "parentCellId": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745", + "parentPath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "de7b5d1f-b370-42a4-a410-4178f1e2d3a6" + } + ] + }, + "aa5d6d07-05f0-45f3-b19a-199af50e35f1": { + "path": "aa5d6d07-05f0-45f3-b19a-199af50e35f1", + "cellName": "Create Status Page: Client Hook Sends API Request - useCreateStatusPage.jsx:L8-11", + "cellId": "aa5d6d07-05f0-45f3-b19a-199af50e35f1", + "visible": true, + "parentCellId": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a" + }, + "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx-simstep-3c99ae6a-b671-4f47-bed0-ddccd8ff5d35": { + "path": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx-simstep-3c99ae6a-b671-4f47-bed0-ddccd8ff5d35", + "fileName": "useCreateStatusPage.jsx", + "wiki": "The `useCreateStatusPage` hook receives the form data and uses the `networkService` to send a request to the backend API to create or update the status page.", + "cellName": "Create Status Page: Client Hook Sends API Request - useCreateStatusPage.jsx:L8-11", + "cellId": "aa5d6d07-05f0-45f3-b19a-199af50e35f1", + "visible": true, + "startLine": 8, + "endLine": 11, + "parentCellId": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a", + "parentPath": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "3c99ae6a-b671-4f47-bed0-ddccd8ff5d35" + } + ] + }, + "3efddcad-588f-4492-9808-b0c81c823c96": { + "path": "3efddcad-588f-4492-9808-b0c81c823c96", + "cellName": "Create Status Page: Backend Route Handling - statusPageRoute.js:L14", + "cellId": "3efddcad-588f-4492-9808-b0c81c823c96", + "visible": true, + "parentCellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc" + }, + "server/src/routes/v1/statusPageRoute.js-simstep-84f11567-b3c4-42a2-aa7f-d78c1412099a": { + "path": "server/src/routes/v1/statusPageRoute.js-simstep-84f11567-b3c4-42a2-aa7f-d78c1412099a", + "fileName": "statusPageRoute.js", + "wiki": "The backend router receives the POST request on `/api/v1/status-page`. It uses `multer` to handle the file upload and forwards the request to the `createStatusPage` method in the `StatusPageController`.", + "cellName": "Create Status Page: Backend Route Handling - statusPageRoute.js:L14", + "cellId": "3efddcad-588f-4492-9808-b0c81c823c96", + "visible": true, + "startLine": 14, + "endLine": 14, + "parentCellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "parentPath": "server/src/routes/v1/statusPageRoute.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "84f11567-b3c4-42a2-aa7f-d78c1412099a" + } + ] + }, + "f15ef77e-380f-4768-98cb-7f0dd4113841": { + "path": "f15ef77e-380f-4768-98cb-7f0dd4113841", + "cellName": "Create Status Page: Controller Logic - statusPageController.js:L18-24", + "cellId": "f15ef77e-380f-4768-98cb-7f0dd4113841", + "visible": true, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a" + }, + "server/src/controllers/v1/statusPageController.js-simstep-01694730-8a75-4949-8815-bb4d6e3e2b42": { + "path": "server/src/controllers/v1/statusPageController.js-simstep-01694730-8a75-4949-8815-bb4d6e3e2b42", + "fileName": "statusPageController.js", + "wiki": "The `StatusPageController` validates the incoming request body and file. It then calls the `statusPageModule` to persist the new status page in the database.", + "cellName": "Create Status Page: Controller Logic - statusPageController.js:L18-24", + "cellId": "f15ef77e-380f-4768-98cb-7f0dd4113841", + "visible": true, + "startLine": 18, + "endLine": 24, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "01694730-8a75-4949-8815-bb4d6e3e2b42" + } + ] + }, + "e234917e-a950-48f5-b864-cd957b2216e1": { + "path": "e234917e-a950-48f5-b864-cd957b2216e1", + "cellName": "Create Status Page: Database Record Creation - statusPageModule.js:L14-34", + "cellId": "e234917e-a950-48f5-b864-cd957b2216e1", + "visible": true, + "parentCellId": "f8541fe3-4bcf-4833-a226-66b5301adf58" + }, + "server/src/db/v1/modules/statusPageModule.js-simstep-b6fa93cd-6fca-4d9e-969a-5400e2437ee8": { + "path": "server/src/db/v1/modules/statusPageModule.js-simstep-b6fa93cd-6fca-4d9e-969a-5400e2437ee8", + "fileName": "statusPageModule.js", + "wiki": "The `StatusPageModule` creates a new instance of the `StatusPage` Mongoose model, populates it with the provided data including the image buffer, and saves it to the MongoDB database. It also handles duplicate URL errors.", + "cellName": "Create Status Page: Database Record Creation - statusPageModule.js:L14-34", + "cellId": "e234917e-a950-48f5-b864-cd957b2216e1", + "visible": true, + "startLine": 14, + "endLine": 34, + "parentCellId": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "parentPath": "server/src/db/v1/modules/statusPageModule.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "b6fa93cd-6fca-4d9e-969a-5400e2437ee8" + } + ] + }, + "058ded1f-83ce-40a7-a99d-d178c81fe557": { + "path": "058ded1f-83ce-40a7-a99d-d178c81fe557", + "cellName": "Create Status Page: Controller Sends Success Response - statusPageController.js:L25-28", + "cellId": "058ded1f-83ce-40a7-a99d-d178c81fe557", + "visible": true, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a" + }, + "server/src/controllers/v1/statusPageController.js-simstep-2a115421-5175-4c8f-8cdd-62f3fe148af5": { + "path": "server/src/controllers/v1/statusPageController.js-simstep-2a115421-5175-4c8f-8cdd-62f3fe148af5", + "fileName": "statusPageController.js", + "wiki": "The controller receives the newly created status page object from the database module and sends a successful JSON response back to the client, including a success message and the new data.", + "cellName": "Create Status Page: Controller Sends Success Response - statusPageController.js:L25-28", + "cellId": "058ded1f-83ce-40a7-a99d-d178c81fe557", + "visible": true, + "startLine": 25, + "endLine": 28, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "2a115421-5175-4c8f-8cdd-62f3fe148af5" + } + ] + }, + "30aaee38-f509-4e19-a917-e6d2f4bba61c": { + "path": "30aaee38-f509-4e19-a917-e6d2f4bba61c", + "cellName": "View Status Page: User Navigates to URL - index.jsx:L148-152", + "cellId": "30aaee38-f509-4e19-a917-e6d2f4bba61c", + "visible": true, + "parentCellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562" + }, + "client/src/Routes/index.jsx-simstep-132354d7-df7a-4e03-9d00-16d54f836a1b": { + "path": "client/src/Routes/index.jsx-simstep-132354d7-df7a-4e03-9d00-16d54f836a1b", + "fileName": "index.jsx", + "wiki": "A user accesses a status page URL (e.g., /status/uptime/my-awesome-service). The React router matches the path and renders the `Status` component, which is responsible for fetching and displaying the page details.", + "cellName": "View Status Page: User Navigates to URL - index.jsx:L148-152", + "cellId": "30aaee38-f509-4e19-a917-e6d2f4bba61c", + "visible": true, + "startLine": 148, + "endLine": 152, + "parentCellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "parentPath": "client/src/Routes/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "132354d7-df7a-4e03-9d00-16d54f836a1b" + } + ] + }, + "ecfb2d04-2681-4f16-af75-632f69e1a868": { + "path": "ecfb2d04-2681-4f16-af75-632f69e1a868", + "cellName": "View Status Page: Client Hook Sends API Request - useStatusPageFetch.jsx:L15-18", + "cellId": "ecfb2d04-2681-4f16-af75-632f69e1a868", + "visible": true, + "parentCellId": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae" + }, + "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx-simstep-b57bb249-9066-454e-aa8c-89108fdfcc2e": { + "path": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx-simstep-b57bb249-9066-454e-aa8c-89108fdfcc2e", + "fileName": "useStatusPageFetch.jsx", + "wiki": "The `useStatusPageFetch` hook uses the `networkService` to send a GET request to the backend API to retrieve the status page data based on its URL.", + "cellName": "View Status Page: Client Hook Sends API Request - useStatusPageFetch.jsx:L15-18", + "cellId": "ecfb2d04-2681-4f16-af75-632f69e1a868", + "visible": true, + "startLine": 15, + "endLine": 18, + "parentCellId": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae", + "parentPath": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "b57bb249-9066-454e-aa8c-89108fdfcc2e" + } + ] + }, + "2e26cd2f-1413-4c16-8e63-ea7808ab6c52": { + "path": "2e26cd2f-1413-4c16-8e63-ea7808ab6c52", + "cellName": "View Status Page: Backend Route Handling - statusPageRoute.js:L16", + "cellId": "2e26cd2f-1413-4c16-8e63-ea7808ab6c52", + "visible": true, + "parentCellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc" + }, + "server/src/routes/v1/statusPageRoute.js-simstep-477c6821-b29b-4b38-8930-ced21b8a75e5": { + "path": "server/src/routes/v1/statusPageRoute.js-simstep-477c6821-b29b-4b38-8930-ced21b8a75e5", + "fileName": "statusPageRoute.js", + "wiki": "The backend router receives the GET request on `/api/v1/status-page/:url` and forwards it to the `getStatusPageByUrl` method in the `StatusPageController`.", + "cellName": "View Status Page: Backend Route Handling - statusPageRoute.js:L16", + "cellId": "2e26cd2f-1413-4c16-8e63-ea7808ab6c52", + "visible": true, + "startLine": 16, + "endLine": 16, + "parentCellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "parentPath": "server/src/routes/v1/statusPageRoute.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "477c6821-b29b-4b38-8930-ced21b8a75e5" + } + ] + }, + "9f217674-4cd7-4ef8-b088-d5620042a7ba": { + "path": "9f217674-4cd7-4ef8-b088-d5620042a7ba", + "cellName": "View Status Page: Controller Logic - statusPageController.js:L75-78", + "cellId": "9f217674-4cd7-4ef8-b088-d5620042a7ba", + "visible": true, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a" + }, + "server/src/controllers/v1/statusPageController.js-simstep-5bdbd179-ce8f-4171-a073-af72a99b4468": { + "path": "server/src/controllers/v1/statusPageController.js-simstep-5bdbd179-ce8f-4171-a073-af72a99b4468", + "fileName": "statusPageController.js", + "wiki": "The `StatusPageController` validates the request parameters and calls the `statusPageModule` to retrieve the status page data from the database.", + "cellName": "View Status Page: Controller Logic - statusPageController.js:L75-78", + "cellId": "9f217674-4cd7-4ef8-b088-d5620042a7ba", + "visible": true, + "startLine": 75, + "endLine": 78, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "5bdbd179-ce8f-4171-a073-af72a99b4468" + } + ] + }, + "26da60f9-dde4-44ec-be89-91858cc14f90": { + "path": "26da60f9-dde4-44ec-be89-91858cc14f90", + "cellName": "View Status Page: Database Record Retrieval - statusPageModule.js:L89-299", + "cellId": "26da60f9-dde4-44ec-be89-91858cc14f90", + "visible": true, + "parentCellId": "f8541fe3-4bcf-4833-a226-66b5301adf58" + }, + "server/src/db/v1/modules/statusPageModule.js-simstep-ceb044e9-6545-4d04-bf79-ec8146eb8ec9": { + "path": "server/src/db/v1/modules/statusPageModule.js-simstep-ceb044e9-6545-4d04-bf79-ec8146eb8ec9", + "fileName": "statusPageModule.js", + "wiki": "The `StatusPageModule` executes a complex MongoDB aggregation pipeline to find the status page by its URL, and then look up and join data for all associated monitors and their recent checks.", + "cellName": "View Status Page: Database Record Retrieval - statusPageModule.js:L89-299", + "cellId": "26da60f9-dde4-44ec-be89-91858cc14f90", + "visible": true, + "startLine": 89, + "endLine": 299, + "parentCellId": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "parentPath": "server/src/db/v1/modules/statusPageModule.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "ceb044e9-6545-4d04-bf79-ec8146eb8ec9" + } + ] + }, + "d76e70e0-41f9-46cc-b775-b2027c5a0d5b": { + "path": "d76e70e0-41f9-46cc-b775-b2027c5a0d5b", + "cellName": "View Status Page: Controller Sends Success Response - statusPageController.js:L79-82", + "cellId": "d76e70e0-41f9-46cc-b775-b2027c5a0d5b", + "visible": true, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a" + }, + "server/src/controllers/v1/statusPageController.js-simstep-83224406-2f1d-4030-9beb-d65c2baf544d": { + "path": "server/src/controllers/v1/statusPageController.js-simstep-83224406-2f1d-4030-9beb-d65c2baf544d", + "fileName": "statusPageController.js", + "wiki": "The controller receives the status page data and sends a successful JSON response back to the client.", + "cellName": "View Status Page: Controller Sends Success Response - statusPageController.js:L79-82", + "cellId": "d76e70e0-41f9-46cc-b775-b2027c5a0d5b", + "visible": true, + "startLine": 79, + "endLine": 82, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "83224406-2f1d-4030-9beb-d65c2baf544d" + } + ] + }, + "f59b91de-5196-4a43-9d57-3f28402c0672": { + "path": "f59b91de-5196-4a43-9d57-3f28402c0672", + "cellName": "View Status Page: Component Renders UI - index.jsx:L145-163", + "cellId": "f59b91de-5196-4a43-9d57-3f28402c0672", + "visible": true, + "parentCellId": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3" + }, + "client/src/Pages/v1/StatusPage/Status/index.jsx-simstep-3d51e811-a659-442e-97e0-00798cd15647": { + "path": "client/src/Pages/v1/StatusPage/Status/index.jsx-simstep-3d51e811-a659-442e-97e0-00798cd15647", + "fileName": "index.jsx", + "wiki": "The `Status` component re-renders with the fetched data. It displays the company name, logo, overall status bar, and a list of monitors with their individual status and historical uptime charts.", + "cellName": "View Status Page: Component Renders UI - index.jsx:L145-163", + "cellId": "f59b91de-5196-4a43-9d57-3f28402c0672", + "visible": true, + "startLine": 145, + "endLine": 163, + "parentCellId": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3", + "parentPath": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "3d51e811-a659-442e-97e0-00798cd15647" + } + ] + }, + "de23eee2-b60a-4236-a890-3c5b78388bb3": { + "path": "de23eee2-b60a-4236-a890-3c5b78388bb3", + "cellName": "Create Status\nPage: Form\nSubmission", + "cellId": "de23eee2-b60a-4236-a890-3c5b78388bb3", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4" + }, + "generated-edge-simstep-d419f7b6-7b9b-4494-9fad-83e3ce208ca4-de23eee2-b60a-4236-a890-3c5b78388bb3": { + "path": "generated-edge-simstep-d419f7b6-7b9b-4494-9fad-83e3ce208ca4-de23eee2-b60a-4236-a890-3c5b78388bb3", + "fileName": "index.jsx", + "cellName": "Create Status Page: Form Submission", + "cellId": "de23eee2-b60a-4236-a890-3c5b78388bb3", + "visible": true, + "startLine": 143, + "endLine": 159, + "parentPath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "d419f7b6-7b9b-4494-9fad-83e3ce208ca4" + } + ] + }, + "d7faa713-4204-4a97-a172-d621da36c287": { + "path": "d7faa713-4204-4a97-a172-d621da36c287", + "cellName": "Create Status\nPage: API\nCall to\nBackend", + "cellId": "d7faa713-4204-4a97-a172-d621da36c287", + "visible": true + }, + "generated-edge-simstep-8a2ab372-73db-4b9f-bc0e-6a20f15a4d79-d7faa713-4204-4a97-a172-d621da36c287": { + "path": "generated-edge-simstep-8a2ab372-73db-4b9f-bc0e-6a20f15a4d79-d7faa713-4204-4a97-a172-d621da36c287", + "fileName": "useCreateStatusPage.jsx", + "cellName": "Create Status Page: API Call to Backend", + "cellId": "d7faa713-4204-4a97-a172-d621da36c287", + "visible": true, + "startLine": 912, + "endLine": 944, + "parentPath": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "8a2ab372-73db-4b9f-bc0e-6a20f15a4d79" + } + ] + }, + "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0": { + "path": "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "cellName": "Create Status\nPage: Data\nPassed to\nController", + "cellId": "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-0923a533-c572-4da3-ad87-cdfff111f3bb-a3f28fa6-8268-4c13-95a0-83e3b1abdcb0": { + "path": "generated-edge-simstep-0923a533-c572-4da3-ad87-cdfff111f3bb-a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "fileName": "statusPageRoute.js", + "cellName": "Create Status Page: Data Passed to Controller", + "cellId": "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "visible": true, + "startLine": 14, + "endLine": 14, + "parentPath": "server/src/routes/v1/statusPageRoute.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "0923a533-c572-4da3-ad87-cdfff111f3bb" + } + ] + }, + "9735611c-924b-49aa-92e8-10d8a86ac288": { + "path": "9735611c-924b-49aa-92e8-10d8a86ac288", + "cellName": "Create Status\nPage: Data\nSent to\nDB Module", + "cellId": "9735611c-924b-49aa-92e8-10d8a86ac288", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-67603f03-c088-4ed0-9f18-072435134cdd-9735611c-924b-49aa-92e8-10d8a86ac288": { + "path": "generated-edge-simstep-67603f03-c088-4ed0-9f18-072435134cdd-9735611c-924b-49aa-92e8-10d8a86ac288", + "fileName": "statusPageController.js", + "cellName": "Create Status Page: Data Sent to DB Module", + "cellId": "9735611c-924b-49aa-92e8-10d8a86ac288", + "visible": true, + "startLine": 21, + "endLine": 21, + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "67603f03-c088-4ed0-9f18-072435134cdd" + } + ] + }, + "40cb7d7b-2888-44de-bb7d-7735af70ba34": { + "path": "40cb7d7b-2888-44de-bb7d-7735af70ba34", + "cellName": "Create Status\nPage: Database\nResult Returned", + "cellId": "40cb7d7b-2888-44de-bb7d-7735af70ba34", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-ea4b3532-baae-4890-a73e-2ca385d0319e-40cb7d7b-2888-44de-bb7d-7735af70ba34": { + "path": "generated-edge-simstep-ea4b3532-baae-4890-a73e-2ca385d0319e-40cb7d7b-2888-44de-bb7d-7735af70ba34", + "fileName": "statusPageModule.js", + "cellName": "Create Status Page: Database Result Returned", + "cellId": "40cb7d7b-2888-44de-bb7d-7735af70ba34", + "visible": true, + "startLine": 27, + "endLine": 27, + "parentPath": "server/src/db/v1/modules/statusPageModule.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "ea4b3532-baae-4890-a73e-2ca385d0319e" + } + ] + }, + "c3f83811-8674-4dbb-9f84-79499f97fb1f": { + "path": "c3f83811-8674-4dbb-9f84-79499f97fb1f", + "cellName": "Create Status\nPage: UI\nReceives Response\nand Redirects", + "cellId": "c3f83811-8674-4dbb-9f84-79499f97fb1f", + "visible": true + }, + "generated-edge-simstep-210acc07-81c3-4394-b566-41f6031e8eeb-c3f83811-8674-4dbb-9f84-79499f97fb1f": { + "path": "generated-edge-simstep-210acc07-81c3-4394-b566-41f6031e8eeb-c3f83811-8674-4dbb-9f84-79499f97fb1f", + "fileName": "statusPageController.js", + "cellName": "Create Status Page: UI Receives Response and Redirects", + "cellId": "c3f83811-8674-4dbb-9f84-79499f97fb1f", + "visible": true, + "startLine": 151, + "endLine": 156, + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "210acc07-81c3-4394-b566-41f6031e8eeb" + } + ] + }, + "0575bf87-5362-4988-b895-7e514148d420": { + "path": "0575bf87-5362-4988-b895-7e514148d420", + "cellName": "View Status\nPage: Component\nTriggers Data\nFetch", + "cellId": "0575bf87-5362-4988-b895-7e514148d420", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-2ba49e28-1cac-47f5-9118-ab930788736a-0575bf87-5362-4988-b895-7e514148d420": { + "path": "generated-edge-simstep-2ba49e28-1cac-47f5-9118-ab930788736a-0575bf87-5362-4988-b895-7e514148d420", + "fileName": "index.jsx", + "cellName": "View Status Page: Component Triggers Data Fetch", + "cellId": "0575bf87-5362-4988-b895-7e514148d420", + "visible": true, + "startLine": 29, + "endLine": 29, + "parentPath": "client/src/Routes/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "2ba49e28-1cac-47f5-9118-ab930788736a" + } + ] + }, + "ae1c9341-0e39-4d91-a9ac-5cc8a627c247": { + "path": "ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "cellName": "View Status\nPage: API\nCall to\nBackend", + "cellId": "ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "visible": true + }, + "generated-edge-simstep-de0714d2-bf1e-4a1a-a389-60793de8b772-ae1c9341-0e39-4d91-a9ac-5cc8a627c247": { + "path": "generated-edge-simstep-de0714d2-bf1e-4a1a-a389-60793de8b772-ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "fileName": "useStatusPageFetch.jsx", + "cellName": "View Status Page: API Call to Backend", + "cellId": "ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "visible": true, + "startLine": 896, + "endLine": 902, + "parentPath": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "de0714d2-bf1e-4a1a-a389-60793de8b772" + } + ] + }, + "5735f30c-b55f-444a-9236-f3d75f68ed27": { + "path": "5735f30c-b55f-444a-9236-f3d75f68ed27", + "cellName": "View Status\nPage: Data\nPassed to\nController", + "cellId": "5735f30c-b55f-444a-9236-f3d75f68ed27", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-b79b8922-4de6-477d-8648-37b4bd9a6767-5735f30c-b55f-444a-9236-f3d75f68ed27": { + "path": "generated-edge-simstep-b79b8922-4de6-477d-8648-37b4bd9a6767-5735f30c-b55f-444a-9236-f3d75f68ed27", + "fileName": "statusPageRoute.js", + "cellName": "View Status Page: Data Passed to Controller", + "cellId": "5735f30c-b55f-444a-9236-f3d75f68ed27", + "visible": true, + "startLine": 16, + "endLine": 16, + "parentPath": "server/src/routes/v1/statusPageRoute.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "b79b8922-4de6-477d-8648-37b4bd9a6767" + } + ] + }, + "dace45d6-fb25-4dae-b940-49ef7d77bbed": { + "path": "dace45d6-fb25-4dae-b940-49ef7d77bbed", + "cellName": "View Status\nPage: URL\nSent to\nDB Module", + "cellId": "dace45d6-fb25-4dae-b940-49ef7d77bbed", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8-dace45d6-fb25-4dae-b940-49ef7d77bbed": { + "path": "generated-edge-simstep-788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8-dace45d6-fb25-4dae-b940-49ef7d77bbed", + "fileName": "statusPageController.js", + "cellName": "View Status Page: URL Sent to DB Module", + "cellId": "dace45d6-fb25-4dae-b940-49ef7d77bbed", + "visible": true, + "startLine": 78, + "endLine": 78, + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8" + } + ] + }, + "137a5ab7-1e49-42ec-869d-58f7f2380cb3": { + "path": "137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "cellName": "View Status\nPage: Database\nResult Returned", + "cellId": "137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4-137a5ab7-1e49-42ec-869d-58f7f2380cb3": { + "path": "generated-edge-simstep-4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4-137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "fileName": "statusPageModule.js", + "cellName": "View Status Page: Database Result Returned", + "cellId": "137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "visible": true, + "startLine": 291, + "endLine": 291, + "parentPath": "server/src/db/v1/modules/statusPageModule.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4" + } + ] + }, + "04a33e4f-1dfe-4c5a-8b30-01ed0591c380": { + "path": "04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "cellName": "View Status\nPage: UI\nReceives Data", + "cellId": "04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "visible": true + }, + "generated-edge-simstep-d5f3c300-9777-4e09-bb05-f1281c2ff29a-04a33e4f-1dfe-4c5a-8b30-01ed0591c380": { + "path": "generated-edge-simstep-d5f3c300-9777-4e09-bb05-f1281c2ff29a-04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "fileName": "statusPageController.js", + "cellName": "View Status Page: UI Receives Data", + "cellId": "04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "visible": true, + "startLine": 20, + "endLine": 26, + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "d5f3c300-9777-4e09-bb05-f1281c2ff29a" + } + ] + }, + "933a6cdc-349c-4725-b7ae-14cf780bfde8": { + "path": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "cellName": "checkHooks.js", + "cellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "d18dbe1d-2f2c-4336-bd21-47c1670f7bab": { + "path": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab", + "cellName": "checkRoute.js", + "cellId": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "d983e54f-2fbe-496a-932d-a827e1124804": { + "path": "d983e54f-2fbe-496a-932d-a827e1124804", + "cellName": "checkController.js", + "cellId": "d983e54f-2fbe-496a-932d-a827e1124804", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "273f3009-69d0-46fe-af57-3cd94a24b55b": { + "path": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "cellName": "Details", + "cellId": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff" + }, + "f90db867-e1e7-4554-b446-fa78fc6a8578": { + "path": "f90db867-e1e7-4554-b446-fa78fc6a8578", + "cellName": "checkService.js", + "cellId": "f90db867-e1e7-4554-b446-fa78fc6a8578", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "860b5eec-a1ce-4e1f-a316-e1586f03af96": { + "path": "860b5eec-a1ce-4e1f-a316-e1586f03af96", + "cellName": "checkModule.js", + "cellId": "860b5eec-a1ce-4e1f-a316-e1586f03af96", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "1807d5b9-54be-430b-8997-cada4dd9b521": { + "path": "1807d5b9-54be-430b-8997-cada4dd9b521", + "cellName": "index.jsx", + "cellId": "1807d5b9-54be-430b-8997-cada4dd9b521", + "visible": true, + "parentCellId": "273f3009-69d0-46fe-af57-3cd94a24b55b" + }, + "6aed78bb-62f1-4ac1-9b9a-9d5977e87404": { + "path": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404", + "cellName": "Components", + "cellId": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404", + "visible": true, + "parentCellId": "273f3009-69d0-46fe-af57-3cd94a24b55b" + }, + "5ac0878f-d238-4763-8464-a85b4885a40f": { + "path": "5ac0878f-d238-4763-8464-a85b4885a40f", + "cellName": "ResponseTable", + "cellId": "5ac0878f-d238-4763-8464-a85b4885a40f", + "visible": true, + "parentCellId": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404" + }, + "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27": { + "path": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27", + "cellName": "index.jsx", + "cellId": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27", + "visible": true, + "parentCellId": "5ac0878f-d238-4763-8464-a85b4885a40f" + }, + "78a8a7c5-41d3-4453-a7f7-ac632bbef288": { + "path": "78a8a7c5-41d3-4453-a7f7-ac632bbef288", + "cellName": "Navigate to Monitor Details - index.jsx:L27-188", + "cellId": "78a8a7c5-41d3-4453-a7f7-ac632bbef288", + "visible": true, + "parentCellId": "1807d5b9-54be-430b-8997-cada4dd9b521" + }, + "client/src/Pages/v1/Uptime/Details/index.jsx-simstep-35195328-9847-4678-b014-9331553f0d19": { + "path": "client/src/Pages/v1/Uptime/Details/index.jsx-simstep-35195328-9847-4678-b014-9331553f0d19", + "fileName": "index.jsx", + "wiki": "The user navigates to the details page for a specific uptime monitor. The `UptimeDetails` component mounts and initializes its state and hooks.", + "cellName": "Navigate to Monitor Details - index.jsx:L27-188", + "cellId": "78a8a7c5-41d3-4453-a7f7-ac632bbef288", + "visible": true, + "startLine": 27, + "endLine": 188, + "parentCellId": "1807d5b9-54be-430b-8997-cada4dd9b521", + "parentPath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "35195328-9847-4678-b014-9331553f0d19" + } + ] + }, + "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4": { + "path": "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4", + "cellName": "Fetch Monitor Checks Hook - checkHooks.js:L89-121", + "cellId": "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4", + "visible": true, + "parentCellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8" + }, + "client/src/Hooks/v1/checkHooks.js-simstep-8dc4347c-3e9e-40d7-ae6f-54684f2e6956": { + "path": "client/src/Hooks/v1/checkHooks.js-simstep-8dc4347c-3e9e-40d7-ae6f-54684f2e6956", + "fileName": "checkHooks.js", + "wiki": "The `useFetchChecksByMonitor` custom hook executes. It sets the loading state and prepares to call the network service to fetch the data from the backend API.", + "cellName": "Fetch Monitor Checks Hook - checkHooks.js:L89-121", + "cellId": "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4", + "visible": true, + "startLine": 89, + "endLine": 121, + "parentCellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "parentPath": "client/src/Hooks/v1/checkHooks.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "8dc4347c-3e9e-40d7-ae6f-54684f2e6956" + } + ] + }, + "cdd090be-0b62-45b4-baa0-fec01fc3ffd1": { + "path": "cdd090be-0b62-45b4-baa0-fec01fc3ffd1", + "cellName": "Route Request to Controller - checkRoute.js:L19", + "cellId": "cdd090be-0b62-45b4-baa0-fec01fc3ffd1", + "visible": true, + "parentCellId": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab" + }, + "server/src/routes/v1/checkRoute.js-simstep-c5fd192f-ea28-48a0-b3e7-5bc04f521192": { + "path": "server/src/routes/v1/checkRoute.js-simstep-c5fd192f-ea28-48a0-b3e7-5bc04f521192", + "fileName": "checkRoute.js", + "wiki": "The backend Express router receives the incoming GET request and matches it to the appropriate route, forwarding the request to the `getChecksByMonitor` method in the `CheckController`.", + "cellName": "Route Request to Controller - checkRoute.js:L19", + "cellId": "cdd090be-0b62-45b4-baa0-fec01fc3ffd1", + "visible": true, + "startLine": 19, + "endLine": 19, + "parentCellId": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab", + "parentPath": "server/src/routes/v1/checkRoute.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "c5fd192f-ea28-48a0-b3e7-5bc04f521192" + } + ] + }, + "499b46a2-f2a2-4ccd-aecd-012eade2bd23": { + "path": "499b46a2-f2a2-4ccd-aecd-012eade2bd23", + "cellName": "Fetch Checks from Database - checkService.js:L36-45", + "cellId": "499b46a2-f2a2-4ccd-aecd-012eade2bd23", + "visible": true, + "parentCellId": "f90db867-e1e7-4554-b446-fa78fc6a8578" + }, + "server/src/service/v1/business/checkService.js-simstep-83fea05d-8306-4a73-a43f-882cdd14f50f": { + "path": "server/src/service/v1/business/checkService.js-simstep-83fea05d-8306-4a73-a43f-882cdd14f50f", + "fileName": "checkService.js", + "wiki": "The `CheckService` calls the `checkModule` to interact with the database. It passes the necessary parameters to query for the check history.", + "cellName": "Fetch Checks from Database - checkService.js:L36-45", + "cellId": "499b46a2-f2a2-4ccd-aecd-012eade2bd23", + "visible": true, + "startLine": 36, + "endLine": 45, + "parentCellId": "f90db867-e1e7-4554-b446-fa78fc6a8578", + "parentPath": "server/src/service/v1/business/checkService.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "83fea05d-8306-4a73-a43f-882cdd14f50f" + } + ] + }, + "14df9f01-1358-442d-96f5-eab9a4c4abbf": { + "path": "14df9f01-1358-442d-96f5-eab9a4c4abbf", + "cellName": "Return Check Data - checkController.js:L87-90", + "cellId": "14df9f01-1358-442d-96f5-eab9a4c4abbf", + "visible": true, + "parentCellId": "d983e54f-2fbe-496a-932d-a827e1124804" + }, + "server/src/controllers/v1/checkController.js-simstep-5768fdc5-045c-48ef-b0c9-79769403648f": { + "path": "server/src/controllers/v1/checkController.js-simstep-5768fdc5-045c-48ef-b0c9-79769403648f", + "fileName": "checkController.js", + "wiki": "The database returns the query results, which flow back up through the `checkModule` and `checkService`. The `checkController` then formats a successful JSON response.", + "cellName": "Return Check Data - checkController.js:L87-90", + "cellId": "14df9f01-1358-442d-96f5-eab9a4c4abbf", + "visible": true, + "startLine": 87, + "endLine": 90, + "parentCellId": "d983e54f-2fbe-496a-932d-a827e1124804", + "parentPath": "server/src/controllers/v1/checkController.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "5768fdc5-045c-48ef-b0c9-79769403648f" + } + ] + }, + "ceb9cda3-968d-44aa-b223-49902adb93b4": { + "path": "ceb9cda3-968d-44aa-b223-49902adb93b4", + "cellName": "Update Frontend State - checkHooks.js:L109-110", + "cellId": "ceb9cda3-968d-44aa-b223-49902adb93b4", + "visible": true, + "parentCellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8" + }, + "client/src/Hooks/v1/checkHooks.js-simstep-faba07f3-7460-47ae-924d-f92f5b2e821d": { + "path": "client/src/Hooks/v1/checkHooks.js-simstep-faba07f3-7460-47ae-924d-f92f5b2e821d", + "fileName": "checkHooks.js", + "wiki": "The `useFetchChecksByMonitor` hook receives the data from the API call and updates the component's state with the new list of checks and the total count.", + "cellName": "Update Frontend State - checkHooks.js:L109-110", + "cellId": "ceb9cda3-968d-44aa-b223-49902adb93b4", + "visible": true, + "startLine": 109, + "endLine": 110, + "parentCellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "parentPath": "client/src/Hooks/v1/checkHooks.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "faba07f3-7460-47ae-924d-f92f5b2e821d" + } + ] + }, + "c996f7ca-9384-4252-b343-7bfc049d14bb": { + "path": "c996f7ca-9384-4252-b343-7bfc049d14bb", + "cellName": "Render Historical Data Table - index.jsx:L11-92", + "cellId": "c996f7ca-9384-4252-b343-7bfc049d14bb", + "visible": true, + "parentCellId": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27" + }, + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx-simstep-2b1651cd-2e70-4cc6-b2bb-23f94fa73d66": { + "path": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx-simstep-2b1651cd-2e70-4cc6-b2bb-23f94fa73d66", + "fileName": "index.jsx", + "wiki": "The `ResponseTable` component receives the list of checks and renders them in a data table, allowing the user to view the incident history, including status, response time, and when the check occurred.", + "cellName": "Render Historical Data Table - index.jsx:L11-92", + "cellId": "c996f7ca-9384-4252-b343-7bfc049d14bb", + "visible": true, + "startLine": 11, + "endLine": 92, + "parentCellId": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27", + "parentPath": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "2b1651cd-2e70-4cc6-b2bb-23f94fa73d66" + } + ] + }, + "e90e4943-6108-46e3-8a2c-a9a1c7361279": { + "path": "e90e4943-6108-46e3-8a2c-a9a1c7361279", + "cellName": "Trigger Check\nHistory Fetch", + "cellId": "e90e4943-6108-46e3-8a2c-a9a1c7361279", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-49979250-1a32-4da5-95e0-58652fe218f9-e90e4943-6108-46e3-8a2c-a9a1c7361279": { + "path": "generated-edge-simstep-49979250-1a32-4da5-95e0-58652fe218f9-e90e4943-6108-46e3-8a2c-a9a1c7361279", + "fileName": "index.jsx", + "cellName": "Trigger Check History Fetch", + "cellId": "e90e4943-6108-46e3-8a2c-a9a1c7361279", + "visible": true, + "startLine": 69, + "endLine": 77, + "parentPath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "49979250-1a32-4da5-95e0-58652fe218f9" + } + ] + }, + "2b8ae59e-ed8d-4837-9a9b-64eb480f982c": { + "path": "2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "cellName": "API Request\nfor Check\nHistory", + "cellId": "2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "visible": true + }, + "generated-edge-simstep-336b534d-8d6a-4639-af8e-889b38f51ab1-2b8ae59e-ed8d-4837-9a9b-64eb480f982c": { + "path": "generated-edge-simstep-336b534d-8d6a-4639-af8e-889b38f51ab1-2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "fileName": "checkHooks.js", + "cellName": "API Request for Check History", + "cellId": "2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "visible": true, + "startLine": 551, + "endLine": 562, + "parentPath": "client/src/Hooks/v1/checkHooks.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "336b534d-8d6a-4639-af8e-889b38f51ab1" + } + ] + }, + "839ba9f6-8a34-4919-a04a-13fba1795c61": { + "path": "839ba9f6-8a34-4919-a04a-13fba1795c61", + "cellName": "Forward Request\nto Service\nLayer", + "cellId": "839ba9f6-8a34-4919-a04a-13fba1795c61", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-4f73f0fe-b390-4fe1-a354-eb0857835126-839ba9f6-8a34-4919-a04a-13fba1795c61": { + "path": "generated-edge-simstep-4f73f0fe-b390-4fe1-a354-eb0857835126-839ba9f6-8a34-4919-a04a-13fba1795c61", + "fileName": "checkRoute.js", + "cellName": "Forward Request to Service Layer", + "cellId": "839ba9f6-8a34-4919-a04a-13fba1795c61", + "visible": true, + "startLine": 81, + "endLine": 85, + "parentPath": "server/src/routes/v1/checkRoute.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "4f73f0fe-b390-4fe1-a354-eb0857835126" + } + ] + }, + "42d3bc70-701a-4be0-96b4-d6c2f0519b49": { + "path": "42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "cellName": "Execute Database\nQuery", + "cellId": "42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-65da1a9e-041c-4656-9c5b-0a92b761a9be-42d3bc70-701a-4be0-96b4-d6c2f0519b49": { + "path": "generated-edge-simstep-65da1a9e-041c-4656-9c5b-0a92b761a9be-42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "fileName": "checkService.js", + "cellName": "Execute Database Query", + "cellId": "42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "visible": true, + "startLine": 28, + "endLine": 108, + "parentPath": "server/src/service/v1/business/checkService.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "65da1a9e-041c-4656-9c5b-0a92b761a9be" + } + ] + }, + "6c5b028c-356c-43c8-a35a-b47448cd8695": { + "path": "6c5b028c-356c-43c8-a35a-b47448cd8695", + "cellName": "API Response\nwith Check\nHistory", + "cellId": "6c5b028c-356c-43c8-a35a-b47448cd8695", + "visible": true + }, + "generated-edge-simstep-2c1bdec8-d0e6-4163-a99c-f27899b987c5-6c5b028c-356c-43c8-a35a-b47448cd8695": { + "path": "generated-edge-simstep-2c1bdec8-d0e6-4163-a99c-f27899b987c5-6c5b028c-356c-43c8-a35a-b47448cd8695", + "fileName": "checkController.js", + "cellName": "API Response with Check History", + "cellId": "6c5b028c-356c-43c8-a35a-b47448cd8695", + "visible": true, + "startLine": 560, + "endLine": 560, + "parentPath": "server/src/controllers/v1/checkController.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "2c1bdec8-d0e6-4163-a99c-f27899b987c5" + } + ] + }, + "74b0dd89-9fea-4219-af1f-9571316a9d5a": { + "path": "74b0dd89-9fea-4219-af1f-9571316a9d5a", + "cellName": "Pass Data\nto UI\nComponents", + "cellId": "74b0dd89-9fea-4219-af1f-9571316a9d5a", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-9405dce1-0565-4dfa-9539-a048472acda9-74b0dd89-9fea-4219-af1f-9571316a9d5a": { + "path": "generated-edge-simstep-9405dce1-0565-4dfa-9539-a048472acda9-74b0dd89-9fea-4219-af1f-9571316a9d5a", + "fileName": "checkHooks.js", + "cellName": "Pass Data to UI Components", + "cellId": "74b0dd89-9fea-4219-af1f-9571316a9d5a", + "visible": true, + "startLine": 175, + "endLine": 186, + "parentPath": "client/src/Hooks/v1/checkHooks.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "9405dce1-0565-4dfa-9539-a048472acda9" + } + ] + }, + "e2f99143-54b7-44dc-b566-1c2fb901bd16": { + "path": "e2f99143-54b7-44dc-b566-1c2fb901bd16", + "cellName": "v2", + "cellId": "e2f99143-54b7-44dc-b566-1c2fb901bd16", + "visible": true, + "parentCellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8" + }, + "043d3dac-d4bb-449b-a3a7-270d53b66dac": { + "path": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "cellName": "PageSpeed", + "cellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "7ef79284-534a-466f-a1f1-4e3a2f56be9d": { + "path": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "cellName": "business", + "cellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e" + }, + "b53384d7-b989-4ac7-bd68-1c3621174ef4": { + "path": "b53384d7-b989-4ac7-bd68-1c3621174ef4", + "cellName": "models", + "cellId": "b53384d7-b989-4ac7-bd68-1c3621174ef4", + "visible": true, + "parentCellId": "e2f99143-54b7-44dc-b566-1c2fb901bd16" + }, + "414c500b-7b68-42ba-8110-e855efbee750": { + "path": "414c500b-7b68-42ba-8110-e855efbee750", + "cellName": "Create", + "cellId": "414c500b-7b68-42ba-8110-e855efbee750", + "visible": true, + "parentCellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac" + }, + "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b": { + "path": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b", + "cellName": "Details", + "cellId": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b", + "visible": true, + "parentCellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac" + }, + "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7": { + "path": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7", + "cellName": "NetworkService.ts", + "cellId": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f" + }, + "f0ebd5da-21bb-4d96-8ff9-ca10409acc80": { + "path": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80", + "cellName": "CheckService.ts", + "cellId": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d" + }, + "7a3824cd-24c8-4b32-8b1d-e0bf357262c4": { + "path": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4", + "cellName": "MonitorService.ts", + "cellId": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d" + }, + "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc": { + "path": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc", + "cellName": "checks", + "cellId": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc", + "visible": true, + "parentCellId": "b53384d7-b989-4ac7-bd68-1c3621174ef4" + }, + "5ffe0baa-054a-4e5b-8b29-1254f07ea975": { + "path": "5ffe0baa-054a-4e5b-8b29-1254f07ea975", + "cellName": "index.jsx", + "cellId": "5ffe0baa-054a-4e5b-8b29-1254f07ea975", + "visible": true, + "parentCellId": "414c500b-7b68-42ba-8110-e855efbee750" + }, + "c4a7bb58-1d9c-4690-b6b8-460c1cc33702": { + "path": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702", + "cellName": "index.jsx", + "cellId": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702", + "visible": true, + "parentCellId": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b" + }, + "1425ec43-f0f4-4c8b-a140-176c079219d6": { + "path": "1425ec43-f0f4-4c8b-a140-176c079219d6", + "cellName": "Check.ts", + "cellId": "1425ec43-f0f4-4c8b-a140-176c079219d6", + "visible": true, + "parentCellId": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc" + }, + "0622b212-3a4a-484d-9b7a-125864d785cd": { + "path": "0622b212-3a4a-484d-9b7a-125864d785cd", + "cellName": "Create PageSpeed Monitor - index.jsx:L111-143", + "cellId": "0622b212-3a4a-484d-9b7a-125864d785cd", + "visible": true, + "parentCellId": "5ffe0baa-054a-4e5b-8b29-1254f07ea975" + }, + "client/src/Pages/v1/PageSpeed/Create/index.jsx-simstep-b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12": { + "path": "client/src/Pages/v1/PageSpeed/Create/index.jsx-simstep-b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12", + "fileName": "index.jsx", + "wiki": "The user navigates to the PageSpeed creation page, fills in the details for a new monitor (URL, name, interval), and submits the form. This triggers the `handleSubmit` function, which calls the `createMonitor` hook to send the data to the backend API.", + "cellName": "Create PageSpeed Monitor - index.jsx:L111-143", + "cellId": "0622b212-3a4a-484d-9b7a-125864d785cd", + "visible": true, + "startLine": 111, + "endLine": 143, + "parentCellId": "5ffe0baa-054a-4e5b-8b29-1254f07ea975", + "parentPath": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12" + } + ] + }, + "6527fb47-157e-4bba-9aed-adde061c5689": { + "path": "6527fb47-157e-4bba-9aed-adde061c5689", + "cellName": "Backend Requests PageSpeed Analysis - NetworkService.ts:L129-162", + "cellId": "6527fb47-157e-4bba-9aed-adde061c5689", + "visible": true, + "parentCellId": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7" + }, + "server/src/service/v2/infrastructure/NetworkService.ts-simstep-53968267-0c6e-42f4-88b3-cd90f44532ae": { + "path": "server/src/service/v2/infrastructure/NetworkService.ts-simstep-53968267-0c6e-42f4-88b3-cd90f44532ae", + "fileName": "NetworkService.ts", + "wiki": "A background job for the newly created monitor triggers the `NetworkService` to request a performance analysis from the Google PageSpeed Insights API. It constructs the API URL using the monitor's target URL and the configured PageSpeed API key.", + "cellName": "Backend Requests PageSpeed Analysis - NetworkService.ts:L129-162", + "cellId": "6527fb47-157e-4bba-9aed-adde061c5689", + "visible": true, + "startLine": 129, + "endLine": 162, + "parentCellId": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7", + "parentPath": "server/src/service/v2/infrastructure/NetworkService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "53968267-0c6e-42f4-88b3-cd90f44532ae" + } + ] + }, + "f89cbd87-c961-4f8b-92b6-714da57718d5": { + "path": "f89cbd87-c961-4f8b-92b6-714da57718d5", + "cellName": "Backend Processes Lighthouse Data - CheckService.ts:L82-102", + "cellId": "f89cbd87-c961-4f8b-92b6-714da57718d5", + "visible": true, + "parentCellId": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80" + }, + "server/src/service/v2/business/CheckService.ts-simstep-01206383-2db8-4b18-84d0-be2a0289c196": { + "path": "server/src/service/v2/business/CheckService.ts-simstep-01206383-2db8-4b18-84d0-be2a0289c196", + "fileName": "CheckService.ts", + "wiki": "The `CheckService` receives the raw data from the Google API. The `buildPagespeedCheck` function extracts and transforms the relevant scores and audit details into a structured `Check` object, preparing it for database storage.", + "cellName": "Backend Processes Lighthouse Data - CheckService.ts:L82-102", + "cellId": "f89cbd87-c961-4f8b-92b6-714da57718d5", + "visible": true, + "startLine": 82, + "endLine": 102, + "parentCellId": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80", + "parentPath": "server/src/service/v2/business/CheckService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "01206383-2db8-4b18-84d0-be2a0289c196" + } + ] + }, + "ca6ee27b-1fbf-49de-978a-d6de259cc3d5": { + "path": "ca6ee27b-1fbf-49de-978a-d6de259cc3d5", + "cellName": "Backend Aggregates Performance Data - MonitorService.ts:L106-120", + "cellId": "ca6ee27b-1fbf-49de-978a-d6de259cc3d5", + "visible": true, + "parentCellId": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4" + }, + "server/src/service/v2/business/MonitorService.ts-simstep-5616ac6d-c281-4879-82f1-6590c721c8f7": { + "path": "server/src/service/v2/business/MonitorService.ts-simstep-5616ac6d-c281-4879-82f1-6590c721c8f7", + "fileName": "MonitorService.ts", + "wiki": "When a user views the details of a PageSpeed monitor, the frontend requests historical data. The `MonitorService` on the backend uses a MongoDB aggregation pipeline to group checks by time intervals and calculate average scores for metrics like performance, SEO, etc. This is defined by helper functions like `getPageSpeedGroup`.", + "cellName": "Backend Aggregates Performance Data - MonitorService.ts:L106-120", + "cellId": "ca6ee27b-1fbf-49de-978a-d6de259cc3d5", + "visible": true, + "startLine": 106, + "endLine": 120, + "parentCellId": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4", + "parentPath": "server/src/service/v2/business/MonitorService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "5616ac6d-c281-4879-82f1-6590c721c8f7" + } + ] + }, + "9ef9e218-4742-487a-9a88-5c70e788ccdc": { + "path": "9ef9e218-4742-487a-9a88-5c70e788ccdc", + "cellName": "Frontend Renders Performance Charts - index.jsx:L91-126", + "cellId": "9ef9e218-4742-487a-9a88-5c70e788ccdc", + "visible": true, + "parentCellId": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702" + }, + "client/src/Pages/v1/PageSpeed/Details/index.jsx-simstep-0ec0eb06-05eb-4204-b077-9e47eb2cc316": { + "path": "client/src/Pages/v1/PageSpeed/Details/index.jsx-simstep-0ec0eb06-05eb-4204-b077-9e47eb2cc316", + "fileName": "index.jsx", + "wiki": "The `PageSpeedDetails` component receives the historical data from the backend. It then passes this data to specialized child components like `PageSpeedAreaChart` and `PerformanceReport` to render visualizations of the website's performance over time.", + "cellName": "Frontend Renders Performance Charts - index.jsx:L91-126", + "cellId": "9ef9e218-4742-487a-9a88-5c70e788ccdc", + "visible": true, + "startLine": 91, + "endLine": 126, + "parentCellId": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702", + "parentPath": "client/src/Pages/v1/PageSpeed/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "0ec0eb06-05eb-4204-b077-9e47eb2cc316" + } + ] + }, + "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438": { + "path": "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "cellName": "API Call:\nCreate Monitor", + "cellId": "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "visible": true + }, + "generated-edge-simstep-f8df1cad-6051-466e-a13e-47d710ba1c14-b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438": { + "path": "generated-edge-simstep-f8df1cad-6051-466e-a13e-47d710ba1c14-b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "fileName": "index.jsx", + "cellName": "API Call: Create Monitor", + "cellId": "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "visible": true, + "startLine": 119, + "endLine": 119, + "parentPath": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "f8df1cad-6051-466e-a13e-47d710ba1c14" + } + ] + }, + "0d3c8091-3d91-4e42-9b46-7e35ed726a7a": { + "path": "0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "cellName": "Google PageSpeed\nAPI Response", + "cellId": "0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e" + }, + "generated-edge-simstep-0b380571-3896-4365-b5dc-187494f02e0d-0d3c8091-3d91-4e42-9b46-7e35ed726a7a": { + "path": "generated-edge-simstep-0b380571-3896-4365-b5dc-187494f02e0d-0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "fileName": "NetworkService.ts", + "cellName": "Google PageSpeed API Response", + "cellId": "0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "visible": true, + "startLine": 156, + "endLine": 156, + "parentPath": "server/src/service/v2/infrastructure/NetworkService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "0b380571-3896-4365-b5dc-187494f02e0d" + } + ] + }, + "27c7191b-80a2-475b-9371-e106108aff1c": { + "path": "27c7191b-80a2-475b-9371-e106108aff1c", + "cellName": "Store Processed\nCheck Data", + "cellId": "27c7191b-80a2-475b-9371-e106108aff1c", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d" + }, + "generated-edge-simstep-a629a102-0df1-4798-a7a2-45c8f6cdad29-27c7191b-80a2-475b-9371-e106108aff1c": { + "path": "generated-edge-simstep-a629a102-0df1-4798-a7a2-45c8f6cdad29-27c7191b-80a2-475b-9371-e106108aff1c", + "fileName": "CheckService.ts", + "cellName": "Store Processed Check Data", + "cellId": "27c7191b-80a2-475b-9371-e106108aff1c", + "visible": true, + "startLine": 237, + "endLine": 268, + "parentPath": "server/src/service/v2/business/CheckService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "a629a102-0df1-4798-a7a2-45c8f6cdad29" + } + ] + }, + "7be432cd-2587-4eab-abdc-f7b9c1923597": { + "path": "7be432cd-2587-4eab-abdc-f7b9c1923597", + "cellName": "Aggregated Data\nTransmitted to\nFrontend", + "cellId": "7be432cd-2587-4eab-abdc-f7b9c1923597", + "visible": true + }, + "generated-edge-simstep-4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08-7be432cd-2587-4eab-abdc-f7b9c1923597": { + "path": "generated-edge-simstep-4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08-7be432cd-2587-4eab-abdc-f7b9c1923597", + "fileName": "MonitorService.ts", + "cellName": "Aggregated Data Transmitted to Frontend", + "cellId": "7be432cd-2587-4eab-abdc-f7b9c1923597", + "visible": true, + "startLine": 368, + "endLine": 372, + "parentPath": "server/src/service/v2/business/MonitorService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08" + } + ] + }, + "fe47576c-c186-4950-82a5-b2fd24eb2561": { + "path": "fe47576c-c186-4950-82a5-b2fd24eb2561", + "cellName": "config", + "cellId": "fe47576c-c186-4950-82a5-b2fd24eb2561", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "cb53d612-9b89-47da-911c-165d0382a7d8": { + "path": "cb53d612-9b89-47da-911c-165d0382a7d8", + "cellName": "routes.js", + "cellId": "cb53d612-9b89-47da-911c-165d0382a7d8", + "visible": true, + "parentCellId": "fe47576c-c186-4950-82a5-b2fd24eb2561" + }, + "8021b55d-2d3a-4416-b598-e134e76c9434": { + "path": "8021b55d-2d3a-4416-b598-e134e76c9434", + "cellName": "Maintenance", + "cellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "5f21f109-1967-416f-a649-9b48e78b90f6": { + "path": "5f21f109-1967-416f-a649-9b48e78b90f6", + "cellName": "maintenanceWindowRoute.js", + "cellId": "5f21f109-1967-416f-a649-9b48e78b90f6", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "a0cae18c-3c0e-40d3-ab03-eef7eace65d6": { + "path": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "cellName": "maintenanceWindowController.js", + "cellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "6f7119ac-d65a-4fdf-a6f2-31052e0f5549": { + "path": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "cellName": "CreateMaintenance", + "cellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434" + }, + "1eae6263-aaa2-4206-b92e-1e67f529c1c9": { + "path": "1eae6263-aaa2-4206-b92e-1e67f529c1c9", + "cellName": "index.jsx", + "cellId": "1eae6263-aaa2-4206-b92e-1e67f529c1c9", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434" + }, + "5ad4d33f-0932-479c-bb57-22ce6405758f": { + "path": "5ad4d33f-0932-479c-bb57-22ce6405758f", + "cellName": "MaintenanceTable", + "cellId": "5ad4d33f-0932-479c-bb57-22ce6405758f", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434" + }, + "26b8daff-aa48-4baa-9d6c-05f771bf0990": { + "path": "26b8daff-aa48-4baa-9d6c-05f771bf0990", + "cellName": "maintenanceWindowService.js", + "cellId": "26b8daff-aa48-4baa-9d6c-05f771bf0990", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "95ed6c41-ebeb-4c08-98c3-bab8b426789e": { + "path": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "cellName": "maintenanceWindowModule.js", + "cellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "92c244d2-e6a7-4804-a643-1e8d36baf9c6": { + "path": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "cellName": "index.jsx", + "cellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549" + }, + "015a17fc-3fcf-45e0-8936-19f5e2ca86a3": { + "path": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3", + "cellName": "hooks", + "cellId": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549" + }, + "e7ec5ca9-6d25-4bf2-9695-639556f7d141": { + "path": "e7ec5ca9-6d25-4bf2-9695-639556f7d141", + "cellName": "index.jsx", + "cellId": "e7ec5ca9-6d25-4bf2-9695-639556f7d141", + "visible": true, + "parentCellId": "5ad4d33f-0932-479c-bb57-22ce6405758f" + }, + "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4": { + "path": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4", + "cellName": "useMaintenanceActions.jsx", + "cellId": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4", + "visible": true, + "parentCellId": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3" + }, + "20335f29-a2ac-4e0c-aba6-28707748ecd4": { + "path": "20335f29-a2ac-4e0c-aba6-28707748ecd4", + "cellName": "Flow: Create Maintenance Window - User Interaction - index.jsx:L46-131", + "cellId": "20335f29-a2ac-4e0c-aba6-28707748ecd4", + "visible": true, + "parentCellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6" + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-2401ef7a-8d8e-4056-868a-547ab213daf1": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-2401ef7a-8d8e-4056-868a-547ab213daf1", + "fileName": "index.jsx", + "wiki": "A user navigates to the 'Create Maintenance Window' page. They fill out a form with details such as a name, start date/time, duration, recurrence, and select the monitors to which this window will apply.", + "cellName": "Flow: Create Maintenance Window - User Interaction - index.jsx:L46-131", + "cellId": "20335f29-a2ac-4e0c-aba6-28707748ecd4", + "visible": true, + "startLine": 46, + "endLine": 131, + "parentCellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "2401ef7a-8d8e-4056-868a-547ab213daf1" + } + ] + }, + "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d": { + "path": "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d", + "cellName": "Flow: Create Maintenance Window - Prepare API Request - useMaintenanceActions.jsx:L25-58", + "cellId": "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d", + "visible": true, + "parentCellId": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4" + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx-simstep-5e0d7085-099b-478a-afaa-47ad4dd07043": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx-simstep-5e0d7085-099b-478a-afaa-47ad4dd07043", + "fileName": "useMaintenanceActions.jsx", + "wiki": "The `handleSubmitForm` hook processes the form data, calculates start and end timestamps, and constructs a request object. It then determines whether to call `createMaintenanceWindow` or `editMaintenanceWindow` on the network service.", + "cellName": "Flow: Create Maintenance Window - Prepare API Request - useMaintenanceActions.jsx:L25-58", + "cellId": "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d", + "visible": true, + "startLine": 25, + "endLine": 58, + "parentCellId": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4", + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "5e0d7085-099b-478a-afaa-47ad4dd07043" + } + ] + }, + "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8": { + "path": "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8", + "cellName": "Flow: Create Maintenance Window - API Routing - routes.js:L43", + "cellId": "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8", + "visible": true, + "parentCellId": "cb53d612-9b89-47da-911c-165d0382a7d8" + }, + "server/src/config/routes.js-simstep-3e93ef65-8e2c-405b-8dc1-56d8e93937b3": { + "path": "server/src/config/routes.js-simstep-3e93ef65-8e2c-405b-8dc1-56d8e93937b3", + "fileName": "routes.js", + "wiki": "The backend Express server receives the request. The routing configuration maps the `/api/v1/maintenance-window` path to the `maintenanceWindowRoutes`, which in turn directs the POST request to the `createMaintenanceWindows` method in the controller.", + "cellName": "Flow: Create Maintenance Window - API Routing - routes.js:L43", + "cellId": "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8", + "visible": true, + "startLine": 43, + "endLine": 43, + "parentCellId": "cb53d612-9b89-47da-911c-165d0382a7d8", + "parentPath": "server/src/config/routes.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "3e93ef65-8e2c-405b-8dc1-56d8e93937b3" + } + ] + }, + "0fae6e75-38ea-444d-a1bf-0f820ba443b9": { + "path": "0fae6e75-38ea-444d-a1bf-0f820ba443b9", + "cellName": "Flow: Create Maintenance Window - Controller Logic - maintenanceWindowController.js:L27-41", + "cellId": "0fae6e75-38ea-444d-a1bf-0f820ba443b9", + "visible": true, + "parentCellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6" + }, + "server/src/controllers/v1/maintenanceWindowController.js-simstep-95fc1919-339d-4659-98cb-3dc50f662075": { + "path": "server/src/controllers/v1/maintenanceWindowController.js-simstep-95fc1919-339d-4659-98cb-3dc50f662075", + "fileName": "maintenanceWindowController.js", + "wiki": "The `maintenanceWindowController` validates the request body using Joi, ensures a `teamId` exists on the user object, and then calls the `maintenanceWindowService` to handle the business logic of creating the window.", + "cellName": "Flow: Create Maintenance Window - Controller Logic - maintenanceWindowController.js:L27-41", + "cellId": "0fae6e75-38ea-444d-a1bf-0f820ba443b9", + "visible": true, + "startLine": 27, + "endLine": 41, + "parentCellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "parentPath": "server/src/controllers/v1/maintenanceWindowController.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "95fc1919-339d-4659-98cb-3dc50f662075" + } + ] + }, + "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f": { + "path": "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f", + "cellName": "Flow: Create Maintenance Window - Business Logic Execution - maintenanceWindowService.js:L18-39", + "cellId": "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f", + "visible": true, + "parentCellId": "26b8daff-aa48-4baa-9d6c-05f771bf0990" + }, + "server/src/service/v1/business/maintenanceWindowService.js-simstep-6e2cf739-1514-4c37-9874-10b68a47010e": { + "path": "server/src/service/v1/business/maintenanceWindowService.js-simstep-6e2cf739-1514-4c37-9874-10b68a47010e", + "fileName": "maintenanceWindowService.js", + "wiki": "The `maintenanceWindowService` verifies that all monitors specified in the request belong to the user's team. It then iterates through each monitor ID and prepares a separate database transaction to create a maintenance window record for each one.", + "cellName": "Flow: Create Maintenance Window - Business Logic Execution - maintenanceWindowService.js:L18-39", + "cellId": "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f", + "visible": true, + "startLine": 18, + "endLine": 39, + "parentCellId": "26b8daff-aa48-4baa-9d6c-05f771bf0990", + "parentPath": "server/src/service/v1/business/maintenanceWindowService.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "6e2cf739-1514-4c37-9874-10b68a47010e" + } + ] + }, + "3890b98d-a133-4279-a961-5d2facc6a17a": { + "path": "3890b98d-a133-4279-a961-5d2facc6a17a", + "cellName": "Flow: Create Maintenance Window - Database Record Creation - maintenanceWindowModule.js:L7-22", + "cellId": "3890b98d-a133-4279-a961-5d2facc6a17a", + "visible": true, + "parentCellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e" + }, + "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-a3c47fd4-c701-46c2-9aff-94a667b3e368": { + "path": "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-a3c47fd4-c701-46c2-9aff-94a667b3e368", + "fileName": "maintenanceWindowModule.js", + "wiki": "The `maintenanceWindowModule` instantiates a new `MaintenanceWindow` Mongoose model. If the window is a one-time event, it sets an `expiry` field to the end time for automatic cleanup by MongoDB's TTL index. Finally, it saves the new document to the database.", + "cellName": "Flow: Create Maintenance Window - Database Record Creation - maintenanceWindowModule.js:L7-22", + "cellId": "3890b98d-a133-4279-a961-5d2facc6a17a", + "visible": true, + "startLine": 7, + "endLine": 22, + "parentCellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "parentPath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "a3c47fd4-c701-46c2-9aff-94a667b3e368" + } + ] + }, + "a5a84b08-8c70-4023-8d08-b74e23aab4d7": { + "path": "a5a84b08-8c70-4023-8d08-b74e23aab4d7", + "cellName": "Flow: Create Maintenance Window - UI Update - index.jsx:L111-119", + "cellId": "a5a84b08-8c70-4023-8d08-b74e23aab4d7", + "visible": true, + "parentCellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6" + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc", + "fileName": "index.jsx", + "wiki": "The frontend receives the success response. A success toast message is displayed, and the user is navigated back to the main maintenance windows table.", + "cellName": "Flow: Create Maintenance Window - UI Update - index.jsx:L111-119", + "cellId": "a5a84b08-8c70-4023-8d08-b74e23aab4d7", + "visible": true, + "startLine": 111, + "endLine": 119, + "parentCellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc" + } + ] + }, + "d145bb4c-e1e0-473a-b6bc-decc932b738b": { + "path": "d145bb4c-e1e0-473a-b6bc-decc932b738b", + "cellName": "Flow: View Maintenance Windows - Page Load - index.jsx:L25-47", + "cellId": "d145bb4c-e1e0-473a-b6bc-decc932b738b", + "visible": true, + "parentCellId": "1eae6263-aaa2-4206-b92e-1e67f529c1c9" + }, + "client/src/Pages/v1/Maintenance/index.jsx-simstep-82b5ea14-bb00-4511-aba4-76196029fd81": { + "path": "client/src/Pages/v1/Maintenance/index.jsx-simstep-82b5ea14-bb00-4511-aba4-76196029fd81", + "fileName": "index.jsx", + "wiki": "User navigates to the '/maintenance' page. The `Maintenance` component mounts and triggers a `useEffect` hook to fetch the list of maintenance windows.", + "cellName": "Flow: View Maintenance Windows - Page Load - index.jsx:L25-47", + "cellId": "d145bb4c-e1e0-473a-b6bc-decc932b738b", + "visible": true, + "startLine": 25, + "endLine": 47, + "parentCellId": "1eae6263-aaa2-4206-b92e-1e67f529c1c9", + "parentPath": "client/src/Pages/v1/Maintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "82b5ea14-bb00-4511-aba4-76196029fd81" + } + ] + }, + "c35a065c-2808-402c-877e-9055d956ce0d": { + "path": "c35a065c-2808-402c-877e-9055d956ce0d", + "cellName": "Flow: View Maintenance Windows - Controller and Service - maintenanceWindowController.js:L75", + "cellId": "c35a065c-2808-402c-877e-9055d956ce0d", + "visible": true, + "parentCellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6" + }, + "server/src/controllers/v1/maintenanceWindowController.js-simstep-1251da83-5949-4e24-a1d2-fc74ff785aa9": { + "path": "server/src/controllers/v1/maintenanceWindowController.js-simstep-1251da83-5949-4e24-a1d2-fc74ff785aa9", + "fileName": "maintenanceWindowController.js", + "wiki": "The backend routes the request to `maintenanceWindowController.getMaintenanceWindowsByTeamId`. This controller calls the corresponding service, which in turn calls the database module to query for the data.", + "cellName": "Flow: View Maintenance Windows - Controller and Service - maintenanceWindowController.js:L75", + "cellId": "c35a065c-2808-402c-877e-9055d956ce0d", + "visible": true, + "startLine": 75, + "endLine": 75, + "parentCellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "parentPath": "server/src/controllers/v1/maintenanceWindowController.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "1251da83-5949-4e24-a1d2-fc74ff785aa9" + } + ] + }, + "27d9160a-4f8d-4dff-a09b-1678bc28cf2c": { + "path": "27d9160a-4f8d-4dff-a09b-1678bc28cf2c", + "cellName": "Flow: View Maintenance Windows - Data Retrieval - maintenanceWindowModule.js:L37-66", + "cellId": "27d9160a-4f8d-4dff-a09b-1678bc28cf2c", + "visible": true, + "parentCellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e" + }, + "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-9bfbae5a-f487-40ef-86f5-04a1cc3c7662": { + "path": "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-9bfbae5a-f487-40ef-86f5-04a1cc3c7662", + "fileName": "maintenanceWindowModule.js", + "wiki": "The `maintenanceWindowModule` constructs a MongoDB query to find all maintenance windows for the given `teamId`, applying pagination (skip, limit) and sorting. It executes the query and returns the results along with the total count.", + "cellName": "Flow: View Maintenance Windows - Data Retrieval - maintenanceWindowModule.js:L37-66", + "cellId": "27d9160a-4f8d-4dff-a09b-1678bc28cf2c", + "visible": true, + "startLine": 37, + "endLine": 66, + "parentCellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "parentPath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "9bfbae5a-f487-40ef-86f5-04a1cc3c7662" + } + ] + }, + "970e2002-7c0e-40ef-80d8-e5e6d0a60735": { + "path": "970e2002-7c0e-40ef-80d8-e5e6d0a60735", + "cellName": "Flow: View Maintenance Windows - UI Rendering - index.jsx:L195-201", + "cellId": "970e2002-7c0e-40ef-80d8-e5e6d0a60735", + "visible": true, + "parentCellId": "e7ec5ca9-6d25-4bf2-9695-639556f7d141" + }, + "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx-simstep-acf97639-e291-4ca6-81a6-d0487397f42a": { + "path": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx-simstep-acf97639-e291-4ca6-81a6-d0487397f42a", + "fileName": "index.jsx", + "wiki": "The client receives the data and updates its state. The `MaintenanceTable` component then renders the list of maintenance windows in a table, displaying their names, schedules, and providing actions like edit or delete.", + "cellName": "Flow: View Maintenance Windows - UI Rendering - index.jsx:L195-201", + "cellId": "970e2002-7c0e-40ef-80d8-e5e6d0a60735", + "visible": true, + "startLine": 195, + "endLine": 201, + "parentCellId": "e7ec5ca9-6d25-4bf2-9695-639556f7d141", + "parentPath": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "acf97639-e291-4ca6-81a6-d0487397f42a" + } + ] + }, + "23630a1f-7452-4373-a864-7ec7821137f2": { + "path": "23630a1f-7452-4373-a864-7ec7821137f2", + "cellName": "Flow: Monitor Suppression - Job Execution Start - SuperSimpleQueueHelper.js:L30-45", + "cellId": "23630a1f-7452-4373-a864-7ec7821137f2", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-a47affaf-b952-46d6-9709-6120a77690e1": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-a47affaf-b952-46d6-9709-6120a77690e1", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "The `SuperSimpleQueue` processing system picks up a job to check a specific monitor. The job handler is retrieved from `SuperSimpleQueueHelper`.", + "cellName": "Flow: Monitor Suppression - Job Execution Start - SuperSimpleQueueHelper.js:L30-45", + "cellId": "23630a1f-7452-4373-a864-7ec7821137f2", + "visible": true, + "startLine": 30, + "endLine": 45, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "a47affaf-b952-46d6-9709-6120a77690e1" + } + ] + }, + "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba": { + "path": "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba", + "cellName": "Flow: Monitor Suppression - Fetching Maintenance Windows - SuperSimpleQueueHelper.js:L80-83", + "cellId": "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-2127e749-80b4-499a-ae14-8dc2dcc9db9b": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-2127e749-80b4-499a-ae14-8dc2dcc9db9b", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "The `isInMaintenanceWindow` function calls the database module to fetch all maintenance windows associated with the given monitor ID and team ID.", + "cellName": "Flow: Monitor Suppression - Fetching Maintenance Windows - SuperSimpleQueueHelper.js:L80-83", + "cellId": "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba", + "visible": true, + "startLine": 80, + "endLine": 83, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "2127e749-80b4-499a-ae14-8dc2dcc9db9b" + } + ] + }, + "0fe32416-db43-49b9-9438-3796ba1a0add": { + "path": "0fe32416-db43-49b9-9438-3796ba1a0add", + "cellName": "Flow: Monitor Suppression - Time Evaluation - SuperSimpleQueueHelper.js:L85-113", + "cellId": "0fe32416-db43-49b9-9438-3796ba1a0add", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-6ba6b9d1-5fc3-454e-8744-2022b5192ed9": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-6ba6b9d1-5fc3-454e-8744-2022b5192ed9", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "The `isInMaintenanceWindow` function iterates through the returned windows. For each active window, it checks if the current time (`new Date()`) is between the `start` and `end` times.", + "cellName": "Flow: Monitor Suppression - Time Evaluation - SuperSimpleQueueHelper.js:L85-113", + "cellId": "0fe32416-db43-49b9-9438-3796ba1a0add", + "visible": true, + "startLine": 85, + "endLine": 113, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "6ba6b9d1-5fc3-454e-8744-2022b5192ed9" + } + ] + }, + "01e042c3-40b4-416a-8d9e-6d6fd323d60b": { + "path": "01e042c3-40b4-416a-8d9e-6d6fd323d60b", + "cellName": "Flow: Monitor Suppression - Conditional Job Termination - SuperSimpleQueueHelper.js:L37-44", + "cellId": "01e042c3-40b4-416a-8d9e-6d6fd323d60b", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e59fe64e-f06b-4855-b631-923bb5a68540": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e59fe64e-f06b-4855-b631-923bb5a68540", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "If the `isInMaintenanceWindow` function returns true, the `getMonitorJob` logs that the monitor is in a maintenance window and terminates its execution for this cycle, effectively suppressing the check.", + "cellName": "Flow: Monitor Suppression - Conditional Job Termination - SuperSimpleQueueHelper.js:L37-44", + "cellId": "01e042c3-40b4-416a-8d9e-6d6fd323d60b", + "visible": true, + "startLine": 37, + "endLine": 44, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "e59fe64e-f06b-4855-b631-923bb5a68540" + } + ] + }, + "d3bdc6ff-875e-472e-88c8-eb10b20c526e": { + "path": "d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "cellName": "Flow: Create\nMaintenance Window\n- Form\nSubmission", + "cellId": "d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549" + }, + "generated-edge-simstep-f881435b-3bc2-453d-b6bf-56ad3a12f120-d3bdc6ff-875e-472e-88c8-eb10b20c526e": { + "path": "generated-edge-simstep-f881435b-3bc2-453d-b6bf-56ad3a12f120-d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "fileName": "index.jsx", + "cellName": "Flow: Create Maintenance Window - Form Submission", + "cellId": "d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "visible": true, + "startLine": 108, + "endLine": 109, + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "f881435b-3bc2-453d-b6bf-56ad3a12f120" + } + ] + }, + "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af": { + "path": "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "cellName": "Flow: Create\nMaintenance Window\n- API\nCall", + "cellId": "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "visible": true + }, + "generated-edge-simstep-0660b48b-139f-4ecf-b7d9-f12f544e0ab5-fd5e4447-123b-4b2d-8bce-1d1ae7cb00af": { + "path": "generated-edge-simstep-0660b48b-139f-4ecf-b7d9-f12f544e0ab5-fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "fileName": "useMaintenanceActions.jsx", + "cellName": "Flow: Create Maintenance Window - API Call", + "cellId": "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "visible": true, + "startLine": 762, + "endLine": 766, + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "0660b48b-139f-4ecf-b7d9-f12f544e0ab5" + } + ] + }, + "e2588541-3ffc-47c6-8f8a-41d30ea7fc72": { + "path": "e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "cellName": "Flow: Create\nMaintenance Window\n- Controller\nInvocation", + "cellId": "e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-e4cc5ce8-6da9-4281-9204-29c061ab13ce-e2588541-3ffc-47c6-8f8a-41d30ea7fc72": { + "path": "generated-edge-simstep-e4cc5ce8-6da9-4281-9204-29c061ab13ce-e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "fileName": "routes.js", + "cellName": "Flow: Create Maintenance Window - Controller Invocation", + "cellId": "e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "visible": true, + "startLine": 10, + "endLine": 10, + "parentPath": "server/src/config/routes.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "e4cc5ce8-6da9-4281-9204-29c061ab13ce" + } + ] + }, + "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f": { + "path": "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "cellName": "Flow: Create\nMaintenance Window\n- Service\nLayer Call", + "cellId": "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-c3d1a95c-e93d-499a-8830-a6d870902414-e1d59cf6-0d96-4e0a-ad17-2a7984a9157f": { + "path": "generated-edge-simstep-c3d1a95c-e93d-499a-8830-a6d870902414-e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "fileName": "maintenanceWindowController.js", + "cellName": "Flow: Create Maintenance Window - Service Layer Call", + "cellId": "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "visible": true, + "startLine": 35, + "endLine": 35, + "parentPath": "server/src/controllers/v1/maintenanceWindowController.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "c3d1a95c-e93d-499a-8830-a6d870902414" + } + ] + }, + "12d0ab8a-392f-48fa-9efa-06ffe1f3a252": { + "path": "12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "cellName": "Flow: Create\nMaintenance Window\n- Database\nModule Call", + "cellId": "12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-80aed28e-68b6-47bf-8bc9-ededb4824066-12d0ab8a-392f-48fa-9efa-06ffe1f3a252": { + "path": "generated-edge-simstep-80aed28e-68b6-47bf-8bc9-ededb4824066-12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "fileName": "maintenanceWindowService.js", + "cellName": "Flow: Create Maintenance Window - Database Module Call", + "cellId": "12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "visible": true, + "startLine": 28, + "endLine": 37, + "parentPath": "server/src/service/v1/business/maintenanceWindowService.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "80aed28e-68b6-47bf-8bc9-ededb4824066" + } + ] + }, + "fcf4eef4-394c-4872-9124-f7a0b32cdc3b": { + "path": "fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "cellName": "Flow: Create\nMaintenance Window\n- API\nResponse", + "cellId": "fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "visible": true + }, + "generated-edge-simstep-69fdc2a9-837f-466c-9562-a12319019580-fcf4eef4-394c-4872-9124-f7a0b32cdc3b": { + "path": "generated-edge-simstep-69fdc2a9-837f-466c-9562-a12319019580-fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "fileName": "maintenanceWindowModule.js", + "cellName": "Flow: Create Maintenance Window - API Response", + "cellId": "fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "visible": true, + "startLine": 37, + "endLine": 39, + "parentPath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "69fdc2a9-837f-466c-9562-a12319019580" + } + ] + }, + "7f921125-500f-4902-abc3-936ecac033d8": { + "path": "7f921125-500f-4902-abc3-936ecac033d8", + "cellName": "Flow: View\nMaintenance Windows\n- API\nCall", + "cellId": "7f921125-500f-4902-abc3-936ecac033d8", + "visible": true + }, + "generated-edge-simstep-7e20cd6c-d24e-4540-86d6-d6a9fa087314-7f921125-500f-4902-abc3-936ecac033d8": { + "path": "generated-edge-simstep-7e20cd6c-d24e-4540-86d6-d6a9fa087314-7f921125-500f-4902-abc3-936ecac033d8", + "fileName": "index.jsx", + "cellName": "Flow: View Maintenance Windows - API Call", + "cellId": "7f921125-500f-4902-abc3-936ecac033d8", + "visible": true, + "startLine": 824, + "endLine": 824, + "parentPath": "client/src/Pages/v1/Maintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "7e20cd6c-d24e-4540-86d6-d6a9fa087314" + } + ] + }, + "c3127c74-43f1-42d7-8da6-b0a494c3658f": { + "path": "c3127c74-43f1-42d7-8da6-b0a494c3658f", + "cellName": "Flow: View\nMaintenance Windows\n- Database\nQuery", + "cellId": "c3127c74-43f1-42d7-8da6-b0a494c3658f", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-feb8bd4f-339f-4132-a709-89c63c4efa9c-c3127c74-43f1-42d7-8da6-b0a494c3658f": { + "path": "generated-edge-simstep-feb8bd4f-339f-4132-a709-89c63c4efa9c-c3127c74-43f1-42d7-8da6-b0a494c3658f", + "fileName": "maintenanceWindowController.js", + "cellName": "Flow: View Maintenance Windows - Database Query", + "cellId": "c3127c74-43f1-42d7-8da6-b0a494c3658f", + "visible": true, + "startLine": 48, + "endLine": 48, + "parentPath": "server/src/controllers/v1/maintenanceWindowController.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "feb8bd4f-339f-4132-a709-89c63c4efa9c" + } + ] + }, + "6c9979cd-1cc1-4227-b535-2ec3a10ca27e": { + "path": "6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "cellName": "Flow: View\nMaintenance Windows\n- Data\nReturn to\nFrontend", + "cellId": "6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "visible": true + }, + "generated-edge-simstep-eda86dca-dea6-4671-a079-7d212f4f18c4-6c9979cd-1cc1-4227-b535-2ec3a10ca27e": { + "path": "generated-edge-simstep-eda86dca-dea6-4671-a079-7d212f4f18c4-6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "fileName": "maintenanceWindowModule.js", + "cellName": "Flow: View Maintenance Windows - Data Return to Frontend", + "cellId": "6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "visible": true, + "startLine": 77, + "endLine": 80, + "parentPath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "eda86dca-dea6-4671-a079-7d212f4f18c4" + } + ] + }, + "c99291bb-7316-440e-9a7f-323ee39e26b8": { + "path": "c99291bb-7316-440e-9a7f-323ee39e26b8", + "cellName": "Flow: Monitor\nSuppression -\nMaintenance Check\nCall", + "cellId": "c99291bb-7316-440e-9a7f-323ee39e26b8", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "generated-edge-simstep-87583096-f1ec-4160-ae52-5238f32ed0ab-c99291bb-7316-440e-9a7f-323ee39e26b8": { + "path": "generated-edge-simstep-87583096-f1ec-4160-ae52-5238f32ed0ab-c99291bb-7316-440e-9a7f-323ee39e26b8", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "Flow: Monitor Suppression - Maintenance Check Call", + "cellId": "c99291bb-7316-440e-9a7f-323ee39e26b8", + "visible": true, + "startLine": 36, + "endLine": 36, + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "87583096-f1ec-4160-ae52-5238f32ed0ab" + } + ] + }, + "8a13e585-2310-420b-9e92-211b6d65f201": { + "path": "8a13e585-2310-420b-9e92-211b6d65f201", + "cellName": "Flow: Monitor\nSuppression -\nDatabase Result\nReturn", + "cellId": "8a13e585-2310-420b-9e92-211b6d65f201", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "generated-edge-simstep-e6542e14-556d-482a-8c35-f7b03997be49-8a13e585-2310-420b-9e92-211b6d65f201": { + "path": "generated-edge-simstep-e6542e14-556d-482a-8c35-f7b03997be49-8a13e585-2310-420b-9e92-211b6d65f201", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "Flow: Monitor Suppression - Database Result Return", + "cellId": "8a13e585-2310-420b-9e92-211b6d65f201", + "visible": true, + "startLine": 69, + "endLine": 76, + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "e6542e14-556d-482a-8c35-f7b03997be49" + } + ] + }, + "40f4d9df-4c0f-4384-901c-8d7117e3b538": { + "path": "40f4d9df-4c0f-4384-901c-8d7117e3b538", + "cellName": "Flow: Monitor\nSuppression -\nReturn Suppression\nFlag", + "cellId": "40f4d9df-4c0f-4384-901c-8d7117e3b538", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "generated-edge-simstep-a715d98f-60bf-4f32-aae6-21ee4e09f4da-40f4d9df-4c0f-4384-901c-8d7117e3b538": { + "path": "generated-edge-simstep-a715d98f-60bf-4f32-aae6-21ee4e09f4da-40f4d9df-4c0f-4384-901c-8d7117e3b538", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "Flow: Monitor Suppression - Return Suppression Flag", + "cellId": "40f4d9df-4c0f-4384-901c-8d7117e3b538", + "visible": true, + "startLine": 114, + "endLine": 114, + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "a715d98f-60bf-4f32-aae6-21ee4e09f4da" + } + ] + }, + "a968db5d-8f6d-4fd7-9df0-8b332b521703": { + "path": "a968db5d-8f6d-4fd7-9df0-8b332b521703", + "cellName": "middleware", + "cellId": "a968db5d-8f6d-4fd7-9df0-8b332b521703", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "09082f73-e215-465f-9206-0a963fb4c7cb": { + "path": "09082f73-e215-465f-9206-0a963fb4c7cb", + "cellName": "v1", + "cellId": "09082f73-e215-465f-9206-0a963fb4c7cb", + "visible": true, + "parentCellId": "a968db5d-8f6d-4fd7-9df0-8b332b521703" + }, + "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19": { + "path": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "cellName": "Account", + "cellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "9edafb8a-3661-4a7e-8510-a47f3cbc7564": { + "path": "9edafb8a-3661-4a7e-8510-a47f3cbc7564", + "cellName": "inviteRoute.js", + "cellId": "9edafb8a-3661-4a7e-8510-a47f3cbc7564", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "001fa2e4-eadd-4474-b2ec-79d7ea43465f": { + "path": "001fa2e4-eadd-4474-b2ec-79d7ea43465f", + "cellName": "isAllowed.js", + "cellId": "001fa2e4-eadd-4474-b2ec-79d7ea43465f", + "visible": true, + "parentCellId": "09082f73-e215-465f-9206-0a963fb4c7cb" + }, + "6a186805-9d50-4bc3-9d56-e627a7ec2eb8": { + "path": "6a186805-9d50-4bc3-9d56-e627a7ec2eb8", + "cellName": "inviteController.js", + "cellId": "6a186805-9d50-4bc3-9d56-e627a7ec2eb8", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "33960806-1b9e-4d61-a163-e4bebf82d87a": { + "path": "33960806-1b9e-4d61-a163-e4bebf82d87a", + "cellName": "index.jsx", + "cellId": "33960806-1b9e-4d61-a163-e4bebf82d87a", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19" + }, + "425a4c15-043f-4d05-8832-da00e18f97b9": { + "path": "425a4c15-043f-4d05-8832-da00e18f97b9", + "cellName": "components", + "cellId": "425a4c15-043f-4d05-8832-da00e18f97b9", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19" + }, + "570278b7-9b72-4299-9fd9-46b253b48e9b": { + "path": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "cellName": "inviteService.js", + "cellId": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "08853f8a-f596-4df0-9190-dad04f54caef": { + "path": "08853f8a-f596-4df0-9190-dad04f54caef", + "cellName": "inviteModule.js", + "cellId": "08853f8a-f596-4df0-9190-dad04f54caef", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "1145f351-2257-411d-86f7-30bf8c26ad68": { + "path": "1145f351-2257-411d-86f7-30bf8c26ad68", + "cellName": "TeamPanel.jsx", + "cellId": "1145f351-2257-411d-86f7-30bf8c26ad68", + "visible": true, + "parentCellId": "425a4c15-043f-4d05-8832-da00e18f97b9" + }, + "3de2d413-5e9b-49f4-9409-74be32c52e0f": { + "path": "3de2d413-5e9b-49f4-9409-74be32c52e0f", + "cellName": "Client: Conditional UI Rendering Based on Role - index.jsx:L37-41", + "cellId": "3de2d413-5e9b-49f4-9409-74be32c52e0f", + "visible": true, + "parentCellId": "33960806-1b9e-4d61-a163-e4bebf82d87a" + }, + "client/src/Pages/v1/Account/index.jsx-simstep-2d5a952a-ce62-4948-9309-96b28ad74e59": { + "path": "client/src/Pages/v1/Account/index.jsx-simstep-2d5a952a-ce62-4948-9309-96b28ad74e59", + "fileName": "index.jsx", + "wiki": "A user with 'superadmin' or 'admin' role navigates to the Account page. The application checks the user's role stored in the Redux state to conditionally render the 'Team' management tab. Non-admin users will not see this option.", + "cellName": "Client: Conditional UI Rendering Based on Role - index.jsx:L37-41", + "cellId": "3de2d413-5e9b-49f4-9409-74be32c52e0f", + "visible": true, + "startLine": 37, + "endLine": 41, + "parentCellId": "33960806-1b9e-4d61-a163-e4bebf82d87a", + "parentPath": "client/src/Pages/v1/Account/index.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "2d5a952a-ce62-4948-9309-96b28ad74e59" + } + ] + }, + "b27f1bba-c655-40c6-a1dc-71e78c590c92": { + "path": "b27f1bba-c655-40c6-a1dc-71e78c590c92", + "cellName": "Client: Initiate User Invitation - TeamPanel.jsx:L348-354", + "cellId": "b27f1bba-c655-40c6-a1dc-71e78c590c92", + "visible": true, + "parentCellId": "1145f351-2257-411d-86f7-30bf8c26ad68" + }, + "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-eebaca25-d588-49c4-a35c-9abc82fde580": { + "path": "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-eebaca25-d588-49c4-a35c-9abc82fde580", + "fileName": "TeamPanel.jsx", + "wiki": "In the `TeamPanel`, the admin clicks the 'Add Team Member' button, then selects 'Invite a team member'. A dialog opens, prompting for the new user's email and role. After filling the form and clicking 'E-mail token', the `handleInviteMember` function is triggered to start the invitation process.", + "cellName": "Client: Initiate User Invitation - TeamPanel.jsx:L348-354", + "cellId": "b27f1bba-c655-40c6-a1dc-71e78c590c92", + "visible": true, + "startLine": 348, + "endLine": 354, + "parentCellId": "1145f351-2257-411d-86f7-30bf8c26ad68", + "parentPath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "eebaca25-d588-49c4-a35c-9abc82fde580" + } + ] + }, + "8b3cb6c2-ed57-4510-be15-decb8e3c89e1": { + "path": "8b3cb6c2-ed57-4510-be15-decb8e3c89e1", + "cellName": "Server: Role-Based Access Control Middleware - isAllowed.js:L39-44", + "cellId": "8b3cb6c2-ed57-4510-be15-decb8e3c89e1", + "visible": true, + "parentCellId": "001fa2e4-eadd-4474-b2ec-79d7ea43465f" + }, + "server/src/middleware/v1/isAllowed.js-simstep-50dd7316-7d01-43f7-9036-d97d51a9c178": { + "path": "server/src/middleware/v1/isAllowed.js-simstep-50dd7316-7d01-43f7-9036-d97d51a9c178", + "fileName": "isAllowed.js", + "wiki": "The server receives the request. Before reaching the main controller, the `isAllowed` middleware intercepts the request. It verifies the JWT from the request header and checks if the user's role (e.g., 'superadmin') is present in the list of allowed roles for this route (`['admin', 'superadmin']`).", + "cellName": "Server: Role-Based Access Control Middleware - isAllowed.js:L39-44", + "cellId": "8b3cb6c2-ed57-4510-be15-decb8e3c89e1", + "visible": true, + "startLine": 39, + "endLine": 44, + "parentCellId": "001fa2e4-eadd-4474-b2ec-79d7ea43465f", + "parentPath": "server/src/middleware/v1/isAllowed.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "50dd7316-7d01-43f7-9036-d97d51a9c178" + } + ] + }, + "ae858158-8db6-4917-8fda-0966a953f6ed": { + "path": "ae858158-8db6-4917-8fda-0966a953f6ed", + "cellName": "Server: Process Invitation in Service Layer - inviteService.js:L24-28", + "cellId": "ae858158-8db6-4917-8fda-0966a953f6ed", + "visible": true, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b" + }, + "server/src/service/v1/business/inviteService.js-simstep-55fe9963-30d2-47f8-859f-b485a77064c4": { + "path": "server/src/service/v1/business/inviteService.js-simstep-55fe9963-30d2-47f8-859f-b485a77064c4", + "fileName": "inviteService.js", + "wiki": "The `inviteController` calls the `inviteService`. The service layer handles the business logic: it first requests an invitation token from the database module and then instructs the email service to send the invitation.", + "cellName": "Server: Process Invitation in Service Layer - inviteService.js:L24-28", + "cellId": "ae858158-8db6-4917-8fda-0966a953f6ed", + "visible": true, + "startLine": 24, + "endLine": 28, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "parentPath": "server/src/service/v1/business/inviteService.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "55fe9963-30d2-47f8-859f-b485a77064c4" + } + ] + }, + "a7be2d0b-0227-4228-a0a6-074f7324fd89": { + "path": "a7be2d0b-0227-4228-a0a6-074f7324fd89", + "cellName": "Server: Dispatch Invitation Email - inviteService.js:L26-28", + "cellId": "a7be2d0b-0227-4228-a0a6-074f7324fd89", + "visible": true, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b" + }, + "server/src/service/v1/business/inviteService.js-simstep-7d1f86f2-c8d5-48fb-a8c1-b337dc56271f": { + "path": "server/src/service/v1/business/inviteService.js-simstep-7d1f86f2-c8d5-48fb-a8c1-b337dc56271f", + "fileName": "inviteService.js", + "wiki": "After successfully storing the token, the `inviteService` uses the `emailService` to compose and send an email to the new user. The email contains a unique registration link including the generated token.", + "cellName": "Server: Dispatch Invitation Email - inviteService.js:L26-28", + "cellId": "a7be2d0b-0227-4228-a0a6-074f7324fd89", + "visible": true, + "startLine": 26, + "endLine": 28, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "parentPath": "server/src/service/v1/business/inviteService.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "7d1f86f2-c8d5-48fb-a8c1-b337dc56271f" + } + ] + }, + "02d87257-883c-4822-a421-823c983532ec": { + "path": "02d87257-883c-4822-a421-823c983532ec", + "cellName": "Client: Display Success Feedback - TeamPanel.jsx:L145-149", + "cellId": "02d87257-883c-4822-a421-823c983532ec", + "visible": true, + "parentCellId": "1145f351-2257-411d-86f7-30bf8c26ad68" + }, + "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-700f35e0-b39d-417c-b0a8-0612feade977": { + "path": "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-700f35e0-b39d-417c-b0a8-0612feade977", + "fileName": "TeamPanel.jsx", + "wiki": "The client application receives the successful API response. It then closes the invitation dialog and displays a toast notification to the admin, confirming that the invitation has been sent.", + "cellName": "Client: Display Success Feedback - TeamPanel.jsx:L145-149", + "cellId": "02d87257-883c-4822-a421-823c983532ec", + "visible": true, + "startLine": 145, + "endLine": 149, + "parentCellId": "1145f351-2257-411d-86f7-30bf8c26ad68", + "parentPath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "700f35e0-b39d-417c-b0a8-0612feade977" + } + ] + }, + "c441312f-f3c6-48c6-b1b9-7c12749d91bb": { + "path": "c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "cellName": "Client: User\nNavigates to\nTeam Panel", + "cellId": "c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19" + }, + "generated-edge-simstep-10f80ada-2d0a-4e98-bd5d-057e7d4fe6de-c441312f-f3c6-48c6-b1b9-7c12749d91bb": { + "path": "generated-edge-simstep-10f80ada-2d0a-4e98-bd5d-057e7d4fe6de-c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "fileName": "index.jsx", + "cellName": "Client: User Navigates to Team Panel", + "cellId": "c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "visible": true, + "startLine": 98, + "endLine": 98, + "parentPath": "client/src/Pages/v1/Account/index.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "10f80ada-2d0a-4e98-bd5d-057e7d4fe6de" + } + ] + }, + "3530244f-2689-444e-bcbf-611db2e3c000": { + "path": "3530244f-2689-444e-bcbf-611db2e3c000", + "cellName": "API Call:\nSend Invitation\nRequest", + "cellId": "3530244f-2689-444e-bcbf-611db2e3c000", + "visible": true + }, + "generated-edge-simstep-16d87608-4458-430c-b459-cd9b1d413fa6-3530244f-2689-444e-bcbf-611db2e3c000": { + "path": "generated-edge-simstep-16d87608-4458-430c-b459-cd9b1d413fa6-3530244f-2689-444e-bcbf-611db2e3c000", + "fileName": "TeamPanel.jsx", + "cellName": "API Call: Send Invitation Request", + "cellId": "3530244f-2689-444e-bcbf-611db2e3c000", + "visible": true, + "startLine": 14, + "endLine": 14, + "parentPath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "16d87608-4458-430c-b459-cd9b1d413fa6" + } + ] + }, + "2ee1c4e8-90fb-4470-ad9f-6ffed5420853": { + "path": "2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "cellName": "Server: Forward\nRequest to\nController", + "cellId": "2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-6c024d4c-178b-45fc-8bca-4fc6c00549a4-2ee1c4e8-90fb-4470-ad9f-6ffed5420853": { + "path": "generated-edge-simstep-6c024d4c-178b-45fc-8bca-4fc6c00549a4-2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "fileName": "isAllowed.js", + "cellName": "Server: Forward Request to Controller", + "cellId": "2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "visible": true, + "startLine": 61, + "endLine": 68, + "parentPath": "server/src/middleware/v1/isAllowed.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "6c024d4c-178b-45fc-8bca-4fc6c00549a4" + } + ] + }, + "bdd52e6f-ddf7-4966-b3af-957f9553082f": { + "path": "bdd52e6f-ddf7-4966-b3af-957f9553082f", + "cellName": "Database Query:\nCreate and\nStore Invite\nToken", + "cellId": "bdd52e6f-ddf7-4966-b3af-957f9553082f", + "visible": true, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b" + }, + "generated-edge-simstep-c796f8ad-d548-45f3-8d62-67f59c6c1aa4-bdd52e6f-ddf7-4966-b3af-957f9553082f": { + "path": "generated-edge-simstep-c796f8ad-d548-45f3-8d62-67f59c6c1aa4-bdd52e6f-ddf7-4966-b3af-957f9553082f", + "fileName": "inviteService.js", + "cellName": "Database Query: Create and Store Invite Token", + "cellId": "bdd52e6f-ddf7-4966-b3af-957f9553082f", + "visible": true, + "startLine": 23, + "endLine": 26, + "parentPath": "server/src/service/v1/business/inviteService.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "c796f8ad-d548-45f3-8d62-67f59c6c1aa4" + } + ] + }, + "52732a37-82b8-40a7-b12d-dc1b82380b2d": { + "path": "52732a37-82b8-40a7-b12d-dc1b82380b2d", + "cellName": "API Response:\nSend Confirmation\nto Client", + "cellId": "52732a37-82b8-40a7-b12d-dc1b82380b2d", + "visible": true + }, + "generated-edge-simstep-6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9-52732a37-82b8-40a7-b12d-dc1b82380b2d": { + "path": "generated-edge-simstep-6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9-52732a37-82b8-40a7-b12d-dc1b82380b2d", + "fileName": "inviteService.js", + "cellName": "API Response: Send Confirmation to Client", + "cellId": "52732a37-82b8-40a7-b12d-dc1b82380b2d", + "visible": true, + "startLine": 69, + "endLine": 72, + "parentPath": "server/src/service/v1/business/inviteService.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9" + } + ] + }, + "56dae13e-5735-4089-a9a4-e18be315aff7": { + "path": "56dae13e-5735-4089-a9a4-e18be315aff7", + "cellName": "BulkImport", + "cellId": "56dae13e-5735-4089-a9a4-e18be315aff7", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff" + }, + "29b6ad3c-9538-47bc-8796-aab942dc4b71": { + "path": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "cellName": "index.jsx", + "cellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7" + }, + "cd84476a-1c8b-4781-811f-58fb9944365b": { + "path": "cd84476a-1c8b-4781-811f-58fb9944365b", + "cellName": "Upload.jsx", + "cellId": "cd84476a-1c8b-4781-811f-58fb9944365b", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7" + }, + "c4caeba8-ed9a-4cbc-9888-38cc8ca55362": { + "path": "c4caeba8-ed9a-4cbc-9888-38cc8ca55362", + "cellName": "Flow 1: CSV Import - Navigate to Bulk Import Page - index.jsx:L88-91", + "cellId": "c4caeba8-ed9a-4cbc-9888-38cc8ca55362", + "visible": true, + "parentCellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562" + }, + "client/src/Routes/index.jsx-simstep-a29da01e-8c5a-4e6d-8d3c-e49af528e4fa": { + "path": "client/src/Routes/index.jsx-simstep-a29da01e-8c5a-4e6d-8d3c-e49af528e4fa", + "fileName": "index.jsx", + "wiki": "The user navigates to the \"/uptime/bulk-import\" URL, which is routed to the BulkImport component for rendering the UI.", + "cellName": "Flow 1: CSV Import - Navigate to Bulk Import Page - index.jsx:L88-91", + "cellId": "c4caeba8-ed9a-4cbc-9888-38cc8ca55362", + "visible": true, + "startLine": 88, + "endLine": 91, + "parentCellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "parentPath": "client/src/Routes/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "a29da01e-8c5a-4e6d-8d3c-e49af528e4fa" + } + ] + }, + "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8": { + "path": "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8", + "cellName": "Flow 1: CSV Import - User Selects File - Upload.jsx:L23-31", + "cellId": "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8", + "visible": true, + "parentCellId": "cd84476a-1c8b-4781-811f-58fb9944365b" + }, + "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx-simstep-bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb": { + "path": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx-simstep-bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb", + "fileName": "Upload.jsx", + "wiki": "The user clicks the 'Select File' button, triggering the file input. When a file is chosen, the `handleFileChange` function validates that it's a CSV file and updates the component's state.", + "cellName": "Flow 1: CSV Import - User Selects File - Upload.jsx:L23-31", + "cellId": "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8", + "visible": true, + "startLine": 23, + "endLine": 31, + "parentCellId": "cd84476a-1c8b-4781-811f-58fb9944365b", + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb" + } + ] + }, + "dd2f9356-fad3-47d4-81d5-a5e038298e2b": { + "path": "dd2f9356-fad3-47d4-81d5-a5e038298e2b", + "cellName": "Flow 1: CSV Import - Submit Upload - index.jsx:L32-45", + "cellId": "dd2f9356-fad3-47d4-81d5-a5e038298e2b", + "visible": true, + "parentCellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71" + }, + "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c": { + "path": "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c", + "fileName": "index.jsx", + "wiki": "The user clicks the submit button, which executes the `handleSubmit` function. This function calls the `createBulkMonitors` function provided by the `useCreateBulkMonitors` custom hook.", + "cellName": "Flow 1: CSV Import - Submit Upload - index.jsx:L32-45", + "cellId": "dd2f9356-fad3-47d4-81d5-a5e038298e2b", + "visible": true, + "startLine": 32, + "endLine": 45, + "parentCellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c" + } + ] + }, + "5967e576-f542-4c0b-a3f4-9446ae992721": { + "path": "5967e576-f542-4c0b-a3f4-9446ae992721", + "cellName": "Flow 1: CSV Import - Send API Request - NetworkService.js:L978-983", + "cellId": "5967e576-f542-4c0b-a3f4-9446ae992721", + "visible": true, + "parentCellId": "16731e9d-3f73-4055-9d79-5ca9be679f68" + }, + "client/src/Utils/NetworkService.js-simstep-2fc087b3-033d-4cf5-81f2-e35b99a14bce": { + "path": "client/src/Utils/NetworkService.js-simstep-2fc087b3-033d-4cf5-81f2-e35b99a14bce", + "fileName": "NetworkService.js", + "wiki": "The NetworkService sends the `FormData` containing the CSV file to the backend via a POST request to the `/monitors/bulk` endpoint. The `Content-Type` is set to `multipart/form-data`.", + "cellName": "Flow 1: CSV Import - Send API Request - NetworkService.js:L978-983", + "cellId": "5967e576-f542-4c0b-a3f4-9446ae992721", + "visible": true, + "startLine": 978, + "endLine": 983, + "parentCellId": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "parentPath": "client/src/Utils/NetworkService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "2fc087b3-033d-4cf5-81f2-e35b99a14bce" + } + ] + }, + "c1ac3c26-996a-43ef-9c34-4c2776188784": { + "path": "c1ac3c26-996a-43ef-9c34-4c2776188784", + "cellName": "Flow 1: CSV Import - Server Receives Request - monitorRoute.js:L46", + "cellId": "c1ac3c26-996a-43ef-9c34-4c2776188784", + "visible": true, + "parentCellId": "10611636-ec73-4a26-87a5-654ae64a4843" + }, + "server/src/routes/v1/monitorRoute.js-simstep-07bf3d83-047d-4e51-9f96-8c9c64848500": { + "path": "server/src/routes/v1/monitorRoute.js-simstep-07bf3d83-047d-4e51-9f96-8c9c64848500", + "fileName": "monitorRoute.js", + "wiki": "The server's monitor route receives the request. The `multer` middleware processes the file upload, making the file available in `req.file`. The request is then passed to the `createBulkMonitors` controller method.", + "cellName": "Flow 1: CSV Import - Server Receives Request - monitorRoute.js:L46", + "cellId": "c1ac3c26-996a-43ef-9c34-4c2776188784", + "visible": true, + "startLine": 46, + "endLine": 46, + "parentCellId": "10611636-ec73-4a26-87a5-654ae64a4843", + "parentPath": "server/src/routes/v1/monitorRoute.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "07bf3d83-047d-4e51-9f96-8c9c64848500" + } + ] + }, + "0e520a6d-34bf-467b-a894-e73979364478": { + "path": "0e520a6d-34bf-467b-a894-e73979364478", + "cellName": "Flow 1: CSV Import - Controller Processes Data - monitorController.js:L228-232", + "cellId": "0e520a6d-34bf-467b-a894-e73979364478", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-a3799570-64bf-481e-9ffe-31dbad8a36e4": { + "path": "server/src/controllers/v1/monitorController.js-simstep-a3799570-64bf-481e-9ffe-31dbad8a36e4", + "fileName": "monitorController.js", + "wiki": "The controller validates the presence of the file and extracts the file data (buffer), `userId`, and `teamId`. It then calls the `monitorService.createBulkMonitors` method to handle the core logic.", + "cellName": "Flow 1: CSV Import - Controller Processes Data - monitorController.js:L228-232", + "cellId": "0e520a6d-34bf-467b-a894-e73979364478", + "visible": true, + "startLine": 228, + "endLine": 232, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "a3799570-64bf-481e-9ffe-31dbad8a36e4" + } + ] + }, + "6139e487-1dd5-405a-ba7d-ededec4b282e": { + "path": "6139e487-1dd5-405a-ba7d-ededec4b282e", + "cellName": "Flow 1: CSV Import - Service Parses CSV - monitorService.js:L85-134", + "cellId": "6139e487-1dd5-405a-ba7d-ededec4b282e", + "visible": true, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e" + }, + "server/src/service/v1/business/monitorService.js-simstep-2c7c3082-e4b6-4525-860f-2ffcd9e86db1": { + "path": "server/src/service/v1/business/monitorService.js-simstep-2c7c3082-e4b6-4525-860f-2ffcd9e86db1", + "fileName": "monitorService.js", + "wiki": "The monitor service uses the `papaparse` library to parse the CSV file buffer into a JSON object array. In the `complete` callback, it enriches each record with `userId` and `teamId`.", + "cellName": "Flow 1: CSV Import - Service Parses CSV - monitorService.js:L85-134", + "cellId": "6139e487-1dd5-405a-ba7d-ededec4b282e", + "visible": true, + "startLine": 85, + "endLine": 134, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "2c7c3082-e4b6-4525-860f-2ffcd9e86db1" + } + ] + }, + "d9d519af-5c4d-4494-929e-a869581c1dc4": { + "path": "d9d519af-5c4d-4494-929e-a869581c1dc4", + "cellName": "Flow 1: CSV Import - Database Bulk Save - monitorModule.js:L465-468", + "cellId": "d9d519af-5c4d-4494-929e-a869581c1dc4", + "visible": true, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5" + }, + "server/src/db/v1/modules/monitorModule.js-simstep-70f18c21-2918-402e-9c35-1bdce8bc82d4": { + "path": "server/src/db/v1/modules/monitorModule.js-simstep-70f18c21-2918-402e-9c35-1bdce8bc82d4", + "fileName": "monitorModule.js", + "wiki": "The `monitorModule` uses `Monitor.bulkSave` to efficiently insert all the new monitor documents into the MongoDB database in a single operation.", + "cellName": "Flow 1: CSV Import - Database Bulk Save - monitorModule.js:L465-468", + "cellId": "d9d519af-5c4d-4494-929e-a869581c1dc4", + "visible": true, + "startLine": 465, + "endLine": 468, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "70f18c21-2918-402e-9c35-1bdce8bc82d4" + } + ] + }, + "1e1d975f-7f0f-4132-bd71-2563b35e99b3": { + "path": "1e1d975f-7f0f-4132-bd71-2563b35e99b3", + "cellName": "Flow 1: CSV Import - Enqueue Monitor Jobs - monitorService.js:L128-132", + "cellId": "1e1d975f-7f0f-4132-bd71-2563b35e99b3", + "visible": true, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e" + }, + "server/src/service/v1/business/monitorService.js-simstep-66fbf913-e662-4bd9-ada6-077be9e37f07": { + "path": "server/src/service/v1/business/monitorService.js-simstep-66fbf913-e662-4bd9-ada6-077be9e37f07", + "fileName": "monitorService.js", + "wiki": "After the monitors are saved, the service iterates through them and adds a new job to the `jobQueue` for each one, scheduling them for their first check. It then resolves the promise, returning the data up the call stack.", + "cellName": "Flow 1: CSV Import - Enqueue Monitor Jobs - monitorService.js:L128-132", + "cellId": "1e1d975f-7f0f-4132-bd71-2563b35e99b3", + "visible": true, + "startLine": 128, + "endLine": 132, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "66fbf913-e662-4bd9-ada6-077be9e37f07" + } + ] + }, + "72b55fa1-d944-4e46-ac98-f6a6cb201afa": { + "path": "72b55fa1-d944-4e46-ac98-f6a6cb201afa", + "cellName": "Flow 1: CSV Import - Send Success Response - monitorController.js:L232-235", + "cellId": "72b55fa1-d944-4e46-ac98-f6a6cb201afa", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-f5377571-156f-4c5c-8bab-b6dacdf0fea6": { + "path": "server/src/controllers/v1/monitorController.js-simstep-f5377571-156f-4c5c-8bab-b6dacdf0fea6", + "fileName": "monitorController.js", + "wiki": "The controller constructs a successful JSON response containing a message and the data for the newly created monitors, and sends it back to the client.", + "cellName": "Flow 1: CSV Import - Send Success Response - monitorController.js:L232-235", + "cellId": "72b55fa1-d944-4e46-ac98-f6a6cb201afa", + "visible": true, + "startLine": 232, + "endLine": 235, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "f5377571-156f-4c5c-8bab-b6dacdf0fea6" + } + ] + }, + "ae3b7e23-ed64-40aa-9760-389f0a869f8d": { + "path": "ae3b7e23-ed64-40aa-9760-389f0a869f8d", + "cellName": "Flow 1: CSV Import - Client Handles Success - index.jsx:L37-41", + "cellId": "ae3b7e23-ed64-40aa-9760-389f0a869f8d", + "visible": true, + "parentCellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71" + }, + "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-72b3f16d-290d-4a61-8748-2ad90d63ec68": { + "path": "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-72b3f16d-290d-4a61-8748-2ad90d63ec68", + "fileName": "index.jsx", + "wiki": "The client-side promise resolves. The `handleSubmit` function in the `BulkImport` component receives the success status, displays a success toast message, and navigates the user to the main uptime monitoring page.", + "cellName": "Flow 1: CSV Import - Client Handles Success - index.jsx:L37-41", + "cellId": "ae3b7e23-ed64-40aa-9760-389f0a869f8d", + "visible": true, + "startLine": 37, + "endLine": 41, + "parentCellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "72b3f16d-290d-4a61-8748-2ad90d63ec68" + } + ] + }, + "26d76516-fd7e-4ae8-956a-d077de4257fa": { + "path": "26d76516-fd7e-4ae8-956a-d077de4257fa", + "cellName": "Flow 2: CSV Export - Initiate Export - monitorRoute.js:L45", + "cellId": "26d76516-fd7e-4ae8-956a-d077de4257fa", + "visible": true, + "parentCellId": "10611636-ec73-4a26-87a5-654ae64a4843" + }, + "server/src/routes/v1/monitorRoute.js-simstep-63d8d81f-f436-4594-a6eb-8c0fd2110bc1": { + "path": "server/src/routes/v1/monitorRoute.js-simstep-63d8d81f-f436-4594-a6eb-8c0fd2110bc1", + "fileName": "monitorRoute.js", + "wiki": "An admin user clicks an 'Export to CSV' button in the UI. This action triggers a call to the `/monitors/export` API endpoint to begin the export process.", + "cellName": "Flow 2: CSV Export - Initiate Export - monitorRoute.js:L45", + "cellId": "26d76516-fd7e-4ae8-956a-d077de4257fa", + "visible": true, + "startLine": 45, + "endLine": 45, + "parentCellId": "10611636-ec73-4a26-87a5-654ae64a4843", + "parentPath": "server/src/routes/v1/monitorRoute.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "63d8d81f-f436-4594-a6eb-8c0fd2110bc1" + } + ] + }, + "a3106388-6a3d-4b68-a3fc-3761a0d54f98": { + "path": "a3106388-6a3d-4b68-a3fc-3761a0d54f98", + "cellName": "Flow 2: CSV Export - Controller Handles Request - monitorController.js:L427-434", + "cellId": "a3106388-6a3d-4b68-a3fc-3761a0d54f98", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-d9db3f40-8add-4551-9323-274348b9800f": { + "path": "server/src/controllers/v1/monitorController.js-simstep-d9db3f40-8add-4551-9323-274348b9800f", + "fileName": "monitorController.js", + "wiki": "The `exportMonitorsToCSV` method in the monitor controller is invoked. It retrieves the `teamId` from the authenticated user's session and calls the monitor service to perform the export.", + "cellName": "Flow 2: CSV Export - Controller Handles Request - monitorController.js:L427-434", + "cellId": "a3106388-6a3d-4b68-a3fc-3761a0d54f98", + "visible": true, + "startLine": 427, + "endLine": 434, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "d9db3f40-8add-4551-9323-274348b9800f" + } + ] + }, + "ab9cb862-0ac7-45a3-9888-f64e87bdda54": { + "path": "ab9cb862-0ac7-45a3-9888-f64e87bdda54", + "cellName": "Flow 2: CSV Export - Generate CSV - monitorService.js:L245-266", + "cellId": "ab9cb862-0ac7-45a3-9888-f64e87bdda54", + "visible": true, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e" + }, + "server/src/service/v1/business/monitorService.js-simstep-85cd1d5d-7ffb-49c5-aacb-907048003fc5": { + "path": "server/src/service/v1/business/monitorService.js-simstep-85cd1d5d-7ffb-49c5-aacb-907048003fc5", + "fileName": "monitorService.js", + "wiki": "The monitor service fetches all monitors for the given `teamId` from the database. It then maps the data into the required format and uses `papaparse` to convert the JSON array into a CSV-formatted string.", + "cellName": "Flow 2: CSV Export - Generate CSV - monitorService.js:L245-266", + "cellId": "ab9cb862-0ac7-45a3-9888-f64e87bdda54", + "visible": true, + "startLine": 245, + "endLine": 266, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "85cd1d5d-7ffb-49c5-aacb-907048003fc5" + } + ] + }, + "f151f89d-6399-48f0-aecd-5eb1572b48cc": { + "path": "f151f89d-6399-48f0-aecd-5eb1572b48cc", + "cellName": "Flow 2: CSV Export - Send File Response - monitorController.js:L434-440", + "cellId": "f151f89d-6399-48f0-aecd-5eb1572b48cc", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-256095b8-5e3a-4c20-abd9-be9e6a9d0bad": { + "path": "server/src/controllers/v1/monitorController.js-simstep-256095b8-5e3a-4c20-abd9-be9e6a9d0bad", + "fileName": "monitorController.js", + "wiki": "The controller receives the CSV string and uses the `res.file` method to send it back to the client as a file. It sets the `Content-Type` to `text/csv` and `Content-Disposition` to trigger a download with the filename `monitors.csv`.", + "cellName": "Flow 2: CSV Export - Send File Response - monitorController.js:L434-440", + "cellId": "f151f89d-6399-48f0-aecd-5eb1572b48cc", + "visible": true, + "startLine": 434, + "endLine": 440, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "256095b8-5e3a-4c20-abd9-be9e6a9d0bad" + } + ] + }, + "d8877474-7203-484f-bd47-fc69e3c7c482": { + "path": "d8877474-7203-484f-bd47-fc69e3c7c482", + "cellName": "Flow 2: CSV Export - Browser Prompts Download - monitorController.js:L434-440", + "cellId": "d8877474-7203-484f-bd47-fc69e3c7c482", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-60abdb27-4078-4678-a010-0f6938d4be48": { + "path": "server/src/controllers/v1/monitorController.js-simstep-60abdb27-4078-4678-a010-0f6938d4be48", + "fileName": "monitorController.js", + "wiki": "The browser receives the server's response. The `Content-Disposition` header instructs the browser to prompt the user to save the received data as a file named `monitors.csv`, completing the export process.", + "cellName": "Flow 2: CSV Export - Browser Prompts Download - monitorController.js:L434-440", + "cellId": "d8877474-7203-484f-bd47-fc69e3c7c482", + "visible": true, + "startLine": 434, + "endLine": 440, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "60abdb27-4078-4678-a010-0f6938d4be48" + } + ] + }, + "c9a012bc-d53f-494f-82f2-1ee3f40ce646": { + "path": "c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "cellName": "Flow 1:\nCSV Import\n- Render\nUI", + "cellId": "c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-ef0bbcff-9535-4c0e-a5ca-96c457f89139-c9a012bc-d53f-494f-82f2-1ee3f40ce646": { + "path": "generated-edge-simstep-ef0bbcff-9535-4c0e-a5ca-96c457f89139-c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "fileName": "index.jsx", + "cellName": "Flow 1: CSV Import - Render UI", + "cellId": "c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "visible": true, + "startLine": 16, + "endLine": 50, + "parentPath": "client/src/Routes/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "ef0bbcff-9535-4c0e-a5ca-96c457f89139" + } + ] + }, + "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48": { + "path": "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "cellName": "Flow 1:\nCSV Import\n- File\nState Update", + "cellId": "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7" + }, + "generated-edge-simstep-610120d2-0521-4444-b245-feb97239a389-e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48": { + "path": "generated-edge-simstep-610120d2-0521-4444-b245-feb97239a389-e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "fileName": "Upload.jsx", + "cellName": "Flow 1: CSV Import - File State Update", + "cellId": "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "visible": true, + "startLine": 22, + "endLine": 22, + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "610120d2-0521-4444-b245-feb97239a389" + } + ] + }, + "b8c0292d-afbb-4648-b484-1c27be96d52f": { + "path": "b8c0292d-afbb-4648-b484-1c27be96d52f", + "cellName": "Flow 1:\nCSV Import\n- Prepare\nForm Data", + "cellId": "b8c0292d-afbb-4648-b484-1c27be96d52f", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-a365d018-e80c-419c-80b3-5ffc8f14dc5c-b8c0292d-afbb-4648-b484-1c27be96d52f": { + "path": "generated-edge-simstep-a365d018-e80c-419c-80b3-5ffc8f14dc5c-b8c0292d-afbb-4648-b484-1c27be96d52f", + "fileName": "index.jsx", + "cellName": "Flow 1: CSV Import - Prepare Form Data", + "cellId": "b8c0292d-afbb-4648-b484-1c27be96d52f", + "visible": true, + "startLine": 490, + "endLine": 493, + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "a365d018-e80c-419c-80b3-5ffc8f14dc5c" + } + ] + }, + "23715cce-c83a-44c0-9853-4c95b85e5ecd": { + "path": "23715cce-c83a-44c0-9853-4c95b85e5ecd", + "cellName": "Flow 1:\nCSV Import\n- Transmit\nData to\nServer", + "cellId": "23715cce-c83a-44c0-9853-4c95b85e5ecd", + "visible": true + }, + "generated-edge-simstep-e6dec1dc-33cb-4669-a3b0-7a3c0b233185-23715cce-c83a-44c0-9853-4c95b85e5ecd": { + "path": "generated-edge-simstep-e6dec1dc-33cb-4669-a3b0-7a3c0b233185-23715cce-c83a-44c0-9853-4c95b85e5ecd", + "fileName": "NetworkService.js", + "cellName": "Flow 1: CSV Import - Transmit Data to Server", + "cellId": "23715cce-c83a-44c0-9853-4c95b85e5ecd", + "visible": true, + "startLine": 46, + "endLine": 46, + "parentPath": "client/src/Utils/NetworkService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "e6dec1dc-33cb-4669-a3b0-7a3c0b233185" + } + ] + }, + "20d0e690-a569-4c6f-8238-916cac920553": { + "path": "20d0e690-a569-4c6f-8238-916cac920553", + "cellName": "Flow 1:\nCSV Import\n- Route\nto Controller", + "cellId": "20d0e690-a569-4c6f-8238-916cac920553", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-c53d3f62-73fe-4fad-9835-ef2c8cf52894-20d0e690-a569-4c6f-8238-916cac920553": { + "path": "generated-edge-simstep-c53d3f62-73fe-4fad-9835-ef2c8cf52894-20d0e690-a569-4c6f-8238-916cac920553", + "fileName": "monitorRoute.js", + "cellName": "Flow 1: CSV Import - Route to Controller", + "cellId": "20d0e690-a569-4c6f-8238-916cac920553", + "visible": true, + "startLine": 206, + "endLine": 212, + "parentPath": "server/src/routes/v1/monitorRoute.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "c53d3f62-73fe-4fad-9835-ef2c8cf52894" + } + ] + }, + "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d": { + "path": "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "cellName": "Flow 1:\nCSV Import\n- Controller\nto Service", + "cellId": "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-6038422c-ab83-4fc6-a58b-c75734276118-7292bf6e-431d-4a9d-94ce-2bcfabcaa79d": { + "path": "generated-edge-simstep-6038422c-ab83-4fc6-a58b-c75734276118-7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "fileName": "monitorController.js", + "cellName": "Flow 1: CSV Import - Controller to Service", + "cellId": "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "visible": true, + "startLine": 84, + "endLine": 84, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "6038422c-ab83-4fc6-a58b-c75734276118" + } + ] + }, + "b55a0dc6-f596-4e5f-9265-63588b46b519": { + "path": "b55a0dc6-f596-4e5f-9265-63588b46b519", + "cellName": "Flow 1:\nCSV Import\n- Service\nto Database\nModule", + "cellId": "b55a0dc6-f596-4e5f-9265-63588b46b519", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-0115fb69-4959-4b32-bd90-1dcd3c2231f6-b55a0dc6-f596-4e5f-9265-63588b46b519": { + "path": "generated-edge-simstep-0115fb69-4959-4b32-bd90-1dcd3c2231f6-b55a0dc6-f596-4e5f-9265-63588b46b519", + "fileName": "monitorService.js", + "cellName": "Flow 1: CSV Import - Service to Database Module", + "cellId": "b55a0dc6-f596-4e5f-9265-63588b46b519", + "visible": true, + "startLine": 126, + "endLine": 126, + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "0115fb69-4959-4b32-bd90-1dcd3c2231f6" + } + ] + }, + "f27c60ef-fb40-43e2-9750-c2617bdeaf43": { + "path": "f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "cellName": "Flow 1:\nCSV Import\n- Database\nto Service", + "cellId": "f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-2ee7bd94-d131-4dd8-bdf8-68433d00472d-f27c60ef-fb40-43e2-9750-c2617bdeaf43": { + "path": "generated-edge-simstep-2ee7bd94-d131-4dd8-bdf8-68433d00472d-f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "fileName": "monitorModule.js", + "cellName": "Flow 1: CSV Import - Database to Service", + "cellId": "f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "visible": true, + "startLine": 467, + "endLine": 467, + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "2ee7bd94-d131-4dd8-bdf8-68433d00472d" + } + ] + }, + "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9": { + "path": "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "cellName": "Flow 1:\nCSV Import\n- Service\nto Controller", + "cellId": "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-94d5772d-3f46-4d43-b764-3493db5c4971-c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9": { + "path": "generated-edge-simstep-94d5772d-3f46-4d43-b764-3493db5c4971-c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "fileName": "monitorService.js", + "cellName": "Flow 1: CSV Import - Service to Controller", + "cellId": "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "visible": true, + "startLine": 230, + "endLine": 230, + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "94d5772d-3f46-4d43-b764-3493db5c4971" + } + ] + }, + "926a330c-a9c5-4983-a4b5-70359813e49a": { + "path": "926a330c-a9c5-4983-a4b5-70359813e49a", + "cellName": "Flow 1:\nCSV Import\n- Transmit\nSuccess to\nClient", + "cellId": "926a330c-a9c5-4983-a4b5-70359813e49a", + "visible": true + }, + "generated-edge-simstep-c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0-926a330c-a9c5-4983-a4b5-70359813e49a": { + "path": "generated-edge-simstep-c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0-926a330c-a9c5-4983-a4b5-70359813e49a", + "fileName": "monitorController.js", + "cellName": "Flow 1: CSV Import - Transmit Success to Client", + "cellId": "926a330c-a9c5-4983-a4b5-70359813e49a", + "visible": true, + "startLine": 494, + "endLine": 494, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0" + } + ] + }, + "febea87e-5033-479c-ba7a-eb35d98db899": { + "path": "febea87e-5033-479c-ba7a-eb35d98db899", + "cellName": "Flow 2:\nCSV Export\n- Transmit\nRequest to\nServer", + "cellId": "febea87e-5033-479c-ba7a-eb35d98db899", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-53cebb8e-cf45-49eb-a7d3-98fd87af21b4-febea87e-5033-479c-ba7a-eb35d98db899": { + "path": "generated-edge-simstep-53cebb8e-cf45-49eb-a7d3-98fd87af21b4-febea87e-5033-479c-ba7a-eb35d98db899", + "fileName": "monitorRoute.js", + "cellName": "Flow 2: CSV Export - Transmit Request to Server", + "cellId": "febea87e-5033-479c-ba7a-eb35d98db899", + "visible": true, + "startLine": 45, + "endLine": 45, + "parentPath": "server/src/routes/v1/monitorRoute.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "53cebb8e-cf45-49eb-a7d3-98fd87af21b4" + } + ] + }, + "3e0f0966-5c2f-41af-9bcc-e8f615e03a00": { + "path": "3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "cellName": "Flow 2:\nCSV Export\n- Controller\nto Service", + "cellId": "3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-74e95fb5-863c-4137-a41b-d7a32efa04a5-3e0f0966-5c2f-41af-9bcc-e8f615e03a00": { + "path": "generated-edge-simstep-74e95fb5-863c-4137-a41b-d7a32efa04a5-3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "fileName": "monitorController.js", + "cellName": "Flow 2: CSV Export - Controller to Service", + "cellId": "3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "visible": true, + "startLine": 245, + "endLine": 245, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "74e95fb5-863c-4137-a41b-d7a32efa04a5" + } + ] + }, + "ad300b1c-6748-40f9-a7c9-5895e0915991": { + "path": "ad300b1c-6748-40f9-a7c9-5895e0915991", + "cellName": "Flow 2:\nCSV Export\n- Service\nto Controller", + "cellId": "ad300b1c-6748-40f9-a7c9-5895e0915991", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-4deb5e47-2ac4-4638-9ee6-48295d027bcd-ad300b1c-6748-40f9-a7c9-5895e0915991": { + "path": "generated-edge-simstep-4deb5e47-2ac4-4638-9ee6-48295d027bcd-ad300b1c-6748-40f9-a7c9-5895e0915991", + "fileName": "monitorService.js", + "cellName": "Flow 2: CSV Export - Service to Controller", + "cellId": "ad300b1c-6748-40f9-a7c9-5895e0915991", + "visible": true, + "startLine": 432, + "endLine": 432, + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "4deb5e47-2ac4-4638-9ee6-48295d027bcd" + } + ] + }, + "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03": { + "path": "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "cellName": "Flow 2:\nCSV Export\n- Transmit\nFile to\nClient", + "cellId": "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "generated-edge-simstep-605f4cbd-1f7f-4e89-b028-f18737ac7fb1-3dfb432a-2ce4-4f92-98ce-5d114b5f7f03": { + "path": "generated-edge-simstep-605f4cbd-1f7f-4e89-b028-f18737ac7fb1-3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "fileName": "monitorController.js", + "cellName": "Flow 2: CSV Export - Transmit File to Client", + "cellId": "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "visible": true, + "startLine": 434, + "endLine": 440, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "605f4cbd-1f7f-4e89-b028-f18737ac7fb1" + } + ] + }, + "50f77798-f41b-4f5b-b096-7afc921a0419": { + "path": "50f77798-f41b-4f5b-b096-7afc921a0419", + "cellName": "Settings", + "cellId": "50f77798-f41b-4f5b-b096-7afc921a0419", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "1716ec41-8b42-4386-9850-55b8a607638e": { + "path": "1716ec41-8b42-4386-9850-55b8a607638e", + "cellName": "Logs", + "cellId": "1716ec41-8b42-4386-9850-55b8a607638e", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "8ee5d524-e61d-4e39-a729-babf54929def": { + "path": "8ee5d524-e61d-4e39-a729-babf54929def", + "cellName": "settingsHooks.js", + "cellId": "8ee5d524-e61d-4e39-a729-babf54929def", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "a1fea5b0-fb7c-4189-9484-9d3312660767": { + "path": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "cellName": "settingsController.js", + "cellId": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "5882419e-8a92-4edd-a71a-1252983f9af7": { + "path": "5882419e-8a92-4edd-a71a-1252983f9af7", + "cellName": "diagnosticController.js", + "cellId": "5882419e-8a92-4edd-a71a-1252983f9af7", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "d4b34195-7e1e-47a0-9303-603987c81f95": { + "path": "d4b34195-7e1e-47a0-9303-603987c81f95", + "cellName": "index.jsx", + "cellId": "d4b34195-7e1e-47a0-9303-603987c81f95", + "visible": true, + "parentCellId": "50f77798-f41b-4f5b-b096-7afc921a0419" + }, + "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee": { + "path": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee", + "cellName": "SettingsEmail.jsx", + "cellId": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee", + "visible": true, + "parentCellId": "50f77798-f41b-4f5b-b096-7afc921a0419" + }, + "e0cb598c-e300-4fd3-8554-db7ffd6201f1": { + "path": "e0cb598c-e300-4fd3-8554-db7ffd6201f1", + "cellName": "Diagnostics", + "cellId": "e0cb598c-e300-4fd3-8554-db7ffd6201f1", + "visible": true, + "parentCellId": "1716ec41-8b42-4386-9850-55b8a607638e" + }, + "643b26c1-3bfb-4c41-8179-96dfc237d99e": { + "path": "643b26c1-3bfb-4c41-8179-96dfc237d99e", + "cellName": "diagnosticService.js", + "cellId": "643b26c1-3bfb-4c41-8179-96dfc237d99e", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf": { + "path": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "cellName": "index.jsx", + "cellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "visible": true, + "parentCellId": "e0cb598c-e300-4fd3-8554-db7ffd6201f1" + }, + "97908daf-d071-4df5-8366-1f5f4d8d534a": { + "path": "97908daf-d071-4df5-8366-1f5f4d8d534a", + "cellName": "Flow 1: Render Settings Page - index.jsx:L25-52", + "cellId": "97908daf-d071-4df5-8366-1f5f4d8d534a", + "visible": true, + "parentCellId": "d4b34195-7e1e-47a0-9303-603987c81f95" + }, + "client/src/Pages/v1/Settings/index.jsx-simstep-64e9f1af-13b7-4361-a2a9-3f45760e316d": { + "path": "client/src/Pages/v1/Settings/index.jsx-simstep-64e9f1af-13b7-4361-a2a9-3f45760e316d", + "fileName": "index.jsx", + "wiki": "The user navigates to the settings page. The main `Settings` component fetches existing settings using the `useFetchSettings` hook and renders various sub-components for different configuration areas like TimeZone, UI, PageSpeed, Email, etc.", + "cellName": "Flow 1: Render Settings Page - index.jsx:L25-52", + "cellId": "97908daf-d071-4df5-8366-1f5f4d8d534a", + "visible": true, + "startLine": 25, + "endLine": 52, + "parentCellId": "d4b34195-7e1e-47a0-9303-603987c81f95", + "parentPath": "client/src/Pages/v1/Settings/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "64e9f1af-13b7-4361-a2a9-3f45760e316d" + } + ] + }, + "83596bd1-dfca-4196-b52e-955f6824fafb": { + "path": "83596bd1-dfca-4196-b52e-955f6824fafb", + "cellName": "Backend: Retrieve Settings from Database - settingsController.js:L40-49", + "cellId": "83596bd1-dfca-4196-b52e-955f6824fafb", + "visible": true, + "parentCellId": "a1fea5b0-fb7c-4189-9484-9d3312660767" + }, + "server/src/controllers/v1/settingsController.js-simstep-441b0c45-5f27-478d-8ca8-58fbd5f460ea": { + "path": "server/src/controllers/v1/settingsController.js-simstep-441b0c45-5f27-478d-8ca8-58fbd5f460ea", + "fileName": "settingsController.js", + "wiki": "The `settingsController` handles the GET request. It calls the `settingsService` to fetch settings from the database. Sensitive data like passwords is sanitized, and flags like `emailPasswordSet` are added before sending the data to the client.", + "cellName": "Backend: Retrieve Settings from Database - settingsController.js:L40-49", + "cellId": "83596bd1-dfca-4196-b52e-955f6824fafb", + "visible": true, + "startLine": 40, + "endLine": 49, + "parentCellId": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "parentPath": "server/src/controllers/v1/settingsController.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "441b0c45-5f27-478d-8ca8-58fbd5f460ea" + } + ] + }, + "09cb194a-0ea4-46e2-b32b-36b62fa87f40": { + "path": "09cb194a-0ea4-46e2-b32b-36b62fa87f40", + "cellName": "User Interaction: Modify Email Settings - SettingsEmail.jsx:L119-167", + "cellId": "09cb194a-0ea4-46e2-b32b-36b62fa87f40", + "visible": true, + "parentCellId": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee" + }, + "client/src/Pages/v1/Settings/SettingsEmail.jsx-simstep-ae5c5146-4dbb-4612-9d23-086e57f72474": { + "path": "client/src/Pages/v1/Settings/SettingsEmail.jsx-simstep-ae5c5146-4dbb-4612-9d23-086e57f72474", + "fileName": "SettingsEmail.jsx", + "wiki": "The user interacts with the `SettingsEmail` component, filling in SMTP server details. The component's state is updated via the `handleChange` function passed down from the parent `Settings` component, which prepares the new configuration data.", + "cellName": "User Interaction: Modify Email Settings - SettingsEmail.jsx:L119-167", + "cellId": "09cb194a-0ea4-46e2-b32b-36b62fa87f40", + "visible": true, + "startLine": 119, + "endLine": 167, + "parentCellId": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee", + "parentPath": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "ae5c5146-4dbb-4612-9d23-086e57f72474" + } + ] + }, + "1b2cf182-9ee3-4218-a7ec-b74e427ccddf": { + "path": "1b2cf182-9ee3-4218-a7ec-b74e427ccddf", + "cellName": "API Call: Update Application Settings - settingsHooks.js:L41-67", + "cellId": "1b2cf182-9ee3-4218-a7ec-b74e427ccddf", + "visible": true, + "parentCellId": "8ee5d524-e61d-4e39-a729-babf54929def" + }, + "client/src/Hooks/v1/settingsHooks.js-simstep-d7098072-9a83-4369-8176-9646ba34e186": { + "path": "client/src/Hooks/v1/settingsHooks.js-simstep-d7098072-9a83-4369-8176-9646ba34e186", + "fileName": "settingsHooks.js", + "wiki": "The `handleSave` function calls `saveSettings` from the `useSaveSettings` hook. This triggers a PUT request to `/api/v1/settings` with the new configuration in the request body.", + "cellName": "API Call: Update Application Settings - settingsHooks.js:L41-67", + "cellId": "1b2cf182-9ee3-4218-a7ec-b74e427ccddf", + "visible": true, + "startLine": 41, + "endLine": 67, + "parentCellId": "8ee5d524-e61d-4e39-a729-babf54929def", + "parentPath": "client/src/Hooks/v1/settingsHooks.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "d7098072-9a83-4369-8176-9646ba34e186" + } + ] + }, + "cce3b9c8-f3b7-434b-b3c6-d8d164714f86": { + "path": "cce3b9c8-f3b7-434b-b3c6-d8d164714f86", + "cellName": "Backend: Persist Updated Settings - settingsController.js:L54-64", + "cellId": "cce3b9c8-f3b7-434b-b3c6-d8d164714f86", + "visible": true, + "parentCellId": "a1fea5b0-fb7c-4189-9484-9d3312660767" + }, + "server/src/controllers/v1/settingsController.js-simstep-7632e8d2-4086-4169-ae3f-bfbf8d657987": { + "path": "server/src/controllers/v1/settingsController.js-simstep-7632e8d2-4086-4169-ae3f-bfbf8d657987", + "fileName": "settingsController.js", + "wiki": "The `settingsController` receives the PUT request, validates the body, and calls the `db.settingsModule.updateAppSettings` method to update the settings document in the database.", + "cellName": "Backend: Persist Updated Settings - settingsController.js:L54-64", + "cellId": "cce3b9c8-f3b7-434b-b3c6-d8d164714f86", + "visible": true, + "startLine": 54, + "endLine": 64, + "parentCellId": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "parentPath": "server/src/controllers/v1/settingsController.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "7632e8d2-4086-4169-ae3f-bfbf8d657987" + } + ] + }, + "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3": { + "path": "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3", + "cellName": "UI Update: Display Success Confirmation - settingsHooks.js:L58", + "cellId": "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3", + "visible": true, + "parentCellId": "8ee5d524-e61d-4e39-a729-babf54929def" + }, + "client/src/Hooks/v1/settingsHooks.js-simstep-a157bd07-b969-44f8-914c-3d8393c1605a": { + "path": "client/src/Hooks/v1/settingsHooks.js-simstep-a157bd07-b969-44f8-914c-3d8393c1605a", + "fileName": "settingsHooks.js", + "wiki": "The frontend network service receives the successful API response. The `saveSettings` hook then calls `createToast` with a success message, which is displayed to the user.", + "cellName": "UI Update: Display Success Confirmation - settingsHooks.js:L58", + "cellId": "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3", + "visible": true, + "startLine": 58, + "endLine": 58, + "parentCellId": "8ee5d524-e61d-4e39-a729-babf54929def", + "parentPath": "client/src/Hooks/v1/settingsHooks.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "a157bd07-b969-44f8-914c-3d8393c1605a" + } + ] + }, + "704e2ec8-9f2f-47e5-852f-c6ada67a02e0": { + "path": "704e2ec8-9f2f-47e5-852f-c6ada67a02e0", + "cellName": "Flow 2: Navigate to Diagnostics Page - index.jsx:L14-15", + "cellId": "704e2ec8-9f2f-47e5-852f-c6ada67a02e0", + "visible": true, + "parentCellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf" + }, + "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-a9b24b5f-534b-4baf-a185-bac7c32a1c42": { + "path": "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-a9b24b5f-534b-4baf-a185-bac7c32a1c42", + "fileName": "index.jsx", + "wiki": "The user navigates to the 'Logs' page and selects the 'Diagnostics' tab. The `Diagnostics` component renders and immediately calls the `useFetchDiagnostics` hook to retrieve system health data.", + "cellName": "Flow 2: Navigate to Diagnostics Page - index.jsx:L14-15", + "cellId": "704e2ec8-9f2f-47e5-852f-c6ada67a02e0", + "visible": true, + "startLine": 14, + "endLine": 15, + "parentCellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "parentPath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "a9b24b5f-534b-4baf-a185-bac7c32a1c42" + } + ] + }, + "09f07e80-185e-4547-aab4-edaf8ae7501e": { + "path": "09f07e80-185e-4547-aab4-edaf8ae7501e", + "cellName": "Backend: Gather System Statistics - diagnosticService.js:L39-95", + "cellId": "09f07e80-185e-4547-aab4-edaf8ae7501e", + "visible": true, + "parentCellId": "643b26c1-3bfb-4c41-8179-96dfc237d99e" + }, + "server/src/service/v1/business/diagnosticService.js-simstep-8fb704ca-67d9-437c-90d8-01302634d568": { + "path": "server/src/service/v1/business/diagnosticService.js-simstep-8fb704ca-67d9-437c-90d8-01302634d568", + "fileName": "diagnosticService.js", + "wiki": "The `diagnosticController` receives the request and calls `diagnosticService.getSystemStats()`. This service uses native Node.js modules like `os` and `v8` to collect metrics on memory, CPU, heap statistics, and process uptime.", + "cellName": "Backend: Gather System Statistics - diagnosticService.js:L39-95", + "cellId": "09f07e80-185e-4547-aab4-edaf8ae7501e", + "visible": true, + "startLine": 39, + "endLine": 95, + "parentCellId": "643b26c1-3bfb-4c41-8179-96dfc237d99e", + "parentPath": "server/src/service/v1/business/diagnosticService.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "8fb704ca-67d9-437c-90d8-01302634d568" + } + ] + }, + "4b418736-6b72-47e0-917a-d86d4bed9ecd": { + "path": "4b418736-6b72-47e0-917a-d86d4bed9ecd", + "cellName": "UI Update: Display System Health - index.jsx:L59-63", + "cellId": "4b418736-6b72-47e0-917a-d86d4bed9ecd", + "visible": true, + "parentCellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf" + }, + "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-e6c94f52-a599-4568-a06f-2db18f963817": { + "path": "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-e6c94f52-a599-4568-a06f-2db18f963817", + "fileName": "index.jsx", + "wiki": "The frontend receives the diagnostic data. The `Diagnostics` component passes this data to its children, `Gauges` and `StatusBoxes`, which then render the information as interactive gauges and statistical boxes.", + "cellName": "UI Update: Display System Health - index.jsx:L59-63", + "cellId": "4b418736-6b72-47e0-917a-d86d4bed9ecd", + "visible": true, + "startLine": 59, + "endLine": 63, + "parentCellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "parentPath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "e6c94f52-a599-4568-a06f-2db18f963817" + } + ] + }, + "f6d80f09-ae83-4c95-aba0-4efffeda38e0": { + "path": "f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "cellName": "API Call:\nFetch Application\nSettings", + "cellId": "f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "visible": true + }, + "generated-edge-simstep-dca4a33c-b666-47b8-896b-370ed9d6f9c5-f6d80f09-ae83-4c95-aba0-4efffeda38e0": { + "path": "generated-edge-simstep-dca4a33c-b666-47b8-896b-370ed9d6f9c5-f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "fileName": "index.jsx", + "cellName": "API Call: Fetch Application Settings", + "cellId": "f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "visible": true, + "startLine": 692, + "endLine": 698, + "parentPath": "client/src/Pages/v1/Settings/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "dca4a33c-b666-47b8-896b-370ed9d6f9c5" + } + ] + }, + "195ad532-9858-493e-876a-6ec457811d1d": { + "path": "195ad532-9858-493e-876a-6ec457811d1d", + "cellName": "API Response:\nSend Settings\nto Client", + "cellId": "195ad532-9858-493e-876a-6ec457811d1d", + "visible": true + }, + "generated-edge-simstep-aacc9302-6ca8-45c6-acb8-956d8d0242c2-195ad532-9858-493e-876a-6ec457811d1d": { + "path": "generated-edge-simstep-aacc9302-6ca8-45c6-acb8-956d8d0242c2-195ad532-9858-493e-876a-6ec457811d1d", + "fileName": "settingsController.js", + "cellName": "API Response: Send Settings to Client", + "cellId": "195ad532-9858-493e-876a-6ec457811d1d", + "visible": true, + "startLine": 45, + "endLine": 48, + "parentPath": "server/src/controllers/v1/settingsController.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "aacc9302-6ca8-45c6-acb8-956d8d0242c2" + } + ] + }, + "8c107d79-f796-49eb-ab88-f553dd6f9d95": { + "path": "8c107d79-f796-49eb-ab88-f553dd6f9d95", + "cellName": "User Action:\nSave Settings", + "cellId": "8c107d79-f796-49eb-ab88-f553dd6f9d95", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-e7e82a97-1409-4bc5-ae27-b4ff5ba746df-8c107d79-f796-49eb-ab88-f553dd6f9d95": { + "path": "generated-edge-simstep-e7e82a97-1409-4bc5-ae27-b4ff5ba746df-8c107d79-f796-49eb-ab88-f553dd6f9d95", + "fileName": "SettingsEmail.jsx", + "cellName": "User Action: Save Settings", + "cellId": "8c107d79-f796-49eb-ab88-f553dd6f9d95", + "visible": true, + "startLine": 258, + "endLine": 261, + "parentPath": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "e7e82a97-1409-4bc5-ae27-b4ff5ba746df" + } + ] + }, + "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812": { + "path": "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "cellName": "Data Transmission:\nSend Updated\nSettings to\nServer", + "cellId": "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "visible": true + }, + "generated-edge-simstep-383387e5-4623-44ab-b2c4-d9ea046f4547-ce3ed3ba-a35b-4881-8dfd-f8425b9bf812": { + "path": "generated-edge-simstep-383387e5-4623-44ab-b2c4-d9ea046f4547-ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "fileName": "settingsHooks.js", + "cellName": "Data Transmission: Send Updated Settings to Server", + "cellId": "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "visible": true, + "startLine": 711, + "endLine": 717, + "parentPath": "client/src/Hooks/v1/settingsHooks.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "383387e5-4623-44ab-b2c4-d9ea046f4547" + } + ] + }, + "fbe9b160-307e-4f51-8f72-713fda5373c8": { + "path": "fbe9b160-307e-4f51-8f72-713fda5373c8", + "cellName": "API Response:\nConfirm Settings\nUpdate", + "cellId": "fbe9b160-307e-4f51-8f72-713fda5373c8", + "visible": true + }, + "generated-edge-simstep-0afcde35-7d51-4939-aae8-bac5ac83c49d-fbe9b160-307e-4f51-8f72-713fda5373c8": { + "path": "generated-edge-simstep-0afcde35-7d51-4939-aae8-bac5ac83c49d-fbe9b160-307e-4f51-8f72-713fda5373c8", + "fileName": "settingsController.js", + "cellName": "API Response: Confirm Settings Update", + "cellId": "fbe9b160-307e-4f51-8f72-713fda5373c8", + "visible": true, + "startLine": 60, + "endLine": 63, + "parentPath": "server/src/controllers/v1/settingsController.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "0afcde35-7d51-4939-aae8-bac5ac83c49d" + } + ] + }, + "499dfe1f-77d4-4b26-a12c-45240691109e": { + "path": "499dfe1f-77d4-4b26-a12c-45240691109e", + "cellName": "API Call:\nFetch System\nDiagnostics", + "cellId": "499dfe1f-77d4-4b26-a12c-45240691109e", + "visible": true + }, + "generated-edge-simstep-23b8ad7d-8967-4e48-814c-1a7bf5b4dfac-499dfe1f-77d4-4b26-a12c-45240691109e": { + "path": "generated-edge-simstep-23b8ad7d-8967-4e48-814c-1a7bf5b4dfac-499dfe1f-77d4-4b26-a12c-45240691109e", + "fileName": "index.jsx", + "cellName": "API Call: Fetch System Diagnostics", + "cellId": "499dfe1f-77d4-4b26-a12c-45240691109e", + "visible": true, + "startLine": 1113, + "endLine": 1119, + "parentPath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "23b8ad7d-8967-4e48-814c-1a7bf5b4dfac" + } + ] + }, + "d0f92604-a6fa-4d85-9716-ea8f32db78e0": { + "path": "d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "cellName": "API Response:\nSend Diagnostics\nData to\nClient", + "cellId": "d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "visible": true + }, + "generated-edge-simstep-44a4372f-a903-4c43-a9a5-c7549c875e11-d0f92604-a6fa-4d85-9716-ea8f32db78e0": { + "path": "generated-edge-simstep-44a4372f-a903-4c43-a9a5-c7549c875e11-d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "fileName": "diagnosticService.js", + "cellName": "API Response: Send Diagnostics Data to Client", + "cellId": "d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "visible": true, + "startLine": 52, + "endLine": 55, + "parentPath": "server/src/service/v1/business/diagnosticService.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "44a4372f-a903-4c43-a9a5-c7549c875e11" + } + ] + } + }, + "simulations": { + "Comprehensive Uptime Monitoring for Various Services": { + "name": "Comprehensive Uptime Monitoring for Various Services", + "simSteps": [ + { + "simStepId": "ab5de0b9-c70d-492c-98d9-8ddee66284ad", + "diagramNodeId": "7a2da928-a65c-49d8-b0a3-c159a6df2bc5", + "simStepLabel": "Uptime Monitor Creation: Display Form", + "simStepDescription": "The user navigates to the 'Create Uptime Monitor' page. The `UptimeCreate` React component renders the form, allowing the user to select the monitor type (HTTP, Ping, Docker, etc.) and input details like URL, name, and check interval.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "startLine": "315", + "endLine": "802", + "relevantVariables": [ + "UptimeCreate", + "monitor", + "setMonitor", + "onChange", + "onSubmit" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\n \"type\": \"http\",\n \"statusWindowSize\": 5,\n \"statusWindowThreshold\": 60,\n \"matchMethod\": \"equal\",\n \"interval\": 60000,\n \"ignoreTlsErrors\": false,\n \"notifications\": [],\n \"url\": \"\",\n \"name\": \"\"\n}" + }, + { + "simStepId": "888df086-41bb-469d-9c0a-daf74bdecb9f", + "diagramNodeId": "0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "simStepLabel": "Uptime Monitor Creation: Form Submission", + "simStepDescription": "After filling the form, the user clicks the 'Create monitor' button, which triggers the `onSubmit` event handler, passing the current state of the form.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "startLine": "435", + "endLine": "443", + "relevantVariables": [ + "onSubmit" + ] + }, + "inputDataExample": "{\n \"type\": \"http\",\n \"url\": \"https://google.com\",\n \"name\": \"My Google Monitor\",\n \"interval\": 60000,\n \"notifications\": []\n}", + "outputDataExample": "{\n \"type\": \"http\",\n \"url\": \"https://google.com\",\n \"name\": \"My Google Monitor\",\n \"interval\": 60000,\n \"notifications\": []\n}" + }, + { + "simStepId": "e02ddf57-270e-4f71-b4c4-d5183bb7879a", + "diagramNodeId": "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c", + "simStepLabel": "Uptime Monitor Creation: Handle Form Submission and Validation", + "simStepDescription": "The `onSubmit` function in `UptimeCreate.jsx` is executed. It prevents the default form submission, constructs a `form` object with monitor data, validates it using `monitorValidation`, and then calls the `createMonitor` function from the `useCreateMonitor` hook.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "startLine": "170", + "endLine": "248", + "relevantVariables": [ + "onSubmit", + "monitorValidation", + "createMonitor" + ] + }, + "inputDataExample": "{\n \"type\": \"http\",\n \"url\": \"google.com\",\n \"name\": \"My Google Monitor\",\n \"interval\": 60000,\n \"notifications\": []\n}", + "outputDataExample": "{\n \"monitor\": {\n \"url\": \"http://google.com\",\n \"name\": \"My Google Monitor\",\n \"statusWindowSize\": 5,\n \"statusWindowThreshold\": 60,\n \"type\": \"http\",\n \"interval\": 60000,\n \"description\": \"My Google Monitor\",\n \"notifications\": []\n },\n \"redirect\": \"/uptime\"\n}" + }, + { + "simStepId": "3fe0afaa-c673-4b52-a0c4-02a3ffd3a075", + "diagramNodeId": "1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "simStepLabel": "Uptime Monitor Creation: Call Network Service", + "simStepDescription": "The `createMonitor` function from the `useCreateMonitor` hook is called, which in turn calls `networkService.createMonitor` to send the monitor data to the backend API.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/monitorHooks.js", + "startLine": "306", + "endLine": "306", + "relevantVariables": [ + "networkService.createMonitor" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"url\": \"http://google.com\",\n \"name\": \"My Google Monitor\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"notifications\": []\n }\n}", + "outputDataExample": "{\n \"monitor\": {\n \"url\": \"http://google.com\",\n \"name\": \"My Google Monitor\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"notifications\": []\n }\n}" + }, + { + "simStepId": "14e9986b-c6ce-475e-97ed-da1fa2b6968d", + "diagramNodeId": "a30ac765-c546-4993-988e-8c8cfdb7532f", + "simStepLabel": "Uptime Monitor Creation: Send API Request", + "simStepDescription": "The `createMonitor` method in `NetworkService.js` makes an HTTP POST request to the `/monitors` endpoint on the server, sending the new monitor's data in the request body.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "119", + "endLine": "121", + "relevantVariables": [ + "createMonitor", + "this.axiosInstance.post" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"url\": \"http://google.com\",\n \"name\": \"My Google Monitor\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"notifications\": []\n }\n}", + "outputDataExample": "{\n \"status\": 200,\n \"data\": {\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"isActive\": true\n }\n}" + }, + { + "simStepId": "13dd0564-44a5-43ea-a163-ce1148cc48e7", + "diagramNodeId": "b6d963d5-c613-407a-9ce9-60a04048dea7", + "simStepLabel": "Uptime Monitor Creation: API Route to Controller", + "simStepDescription": "The server's router receives the POST request on `/api/v1/monitors` and forwards it to the `createMonitor` method in the `MonitorController`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "35", + "endLine": "35", + "relevantVariables": [ + "this.router.post", + "this.monitorController.createMonitor" + ] + }, + "inputDataExample": "{\n \"headers\": {\n \"Authorization\": \"Bearer \"\n },\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n }\n}", + "outputDataExample": "{\n \"headers\": {\n \"Authorization\": \"Bearer \"\n },\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n }\n}" + }, + { + "simStepId": "38fd069d-9c44-4078-954b-3a0899e9acf4", + "diagramNodeId": "f0409948-75dc-457f-a02d-6b9774258e48", + "simStepLabel": "Uptime Monitor Creation: Controller to Service", + "simStepDescription": "The `MonitorController` extracts the user and team ID from the request and calls the `createMonitor` method in the `MonitorService`, passing along the monitor data from the request body.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "195", + "endLine": "195", + "relevantVariables": [ + "this.monitorService.createMonitor" + ] + }, + "inputDataExample": "{\n \"user\": {\n \"_id\": \"user-123\",\n \"teamId\": \"team-abc\"\n },\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}", + "outputDataExample": "{\n \"teamId\": \"team-abc\",\n \"userId\": \"user-123\",\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}" + }, + { + "simStepId": "a6f4c967-b1a8-4804-bc2e-67f47a7b68f9", + "diagramNodeId": "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "simStepLabel": "Uptime Monitor Creation: Service to DB Module", + "simStepDescription": "The `MonitorService` calls the `createMonitor` method of the `monitorModule` to handle the database interaction.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "74", + "endLine": "78", + "relevantVariables": [ + "this.db.monitorModule.createMonitor" + ] + }, + "inputDataExample": "{\n \"teamId\": \"team-abc\",\n \"userId\": \"user-123\",\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}", + "outputDataExample": "{\n \"teamId\": \"team-abc\",\n \"userId\": \"user-123\",\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}" + }, + { + "simStepId": "ca3ad309-323a-418d-a171-c16f56fae23c", + "diagramNodeId": "2d407bfd-e4b0-46b8-99e7-121c507d525f", + "simStepLabel": "Uptime Monitor Creation: Save Monitor to Database", + "simStepDescription": "The `monitorModule` creates a new `Monitor` model instance with the provided data and saves it to the MongoDB database. The newly created document, including its generated `_id`, is returned.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModule.js", + "startLine": "454", + "endLine": "455", + "relevantVariables": [ + "this.Monitor", + "monitor.save" + ] + }, + "inputDataExample": "{\n \"teamId\": \"team-abc\",\n \"userId\": \"user-123\",\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}", + "outputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"userId\": \"user-123\",\n \"teamId\": \"team-abc\",\n \"isActive\": true,\n \"createdAt\": \"2023-01-01T12:00:00Z\",\n \"updatedAt\": \"2023-01-01T12:00:00Z\"\n}" + }, + { + "simStepId": "71ec03b6-1166-4c5d-9ada-0875a0866e30", + "diagramNodeId": "de320ac1-0949-4297-be7c-b9d7a14e0fec", + "simStepLabel": "Uptime Monitor Creation: Service to Job Queue", + "simStepDescription": "After successfully saving the monitor to the database, the `MonitorService` returns the new monitor object and then calls `jobQueue.addJob` to schedule the first and subsequent checks for this monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "80", + "endLine": "80", + "relevantVariables": [ + "this.jobQueue.addJob" + ] + }, + "inputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"isActive\": true\n}", + "outputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"isActive\": true\n}" + }, + { + "simStepId": "2702bfca-7611-4562-8d3e-a05af8257871", + "diagramNodeId": "51e8db5f-2fdd-4ddc-9605-43981db9d940", + "simStepLabel": "Uptime Monitor Creation: Schedule Job", + "simStepDescription": "The `SuperSimpleQueue` receives the new monitor and uses its `scheduler.addJob` method to create a recurring job based on the monitor's interval. The monitor object itself is stored as data for the job.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "startLine": "50", + "endLine": "57", + "relevantVariables": [ + "addJob", + "this.scheduler.addJob" + ] + }, + "inputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"isActive\": true\n}", + "outputDataExample": "{\n \"id\": \"60d21b4667d0d8992e610c85\",\n \"template\": \"monitor-job\",\n \"repeat\": 60000,\n \"active\": true,\n \"data\": { ...monitor object ... }\n}" + }, + { + "simStepId": "e22a708a-a348-4775-8ee3-f32a35446e24", + "diagramNodeId": "18c13391-0cf7-4303-8180-0a40cfd64b58", + "simStepLabel": "Service Monitoring: Job Execution", + "simStepDescription": "The scheduler triggers a monitor job at the specified interval. The `getMonitorJob` function in `SuperSimpleQueueHelper` is executed, which first checks if the monitor is in a maintenance window.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "27", + "endLine": "73", + "relevantVariables": [ + "getMonitorJob", + "isInMaintenanceWindow" + ] + }, + "inputDataExample": "{\n \"id\": \"60d21b4667d0d8992e610c85\",\n \"data\": {\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n }\n}", + "outputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n}" + }, + { + "simStepId": "1d7580f2-f107-4008-a207-b8ec07998721", + "diagramNodeId": "38ba9b22-5b26-432d-a856-0eceb792c737", + "simStepLabel": "Service Monitoring: Request Service Status", + "simStepDescription": "Assuming no active maintenance, the job helper calls `networkService.requestStatus`, passing the monitor object to perform the actual availability check.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "45", + "endLine": "45", + "relevantVariables": [ + "this.networkService.requestStatus" + ] + }, + "inputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n}", + "outputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n}" + }, + { + "simStepId": "84f82fea-e306-4e0b-8222-e9705803d51a", + "diagramNodeId": "5c900373-e6ba-44d5-b341-676a32d659c8", + "simStepLabel": "Service Monitoring: Perform Network Check", + "simStepDescription": "The `requestStatus` method in the network service acts as a router. Based on the monitor's `type` (e.g., 'http', 'ping', 'docker'), it calls the corresponding specialized request function (e.g., `requestHttp`) to perform the check and constructs a standardized response object.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/networkService.js", + "startLine": "44", + "endLine": "59", + "relevantVariables": [ + "requestStatus", + "requestHttp", + "requestPing", + "requestDocker", + "requestPort", + "requestGame" + ] + }, + "inputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n}", + "outputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"type\": \"http\",\n \"status\": true,\n \"code\": 200,\n \"responseTime\": 150,\n \"message\": \"OK\"\n}" + }, + { + "simStepId": "9b0ada30-d3e6-4b2c-b3ff-f50615139588", + "diagramNodeId": "5e80734c-7b72-41be-9068-30cda3ffb425", + "simStepLabel": "Service Monitoring: Propagate Network Response", + "simStepDescription": "The network response object, containing the result of the check (status, response time, status code), is returned to the job handler in `SuperSimpleQueueHelper`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "45", + "endLine": "45", + "relevantVariables": [ + "networkResponse" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"type\": \"http\",\n \"status\": true,\n \"code\": 200,\n \"responseTime\": 150,\n \"message\": \"OK\"\n}", + "outputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"type\": \"http\",\n \"status\": true,\n \"code\": 200,\n \"responseTime\": 150,\n \"message\": \"OK\"\n}" + }, + { + "simStepId": "c66a04c6-113e-4391-ab71-93fc612279ef", + "diagramNodeId": "45e38f3a-a7c3-4de7-b3dc-589361d793fd", + "simStepLabel": "Service Monitoring: Update Monitor Status", + "simStepDescription": "The job handler passes the network response to `statusService.updateStatus`. This service creates a new `Check` document in the database, updates the monitor's status based on a sliding window and threshold, and calculates running statistics.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/statusService.js", + "startLine": "108", + "endLine": "183", + "relevantVariables": [ + "updateStatus", + "buildCheck", + "insertCheck", + "updateRunningStats" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"status\": true,\n \"responseTime\": 150,\n \"code\": 200\n}", + "outputDataExample": "{\n \"monitor\": { ...updated monitor object ... },\n \"statusChanged\": false,\n \"prevStatus\": true\n}" + }, + { + "simStepId": "9896c387-4fca-47ad-82eb-7301286decb7", + "diagramNodeId": "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "simStepLabel": "Service Monitoring: Check for Status Change", + "simStepDescription": "The result from `statusService`, indicating whether the monitor's overall status changed (e.g., from 'up' to 'down'), is returned to the job handler.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "51", + "endLine": "51", + "relevantVariables": [ + "updatedMonitor", + "statusChanged", + "prevStatus" + ] + }, + "inputDataExample": "{\n \"monitor\": { ...updated monitor object ... },\n \"statusChanged\": false,\n \"prevStatus\": true\n}", + "outputDataExample": "{\n \"monitor\": { ...updated monitor object ... },\n \"statusChanged\": false,\n \"prevStatus\": true\n}" + }, + { + "simStepId": "1a260a75-cf50-4f7a-90b0-c280b512a5cb", + "diagramNodeId": "9f3ea7fc-16d3-4d36-97eb-66f8808425cc", + "simStepLabel": "Service Monitoring: Handle Notifications (Conditional)", + "simStepDescription": "If `statusChanged` is true, the job handler calls `notificationService.handleNotifications`. The service identifies the notification channels associated with the monitor and sends out alerts (e.g., email, webhook, Discord) with details about the status change.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationService.js", + "startLine": "76", + "endLine": "92", + "relevantVariables": [ + "handleNotifications", + "notifyAll", + "buildStatusEmail", + "buildWebhookMessage", + "buildHardwareAlerts" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"status\": false,\n \"monitor\": { ... },\n \"statusChanged\": true,\n \"prevStatus\": true\n}", + "outputDataExample": "{}" + } + ], + "description": "", + "simulationNodesAndEdges": { + "7a2da928-a65c-49d8-b0a3-c159a6df2bc5": { + "simStepIds": [ + "ab5de0b9-c70d-492c-98d9-8ddee66284ad" + ] + }, + "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c": { + "simStepIds": [ + "e02ddf57-270e-4f71-b4c4-d5183bb7879a" + ] + }, + "a30ac765-c546-4993-988e-8c8cfdb7532f": { + "simStepIds": [ + "14e9986b-c6ce-475e-97ed-da1fa2b6968d" + ] + }, + "f0409948-75dc-457f-a02d-6b9774258e48": { + "simStepIds": [ + "38fd069d-9c44-4078-954b-3a0899e9acf4" + ] + }, + "2d407bfd-e4b0-46b8-99e7-121c507d525f": { + "simStepIds": [ + "ca3ad309-323a-418d-a171-c16f56fae23c" + ] + }, + "51e8db5f-2fdd-4ddc-9605-43981db9d940": { + "simStepIds": [ + "2702bfca-7611-4562-8d3e-a05af8257871" + ] + }, + "18c13391-0cf7-4303-8180-0a40cfd64b58": { + "simStepIds": [ + "e22a708a-a348-4775-8ee3-f32a35446e24" + ] + }, + "5c900373-e6ba-44d5-b341-676a32d659c8": { + "simStepIds": [ + "84f82fea-e306-4e0b-8222-e9705803d51a" + ] + }, + "45e38f3a-a7c3-4de7-b3dc-589361d793fd": { + "simStepIds": [ + "c66a04c6-113e-4391-ab71-93fc612279ef" + ] + }, + "9f3ea7fc-16d3-4d36-97eb-66f8808425cc": { + "simStepIds": [ + "1a260a75-cf50-4f7a-90b0-c280b512a5cb" + ] + }, + "0e09aa33-d7c7-4c89-9da8-f0937658f4d3": { + "simStepIds": [ + "888df086-41bb-469d-9c0a-daf74bdecb9f" + ] + }, + "1f6f04a9-0123-4729-b06a-8f3da6d1b58c": { + "simStepIds": [ + "3fe0afaa-c673-4b52-a0c4-02a3ffd3a075" + ] + }, + "b6d963d5-c613-407a-9ce9-60a04048dea7": { + "simStepIds": [ + "13dd0564-44a5-43ea-a163-ce1148cc48e7" + ] + }, + "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7": { + "simStepIds": [ + "a6f4c967-b1a8-4804-bc2e-67f47a7b68f9" + ] + }, + "de320ac1-0949-4297-be7c-b9d7a14e0fec": { + "simStepIds": [ + "71ec03b6-1166-4c5d-9ada-0875a0866e30" + ] + }, + "38ba9b22-5b26-432d-a856-0eceb792c737": { + "simStepIds": [ + "1d7580f2-f107-4008-a207-b8ec07998721" + ] + }, + "5e80734c-7b72-41be-9068-30cda3ffb425": { + "simStepIds": [ + "9b0ada30-d3e6-4b2c-b3ff-f50615139588" + ] + }, + "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c": { + "simStepIds": [ + "9896c387-4fca-47ad-82eb-7301286decb7" + ] + } + }, + "isAIGenerated": true, + "keywords": "Monitor, checkService, UptimeCreate", + "generationPrompt": "Comprehensive Uptime Monitoring for Various Services", + "generationKeywords": "Monitor, checkService, UptimeCreate" + }, + "Infrastructure and Hardware Health Monitoring": { + "name": "Infrastructure and Hardware Health Monitoring", + "simSteps": [ + { + "simStepId": "80dd889a-1fc9-4d56-b9cf-5b45bf1110b1", + "diagramNodeId": "9651d084-5c16-4417-89c1-f52ae6572a95", + "simStepLabel": "Configuration: User Sets Alert Thresholds", + "simStepDescription": "A user navigates to the 'Create Infrastructure Monitor' page to set up monitoring for a new server. They enable alerts for CPU, memory, and disk usage, and define specific thresholds for each metric. For example, an alert is configured to trigger if CPU usage exceeds 80%.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "startLine": "45", + "endLine": "53", + "relevantVariables": [ + "CustomThreshold", + "METRICS", + "METRIC_PREFIX", + "handleCheckboxChange", + "infrastructureMonitor" + ] + }, + "inputDataExample": "{\n \"infrastructureMonitor\": {\n \"url\": \"my-server.example.com\",\n \"name\": \"Production Web Server\",\n \"cpu\": false,\n \"usage_cpu\": \"\",\n \"memory\": false,\n \"usage_memory\": \"\",\n \"disk\": false,\n \"usage_disk\": \"\"\n }\n}", + "outputDataExample": "{\n \"infrastructureMonitor\": {\n \"url\": \"my-server.example.com\",\n \"name\": \"Production Web Server\",\n \"cpu\": true,\n \"usage_cpu\": \"80\",\n \"memory\": true,\n \"usage_memory\": \"75\",\n \"disk\": true,\n \"usage_disk\": \"90\"\n }\n}" + }, + { + "simStepId": "815f96af-c28e-4f80-b4ab-18a6fb23eee0", + "diagramNodeId": "1c5c9857-bf32-40e4-921b-957c252dac2a", + "simStepLabel": "API Call: Submit Monitor Configuration", + "simStepDescription": "After configuring the monitor and its alert thresholds, the user saves the configuration. The frontend application constructs a monitor object with `type: 'hardware'` and sends it to the backend via a POST request to create the new monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/monitorHooks.js", + "startLine": "386", + "endLine": "389", + "relevantVariables": [ + "updatedFields", + "monitor.type", + "monitor.thresholds", + "monitor.secret" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"name\": \"Production Web Server\",\n \"url\": \"http://my-server.example.com\",\n \"type\": \"hardware\",\n \"interval\": 15000,\n \"secret\": \"super-secret-key\",\n \"thresholds\": {\n \"usage_cpu\": 0.8,\n \"usage_memory\": 0.75,\n \"usage_disk\": 0.9\n }\n }\n}", + "outputDataExample": "{\n \"monitor\": {\n \"name\": \"Production Web Server\",\n \"url\": \"http://my-server.example.com\",\n \"type\": \"hardware\",\n \"interval\": 15000,\n \"secret\": \"super-secret-key\",\n \"thresholds\": {\n \"usage_cpu\": 0.8,\n \"usage_memory\": 0.75,\n \"usage_disk\": 0.9\n }\n }\n}" + }, + { + "simStepId": "ed6e7faf-69dd-4d0b-bda8-137739ed288b", + "diagramNodeId": "4c47b5f2-5363-4816-9745-30e61d184c7a", + "simStepLabel": "Automated Check: Backend Fetches Hardware Data", + "simStepDescription": "The Checkmate server's job queue periodically initiates a check for the newly created hardware monitor. It makes an HTTP request to the monitor's URL, which points to the external 'Capture' agent running on the target server.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/networkService.js", + "startLine": "7", + "endLine": "14", + "relevantVariables": [ + "TYPE_HARDWARE" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"_id\": \"65ada27a51442afd52857053\",\n \"name\": \"Production Web Server\",\n \"url\": \"http://my-server.example.com\",\n \"type\": \"hardware\",\n \"interval\": 15000\n }\n}", + "outputDataExample": "{\n \"networkResponse\": {\n \"ok\": true,\n \"status\": 200,\n \"statusText\": \"OK\",\n \"payload\": {\n \"data\": {\n \"cpu\": {\n \"usage_percent\": 0.85\n },\n \"memory\": {\n \"usage_percent\": 0.65\n },\n \"disk\": [\n {\n \"usage_percent\": 0.5\n }\n ]\n }\n }\n }\n}" + }, + { + "simStepId": "4434dca1-2048-4458-a70b-b70d569e378d", + "diagramNodeId": "a64547c9-d9e6-4941-8e5e-f49add06516b", + "simStepLabel": "Automated Check: Data Received from Capture Agent", + "simStepDescription": "The 'Capture' agent responds to the server's request with a JSON payload containing detailed hardware metrics, including CPU usage, memory consumption, disk space, and network statistics.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/statusService.js", + "startLine": "266", + "endLine": "273", + "relevantVariables": [ + "payload", + "cpu", + "memory", + "disk", + "host" + ] + }, + "inputDataExample": "{\n \"data\": {\n \"cpu\": {\n \"physical_core\": 4,\n \"logical_core\": 8,\n \"frequency\": 3400,\n \"usage_percent\": 0.85,\n \"temperature\": [65, 66, 65, 67]\n },\n \"memory\": {\n \"total_bytes\": 17179869184,\n \"used_bytes\": 11166914970,\n \"usage_percent\": 0.65\n },\n \"disk\": [\n {\n \"name\": \"/dev/sda1\",\n \"total_bytes\": 250685575168,\n \"used_bytes\": 125342787584,\n \"usage_percent\": 0.5\n }\n ],\n \"host\": {\n \"os\": \"Ubuntu 22.04 LTS\"\n }\n },\n \"errors\": []\n}", + "outputDataExample": "{\n \"data\": {\n \"cpu\": {\n \"physical_core\": 4,\n \"logical_core\": 8,\n \"frequency\": 3400,\n \"usage_percent\": 0.85,\n \"temperature\": [65, 66, 65, 67]\n },\n \"memory\": {\n \"total_bytes\": 17179869184,\n \"used_bytes\": 11166914970,\n \"usage_percent\": 0.65\n },\n \"disk\": [\n {\n \"name\": \"/dev/sda1\",\n \"total_bytes\": 250685575168,\n \"used_bytes\": 125342787584,\n \"usage_percent\": 0.5\n }\n ],\n \"host\": {\n \"os\": \"Ubuntu 22.04 LTS\"\n }\n },\n \"errors\": []\n}" + }, + { + "simStepId": "e5b90cf2-9369-4741-9803-c835a81c009b", + "diagramNodeId": "c1bf10c9-8c1a-44f4-8e23-39708f0301e8", + "simStepLabel": "Automated Check: Process and Store Hardware Data", + "simStepDescription": "The backend's StatusService receives the payload from the Capture agent. It parses the data and constructs a 'check' document. This document, containing the detailed hardware metrics, is then saved to the database for historical tracking and visualization.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/statusService.js", + "startLine": "265", + "endLine": "274", + "relevantVariables": [ + "buildCheck", + "type", + "payload", + "check.cpu", + "check.memory", + "check.disk", + "check.host" + ] + }, + "inputDataExample": "{\n \"networkResponse\": {\n \"type\": \"hardware\",\n \"payload\": {\n \"data\": {\n \"cpu\": { \"usage_percent\": 0.85 },\n \"memory\": { \"usage_percent\": 0.65 },\n \"disk\": [{ \"usage_percent\": 0.5 }],\n \"host\": { \"os\": \"Ubuntu 22.04 LTS\" }\n }\n }\n }\n}", + "outputDataExample": "{\n \"check\": {\n \"monitorId\": \"65ada27a51442afd52857053\",\n \"type\": \"hardware\",\n \"status\": true,\n \"cpu\": { \"usage_percent\": 0.85 },\n \"memory\": { \"usage_percent\": 0.65 },\n \"disk\": [{ \"usage_percent\": 0.5 }],\n \"host\": { \"os\": \"Ubuntu 22.04 LTS\" }\n }\n}" + }, + { + "simStepId": "b019a6d0-a23a-4a1d-bd21-644f01996f14", + "diagramNodeId": "7a250d09-50c6-4935-b10c-626b0ebe1fba", + "simStepLabel": "Automated Check: Pass Data for Alert Evaluation", + "simStepDescription": "After storing the check data, the system passes the complete network response, including monitor details and the latest metrics, to the notification service to evaluate if any alerts need to be triggered.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationService.js", + "startLine": "60", + "endLine": "79", + "relevantVariables": [ + "handleNotifications", + "networkResponse", + "monitor.type", + "thresholds", + "metrics" + ] + }, + "inputDataExample": "{\n \"networkResponse\": {\n \"monitor\": {\n \"_id\": \"65ada27a51442afd52857053\",\n \"name\": \"Production Web Server\",\n \"thresholds\": {\n \"usage_cpu\": 0.8\n },\n \"notifications\": [\"65a5b0b3e8a2b0e6e8c7b8e3\"]\n },\n \"payload\": {\n \"data\": {\n \"cpu\": { \"usage_percent\": 0.85 }\n }\n }\n }\n}", + "outputDataExample": "{\n \"networkResponse\": {\n \"monitor\": {\n \"_id\": \"65ada27a51442afd52857053\",\n \"name\": \"Production Web Server\",\n \"thresholds\": {\n \"usage_cpu\": 0.8\n },\n \"notifications\": [\"65a5b0b3e8a2b0e6e8c7b8e3\"]\n },\n \"payload\": {\n \"data\": {\n \"cpu\": { \"usage_percent\": 0.85 }\n }\n }\n }\n}" + }, + { + "simStepId": "07fe8489-1d94-4afb-83f8-7cebb0dff49a", + "diagramNodeId": "d4f48f1a-61de-4cde-87d2-4b5215464899", + "simStepLabel": "Automated Check: Evaluate Data Against Thresholds", + "simStepDescription": "The notification utility function compares the received hardware metrics against the user-defined thresholds stored in the monitor's configuration. In this case, the CPU usage of 85% is greater than the 80% threshold, so an alert condition is met.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationUtils.js", + "startLine": "95", + "endLine": "109", + "relevantVariables": [ + "buildHardwareAlerts", + "thresholds", + "metrics", + "cpuThreshold", + "cpuUsage", + "alerts" + ] + }, + "inputDataExample": "{\n \"networkResponse\": {\n \"monitor\": {\n \"thresholds\": {\n \"usage_cpu\": 0.8\n }\n },\n \"payload\": {\n \"data\": {\n \"cpu\": {\n \"usage_percent\": 0.85\n }\n }\n }\n }\n}", + "outputDataExample": "{\n \"alerts\": {\n \"cpu\": true,\n \"memory\": false,\n \"disk\": false\n }\n}" + }, + { + "simStepId": "c691b3b9-88b3-4c88-a399-ff9fd5866194", + "diagramNodeId": "3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "simStepLabel": "Automated Check: Generate Alert Notification Content", + "simStepDescription": "Since the CPU threshold was breached, the system constructs a user-friendly alert message. For email notifications, it builds an HTML email detailing the alert, including the monitor name, the metric that triggered the alert, the current value, and the threshold.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationUtils.js", + "startLine": "177", + "endLine": "183", + "relevantVariables": [ + "buildHardwareEmail", + "networkResponse", + "alerts", + "template", + "context", + "subject", + "html" + ] + }, + "inputDataExample": "{\n \"networkResponse\": {\n \"monitor\": {\n \"name\": \"Production Web Server\",\n \"url\": \"http://my-server.example.com\"\n }\n },\n \"alerts\": [\n \"Your current CPU usage (85%) is above your threshold (80%)\"\n ]\n}", + "outputDataExample": "{\n \"subject\": \"Monitor Production Web Server infrastructure alerts\",\n \"html\": \"...HTML content for the email...\"\n}" + }, + { + "simStepId": "c27365db-9db6-4cc9-96f0-4f046fd38562", + "diagramNodeId": "801fe70a-1a19-406c-8f0d-770b0cb74409", + "simStepLabel": "Visualization: User Views Monitor Details", + "simStepDescription": "A user navigates to the details page for their 'Production Web Server' monitor in the Checkmate UI to view its performance. The React component for the details page mounts and prepares to fetch data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "startLine": "23", + "endLine": "31", + "relevantVariables": [ + "InfrastructureDetails", + "useParams", + "useState", + "dateRange", + "monitorId" + ] + }, + "inputDataExample": "{\n \"path\": \"/infrastructure/65ada27a51442afd52857053\"\n}", + "outputDataExample": "{\n \"monitorId\": \"65ada27a51442afd52857053\",\n \"dateRange\": \"recent\"\n}" + }, + { + "simStepId": "a5d20172-1c95-4c9e-af39-f86a498734bb", + "diagramNodeId": "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "simStepLabel": "API Call: Request for Historical Hardware Data", + "simStepDescription": "The frontend makes a GET request to the backend API to fetch the historical hardware data for the specified monitor and the selected time range (e.g., 'recent').", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "198", + "endLine": "206", + "relevantVariables": [ + "getHardwareDetailsByMonitorId", + "config.monitorId", + "config.dateRange", + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"65ada27a51442afd52857053\",\n \"dateRange\": \"recent\"\n}", + "outputDataExample": "{\n \"request\": \"GET /api/v1/monitors/hardware/details/65ada27a51442afd52857053?dateRange=recent\"\n}" + }, + { + "simStepId": "8502ae1f-f029-446e-9dde-59c2aba87a8a", + "diagramNodeId": "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03", + "simStepLabel": "Backend: Aggregate Historical Data", + "simStepDescription": "The backend receives the request and executes a database aggregation pipeline. This query fetches all 'hardware' checks for the monitor within the given date range and calculates metrics like average CPU usage, average memory usage, and average temperatures, grouped by time intervals.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModule.js", + "startLine": "312", + "endLine": "322", + "relevantVariables": [ + "getHardwareDetailsById", + "monitorId", + "dateRange", + "buildHardwareDetailsPipeline", + "hardwareStats" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"65ada27a51442afd52857053\",\n \"dateRange\": \"recent\"\n}", + "outputDataExample": "{\n \"stats\": {\n \"checks\": [\n {\n \"_id\": \"2023-10-27T10:00:00Z\",\n \"avgCpuUsage\": 0.85,\n \"avgMemoryUsage\": 0.65\n },\n {\n \"_id\": \"2023-10-27T10:01:00Z\",\n \"avgCpuUsage\": 0.82,\n \"avgMemoryUsage\": 0.66\n }\n ],\n \"latestCheck\": {\n \"cpu\": { \"usage_percent\": 0.85 },\n \"memory\": { \"usage_percent\": 0.65 }\n }\n }\n}" + }, + { + "simStepId": "627fcb0c-0282-4b4d-8c55-aceb636e4da0", + "diagramNodeId": "60308696-6f8a-43c8-b9a1-da199ef500cd", + "simStepLabel": "API Response: Send Aggregated Data to Frontend", + "simStepDescription": "The backend sends the aggregated hardware statistics back to the frontend in a structured JSON format, ready for visualization.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModuleQueries.js", + "startLine": "247", + "endLine": "262", + "relevantVariables": [ + "buildHardwareDetailsPipeline", + "$group", + "avgCpuUsage", + "avgMemoryUsage", + "avgTemperatures" + ] + }, + "inputDataExample": "{\n \"stats\": {\n \"checks\": [\n {\n \"_id\": \"2023-10-27T10:00:00Z\",\n \"avgCpuUsage\": 0.85,\n \"avgMemoryUsage\": 0.65,\n \"avgTemperature\": [65.5]\n },\n {\n \"_id\": \"2023-10-27T10:01:00Z\",\n \"avgCpuUsage\": 0.82,\n \"avgMemoryUsage\": 0.66,\n \"avgTemperature\": [66.0]\n }\n ],\n \"latestCheck\": {\n \"cpu\": { \"usage_percent\": 0.85, \"physical_core\": 4, \"frequency\": 3400 },\n \"memory\": { \"usage_percent\": 0.65, \"total_bytes\": 17179869184, \"used_bytes\": 11166914970 }\n }\n }\n}", + "outputDataExample": "{\n \"stats\": {\n \"checks\": [\n {\n \"_id\": \"2023-10-27T10:00:00Z\",\n \"avgCpuUsage\": 0.85,\n \"avgMemoryUsage\": 0.65,\n \"avgTemperature\": [65.5]\n },\n {\n \"_id\": \"2023-10-27T10:01:00Z\",\n \"avgCpuUsage\": 0.82,\n \"avgMemoryUsage\": 0.66,\n \"avgTemperature\": [66.0]\n }\n ],\n \"latestCheck\": {\n \"cpu\": { \"usage_percent\": 0.85, \"physical_core\": 4, \"frequency\": 3400 },\n \"memory\": { \"usage_percent\": 0.65, \"total_bytes\": 17179869184, \"used_bytes\": 11166914970 }\n }\n }\n}" + }, + { + "simStepId": "6eedce82-b9c8-4022-95e5-3a6e00628105", + "diagramNodeId": "9ea2e32d-1ea8-42f2-9a17-613844723ae6", + "simStepLabel": "Visualization: Frontend Renders Charts and Gauges", + "simStepDescription": "The frontend components on the Infrastructure Details page receive the aggregated data. The 'GaugeBoxes' component uses the 'latestCheck' data to render real-time gauges for current CPU and memory usage, while the 'AreaChartBoxes' component uses the historical 'checks' data to render time-series charts.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx", + "startLine": "22", + "endLine": "51", + "relevantVariables": [ + "Gauges", + "latestCheck", + "memoryUsagePercent", + "cpuUsagePercent", + "gauges" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"stats\": {\n \"checks\": [\n { \"_id\": \"2023-10-27T10:00:00Z\", \"avgCpuUsage\": 0.85 },\n { \"_id\": \"2023-10-27T10:01:00Z\", \"avgCpuUsage\": 0.82 }\n ],\n \"latestCheck\": {\n \"cpu\": { \"usage_percent\": 0.85, \"physical_core\": 4, \"frequency\": 3400 },\n \"memory\": { \"usage_percent\": 0.65, \"total_bytes\": 17179869184, \"used_bytes\": 11166914970 }\n }\n }\n }\n}", + "outputDataExample": "{\n \"rendered_gauges\": [\n {\n \"type\": \"memory\",\n \"value\": \"65%\",\n \"heading\": \"Memory usage\"\n },\n {\n \"type\": \"cpu\",\n \"value\": \"85%\",\n \"heading\": \"CPU usage\"\n }\n ]\n}" + } + ], + "description": "", + "simulationNodesAndEdges": { + "9651d084-5c16-4417-89c1-f52ae6572a95": { + "simStepIds": [ + "80dd889a-1fc9-4d56-b9cf-5b45bf1110b1" + ] + }, + "4c47b5f2-5363-4816-9745-30e61d184c7a": { + "simStepIds": [ + "ed6e7faf-69dd-4d0b-bda8-137739ed288b" + ] + }, + "c1bf10c9-8c1a-44f4-8e23-39708f0301e8": { + "simStepIds": [ + "e5b90cf2-9369-4741-9803-c835a81c009b" + ] + }, + "d4f48f1a-61de-4cde-87d2-4b5215464899": { + "simStepIds": [ + "07fe8489-1d94-4afb-83f8-7cebb0dff49a" + ] + }, + "801fe70a-1a19-406c-8f0d-770b0cb74409": { + "simStepIds": [ + "c27365db-9db6-4cc9-96f0-4f046fd38562" + ] + }, + "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03": { + "simStepIds": [ + "8502ae1f-f029-446e-9dde-59c2aba87a8a" + ] + }, + "9ea2e32d-1ea8-42f2-9a17-613844723ae6": { + "simStepIds": [ + "6eedce82-b9c8-4022-95e5-3a6e00628105" + ] + }, + "1c5c9857-bf32-40e4-921b-957c252dac2a": { + "simStepIds": [ + "815f96af-c28e-4f80-b4ab-18a6fb23eee0" + ] + }, + "a64547c9-d9e6-4941-8e5e-f49add06516b": { + "simStepIds": [ + "4434dca1-2048-4458-a70b-b70d569e378d" + ] + }, + "7a250d09-50c6-4935-b10c-626b0ebe1fba": { + "simStepIds": [ + "b019a6d0-a23a-4a1d-bd21-644f01996f14" + ] + }, + "3d357de8-9fd2-4d0b-9623-f36fce4cf1de": { + "simStepIds": [ + "c691b3b9-88b3-4c88-a399-ff9fd5866194" + ] + }, + "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4": { + "simStepIds": [ + "a5d20172-1c95-4c9e-af39-f86a498734bb" + ] + }, + "60308696-6f8a-43c8-b9a1-da199ef500cd": { + "simStepIds": [ + "627fcb0c-0282-4b4d-8c55-aceb636e4da0" + ] + } + }, + "isAIGenerated": true, + "keywords": "hardware, InfrastructureDetails, cpu", + "generationPrompt": "Infrastructure and Hardware Health Monitoring", + "generationKeywords": "hardware, InfrastructureDetails, cpu" + }, + "Multi-Channel Alerting and Notifications": { + "name": "Multi-Channel Alerting and Notifications", + "simSteps": [ + { + "simStepId": "e924ec15-1bea-4e3b-befb-a8bf30cbf740", + "diagramNodeId": "15486629-84b3-463a-962b-06083f69e4bf", + "simStepLabel": "Flow 1: Render Notification Creation Form", + "simStepDescription": "User accesses the `/notifications/create` route, which renders the `CreateNotifications` component. This component displays a form for setting up a new notification channel, including fields for name, type, and provider-specific details.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Notifications/create/index.jsx", + "startLine": "157", + "endLine": "321", + "relevantVariables": [ + "CreateNotifications", + "notification", + "onChange", + "onSubmit" + ] + }, + "inputDataExample": "{\n \"route\": \"/notifications/create\"\n}", + "outputDataExample": "{\n \"renderedComponent\": \"CreateNotifications\",\n \"formFields\": [\"notificationName\", \"type\", \"address\", \"slackWebhookUrl\", etc...]\n}" + }, + { + "simStepId": "71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f", + "diagramNodeId": "54497b9a-4910-4df4-b016-480b9ac250a7", + "simStepLabel": "Flow 1: Submit Form Data for Creation", + "simStepDescription": "After filling the form, the user clicks 'Save'. The `onSubmit` function is triggered, which calls the `createNotification` function from the `useCreateNotification` hook with the form data.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Notifications/create/index.jsx", + "startLine": "96", + "endLine": "101", + "relevantVariables": [ + "onSubmit", + "createNotification" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}", + "outputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}" + }, + { + "simStepId": "383c6be8-aa51-4398-9ae3-21035413f73a", + "diagramNodeId": "a07b805e-e55f-46cb-b54b-765b7813b0ca", + "simStepLabel": "Flow 1: Frontend API Call", + "simStepDescription": "The `createNotification` hook executes an asynchronous function that calls `networkService.createNotification` to send the new notification data to the backend API.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/useNotifications.js", + "startLine": "14", + "endLine": "17", + "relevantVariables": [ + "createNotification", + "networkService" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}", + "outputDataExample": "{\n \"status\": \"pending\",\n \"promise\": {}\n}" + }, + { + "simStepId": "162d8d2f-6cd1-45fa-8711-1ee85c5f86ef", + "diagramNodeId": "dc135d8d-0515-403d-ae58-f3c04647b2ae", + "simStepLabel": "Flow 1: HTTP POST Request", + "simStepDescription": "The `NetworkService` constructs and sends an HTTP POST request to the `/notifications` endpoint with the notification data in the request body.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "1064", + "endLine": "1066", + "relevantVariables": [ + "createNotification", + "this.axiosInstance" + ] + }, + "inputDataExample": "{\n \"url\": \"/notifications\",\n \"method\": \"POST\",\n \"data\": {\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n }\n}", + "outputDataExample": "{\n \"url\": \"/notifications\",\n \"method\": \"POST\",\n \"data\": {\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n }\n}" + }, + { + "simStepId": "2f9266c9-4cbc-4b50-a280-cf8b5800d540", + "diagramNodeId": "d0a4737f-715f-4f50-b6d5-5e4b31f86f93", + "simStepLabel": "Flow 1: Backend Route Handling", + "simStepDescription": "The backend server receives the POST request. The Express router matches the `/notifications` path and forwards the request to the `createNotification` method of the `NotificationController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/notificationRoute.js", + "startLine": "10", + "endLine": "10", + "relevantVariables": [ + "this.router", + "this.notificationController.createNotification" + ] + }, + "inputDataExample": "{\n \"request\": {\n \"path\": \"/notifications\",\n \"method\": \"POST\",\n \"body\": {\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n }\n }\n}", + "outputDataExample": "{\n \"handler\": \"notificationController.createNotification\"\n}" + }, + { + "simStepId": "73e63d2f-2b43-488d-9e86-170f511673f5", + "diagramNodeId": "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "simStepLabel": "Flow 1: Controller to Database Module", + "simStepDescription": "The `createNotification` method in the controller validates the request body, appends user/team context, and then calls the `createNotification` method in the `notificationModule` to persist the data.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/notificationController.js", + "startLine": "55", + "endLine": "55", + "relevantVariables": [ + "body", + "this.db.notificationModule.createNotification" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"userId\": \"user-123\",\n \"teamId\": \"team-456\"\n}", + "outputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"userId\": \"user-123\",\n \"teamId\": \"team-456\"\n}" + }, + { + "simStepId": "d29ab4ab-be50-4f17-af6e-2c61cec9d0ab", + "diagramNodeId": "4a796ccf-40a9-4446-a109-6a6eed52e898", + "simStepLabel": "Flow 1: Database Insertion", + "simStepDescription": "The `notificationModule` creates a new instance of the `Notification` model with the provided data and saves it to the MongoDB database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/notificationModule.js", + "startLine": "10", + "endLine": "13", + "relevantVariables": [ + "notificationData", + "this.Notification" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"userId\": \"user-123\",\n \"teamId\": \"team-456\"\n}", + "outputDataExample": "{\n \"_id\": \"60d5ecf31c9d440000a1b2c3\",\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"userId\": \"user-123\",\n \"teamId\": \"team-456\",\n \"createdAt\": \"2023-10-27T10:00:00Z\",\n \"updatedAt\": \"2023-10-27T10:00:00Z\"\n}" + }, + { + "simStepId": "d390ef45-f2fe-4806-a45a-cc9c84b48a5a", + "diagramNodeId": "279155cd-ff31-41a2-8f5f-a538f01bfa66", + "simStepLabel": "Flow 1: User Clicks Test Notification Button", + "simStepDescription": "The user clicks the 'Test notification' button, which invokes the `onTestNotification` handler. This handler calls the `testNotification` function from the `useTestNotification` hook.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Notifications/create/index.jsx", + "startLine": "281", + "endLine": "285", + "relevantVariables": [ + "onTestNotification", + "testNotification" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}", + "outputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}" + }, + { + "simStepId": "c1b17b64-90e9-48bc-8927-25d7ebfc3036", + "diagramNodeId": "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39", + "simStepLabel": "Flow 1: Backend Receives Test Request", + "simStepDescription": "The client sends a POST request to `/notifications/test`. The router directs this request to the `testNotification` method in the `NotificationController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/notificationRoute.js", + "startLine": "13", + "endLine": "13", + "relevantVariables": [ + "this.router", + "this.notificationController.testNotification" + ] + }, + "inputDataExample": "{\n \"request\": {\n \"path\": \"/notifications/test\",\n \"method\": \"POST\",\n \"body\": {\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n }\n }\n}", + "outputDataExample": "{\n \"handler\": \"notificationController.testNotification\"\n}" + }, + { + "simStepId": "6f2bf027-50d9-472e-9569-808a64fce7cf", + "diagramNodeId": "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "simStepLabel": "Flow 1: Controller to Notification Service", + "simStepDescription": "The `testNotification` controller method passes the notification configuration from the request body to the `sendTestNotification` method of the `NotificationService`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/notificationController.js", + "startLine": "22", + "endLine": "22", + "relevantVariables": [ + "notification", + "this.notificationService.sendTestNotification" + ] + }, + "inputDataExample": "{\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n}", + "outputDataExample": "{\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n}" + }, + { + "simStepId": "f29f9be1-a0cd-43a3-b259-e5105a25ce41", + "diagramNodeId": "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709", + "simStepLabel": "Flow 1: Send Test Notification", + "simStepDescription": "The `NotificationService` prepares a standard test message and calls its internal `sendNotification` method. This method identifies the notification type (e.g., email, Slack) and uses the corresponding service (e.g., EmailService, NetworkService for webhooks) to dispatch the test message.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationService.js", + "startLine": "120", + "endLine": "123", + "relevantVariables": [ + "sendTestNotification", + "sendNotification" + ] + }, + "inputDataExample": "{\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n}", + "outputDataExample": "{\n \"success\": true\n}" + }, + { + "simStepId": "157cec87-9b76-4a1c-9d85-0a6d3decbaa3", + "diagramNodeId": "28f1eabb-4f24-4944-8a0a-04f41ceb8589", + "simStepLabel": "Flow 2: Job Execution and Status Update", + "simStepDescription": "A background job is generated for a monitor. When executed, it performs a network check, saves the result, and updates the monitor's status via `statusService.updateMonitorStatus`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "startLine": "43", + "endLine": "66", + "relevantVariables": [ + "generateJob", + "networkService.check", + "statusService.updateMonitorStatus" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"type\": \"http\",\n \"url\": \"https://api.example.com/health\",\n \"status\": \"up\"\n }\n}", + "outputDataExample": "{\n \"updatedMonitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"url\": \"https://api.example.com/health\",\n \"status\": \"down\"\n },\n \"statusChanged\": true\n}" + }, + { + "simStepId": "b7ec7415-fcae-4077-8c19-302a015b00b5", + "diagramNodeId": "50797935-1447-47a4-891b-9e886e5601fa", + "simStepLabel": "Flow 2: Status Change Detected", + "simStepDescription": "The `updateMonitorStatus` function returns a boolean indicating if the monitor's status has changed (e.g., from 'up' to 'down'). This boolean is used as a condition to trigger notifications.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "startLine": "60", + "endLine": "60", + "relevantVariables": [ + "statusChanged" + ] + }, + "inputDataExample": "{\n \"statusChanged\": true\n}", + "outputDataExample": "{\n \"statusChanged\": true\n}" + }, + { + "simStepId": "55ad645b-118a-478e-a00c-09ef82357008", + "diagramNodeId": "31fe5a28-0640-4b52-841b-dd02e2ae646e", + "simStepLabel": "Flow 2: Initiate Notification Handling", + "simStepDescription": "If the monitor's status has changed, the `handleNotifications` method of the `NotificationService` is invoked with the updated monitor's data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "startLine": "61", + "endLine": "61", + "relevantVariables": [ + "this.notificationService.handleNotifications", + "updatedMonitor" + ] + }, + "inputDataExample": "{\n \"updatedMonitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"url\": \"https://api.example.com/health\",\n \"status\": \"down\",\n \"notifications\": [\"60d5ecf31c9d440000a1b2c3\"]\n }\n}", + "outputDataExample": "{\n \"status\": \"pending\",\n \"promise\": {}\n}" + }, + { + "simStepId": "b7259d77-c8df-4060-ac03-58728ae3eb54", + "diagramNodeId": "c778c6b9-c12e-45c0-8682-2c9e892867b0", + "simStepLabel": "Flow 2: Monitor Data Passed to Notification Service", + "simStepDescription": "The monitor object, which includes its current status ('down') and a list of associated notification channels, is passed to the `NotificationService`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/NotificationService.ts", + "startLine": "7", + "endLine": "7", + "relevantVariables": [ + "handleNotifications", + "monitor" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"status\": \"down\",\n \"notifications\": [\"60d5ecf31c9d440000a1b2c3\"]\n }\n}", + "outputDataExample": "{\n \"monitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"status\": \"down\",\n \"notifications\": [\"60d5ecf31c9d440000a1b2c3\"]\n }\n}" + }, + { + "simStepId": "8d1176f3-c755-401d-9ca8-234689b4b837", + "diagramNodeId": "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1", + "simStepLabel": "Flow 2: Dispatch Alerts", + "simStepDescription": "The `handleNotifications` method retrieves the full details for each associated notification channel. It then iterates through them, using specific provider services (e.g., `SlackService`, `EmailService`) to format and send a tailored alert message for each channel, notifying users that the monitor is down.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/NotificationService.ts", + "startLine": "28", + "endLine": "57", + "relevantVariables": [ + "handleNotifications", + "channels", + "emailService.send", + "slackService.send", + "discordService.send" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"status\": \"down\",\n \"notifications\": [\"60d5ecf31c9d440000a1b2c3\"]\n }\n}", + "outputDataExample": "{\n \"dispatchResults\": [\n {\n \"channelId\": \"60d5ecf31c9d440000a1b2c3\",\n \"status\": \"success\"\n }\n ]\n}" + } + ], + "description": "", + "simulationNodesAndEdges": { + "15486629-84b3-463a-962b-06083f69e4bf": { + "simStepIds": [ + "e924ec15-1bea-4e3b-befb-a8bf30cbf740" + ] + }, + "a07b805e-e55f-46cb-b54b-765b7813b0ca": { + "simStepIds": [ + "383c6be8-aa51-4398-9ae3-21035413f73a" + ] + }, + "d0a4737f-715f-4f50-b6d5-5e4b31f86f93": { + "simStepIds": [ + "2f9266c9-4cbc-4b50-a280-cf8b5800d540" + ] + }, + "4a796ccf-40a9-4446-a109-6a6eed52e898": { + "simStepIds": [ + "d29ab4ab-be50-4f17-af6e-2c61cec9d0ab" + ] + }, + "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39": { + "simStepIds": [ + "c1b17b64-90e9-48bc-8927-25d7ebfc3036" + ] + }, + "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709": { + "simStepIds": [ + "f29f9be1-a0cd-43a3-b259-e5105a25ce41" + ] + }, + "28f1eabb-4f24-4944-8a0a-04f41ceb8589": { + "simStepIds": [ + "157cec87-9b76-4a1c-9d85-0a6d3decbaa3" + ] + }, + "31fe5a28-0640-4b52-841b-dd02e2ae646e": { + "simStepIds": [ + "55ad645b-118a-478e-a00c-09ef82357008" + ] + }, + "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1": { + "simStepIds": [ + "8d1176f3-c755-401d-9ca8-234689b4b837" + ] + }, + "54497b9a-4910-4df4-b016-480b9ac250a7": { + "simStepIds": [ + "71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f" + ] + }, + "dc135d8d-0515-403d-ae58-f3c04647b2ae": { + "simStepIds": [ + "162d8d2f-6cd1-45fa-8711-1ee85c5f86ef" + ] + }, + "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc": { + "simStepIds": [ + "73e63d2f-2b43-488d-9e86-170f511673f5" + ] + }, + "279155cd-ff31-41a2-8f5f-a538f01bfa66": { + "simStepIds": [ + "d390ef45-f2fe-4806-a45a-cc9c84b48a5a" + ] + }, + "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f": { + "simStepIds": [ + "6f2bf027-50d9-472e-9569-808a64fce7cf" + ] + }, + "50797935-1447-47a4-891b-9e886e5601fa": { + "simStepIds": [ + "b7ec7415-fcae-4077-8c19-302a015b00b5" + ] + }, + "c778c6b9-c12e-45c0-8682-2c9e892867b0": { + "simStepIds": [ + "b7259d77-c8df-4060-ac03-58728ae3eb54" + ] + } + }, + "isAIGenerated": true, + "keywords": "NotificationService, sendNotification, createNotification", + "generationPrompt": "Multi-Channel Alerting and Notifications", + "generationKeywords": "NotificationService, sendNotification, createNotification" + }, + "Customizable Public and Private Status Pages": { + "name": "Customizable Public and Private Status Pages", + "simSteps": [ + { + "simStepId": "de7b5d1f-b370-42a4-a410-4178f1e2d3a6", + "diagramNodeId": "00289d11-0015-4b2a-9c5b-dc9fd0f32b80", + "simStepLabel": "Create Status Page: User Initiates Creation", + "simStepDescription": "The user navigates to the 'Create Status Page' screen. The `CreateStatusPage` component renders the UI, including tabs for configuration, and initializes the form state.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "startLine": "26", + "endLine": "313", + "relevantVariables": [ + "CreateStatusPage", + "form", + "setForm", + "errors", + "setErrors", + "handleFormChange", + "handleSubmit" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"form\":{\"companyName\":\"\",\"url\":\"\",\"timezone\":\"UTC\",\"color\":\"#FF5733\",\"isPublished\":false,\"logo\":null,\"monitors\":[],\"showUptimePercentage\":true,\"showCharts\":true,\"showAdminLoginLink\":false},\"errors\":{}}" + }, + { + "simStepId": "d419f7b6-7b9b-4494-9fad-83e3ce208ca4", + "diagramNodeId": "de23eee2-b60a-4236-a890-3c5b78388bb3", + "simStepLabel": "Create Status Page: Form Submission", + "simStepDescription": "The user fills out the form and clicks the 'Save' button, which triggers the `handleSubmit` function. This function validates the form data and calls the `createStatusPage` function from the `useCreateStatusPage` hook.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "startLine": "143", + "endLine": "159", + "relevantVariables": [ + "handleSubmit", + "createStatusPage" + ] + }, + "inputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",\"timezone\":\"America/New_York\",\"color\":\"#3B82F6\",\"isPublished\":true,\"logo\":{\"src\":\"data:image/png;base64,...\",\"name\":\"logo.png\",\"type\":\"image/png\",\"size\":12345},\"monitors\":[\"65aee7cc1c363d5032a03972\"],\"showUptimePercentage\":true,\"showCharts\":true,\"showAdminLoginLink\":true}}", + "outputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",\"timezone\":\"America/New_York\",\"color\":\"#3B82F6\",\"isPublished\":true,\"logo\":{\"src\":\"data:image/png;base64,...\",\"name\":\"logo.png\",\"type\":\"image/png\",\"size\":12345},\"monitors\":[\"65aee7cc1c363d5032a03972\"],\"showUptimePercentage\":true,\"showCharts\":true,\"showAdminLoginLink\":true}}" + }, + { + "simStepId": "3c99ae6a-b671-4f47-bed0-ddccd8ff5d35", + "diagramNodeId": "aa5d6d07-05f0-45f3-b19a-199af50e35f1", + "simStepLabel": "Create Status Page: Client Hook Sends API Request", + "simStepDescription": "The `useCreateStatusPage` hook receives the form data and uses the `networkService` to send a request to the backend API to create or update the status page.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "startLine": "8", + "endLine": "11", + "relevantVariables": [ + "createStatusPage", + "networkService.createStatusPage" + ] + }, + "inputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",\"timezone\":\"America/New_York\",\"color\":\"#3B82F6\",\"isPublished\":true,\"logo\":{\"src\":\"data:image/png;base64,...\"},\"monitors\":[\"65aee7cc1c363d5032a03972\"]}}", + "outputDataExample": "{}" + }, + { + "simStepId": "8a2ab372-73db-4b9f-bc0e-6a20f15a4d79", + "diagramNodeId": "d7faa713-4204-4a97-a172-d621da36c287", + "simStepLabel": "Create Status Page: API Call to Backend", + "simStepDescription": "The `NetworkService` class constructs a `FormData` object from the form data and sends a POST request to the `/api/v1/status-page` endpoint. The logo is sent as a file upload.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "912", + "endLine": "944", + "relevantVariables": [ + "createStatusPage", + "axiosInstance.post" + ] + }, + "inputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\"...}, \"isCreate\": true}", + "outputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\"...}, \"isCreate\": true}" + }, + { + "simStepId": "84f11567-b3c4-42a2-aa7f-d78c1412099a", + "diagramNodeId": "3efddcad-588f-4492-9808-b0c81c823c96", + "simStepLabel": "Create Status Page: Backend Route Handling", + "simStepDescription": "The backend router receives the POST request on `/api/v1/status-page`. It uses `multer` to handle the file upload and forwards the request to the `createStatusPage` method in the `StatusPageController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/statusPageRoute.js", + "startLine": "14", + "endLine": "14", + "relevantVariables": [ + "router.post", + "upload.single", + "verifyJWT", + "this.statusPageController.createStatusPage" + ] + }, + "inputDataExample": "{\"method\":\"POST\",\"url\":\"/api/v1/status-page\",\"body\":{\"companyName\":\"My Awesome Service\",...},\"file\":{...}}", + "outputDataExample": "{\"req\":{...},\"res\":{...}}" + }, + { + "simStepId": "0923a533-c572-4da3-ad87-cdfff111f3bb", + "diagramNodeId": "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "simStepLabel": "Create Status Page: Data Passed to Controller", + "simStepDescription": "The router passes the request object, containing form data in `req.body`, the uploaded logo in `req.file`, and user details in `req.user`, to the controller method.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/statusPageRoute.js", + "startLine": "14", + "endLine": "14", + "relevantVariables": [ + "statusPageController.createStatusPage" + ] + }, + "inputDataExample": "{\"req\":{\"body\":{\"companyName\":\"My Awesome Service\",...},\"file\":{\"fieldname\":\"logo\",...},\"user\":{\"_id\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}}}", + "outputDataExample": "{\"req\":{\"body\":{\"companyName\":\"My Awesome Service\",...},\"file\":{\"fieldname\":\"logo\",...},\"user\":{\"_id\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}}}" + }, + { + "simStepId": "01694730-8a75-4949-8815-bb4d6e3e2b42", + "diagramNodeId": "f15ef77e-380f-4768-98cb-7f0dd4113841", + "simStepLabel": "Create Status Page: Controller Logic", + "simStepDescription": "The `StatusPageController` validates the incoming request body and file. It then calls the `statusPageModule` to persist the new status page in the database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "18", + "endLine": "24", + "relevantVariables": [ + "createStatusPageBodyValidation", + "imageValidation", + "this.db.statusPageModule.createStatusPage" + ] + }, + "inputDataExample": "{\"req\":{\"body\":{...},\"file\":{...},\"user\":{...}}}", + "outputDataExample": "{\"statusPageData\":{...},\"image\":{...},\"userId\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}" + }, + { + "simStepId": "67603f03-c088-4ed0-9f18-072435134cdd", + "diagramNodeId": "9735611c-924b-49aa-92e8-10d8a86ac288", + "simStepLabel": "Create Status Page: Data Sent to DB Module", + "simStepDescription": "The controller passes the processed status page data, image buffer, user ID, and team ID to the `createStatusPage` function in the database module.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "21", + "endLine": "21", + "relevantVariables": [ + "this.db.statusPageModule.createStatusPage" + ] + }, + "inputDataExample": "{\"statusPageData\":{\"companyName\":\"My Awesome Service\",...},\"image\":{\"buffer\":,\"mimetype\":\"image/png\"},\"userId\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}", + "outputDataExample": "{\"statusPageData\":{\"companyName\":\"My Awesome Service\",...},\"image\":{\"buffer\":,\"mimetype\":\"image/png\"},\"userId\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}" + }, + { + "simStepId": "b6fa93cd-6fca-4d9e-969a-5400e2437ee8", + "diagramNodeId": "e234917e-a950-48f5-b864-cd957b2216e1", + "simStepLabel": "Create Status Page: Database Record Creation", + "simStepDescription": "The `StatusPageModule` creates a new instance of the `StatusPage` Mongoose model, populates it with the provided data including the image buffer, and saves it to the MongoDB database. It also handles duplicate URL errors.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/statusPageModule.js", + "startLine": "14", + "endLine": "34", + "relevantVariables": [ + "createStatusPage", + "new this.StatusPage", + "statusPage.save" + ] + }, + "inputDataExample": "{\"statusPageData\":{...},\"image\":{...},\"userId\":\"...\",\"teamId\":\"...\"}", + "outputDataExample": "{\"_id\":\"65aee7dd1c363d5032a03975\",\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",\"userId\":\"65aee7cc1c363d5032a03970\",...}" + }, + { + "simStepId": "ea4b3532-baae-4890-a73e-2ca385d0319e", + "diagramNodeId": "40cb7d7b-2888-44de-bb7d-7735af70ba34", + "simStepLabel": "Create Status Page: Database Result Returned", + "simStepDescription": "The newly created status page document is returned from the database module to the controller.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/statusPageModule.js", + "startLine": "27", + "endLine": "27", + "relevantVariables": [ + "statusPage" + ] + }, + "inputDataExample": "{\"_id\":\"65aee7dd1c363d5032a03975\",\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",...}", + "outputDataExample": "{\"_id\":\"65aee7dd1c363d5032a03975\",\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",...}" + }, + { + "simStepId": "2a115421-5175-4c8f-8cdd-62f3fe148af5", + "diagramNodeId": "058ded1f-83ce-40a7-a99d-d178c81fe557", + "simStepLabel": "Create Status Page: Controller Sends Success Response", + "simStepDescription": "The controller receives the newly created status page object from the database module and sends a successful JSON response back to the client, including a success message and the new data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "25", + "endLine": "28", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"_id\":\"65aee7dd1c363d5032a03975\",\"companyName\":\"My Awesome Service\",...}", + "outputDataExample": "{\"msg\":\"Status page created successfully\",\"data\":{\"_id\":\"65aee7dd1c363d5032a03975\",...}}" + }, + { + "simStepId": "210acc07-81c3-4394-b566-41f6031e8eeb", + "diagramNodeId": "c3f83811-8674-4dbb-9f84-79499f97fb1f", + "simStepLabel": "Create Status Page: UI Receives Response and Redirects", + "simStepDescription": "The client-side `handleSubmit` function receives the successful response. It displays a success toast message and navigates the user to the newly created status page.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "startLine": "151", + "endLine": "156", + "relevantVariables": [ + "createToast", + "navigate" + ] + }, + "inputDataExample": "{\"success\":true}", + "outputDataExample": "{\"success\":true}" + }, + { + "simStepId": "132354d7-df7a-4e03-9d00-16d54f836a1b", + "diagramNodeId": "30aaee38-f509-4e19-a917-e6d2f4bba61c", + "simStepLabel": "View Status Page: User Navigates to URL", + "simStepDescription": "A user accesses a status page URL (e.g., /status/uptime/my-awesome-service). The React router matches the path and renders the `Status` component, which is responsible for fetching and displaying the page details.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Routes/index.jsx", + "startLine": "148", + "endLine": "152", + "relevantVariables": [ + "Route", + "Status" + ] + }, + "inputDataExample": "{\"url\":\"/status/uptime/my-awesome-service\"}", + "outputDataExample": "{}" + }, + { + "simStepId": "2ba49e28-1cac-47f5-9118-ab930788736a", + "diagramNodeId": "0575bf87-5362-4988-b895-7e514148d420", + "simStepLabel": "View Status Page: Component Triggers Data Fetch", + "simStepDescription": "The `Status` component mounts and calls the `useStatusPageFetch` hook, which is responsible for initiating the API call to fetch the status page data from the backend.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "startLine": "29", + "endLine": "29", + "relevantVariables": [ + "useStatusPageFetch" + ] + }, + "inputDataExample": "{\"isCreate\":false, \"url\":\"my-awesome-service\"}", + "outputDataExample": "{\"isCreate\":false, \"url\":\"my-awesome-service\"}" + }, + { + "simStepId": "b57bb249-9066-454e-aa8c-89108fdfcc2e", + "diagramNodeId": "ecfb2d04-2681-4f16-af75-632f69e1a868", + "simStepLabel": "View Status Page: Client Hook Sends API Request", + "simStepDescription": "The `useStatusPageFetch` hook uses the `networkService` to send a GET request to the backend API to retrieve the status page data based on its URL.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "startLine": "15", + "endLine": "18", + "relevantVariables": [ + "fetchStatusPage", + "networkService.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"url\":\"my-awesome-service\", \"type\":\"uptime\"}", + "outputDataExample": "{}" + }, + { + "simStepId": "de0714d2-bf1e-4a1a-a389-60793de8b772", + "diagramNodeId": "ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "simStepLabel": "View Status Page: API Call to Backend", + "simStepDescription": "The `NetworkService` class makes a GET request to the `/api/v1/status-page/:url` endpoint to fetch the status page details.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "896", + "endLine": "902", + "relevantVariables": [ + "getStatusPageByUrl", + "axiosInstance.get" + ] + }, + "inputDataExample": "{\"url\":\"my-awesome-service\",\"type\":\"uptime\"}", + "outputDataExample": "{\"url\":\"my-awesome-service\",\"type\":\"uptime\"}" + }, + { + "simStepId": "477c6821-b29b-4b38-8930-ced21b8a75e5", + "diagramNodeId": "2e26cd2f-1413-4c16-8e63-ea7808ab6c52", + "simStepLabel": "View Status Page: Backend Route Handling", + "simStepDescription": "The backend router receives the GET request on `/api/v1/status-page/:url` and forwards it to the `getStatusPageByUrl` method in the `StatusPageController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/statusPageRoute.js", + "startLine": "16", + "endLine": "16", + "relevantVariables": [ + "router.get", + "this.statusPageController.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"method\":\"GET\",\"url\":\"/api/v1/status-page/my-awesome-service\"}", + "outputDataExample": "{\"req\":{...},\"res\":{...}}" + }, + { + "simStepId": "b79b8922-4de6-477d-8648-37b4bd9a6767", + "diagramNodeId": "5735f30c-b55f-444a-9236-f3d75f68ed27", + "simStepLabel": "View Status Page: Data Passed to Controller", + "simStepDescription": "The router passes the request object, containing the URL in `req.params`, to the controller method.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/statusPageRoute.js", + "startLine": "16", + "endLine": "16", + "relevantVariables": [ + "statusPageController.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"req\":{\"params\":{\"url\":\"my-awesome-service\"}}}", + "outputDataExample": "{\"req\":{\"params\":{\"url\":\"my-awesome-service\"}}}" + }, + { + "simStepId": "5bdbd179-ce8f-4171-a073-af72a99b4468", + "diagramNodeId": "9f217674-4cd7-4ef8-b088-d5620042a7ba", + "simStepLabel": "View Status Page: Controller Logic", + "simStepDescription": "The `StatusPageController` validates the request parameters and calls the `statusPageModule` to retrieve the status page data from the database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "75", + "endLine": "78", + "relevantVariables": [ + "getStatusPageByUrl", + "this.db.statusPageModule.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"req\":{\"params\":{\"url\":\"my-awesome-service\"}}}", + "outputDataExample": "{\"url\":\"my-awesome-service\"}" + }, + { + "simStepId": "788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8", + "diagramNodeId": "dace45d6-fb25-4dae-b940-49ef7d77bbed", + "simStepLabel": "View Status Page: URL Sent to DB Module", + "simStepDescription": "The controller passes the URL string to the `getStatusPageByUrl` function in the database module.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "78", + "endLine": "78", + "relevantVariables": [ + "this.db.statusPageModule.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"url\":\"my-awesome-service\"}", + "outputDataExample": "{\"url\":\"my-awesome-service\"}" + }, + { + "simStepId": "ceb044e9-6545-4d04-bf79-ec8146eb8ec9", + "diagramNodeId": "26da60f9-dde4-44ec-be89-91858cc14f90", + "simStepLabel": "View Status Page: Database Record Retrieval", + "simStepDescription": "The `StatusPageModule` executes a complex MongoDB aggregation pipeline to find the status page by its URL, and then look up and join data for all associated monitors and their recent checks.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/statusPageModule.js", + "startLine": "89", + "endLine": "299", + "relevantVariables": [ + "getStatusPage", + "this.StatusPage.aggregate" + ] + }, + "inputDataExample": "{\"url\":\"my-awesome-service\"}", + "outputDataExample": "{\"statusPage\":{\"_id\":\"...\",\"companyName\":\"My Awesome Service\",...},\"monitors\":[{\"_id\":\"...\",\"name\":\"Main Website\",...}]}" + }, + { + "simStepId": "4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4", + "diagramNodeId": "137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "simStepLabel": "View Status Page: Database Result Returned", + "simStepDescription": "The aggregated status page and monitor data is returned from the database module to the controller.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/statusPageModule.js", + "startLine": "291", + "endLine": "291", + "relevantVariables": [ + "statusPage", + "normalizedMonitors" + ] + }, + "inputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}", + "outputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}" + }, + { + "simStepId": "83224406-2f1d-4030-9beb-d65c2baf544d", + "diagramNodeId": "d76e70e0-41f9-46cc-b775-b2027c5a0d5b", + "simStepLabel": "View Status Page: Controller Sends Success Response", + "simStepDescription": "The controller receives the status page data and sends a successful JSON response back to the client.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "79", + "endLine": "82", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}", + "outputDataExample": "{\"msg\":\"Got status page by url successfully\",\"data\":{\"statusPage\":{...},\"monitors\":[...]}}" + }, + { + "simStepId": "d5f3c300-9777-4e09-bb05-f1281c2ff29a", + "diagramNodeId": "04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "simStepLabel": "View Status Page: UI Receives Data", + "simStepDescription": "The `useStatusPageFetch` hook receives the API response, processes the data, and updates the component's state, triggering a re-render.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "startLine": "20", + "endLine": "26", + "relevantVariables": [ + "setStatusPage", + "setMonitors", + "getMonitorWithPercentage" + ] + }, + "inputDataExample": "{\"data\":{\"data\":{\"statusPage\":{...},\"monitors\":[...]}}}", + "outputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}" + }, + { + "simStepId": "3d51e811-a659-442e-97e0-00798cd15647", + "diagramNodeId": "f59b91de-5196-4a43-9d57-3f28402c0672", + "simStepLabel": "View Status Page: Component Renders UI", + "simStepDescription": "The `Status` component re-renders with the fetched data. It displays the company name, logo, overall status bar, and a list of monitors with their individual status and historical uptime charts.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "startLine": "145", + "endLine": "163", + "relevantVariables": [ + "ControlsHeader", + "StatusBar", + "MonitorsList" + ] + }, + "inputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}", + "outputDataExample": "{}" + } + ], + "description": "
  • Allows users to create dedicated pages to communicate the status of their services to internal teams or external customers
  • - Users can create a status page with a custom URL, company name, and logo
  • - Select which monitors to display on the status page, providing granular control over what information is shared
  • - Options to show or hide historical charts and uptime percentages for transparency
  • - Includes settings for custom CSS and an optional link back to the admin login page
", + "simulationNodesAndEdges": { + "00289d11-0015-4b2a-9c5b-dc9fd0f32b80": { + "simStepIds": [ + "de7b5d1f-b370-42a4-a410-4178f1e2d3a6" + ] + }, + "aa5d6d07-05f0-45f3-b19a-199af50e35f1": { + "simStepIds": [ + "3c99ae6a-b671-4f47-bed0-ddccd8ff5d35" + ] + }, + "3efddcad-588f-4492-9808-b0c81c823c96": { + "simStepIds": [ + "84f11567-b3c4-42a2-aa7f-d78c1412099a" + ] + }, + "f15ef77e-380f-4768-98cb-7f0dd4113841": { + "simStepIds": [ + "01694730-8a75-4949-8815-bb4d6e3e2b42" + ] + }, + "e234917e-a950-48f5-b864-cd957b2216e1": { + "simStepIds": [ + "b6fa93cd-6fca-4d9e-969a-5400e2437ee8" + ] + }, + "058ded1f-83ce-40a7-a99d-d178c81fe557": { + "simStepIds": [ + "2a115421-5175-4c8f-8cdd-62f3fe148af5" + ] + }, + "30aaee38-f509-4e19-a917-e6d2f4bba61c": { + "simStepIds": [ + "132354d7-df7a-4e03-9d00-16d54f836a1b" + ] + }, + "ecfb2d04-2681-4f16-af75-632f69e1a868": { + "simStepIds": [ + "b57bb249-9066-454e-aa8c-89108fdfcc2e" + ] + }, + "2e26cd2f-1413-4c16-8e63-ea7808ab6c52": { + "simStepIds": [ + "477c6821-b29b-4b38-8930-ced21b8a75e5" + ] + }, + "9f217674-4cd7-4ef8-b088-d5620042a7ba": { + "simStepIds": [ + "5bdbd179-ce8f-4171-a073-af72a99b4468" + ] + }, + "26da60f9-dde4-44ec-be89-91858cc14f90": { + "simStepIds": [ + "ceb044e9-6545-4d04-bf79-ec8146eb8ec9" + ] + }, + "d76e70e0-41f9-46cc-b775-b2027c5a0d5b": { + "simStepIds": [ + "83224406-2f1d-4030-9beb-d65c2baf544d" + ] + }, + "f59b91de-5196-4a43-9d57-3f28402c0672": { + "simStepIds": [ + "3d51e811-a659-442e-97e0-00798cd15647" + ] + }, + "de23eee2-b60a-4236-a890-3c5b78388bb3": { + "simStepIds": [ + "d419f7b6-7b9b-4494-9fad-83e3ce208ca4" + ] + }, + "d7faa713-4204-4a97-a172-d621da36c287": { + "simStepIds": [ + "8a2ab372-73db-4b9f-bc0e-6a20f15a4d79" + ] + }, + "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0": { + "simStepIds": [ + "0923a533-c572-4da3-ad87-cdfff111f3bb" + ] + }, + "9735611c-924b-49aa-92e8-10d8a86ac288": { + "simStepIds": [ + "67603f03-c088-4ed0-9f18-072435134cdd" + ] + }, + "40cb7d7b-2888-44de-bb7d-7735af70ba34": { + "simStepIds": [ + "ea4b3532-baae-4890-a73e-2ca385d0319e" + ] + }, + "c3f83811-8674-4dbb-9f84-79499f97fb1f": { + "simStepIds": [ + "210acc07-81c3-4394-b566-41f6031e8eeb" + ] + }, + "0575bf87-5362-4988-b895-7e514148d420": { + "simStepIds": [ + "2ba49e28-1cac-47f5-9118-ab930788736a" + ] + }, + "ae1c9341-0e39-4d91-a9ac-5cc8a627c247": { + "simStepIds": [ + "de0714d2-bf1e-4a1a-a389-60793de8b772" + ] + }, + "5735f30c-b55f-444a-9236-f3d75f68ed27": { + "simStepIds": [ + "b79b8922-4de6-477d-8648-37b4bd9a6767" + ] + }, + "dace45d6-fb25-4dae-b940-49ef7d77bbed": { + "simStepIds": [ + "788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8" + ] + }, + "137a5ab7-1e49-42ec-869d-58f7f2380cb3": { + "simStepIds": [ + "4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4" + ] + }, + "04a33e4f-1dfe-4c5a-8b30-01ed0591c380": { + "simStepIds": [ + "d5f3c300-9777-4e09-bb05-f1281c2ff29a" + ] + } + }, + "isAIGenerated": true, + "keywords": "StatusPage, createStatusPage, getStatusPageByUrl", + "generationPrompt": "Customizable Public and Private Status Pages", + "generationKeywords": "StatusPage, createStatusPage, getStatusPageByUrl" + }, + "Incident History, Analysis, and Management": { + "name": "Incident History, Analysis, and Management", + "simSteps": [ + { + "simStepId": "35195328-9847-4678-b014-9331553f0d19", + "diagramNodeId": "78a8a7c5-41d3-4453-a7f7-ac632bbef288", + "simStepLabel": "Navigate to Monitor Details", + "simStepDescription": "The user navigates to the details page for a specific uptime monitor. The `UptimeDetails` component mounts and initializes its state and hooks.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "startLine": "27", + "endLine": "188", + "relevantVariables": [ + "UptimeDetails", + "useParams", + "useFetchChecksByMonitor" + ] + }, + "inputDataExample": "{\"url\": \"/uptime/65a12f60e9a5d109f7596a77\"}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\"}" + }, + { + "simStepId": "49979250-1a32-4da5-95e0-58652fe218f9", + "diagramNodeId": "e90e4943-6108-46e3-8a2c-a9a1c7361279", + "simStepLabel": "Trigger Check History Fetch", + "simStepDescription": "The `UptimeDetails` component calls the `useFetchChecksByMonitor` hook to retrieve the historical check data for the specified monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "startLine": "69", + "endLine": "77", + "relevantVariables": [ + "useFetchChecksByMonitor" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"type\": \"http\", \"sortOrder\": \"desc\", \"page\": 0, \"rowsPerPage\": 10}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"type\": \"http\", \"sortOrder\": \"desc\", \"page\": 0, \"rowsPerPage\": 10}" + }, + { + "simStepId": "8dc4347c-3e9e-40d7-ae6f-54684f2e6956", + "diagramNodeId": "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4", + "simStepLabel": "Fetch Monitor Checks Hook", + "simStepDescription": "The `useFetchChecksByMonitor` custom hook executes. It sets the loading state and prepares to call the network service to fetch the data from the backend API.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/checkHooks.js", + "startLine": "89", + "endLine": "121", + "relevantVariables": [ + "useFetchChecksByMonitor", + "useEffect", + "networkService.getChecksByMonitor" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"page\": 0, \"rowsPerPage\": 10, \"sortOrder\": \"desc\"}", + "outputDataExample": "{\"config\": {\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\", \"status\": undefined}}" + }, + { + "simStepId": "336b534d-8d6a-4639-af8e-889b38f51ab1", + "diagramNodeId": "2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "simStepLabel": "API Request for Check History", + "simStepDescription": "The `NetworkService` constructs and sends an HTTP GET request to the backend API endpoint to retrieve the paginated list of checks for the specified monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "551", + "endLine": "562", + "relevantVariables": [ + "getChecksByMonitor", + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\"config\": {\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}}", + "outputDataExample": "{\"config\": {\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}}" + }, + { + "simStepId": "c5fd192f-ea28-48a0-b3e7-5bc04f521192", + "diagramNodeId": "cdd090be-0b62-45b4-baa0-fec01fc3ffd1", + "simStepLabel": "Route Request to Controller", + "simStepDescription": "The backend Express router receives the incoming GET request and matches it to the appropriate route, forwarding the request to the `getChecksByMonitor` method in the `CheckController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/checkRoute.js", + "startLine": "19", + "endLine": "19", + "relevantVariables": [ + "this.router.get" + ] + }, + "inputDataExample": "{\"request\": \"GET /api/v1/checks/65a12f60e9a5d109f7596a77?sortOrder=desc&page=0&rowsPerPage=10\"}", + "outputDataExample": "{\"controllerMethod\": \"getChecksByMonitor\"}" + }, + { + "simStepId": "4f73f0fe-b390-4fe1-a354-eb0857835126", + "diagramNodeId": "839ba9f6-8a34-4919-a04a-13fba1795c61", + "simStepLabel": "Forward Request to Service Layer", + "simStepDescription": "The `CheckController` validates the request parameters and calls the `CheckService` to handle the business logic of fetching the monitor checks.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/checkController.js", + "startLine": "81", + "endLine": "85", + "relevantVariables": [ + "getChecksByMonitor", + "this.checkService.getChecksByMonitor" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"query\": {\"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}, \"teamId\": \"65a12f50e9a5d109f7596a66\"}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"query\": {\"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}, \"teamId\": \"65a12f50e9a5d109f7596a66\"}" + }, + { + "simStepId": "83fea05d-8306-4a73-a43f-882cdd14f50f", + "diagramNodeId": "499b46a2-f2a2-4ccd-aecd-012eade2bd23", + "simStepLabel": "Fetch Checks from Database", + "simStepDescription": "The `CheckService` calls the `checkModule` to interact with the database. It passes the necessary parameters to query for the check history.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/checkService.js", + "startLine": "36", + "endLine": "45", + "relevantVariables": [ + "getChecksByMonitor", + "this.db.checkModule.getChecksByMonitor" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"query\": {\"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}, \"teamId\": \"65a12f50e9a5d109f7596a66\"}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": \"desc\", \"dateRange\": undefined, \"filter\": undefined, \"ack\": undefined, \"page\": \"0\", \"rowsPerPage\": \"10\", \"status\": undefined}" + }, + { + "simStepId": "65da1a9e-041c-4656-9c5b-0a92b761a9be", + "diagramNodeId": "42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "simStepLabel": "Execute Database Query", + "simStepDescription": "The `checkModule` constructs and executes a MongoDB aggregation pipeline on the `Check` collection to find, sort, and paginate the historical checks for the given monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/checkModule.js", + "startLine": "28", + "endLine": "108", + "relevantVariables": [ + "getChecksByMonitor", + "this.Check.aggregate" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": -1, \"page\": 0, \"rowsPerPage\": 10}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": -1, \"page\": 0, \"rowsPerPage\": 10}" + }, + { + "simStepId": "5768fdc5-045c-48ef-b0c9-79769403648f", + "diagramNodeId": "14df9f01-1358-442d-96f5-eab9a4c4abbf", + "simStepLabel": "Return Check Data", + "simStepDescription": "The database returns the query results, which flow back up through the `checkModule` and `checkService`. The `checkController` then formats a successful JSON response.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/checkController.js", + "startLine": "87", + "endLine": "90", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"checksCount\": 152, \"checks\": [{\"_id\": \"...\", \"status\": false}, {\"...\"}]}", + "outputDataExample": "{\"msg\": \"Got checks successfully\", \"data\": {\"checksCount\": 152, \"checks\": [{\"_id\": \"...\"}]}}" + }, + { + "simStepId": "2c1bdec8-d0e6-4163-a99c-f27899b987c5", + "diagramNodeId": "6c5b028c-356c-43c8-a35a-b47448cd8695", + "simStepLabel": "API Response with Check History", + "simStepDescription": "The backend sends the HTTP response containing the historical check data back to the client.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "560", + "endLine": "560", + "relevantVariables": [ + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\"success\": true, \"msg\": \"Got checks successfully\", \"data\": {\"checksCount\": 152, \"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503, \"responseTime\": 1200}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200, \"responseTime\": 250}]}}", + "outputDataExample": "{\"success\": true, \"msg\": \"Got checks successfully\", \"data\": {\"checksCount\": 152, \"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503, \"responseTime\": 1200}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200, \"responseTime\": 250}]}}" + }, + { + "simStepId": "faba07f3-7460-47ae-924d-f92f5b2e821d", + "diagramNodeId": "ceb9cda3-968d-44aa-b223-49902adb93b4", + "simStepLabel": "Update Frontend State", + "simStepDescription": "The `useFetchChecksByMonitor` hook receives the data from the API call and updates the component's state with the new list of checks and the total count.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/checkHooks.js", + "startLine": "109", + "endLine": "110", + "relevantVariables": [ + "setChecks", + "setChecksCount" + ] + }, + "inputDataExample": "{\"data\": {\"checksCount\": 152, \"checks\": [{\"_id\": \"...\"}]}}", + "outputDataExample": "{\"checks\": [{\"_id\": \"...\"}], \"checksCount\": 152}" + }, + { + "simStepId": "9405dce1-0565-4dfa-9539-a048472acda9", + "diagramNodeId": "74b0dd89-9fea-4219-af1f-9571316a9d5a", + "simStepLabel": "Pass Data to UI Components", + "simStepDescription": "The state update triggers a re-render of the `UptimeDetails` page, passing the `checks` and `checksCount` as props to child components like `ResponseTable`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "startLine": "175", + "endLine": "186", + "relevantVariables": [ + "ResponseTable", + "checks", + "checksCount" + ] + }, + "inputDataExample": "{\"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200}], \"checksCount\": 152}", + "outputDataExample": "{\"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200}], \"checksCount\": 152}" + }, + { + "simStepId": "2b1651cd-2e70-4cc6-b2bb-23f94fa73d66", + "diagramNodeId": "c996f7ca-9384-4252-b343-7bfc049d14bb", + "simStepLabel": "Render Historical Data Table", + "simStepDescription": "The `ResponseTable` component receives the list of checks and renders them in a data table, allowing the user to view the incident history, including status, response time, and when the check occurred.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx", + "startLine": "11", + "endLine": "92", + "relevantVariables": [ + "ResponseTable", + "Table", + "TablePagination" + ] + }, + "inputDataExample": "{\"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200}]}", + "outputDataExample": "{\"tableRows\": [{\"id\": \"status\", \"content\": \"Status\", \"render\": \"...\"}, {\"id\": \"httpStatus\", \"content\": \"HTTP Status\", \"render\": \"...\"}]}" + } + ], + "description": "
  • Provides detailed historical data and tools to manage monitoring events, which are treated as incidents
  • - Every check result is stored, creating a comprehensive log of uptime, downtime, and response time history for each monitor
  • - The UI includes detailed views with charts and tables to analyze performance trends and past incidents
  • - Users can acknowledge incidents (failed checks), which helps in tracking the response to an outage
  • - All historical check data can be purged on a per-monitor or per-team basis
", + "simulationNodesAndEdges": { + "78a8a7c5-41d3-4453-a7f7-ac632bbef288": { + "simStepIds": [ + "35195328-9847-4678-b014-9331553f0d19" + ] + }, + "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4": { + "simStepIds": [ + "8dc4347c-3e9e-40d7-ae6f-54684f2e6956" + ] + }, + "cdd090be-0b62-45b4-baa0-fec01fc3ffd1": { + "simStepIds": [ + "c5fd192f-ea28-48a0-b3e7-5bc04f521192" + ] + }, + "499b46a2-f2a2-4ccd-aecd-012eade2bd23": { + "simStepIds": [ + "83fea05d-8306-4a73-a43f-882cdd14f50f" + ] + }, + "14df9f01-1358-442d-96f5-eab9a4c4abbf": { + "simStepIds": [ + "5768fdc5-045c-48ef-b0c9-79769403648f" + ] + }, + "ceb9cda3-968d-44aa-b223-49902adb93b4": { + "simStepIds": [ + "faba07f3-7460-47ae-924d-f92f5b2e821d" + ] + }, + "c996f7ca-9384-4252-b343-7bfc049d14bb": { + "simStepIds": [ + "2b1651cd-2e70-4cc6-b2bb-23f94fa73d66" + ] + }, + "e90e4943-6108-46e3-8a2c-a9a1c7361279": { + "simStepIds": [ + "49979250-1a32-4da5-95e0-58652fe218f9" + ] + }, + "2b8ae59e-ed8d-4837-9a9b-64eb480f982c": { + "simStepIds": [ + "336b534d-8d6a-4639-af8e-889b38f51ab1" + ] + }, + "839ba9f6-8a34-4919-a04a-13fba1795c61": { + "simStepIds": [ + "4f73f0fe-b390-4fe1-a354-eb0857835126" + ] + }, + "42d3bc70-701a-4be0-96b4-d6c2f0519b49": { + "simStepIds": [ + "65da1a9e-041c-4656-9c5b-0a92b761a9be" + ] + }, + "6c5b028c-356c-43c8-a35a-b47448cd8695": { + "simStepIds": [ + "2c1bdec8-d0e6-4163-a99c-f27899b987c5" + ] + }, + "74b0dd89-9fea-4219-af1f-9571316a9d5a": { + "simStepIds": [ + "9405dce1-0565-4dfa-9539-a048472acda9" + ] + } + }, + "isAIGenerated": true, + "keywords": "Check, getChecksByMonitor, ResponseTable", + "generationPrompt": "Incident History, Analysis, and Management", + "generationKeywords": "Check, getChecksByMonitor, ResponseTable" + }, + "Website Performance and Page Speed Monitoring": { + "name": "Website Performance and Page Speed Monitoring", + "simSteps": [ + { + "simStepId": "b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12", + "diagramNodeId": "0622b212-3a4a-484d-9b7a-125864d785cd", + "simStepLabel": "Create PageSpeed Monitor", + "simStepDescription": "The user navigates to the PageSpeed creation page, fills in the details for a new monitor (URL, name, interval), and submits the form. This triggers the `handleSubmit` function, which calls the `createMonitor` hook to send the data to the backend API.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "startLine": "111", + "endLine": "143", + "relevantVariables": [ + "handleSubmit", + "createMonitor", + "updateMonitor", + "monitor", + "form" + ] + }, + "inputDataExample": "{\"formState\": {\"url\": \"https://example.com\",\"name\": \"Example Homepage\",\"type\": \"pagespeed\",\"interval\": 180000}}", + "outputDataExample": "{\"monitor\": {\"url\": \"https://example.com\",\"name\": \"Example Homepage\",\"type\": \"pagespeed\",\"interval\": 180000}, \"redirect\": \"/pagespeed\"}" + }, + { + "simStepId": "f8df1cad-6051-466e-a13e-47d710ba1c14", + "diagramNodeId": "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "simStepLabel": "API Call: Create Monitor", + "simStepDescription": "The frontend sends a POST request to the backend API to create a new monitor record in the database. This request includes the monitor's configuration details.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "startLine": "119", + "endLine": "119", + "relevantVariables": [ + "createMonitor" + ] + }, + "inputDataExample": "{\"monitor\": {\"url\": \"https://example.com\",\"name\": \"Example Homepage\",\"type\": \"pagespeed\",\"notifications\": [],\"interval\": 180000}, \"redirect\": \"/pagespeed\"}", + "outputDataExample": "{\"monitor\": {\"url\": \"https://example.com\",\"name\": \"Example Homepage\",\"type\": \"pagespeed\",\"notifications\": [],\"interval\": 180000}, \"redirect\": \"/pagespeed\"}" + }, + { + "simStepId": "53968267-0c6e-42f4-88b3-cd90f44532ae", + "diagramNodeId": "6527fb47-157e-4bba-9aed-adde061c5689", + "simStepLabel": "Backend Requests PageSpeed Analysis", + "simStepDescription": "A background job for the newly created monitor triggers the `NetworkService` to request a performance analysis from the Google PageSpeed Insights API. It constructs the API URL using the monitor's target URL and the configured PageSpeed API key.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/NetworkService.ts", + "startLine": "129", + "endLine": "162", + "relevantVariables": [ + "requestPagespeed", + "apiKey", + "pagespeedUrl", + "pagespeedResponse" + ] + }, + "inputDataExample": "{\"monitor\": {\"_id\": \"64b9a6b3e1b6f0b3e8a7d1c0\",\"url\": \"https://example.com\",\"type\": \"pagespeed\"}}", + "outputDataExample": "{\"statusResponse\": {\"monitorId\": \"64b9a6b3e1b6f0b3e8a7d1c0\",\"status\": \"up\",\"responseTime\": 450, \"payload\": {\"lighthouseResult\": {...}}}}" + }, + { + "simStepId": "0b380571-3896-4365-b5dc-187494f02e0d", + "diagramNodeId": "0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "simStepLabel": "Google PageSpeed API Response", + "simStepDescription": "The Google PageSpeed Insights API responds with a detailed JSON payload containing the Lighthouse analysis results, including scores for performance, accessibility, SEO, and best practices.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/NetworkService.ts", + "startLine": "156", + "endLine": "156", + "relevantVariables": [ + "payload" + ] + }, + "inputDataExample": "{\"lighthouseResult\": {\"categories\": {\"performance\": {\"score\": 0.92},\"accessibility\": {\"score\": 0.98},\"best-practices\": {\"score\": 1},\"seo\": {\"score\": 0.95}},\"audits\": {\"first-contentful-paint\": {\"score\": 0.99,\"numericValue\": 850},\"largest-contentful-paint\": {\"score\": 0.91,\"numericValue\": 1450}}}}", + "outputDataExample": "{\"lighthouseResult\": {\"categories\": {\"performance\": {\"score\": 0.92},\"accessibility\": {\"score\": 0.98},\"best-practices\": {\"score\": 1},\"seo\": {\"score\": 0.95}},\"audits\": {\"first-contentful-paint\": {\"score\": 0.99,\"numericValue\": 850},\"largest-contentful-paint\": {\"score\": 0.91,\"numericValue\": 1450}}}}" + }, + { + "simStepId": "01206383-2db8-4b18-84d0-be2a0289c196", + "diagramNodeId": "f89cbd87-c961-4f8b-92b6-714da57718d5", + "simStepLabel": "Backend Processes Lighthouse Data", + "simStepDescription": "The `CheckService` receives the raw data from the Google API. The `buildPagespeedCheck` function extracts and transforms the relevant scores and audit details into a structured `Check` object, preparing it for database storage.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/business/CheckService.ts", + "startLine": "82", + "endLine": "102", + "relevantVariables": [ + "buildPagespeedCheck", + "isPagespeedPayload", + "lighthouseResult", + "check.lighthouse" + ] + }, + "inputDataExample": "{\"statusResponse\": {\"payload\": {\"lighthouseResult\": {\"categories\": {\"performance\": {\"score\": 0.92},\"accessibility\": {\"score\": 0.98},\"best-practices\": {\"score\": 1},\"seo\": {\"score\": 0.95}},\"audits\": {\"cumulative-layout-shift\": {},\"speed-index\": {},\"first-contentful-paint\": {},\"largest-contentful-paint\": {},\"total-blocking-time\": {}}}}}}", + "outputDataExample": "{\"check\": {\"lighthouse\": {\"accessibility\": 0.98,\"bestPractices\": 1,\"seo\": 0.95,\"performance\": 0.92,\"audits\": {\"cls\": {},\"si\": {},\"fcp\": {},\"lcp\": {},\"tbt\": {}}}}}" + }, + { + "simStepId": "a629a102-0df1-4798-a7a2-45c8f6cdad29", + "diagramNodeId": "27c7191b-80a2-475b-9371-e106108aff1c", + "simStepLabel": "Store Processed Check Data", + "simStepDescription": "The processed and structured check data, containing the key performance metrics, is saved to the 'checks' collection in the database, associated with the corresponding monitor ID.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v2/models/checks/Check.ts", + "startLine": "237", + "endLine": "268", + "relevantVariables": [ + "lighthouse" + ] + }, + "inputDataExample": "{\"check\": {\"monitorId\": \"64b9a6b3e1b6f0b3e8a7d1c0\", \"status\": \"up\", \"responseTime\": 450, \"lighthouse\": {\"performance\": 0.92, \"accessibility\": 0.98, \"bestPractices\": 1, \"seo\": 0.95, \"audits\": {...}}}}", + "outputDataExample": "{\"check\": {\"monitorId\": \"64b9a6b3e1b6f0b3e8a7d1c0\", \"status\": \"up\", \"responseTime\": 450, \"lighthouse\": {\"performance\": 0.92, \"accessibility\": 0.98, \"bestPractices\": 1, \"seo\": 0.95, \"audits\": {...}}}}" + }, + { + "simStepId": "5616ac6d-c281-4879-82f1-6590c721c8f7", + "diagramNodeId": "ca6ee27b-1fbf-49de-978a-d6de259cc3d5", + "simStepLabel": "Backend Aggregates Performance Data", + "simStepDescription": "When a user views the details of a PageSpeed monitor, the frontend requests historical data. The `MonitorService` on the backend uses a MongoDB aggregation pipeline to group checks by time intervals and calculate average scores for metrics like performance, SEO, etc. This is defined by helper functions like `getPageSpeedGroup`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/business/MonitorService.ts", + "startLine": "106", + "endLine": "120", + "relevantVariables": [ + "getPageSpeedGroup", + "accessibility", + "bestPractices", + "seo", + "performance" + ] + }, + "inputDataExample": "{\"monitorId\": \"64b9a6b3e1b6f0b3e8a7d1c0\", \"range\": \"7d\"}", + "outputDataExample": "[{\"_id\": \"2023-07-20T10:00:00Z\", \"avgResponseTime\": 450, \"performance\": 0.92, \"seo\": 0.95, \"accessibility\": 0.98, \"bestPractices\": 1.0}, {\"_id\": \"2023-07-20T11:00:00Z\", \"avgResponseTime\": 465, \"performance\": 0.91, \"seo\": 0.95, \"accessibility\": 0.98, \"bestPractices\": 1.0}]" + }, + { + "simStepId": "4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08", + "diagramNodeId": "7be432cd-2587-4eab-abdc-f7b9c1923597", + "simStepLabel": "Aggregated Data Transmitted to Frontend", + "simStepDescription": "The backend sends the aggregated and time-series formatted performance data to the frontend in a JSON response.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/business/MonitorService.ts", + "startLine": "368", + "endLine": "372", + "relevantVariables": [ + "Check.aggregate" + ] + }, + "inputDataExample": "[{\"_id\": \"2023-07-20T10:00:00Z\", \"performance\": 0.92, \"seo\": 0.95, \"accessibility\": 0.98}, {\"_id\": \"2023-07-20T11:00:00Z\", \"performance\": 0.91, \"seo\": 0.95, \"accessibility\": 0.98}]", + "outputDataExample": "[{\"_id\": \"2023-07-20T10:00:00Z\", \"performance\": 0.92, \"seo\": 0.95, \"accessibility\": 0.98}, {\"_id\": \"2023-07-20T11:00:00Z\", \"performance\": 0.91, \"seo\": 0.95, \"accessibility\": 0.98}]" + }, + { + "simStepId": "0ec0eb06-05eb-4204-b077-9e47eb2cc316", + "diagramNodeId": "9ef9e218-4742-487a-9a88-5c70e788ccdc", + "simStepLabel": "Frontend Renders Performance Charts", + "simStepDescription": "The `PageSpeedDetails` component receives the historical data from the backend. It then passes this data to specialized child components like `PageSpeedAreaChart` and `PerformanceReport` to render visualizations of the website's performance over time.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/PageSpeed/Details/index.jsx", + "startLine": "91", + "endLine": "126", + "relevantVariables": [ + "PageSpeedStatusBoxes", + "MonitorTimeFrameHeader", + "PageSpeedAreaChart", + "PerformanceReport", + "monitor", + "audits" + ] + }, + "inputDataExample": "{\"monitor\": {\"checks\": [{\"createdAt\": \"2023-07-20T10:00:00Z\", \"performance\": 0.92, \"seo\": 0.95}, {\"createdAt\": \"2023-07-20T11:00:00Z\", \"performance\": 0.91, \"seo\": 0.95}]}}", + "outputDataExample": "{\"renderedUI\": \"A page with charts and graphs showing performance trends.\"}" + } + ], + "description": "
  • Integrates with Google's PageSpeed Insights to monitor and analyze web page performance
  • - Tracks key web vitals and performance metrics such as SEO, Accessibility, Best Practices, and Performance scores
  • - Provides a grid view of all page speed monitors for a quick overview of website health
  • - Detailed reports show historical performance data, allowing users to track improvements or regressions over time
  • - This feature requires a Google PageSpeed API key to be configured in the settings
", + "simulationNodesAndEdges": { + "0622b212-3a4a-484d-9b7a-125864d785cd": { + "simStepIds": [ + "b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12" + ] + }, + "6527fb47-157e-4bba-9aed-adde061c5689": { + "simStepIds": [ + "53968267-0c6e-42f4-88b3-cd90f44532ae" + ] + }, + "f89cbd87-c961-4f8b-92b6-714da57718d5": { + "simStepIds": [ + "01206383-2db8-4b18-84d0-be2a0289c196" + ] + }, + "ca6ee27b-1fbf-49de-978a-d6de259cc3d5": { + "simStepIds": [ + "5616ac6d-c281-4879-82f1-6590c721c8f7" + ] + }, + "9ef9e218-4742-487a-9a88-5c70e788ccdc": { + "simStepIds": [ + "0ec0eb06-05eb-4204-b077-9e47eb2cc316" + ] + }, + "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438": { + "simStepIds": [ + "f8df1cad-6051-466e-a13e-47d710ba1c14" + ] + }, + "0d3c8091-3d91-4e42-9b46-7e35ed726a7a": { + "simStepIds": [ + "0b380571-3896-4365-b5dc-187494f02e0d" + ] + }, + "27c7191b-80a2-475b-9371-e106108aff1c": { + "simStepIds": [ + "a629a102-0df1-4798-a7a2-45c8f6cdad29" + ] + }, + "7be432cd-2587-4eab-abdc-f7b9c1923597": { + "simStepIds": [ + "4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08" + ] + } + }, + "isAIGenerated": true, + "keywords": "pagespeed, lighthouse, performance", + "generationPrompt": "Website Performance and Page Speed Monitoring", + "generationKeywords": "pagespeed, lighthouse, performance" + }, + "Scheduled Maintenance Windows": { + "name": "Scheduled Maintenance Windows", + "simSteps": [ + { + "simStepId": "2401ef7a-8d8e-4056-868a-547ab213daf1", + "diagramNodeId": "20335f29-a2ac-4e0c-aba6-28707748ecd4", + "simStepLabel": "Flow: Create Maintenance Window - User Interaction", + "simStepDescription": "A user navigates to the 'Create Maintenance Window' page. They fill out a form with details such as a name, start date/time, duration, recurrence, and select the monitors to which this window will apply.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "startLine": "46", + "endLine": "131", + "relevantVariables": [ + "CreateMaintenance", + "form", + "setForm", + "handleFormChange", + "handleSubmit" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"name\":\"Server Patching Window\",\"startDate\":\"2023-10-27T00:00:00.000Z\",\"startTime\":\"2023-10-27T02:00:00.000Z\",\"duration\":\"60\",\"durationUnit\":\"minutes\",\"repeat\":\"none\",\"monitors\":[{\"_id\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Main Production Server\"}]}" + }, + { + "simStepId": "f881435b-3bc2-453d-b6bf-56ad3a12f120", + "diagramNodeId": "d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "simStepLabel": "Flow: Create Maintenance Window - Form Submission", + "simStepDescription": "Upon clicking the 'Create Maintenance' button, the form state is passed to the `handleSubmit` function which then calls the `handleSubmitForm` action from a custom hook.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "startLine": "108", + "endLine": "109", + "relevantVariables": [ + "handleSubmit", + "handleSubmitForm" + ] + }, + "inputDataExample": "{\"name\":\"Server Patching Window\",\"startDate\":\"2023-10-27T00:00:00.000Z\",\"startTime\":\"2023-10-27T02:00:00.000Z\",\"duration\":\"60\",\"durationUnit\":\"minutes\",\"repeat\":\"none\",\"monitors\":[{\"_id\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Main Production Server\"}]}", + "outputDataExample": "{\"name\":\"Server Patching Window\",\"startDate\":\"2023-10-27T00:00:00.000Z\",\"startTime\":\"2023-10-27T02:00:00.000Z\",\"duration\":\"60\",\"durationUnit\":\"minutes\",\"repeat\":\"none\",\"monitors\":[{\"_id\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Main Production Server\"}]}" + }, + { + "simStepId": "5e0d7085-099b-478a-afaa-47ad4dd07043", + "diagramNodeId": "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d", + "simStepLabel": "Flow: Create Maintenance Window - Prepare API Request", + "simStepDescription": "The `handleSubmitForm` hook processes the form data, calculates start and end timestamps, and constructs a request object. It then determines whether to call `createMaintenanceWindow` or `editMaintenanceWindow` on the network service.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "startLine": "25", + "endLine": "58", + "relevantVariables": [ + "handleSubmitForm", + "networkService.createMaintenanceWindow", + "networkService.editMaintenanceWindow" + ] + }, + "inputDataExample": "{\"form\":{\"name\":\"Server Patching Window\",\"startDate\":\"2023-10-27T00:00:00.000Z\",\"startTime\":\"2023-10-27T02:00:00.000Z\",\"duration\":\"60\",\"durationUnit\":\"minutes\",\"repeat\":\"none\",\"monitors\":[{\"_id\":\"652ff379963e792a5a3a0e4e\"}]}}", + "outputDataExample": "{\"maintenanceWindow\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"repeat\":0,\"oneTime\":true,\"monitors\":[\"652ff379963e792a5a3a0e4e\"],\"expiry\":\"2023-10-27T03:00:00.000Z\"}}" + }, + { + "simStepId": "0660b48b-139f-4ecf-b7d9-f12f544e0ab5", + "diagramNodeId": "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "simStepLabel": "Flow: Create Maintenance Window - API Call", + "simStepDescription": "The `NetworkService` sends the prepared maintenance window data as a POST request to the `/api/v1/maintenance-window` endpoint.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "762", + "endLine": "766", + "relevantVariables": [ + "createMaintenanceWindow", + "this.axiosInstance.post" + ] + }, + "inputDataExample": "{\"maintenanceWindow\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"repeat\":0,\"oneTime\":true,\"monitors\":[\"652ff379963e792a5a3a0e4e\"],\"expiry\":\"2023-10-27T03:00:00.000Z\"}}", + "outputDataExample": "{\"maintenanceWindow\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"repeat\":0,\"oneTime\":true,\"monitors\":[\"652ff379963e792a5a3a0e4e\"],\"expiry\":\"2023-10-27T03:00:00.000Z\"}}" + }, + { + "simStepId": "3e93ef65-8e2c-405b-8dc1-56d8e93937b3", + "diagramNodeId": "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8", + "simStepLabel": "Flow: Create Maintenance Window - API Routing", + "simStepDescription": "The backend Express server receives the request. The routing configuration maps the `/api/v1/maintenance-window` path to the `maintenanceWindowRoutes`, which in turn directs the POST request to the `createMaintenanceWindows` method in the controller.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/config/routes.js", + "startLine": "43", + "endLine": "43", + "relevantVariables": [ + "app", + "verifyJWT", + "maintenanceWindowRoutes" + ] + }, + "inputDataExample": "{\"method\":\"POST\",\"url\":\"/api/v1/maintenance-window\",\"body\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"repeat\":0,\"oneTime\":true,\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}", + "outputDataExample": "{\"controllerMethod\":\"createMaintenanceWindows\",\"params\":{}}" + }, + { + "simStepId": "e4cc5ce8-6da9-4281-9204-29c061ab13ce", + "diagramNodeId": "e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "simStepLabel": "Flow: Create Maintenance Window - Controller Invocation", + "simStepDescription": "The request and response objects are passed from the router to the `createMaintenanceWindows` controller method.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/maintenanceWindowRoute.js", + "startLine": "10", + "endLine": "10", + "relevantVariables": [ + "this.router.post", + "this.mwController.createMaintenanceWindows" + ] + }, + "inputDataExample": "{\"req\": {\"body\": {\"name\":\"Server Patching Window\", \"monitors\":[\"652ff379963e792a5a3a0e4e\"]}, \"user\": {\"teamId\":\"652ff379963e792a5a3a0e4d\"}}}", + "outputDataExample": "{\"req\": {\"body\": {\"name\":\"Server Patching Window\", \"monitors\":[\"652ff379963e792a5a3a0e4e\"]}, \"user\": {\"teamId\":\"652ff379963e792a5a3a0e4d\"}}}" + }, + { + "simStepId": "95fc1919-339d-4659-98cb-3dc50f662075", + "diagramNodeId": "0fae6e75-38ea-444d-a1bf-0f820ba443b9", + "simStepLabel": "Flow: Create Maintenance Window - Controller Logic", + "simStepDescription": "The `maintenanceWindowController` validates the request body using Joi, ensures a `teamId` exists on the user object, and then calls the `maintenanceWindowService` to handle the business logic of creating the window.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "27", + "endLine": "41", + "relevantVariables": [ + "createMaintenanceWindows", + "createMaintenanceWindowBodyValidation", + "this.maintenanceWindowService.createMaintenanceWindow" + ] + }, + "inputDataExample": "{\"req\": {\"body\": {\"name\":\"Server Patching Window\", \"monitors\":[\"652ff379963e792a5a3a0e4e\"]}, \"user\": {\"teamId\":\"652ff379963e792a5a3a0e4d\"}}}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"body\":{\"name\":\"Server Patching Window\",\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}" + }, + { + "simStepId": "c3d1a95c-e93d-499a-8830-a6d870902414", + "diagramNodeId": "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "simStepLabel": "Flow: Create Maintenance Window - Service Layer Call", + "simStepDescription": "The controller passes the teamId and request body to the `createMaintenanceWindow` method in the `maintenanceWindowService`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "35", + "endLine": "35", + "relevantVariables": [ + "this.maintenanceWindowService.createMaintenanceWindow" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"body\":{\"name\":\"Server Patching Window\",\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"body\":{\"name\":\"Server Patching Window\",\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}" + }, + { + "simStepId": "6e2cf739-1514-4c37-9874-10b68a47010e", + "diagramNodeId": "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f", + "simStepLabel": "Flow: Create Maintenance Window - Business Logic Execution", + "simStepDescription": "The `maintenanceWindowService` verifies that all monitors specified in the request belong to the user's team. It then iterates through each monitor ID and prepares a separate database transaction to create a maintenance window record for each one.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/maintenanceWindowService.js", + "startLine": "18", + "endLine": "39", + "relevantVariables": [ + "createMaintenanceWindow", + "this.db.monitorModule.getMonitorsByIds", + "this.db.maintenanceWindowModule.createMaintenanceWindow", + "Promise.all" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"body\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}", + "outputDataExample": "[{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\"}]" + }, + { + "simStepId": "80aed28e-68b6-47bf-8bc9-ededb4824066", + "diagramNodeId": "12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "simStepLabel": "Flow: Create Maintenance Window - Database Module Call", + "simStepDescription": "For each monitor, the service layer calls the `createMaintenanceWindow` method in the `maintenanceWindowModule`, passing the structured data for the new record.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/maintenanceWindowService.js", + "startLine": "28", + "endLine": "37", + "relevantVariables": [ + "dbTransactions", + "this.db.maintenanceWindowModule.createMaintenanceWindow" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"oneTime\":true}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"oneTime\":true}" + }, + { + "simStepId": "a3c47fd4-c701-46c2-9aff-94a667b3e368", + "diagramNodeId": "3890b98d-a133-4279-a961-5d2facc6a17a", + "simStepLabel": "Flow: Create Maintenance Window - Database Record Creation", + "simStepDescription": "The `maintenanceWindowModule` instantiates a new `MaintenanceWindow` Mongoose model. If the window is a one-time event, it sets an `expiry` field to the end time for automatic cleanup by MongoDB's TTL index. Finally, it saves the new document to the database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "startLine": "7", + "endLine": "22", + "relevantVariables": [ + "createMaintenanceWindow", + "this.MaintenanceWindow", + "maintenanceWindow.save" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"oneTime\":true}", + "outputDataExample": "{\"_id\":\"653ba9a6331a31004887396a\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\",\"name\":\"Server Patching Window\",\"active\":true,\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"expiry\":\"2023-10-27T03:00:00.000Z\"}" + }, + { + "simStepId": "69fdc2a9-837f-466c-9562-a12319019580", + "diagramNodeId": "fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "simStepLabel": "Flow: Create Maintenance Window - API Response", + "simStepDescription": "After the database operations are complete, a success response is sent back through the layers to the frontend.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "37", + "endLine": "39", + "relevantVariables": [ + "res.success", + "this.stringService.maintenanceWindowCreate" + ] + }, + "inputDataExample": "{\"msg\":\"Maintenance Window created successfully.\"}", + "outputDataExample": "{\"msg\":\"Maintenance Window created successfully.\"}" + }, + { + "simStepId": "ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc", + "diagramNodeId": "a5a84b08-8c70-4023-8d08-b74e23aab4d7", + "simStepLabel": "Flow: Create Maintenance Window - UI Update", + "simStepDescription": "The frontend receives the success response. A success toast message is displayed, and the user is navigated back to the main maintenance windows table.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "startLine": "111", + "endLine": "119", + "relevantVariables": [ + "request", + "createToast", + "navigate" + ] + }, + "inputDataExample": "{\"data\":{\"msg\":\"Maintenance Window created successfully.\"}}", + "outputDataExample": "{}" + }, + { + "simStepId": "82b5ea14-bb00-4511-aba4-76196029fd81", + "diagramNodeId": "d145bb4c-e1e0-473a-b6bc-decc932b738b", + "simStepLabel": "Flow: View Maintenance Windows - Page Load", + "simStepDescription": "User navigates to the '/maintenance' page. The `Maintenance` component mounts and triggers a `useEffect` hook to fetch the list of maintenance windows.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/index.jsx", + "startLine": "25", + "endLine": "47", + "relevantVariables": [ + "useEffect", + "networkService.getMaintenanceWindowsByTeamId" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"page\":0,\"rowsPerPage\":5}" + }, + { + "simStepId": "7e20cd6c-d24e-4540-86d6-d6a9fa087314", + "diagramNodeId": "7f921125-500f-4902-abc3-936ecac033d8", + "simStepLabel": "Flow: View Maintenance Windows - API Call", + "simStepDescription": "The `networkService` makes a GET request to the backend API to fetch the maintenance windows for the user's team, including pagination parameters.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "824", + "endLine": "824", + "relevantVariables": [ + "getMaintenanceWindowsByTeamId", + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\"page\":0,\"rowsPerPage\":5}", + "outputDataExample": "{\"page\":0,\"rowsPerPage\":5}" + }, + { + "simStepId": "1251da83-5949-4e24-a1d2-fc74ff785aa9", + "diagramNodeId": "c35a065c-2808-402c-877e-9055d956ce0d", + "simStepLabel": "Flow: View Maintenance Windows - Controller and Service", + "simStepDescription": "The backend routes the request to `maintenanceWindowController.getMaintenanceWindowsByTeamId`. This controller calls the corresponding service, which in turn calls the database module to query for the data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "75", + "endLine": "75", + "relevantVariables": [ + "this.maintenanceWindowService.getMaintenanceWindowsByTeamId" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}" + }, + { + "simStepId": "feb8bd4f-339f-4132-a709-89c63c4efa9c", + "diagramNodeId": "c3127c74-43f1-42d7-8da6-b0a494c3658f", + "simStepLabel": "Flow: View Maintenance Windows - Database Query", + "simStepDescription": "The service layer invokes the database module to fetch the maintenance windows from the database.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/maintenanceWindowService.js", + "startLine": "48", + "endLine": "48", + "relevantVariables": [ + "this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}" + }, + { + "simStepId": "9bfbae5a-f487-40ef-86f5-04a1cc3c7662", + "diagramNodeId": "27d9160a-4f8d-4dff-a09b-1678bc28cf2c", + "simStepLabel": "Flow: View Maintenance Windows - Data Retrieval", + "simStepDescription": "The `maintenanceWindowModule` constructs a MongoDB query to find all maintenance windows for the given `teamId`, applying pagination (skip, limit) and sorting. It executes the query and returns the results along with the total count.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "startLine": "37", + "endLine": "66", + "relevantVariables": [ + "getMaintenanceWindowsByTeamId", + "this.MaintenanceWindow.countDocuments", + "this.MaintenanceWindow.find" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}", + "outputDataExample": "{\"maintenanceWindows\":[{\"_id\":\"653ba9a6331a31004887396a\",\"name\":\"Server Patching Window\"}],\"maintenanceWindowCount\":1}" + }, + { + "simStepId": "eda86dca-dea6-4671-a079-7d212f4f18c4", + "diagramNodeId": "6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "simStepLabel": "Flow: View Maintenance Windows - Data Return to Frontend", + "simStepDescription": "The fetched data is sent back in the API response to the client.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "77", + "endLine": "80", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"data\":{\"maintenanceWindows\":[{\"_id\":\"653ba9a6331a31004887396a\",\"name\":\"Server Patching Window\"}],\"maintenanceWindowCount\":1}}", + "outputDataExample": "{\"data\":{\"maintenanceWindows\":[{\"_id\":\"653ba9a6331a31004887396a\",\"name\":\"Server Patching Window\"}],\"maintenanceWindowCount\":1}}" + }, + { + "simStepId": "acf97639-e291-4ca6-81a6-d0487397f42a", + "diagramNodeId": "970e2002-7c0e-40ef-80d8-e5e6d0a60735", + "simStepLabel": "Flow: View Maintenance Windows - UI Rendering", + "simStepDescription": "The client receives the data and updates its state. The `MaintenanceTable` component then renders the list of maintenance windows in a table, displaying their names, schedules, and providing actions like edit or delete.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx", + "startLine": "195", + "endLine": "201", + "relevantVariables": [ + "Table", + "headers", + "maintenanceWindows" + ] + }, + "inputDataExample": "{\"maintenanceWindows\":[{\"_id\":\"653ba9a6331a31004887396a\",\"name\":\"Server Patching Window\"}],\"maintenanceWindowCount\":1}", + "outputDataExample": "{}" + }, + { + "simStepId": "a47affaf-b952-46d6-9709-6120a77690e1", + "diagramNodeId": "23630a1f-7452-4373-a864-7ec7821137f2", + "simStepLabel": "Flow: Monitor Suppression - Job Execution Start", + "simStepDescription": "The `SuperSimpleQueue` processing system picks up a job to check a specific monitor. The job handler is retrieved from `SuperSimpleQueueHelper`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "30", + "endLine": "45", + "relevantVariables": [ + "getMonitorJob", + "this.isInMaintenanceWindow" + ] + }, + "inputDataExample": "{\"monitor\":{\"_id\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\",\"name\":\"Main Production Server\"}}", + "outputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}" + }, + { + "simStepId": "87583096-f1ec-4160-ae52-5238f32ed0ab", + "diagramNodeId": "c99291bb-7316-440e-9a7f-323ee39e26b8", + "simStepLabel": "Flow: Monitor Suppression - Maintenance Check Call", + "simStepDescription": "Before performing the monitor check, the `getMonitorJob` function calls `isInMaintenanceWindow`, passing the monitor's ID and team ID to see if it should be suppressed.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "36", + "endLine": "36", + "relevantVariables": [ + "this.isInMaintenanceWindow" + ] + }, + "inputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}", + "outputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}" + }, + { + "simStepId": "2127e749-80b4-499a-ae14-8dc2dcc9db9b", + "diagramNodeId": "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba", + "simStepLabel": "Flow: Monitor Suppression - Fetching Maintenance Windows", + "simStepDescription": "The `isInMaintenanceWindow` function calls the database module to fetch all maintenance windows associated with the given monitor ID and team ID.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "80", + "endLine": "83", + "relevantVariables": [ + "this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId" + ] + }, + "inputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}", + "outputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}" + }, + { + "simStepId": "e6542e14-556d-482a-8c35-f7b03997be49", + "diagramNodeId": "8a13e585-2310-420b-9e92-211b6d65f201", + "simStepLabel": "Flow: Monitor Suppression - Database Result Return", + "simStepDescription": "The database module returns a list of maintenance window documents that match the monitor ID.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "startLine": "69", + "endLine": "76", + "relevantVariables": [ + "getMaintenanceWindowsByMonitorId", + "this.MaintenanceWindow.find" + ] + }, + "inputDataExample": "[{\"_id\":\"653ba9a6331a31004887396a\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"active\":true,\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\"}]", + "outputDataExample": "[{\"_id\":\"653ba9a6331a31004887396a\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"active\":true,\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\"}]" + }, + { + "simStepId": "6ba6b9d1-5fc3-454e-8744-2022b5192ed9", + "diagramNodeId": "0fe32416-db43-49b9-9438-3796ba1a0add", + "simStepLabel": "Flow: Monitor Suppression - Time Evaluation", + "simStepDescription": "The `isInMaintenanceWindow` function iterates through the returned windows. For each active window, it checks if the current time (`new Date()`) is between the `start` and `end` times.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "85", + "endLine": "113", + "relevantVariables": [ + "maintenanceWindows.reduce", + "start", + "end", + "now" + ] + }, + "inputDataExample": "[{\"_id\":\"653ba9a6331a31004887396a\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"active\":true,\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\"}]", + "outputDataExample": "true" + }, + { + "simStepId": "a715d98f-60bf-4f32-aae6-21ee4e09f4da", + "diagramNodeId": "40f4d9df-4c0f-4384-901c-8d7117e3b538", + "simStepLabel": "Flow: Monitor Suppression - Return Suppression Flag", + "simStepDescription": "The boolean result (true if in maintenance, false otherwise) is returned to the `getMonitorJob` function.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "114", + "endLine": "114", + "relevantVariables": [ + "maintenanceWindowIsActive" + ] + }, + "inputDataExample": "true", + "outputDataExample": "true" + }, + { + "simStepId": "e59fe64e-f06b-4855-b631-923bb5a68540", + "diagramNodeId": "01e042c3-40b4-416a-8d9e-6d6fd323d60b", + "simStepLabel": "Flow: Monitor Suppression - Conditional Job Termination", + "simStepDescription": "If the `isInMaintenanceWindow` function returns true, the `getMonitorJob` logs that the monitor is in a maintenance window and terminates its execution for this cycle, effectively suppressing the check.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "37", + "endLine": "44", + "relevantVariables": [ + "maintenanceWindowActive", + "this.logger.info", + "return" + ] + }, + "inputDataExample": "{\"maintenanceWindowActive\": true}", + "outputDataExample": "{\"message\":\"Monitor 652ff379963e792a5a3a0e4e is in maintenance window\"}" + } + ], + "description": "
  • Allows users to schedule maintenance periods to prevent false alarms and avoid skewing uptime statistics during planned downtime
  • - Users can define a start and end time for a maintenance window
  • - Specific monitors or all monitors can be associated with a maintenance window
  • - During a scheduled maintenance, notifications for associated monitors are suppressed
  • - Provides a central table to view all active, upcoming, and past maintenance windows
", + "simulationNodesAndEdges": { + "20335f29-a2ac-4e0c-aba6-28707748ecd4": { + "simStepIds": [ + "2401ef7a-8d8e-4056-868a-547ab213daf1" + ] + }, + "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d": { + "simStepIds": [ + "5e0d7085-099b-478a-afaa-47ad4dd07043" + ] + }, + "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8": { + "simStepIds": [ + "3e93ef65-8e2c-405b-8dc1-56d8e93937b3" + ] + }, + "0fae6e75-38ea-444d-a1bf-0f820ba443b9": { + "simStepIds": [ + "95fc1919-339d-4659-98cb-3dc50f662075" + ] + }, + "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f": { + "simStepIds": [ + "6e2cf739-1514-4c37-9874-10b68a47010e" + ] + }, + "3890b98d-a133-4279-a961-5d2facc6a17a": { + "simStepIds": [ + "a3c47fd4-c701-46c2-9aff-94a667b3e368" + ] + }, + "a5a84b08-8c70-4023-8d08-b74e23aab4d7": { + "simStepIds": [ + "ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc" + ] + }, + "d145bb4c-e1e0-473a-b6bc-decc932b738b": { + "simStepIds": [ + "82b5ea14-bb00-4511-aba4-76196029fd81" + ] + }, + "c35a065c-2808-402c-877e-9055d956ce0d": { + "simStepIds": [ + "1251da83-5949-4e24-a1d2-fc74ff785aa9" + ] + }, + "27d9160a-4f8d-4dff-a09b-1678bc28cf2c": { + "simStepIds": [ + "9bfbae5a-f487-40ef-86f5-04a1cc3c7662" + ] + }, + "970e2002-7c0e-40ef-80d8-e5e6d0a60735": { + "simStepIds": [ + "acf97639-e291-4ca6-81a6-d0487397f42a" + ] + }, + "23630a1f-7452-4373-a864-7ec7821137f2": { + "simStepIds": [ + "a47affaf-b952-46d6-9709-6120a77690e1" + ] + }, + "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba": { + "simStepIds": [ + "2127e749-80b4-499a-ae14-8dc2dcc9db9b" + ] + }, + "0fe32416-db43-49b9-9438-3796ba1a0add": { + "simStepIds": [ + "6ba6b9d1-5fc3-454e-8744-2022b5192ed9" + ] + }, + "01e042c3-40b4-416a-8d9e-6d6fd323d60b": { + "simStepIds": [ + "e59fe64e-f06b-4855-b631-923bb5a68540" + ] + }, + "d3bdc6ff-875e-472e-88c8-eb10b20c526e": { + "simStepIds": [ + "f881435b-3bc2-453d-b6bf-56ad3a12f120" + ] + }, + "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af": { + "simStepIds": [ + "0660b48b-139f-4ecf-b7d9-f12f544e0ab5" + ] + }, + "e2588541-3ffc-47c6-8f8a-41d30ea7fc72": { + "simStepIds": [ + "e4cc5ce8-6da9-4281-9204-29c061ab13ce" + ] + }, + "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f": { + "simStepIds": [ + "c3d1a95c-e93d-499a-8830-a6d870902414" + ] + }, + "12d0ab8a-392f-48fa-9efa-06ffe1f3a252": { + "simStepIds": [ + "80aed28e-68b6-47bf-8bc9-ededb4824066" + ] + }, + "fcf4eef4-394c-4872-9124-f7a0b32cdc3b": { + "simStepIds": [ + "69fdc2a9-837f-466c-9562-a12319019580" + ] + }, + "7f921125-500f-4902-abc3-936ecac033d8": { + "simStepIds": [ + "7e20cd6c-d24e-4540-86d6-d6a9fa087314" + ] + }, + "c3127c74-43f1-42d7-8da6-b0a494c3658f": { + "simStepIds": [ + "feb8bd4f-339f-4132-a709-89c63c4efa9c" + ] + }, + "6c9979cd-1cc1-4227-b535-2ec3a10ca27e": { + "simStepIds": [ + "eda86dca-dea6-4671-a079-7d212f4f18c4" + ] + }, + "c99291bb-7316-440e-9a7f-323ee39e26b8": { + "simStepIds": [ + "87583096-f1ec-4160-ae52-5238f32ed0ab" + ] + }, + "8a13e585-2310-420b-9e92-211b6d65f201": { + "simStepIds": [ + "e6542e14-556d-482a-8c35-f7b03997be49" + ] + }, + "40f4d9df-4c0f-4384-901c-8d7117e3b538": { + "simStepIds": [ + "a715d98f-60bf-4f32-aae6-21ee4e09f4da" + ] + } + }, + "isAIGenerated": true, + "keywords": "MaintenanceWindow, createMaintenanceWindow, maintenanceWindowController", + "generationPrompt": "Scheduled Maintenance Windows", + "generationKeywords": "MaintenanceWindow, createMaintenanceWindow, maintenanceWindowController" + }, + "User and Team Management with Role-Based Access": { + "name": "User and Team Management with Role-Based Access", + "simSteps": [ + { + "simStepId": "2d5a952a-ce62-4948-9309-96b28ad74e59", + "diagramNodeId": "3de2d413-5e9b-49f4-9409-74be32c52e0f", + "simStepLabel": "Client: Conditional UI Rendering Based on Role", + "simStepDescription": "A user with 'superadmin' or 'admin' role navigates to the Account page. The application checks the user's role stored in the Redux state to conditionally render the 'Team' management tab. Non-admin users will not see this option.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Account/index.jsx", + "startLine": "37", + "endLine": "41", + "relevantVariables": [ + "user.role", + "requiredRoles", + "hideTeams" + ] + }, + "inputDataExample": "{\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"firstName\": \"Jane\",\n \"lastName\": \"Doe\",\n \"email\": \"jane.doe@example.com\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n }\n}", + "outputDataExample": "{\n \"hideTeams\": false,\n \"tabList\": [\n {\n \"name\": \"Profile\",\n \"value\": \"profile\"\n },\n {\n \"name\": \"Password\",\n \"value\": \"password\"\n },\n {\n \"name\": \"Team\",\n \"value\": \"team\"\n }\n ]\n}" + }, + { + "simStepId": "10f80ada-2d0a-4e98-bd5d-057e7d4fe6de", + "diagramNodeId": "c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "simStepLabel": "Client: User Navigates to Team Panel", + "simStepDescription": "The user clicks on the 'Team' tab, which navigates them to the team management view. The user's role data is implicitly passed through the application state to render the appropriate components.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Account/index.jsx", + "startLine": "98", + "endLine": "98", + "relevantVariables": [ + "TeamPanel" + ] + }, + "inputDataExample": "{\n \"navigationPath\": \"/account/team\",\n \"userState\": {\n \"role\": [\"superadmin\"]\n }\n}", + "outputDataExample": "{\n \"navigationPath\": \"/account/team\",\n \"userState\": {\n \"role\": [\"superadmin\"]\n }\n}" + }, + { + "simStepId": "eebaca25-d588-49c4-a35c-9abc82fde580", + "diagramNodeId": "b27f1bba-c655-40c6-a1dc-71e78c590c92", + "simStepLabel": "Client: Initiate User Invitation", + "simStepDescription": "In the `TeamPanel`, the admin clicks the 'Add Team Member' button, then selects 'Invite a team member'. A dialog opens, prompting for the new user's email and role. After filling the form and clicking 'E-mail token', the `handleInviteMember` function is triggered to start the invitation process.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "startLine": "348", + "endLine": "354", + "relevantVariables": [ + "handleInviteMember", + "Button" + ] + }, + "inputDataExample": "{\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n}", + "outputDataExample": "{\n \"apiCall\": \"networkService.sendInviteEmail\",\n \"payload\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n}" + }, + { + "simStepId": "16d87608-4458-430c-b459-cd9b1d413fa6", + "diagramNodeId": "3530244f-2689-444e-bcbf-611db2e3c000", + "simStepLabel": "API Call: Send Invitation Request", + "simStepDescription": "The client sends a POST request to the `/api/v1/invite/send` endpoint. The request includes the new user's email and selected role in the body, and the admin's authentication token in the headers.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/inviteRoute.js", + "startLine": "14", + "endLine": "14", + "relevantVariables": [ + "this.router.post" + ] + }, + "inputDataExample": "{\n \"endpoint\": \"/api/v1/invite/send\",\n \"method\": \"POST\",\n \"headers\": {\n \"Authorization\": \"Bearer \"\n },\n \"body\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n}", + "outputDataExample": "{\n \"endpoint\": \"/api/v1/invite/send\",\n \"method\": \"POST\",\n \"headers\": {\n \"Authorization\": \"Bearer \"\n },\n \"body\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n}" + }, + { + "simStepId": "50dd7316-7d01-43f7-9036-d97d51a9c178", + "diagramNodeId": "8b3cb6c2-ed57-4510-be15-decb8e3c89e1", + "simStepLabel": "Server: Role-Based Access Control Middleware", + "simStepDescription": "The server receives the request. Before reaching the main controller, the `isAllowed` middleware intercepts the request. It verifies the JWT from the request header and checks if the user's role (e.g., 'superadmin') is present in the list of allowed roles for this route (`['admin', 'superadmin']`).", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/middleware/v1/isAllowed.js", + "startLine": "39", + "endLine": "44", + "relevantVariables": [ + "decoded.role", + "allowedRoles", + "hasRole" + ] + }, + "inputDataExample": "{\n \"req\": {\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"headers\": {\n \"authorization\": \"Bearer \"\n }\n },\n \"allowedRoles\": [\"admin\", \"superadmin\"]\n}", + "outputDataExample": "{\n \"result\": \"next() called\",\n \"req\": {\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"headers\": {\n \"authorization\": \"Bearer \"\n }\n }\n}" + }, + { + "simStepId": "6c024d4c-178b-45fc-8bca-4fc6c00549a4", + "diagramNodeId": "2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "simStepLabel": "Server: Forward Request to Controller", + "simStepDescription": "Since the user's role is authorized, the middleware calls `next()` to pass the request object, now populated with user data, to the next handler in the chain: `inviteController.sendInviteEmail`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/inviteController.js", + "startLine": "61", + "endLine": "68", + "relevantVariables": [ + "sendInviteEmail", + "this.inviteService.sendInviteEmail" + ] + }, + "inputDataExample": "{\n \"req\": {\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"firstName\": \"Jane\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"body\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n }\n}", + "outputDataExample": "{\n \"req\": {\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"firstName\": \"Jane\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"body\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n }\n}" + }, + { + "simStepId": "55fe9963-30d2-47f8-859f-b485a77064c4", + "diagramNodeId": "ae858158-8db6-4917-8fda-0966a953f6ed", + "simStepLabel": "Server: Process Invitation in Service Layer", + "simStepDescription": "The `inviteController` calls the `inviteService`. The service layer handles the business logic: it first requests an invitation token from the database module and then instructs the email service to send the invitation.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/inviteService.js", + "startLine": "24", + "endLine": "28", + "relevantVariables": [ + "sendInviteEmail", + "this.db.inviteModule.requestInviteToken", + "this.emailService.sendInviteEmail" + ] + }, + "inputDataExample": "{\n \"inviteRequest\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"firstName\": \"Jane\"\n}", + "outputDataExample": "{\n \"inviteToken\": {\n \"token\": \"a1b2c3d4e5f6...\",\n \"email\": \"new.developer@example.com\"\n }\n}" + }, + { + "simStepId": "c796f8ad-d548-45f3-8d62-67f59c6c1aa4", + "diagramNodeId": "bdd52e6f-ddf7-4966-b3af-957f9553082f", + "simStepLabel": "Database Query: Create and Store Invite Token", + "simStepDescription": "The `inviteService` calls the `inviteModule` to generate and persist a unique invitation token. This involves creating a new document in the `InviteToken` collection with the invitee's details and a securely generated random token.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/inviteModule.js", + "startLine": "23", + "endLine": "26", + "relevantVariables": [ + "requestInviteToken", + "InviteToken", + "invite.save" + ] + }, + "inputDataExample": "{\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n}", + "outputDataExample": "{\n \"_id\": \"61f8f7b3a7b3c2a3e4f5a6b9\",\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\",\n \"token\": \"a1b2c3d4e5f6...\",\n \"createdAt\": \"2023-10-27T10:00:00.000Z\"\n}" + }, + { + "simStepId": "7d1f86f2-c8d5-48fb-a8c1-b337dc56271f", + "diagramNodeId": "a7be2d0b-0227-4228-a0a6-074f7324fd89", + "simStepLabel": "Server: Dispatch Invitation Email", + "simStepDescription": "After successfully storing the token, the `inviteService` uses the `emailService` to compose and send an email to the new user. The email contains a unique registration link including the generated token.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/inviteService.js", + "startLine": "26", + "endLine": "28", + "relevantVariables": [ + "this.emailService.sendInviteEmail", + "inviteToken" + ] + }, + "inputDataExample": "{\n \"to\": \"new.developer@example.com\",\n \"token\": \"a1b2c3d4e5f6...\",\n \"inviterName\": \"Jane\"\n}", + "outputDataExample": "{\n \"status\": \"success\",\n \"messageId\": \"\"\n}" + }, + { + "simStepId": "6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9", + "diagramNodeId": "52732a37-82b8-40a7-b12d-dc1b82380b2d", + "simStepLabel": "API Response: Send Confirmation to Client", + "simStepDescription": "The server's controller, having successfully processed the invitation, sends a JSON response back to the client indicating that the invite was sent successfully.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/inviteController.js", + "startLine": "69", + "endLine": "72", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\n \"success\": true,\n \"msg\": \"Invite sent successfully\",\n \"data\": {\n \"token\": \"a1b2c3d4e5f6...\",\n \"email\": \"new.developer@example.com\"\n }\n}", + "outputDataExample": "{\n \"success\": true,\n \"msg\": \"Invite sent successfully\",\n \"data\": {\n \"token\": \"a1b2c3d4e5f6...\",\n \"email\": \"new.developer@example.com\"\n }\n}" + }, + { + "simStepId": "700f35e0-b39d-417c-b0a8-0612feade977", + "diagramNodeId": "02d87257-883c-4822-a421-823c983532ec", + "simStepLabel": "Client: Display Success Feedback", + "simStepDescription": "The client application receives the successful API response. It then closes the invitation dialog and displays a toast notification to the admin, confirming that the invitation has been sent.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "startLine": "145", + "endLine": "149", + "relevantVariables": [ + "createToast", + "closeInviteModal" + ] + }, + "inputDataExample": "{\n \"response\": {\n \"status\": 200,\n \"data\": {\n \"success\": true,\n \"msg\": \"Invite sent successfully\"\n }\n }\n}", + "outputDataExample": "{\n \"uiState\": {\n \"toast\": {\n \"type\": \"success\",\n \"message\": \"Invite sent successfully\"\n }\n }\n}" + } + ], + "description": "
  • Enables collaborative use of the application within an organization with proper access controls
  • - The system supports multiple users organized into teams
  • - Implements role-based access control (RBAC) with roles like 'superadmin' and 'admin', restricting access to sensitive areas like settings and user management
  • - Superadmins can invite new users to the team via email, assigning them a specific role
  • - Users can manage their own profiles and change their passwords
", + "simulationNodesAndEdges": { + "3de2d413-5e9b-49f4-9409-74be32c52e0f": { + "simStepIds": [ + "2d5a952a-ce62-4948-9309-96b28ad74e59" + ] + }, + "b27f1bba-c655-40c6-a1dc-71e78c590c92": { + "simStepIds": [ + "eebaca25-d588-49c4-a35c-9abc82fde580" + ] + }, + "8b3cb6c2-ed57-4510-be15-decb8e3c89e1": { + "simStepIds": [ + "50dd7316-7d01-43f7-9036-d97d51a9c178" + ] + }, + "ae858158-8db6-4917-8fda-0966a953f6ed": { + "simStepIds": [ + "55fe9963-30d2-47f8-859f-b485a77064c4" + ] + }, + "a7be2d0b-0227-4228-a0a6-074f7324fd89": { + "simStepIds": [ + "7d1f86f2-c8d5-48fb-a8c1-b337dc56271f" + ] + }, + "02d87257-883c-4822-a421-823c983532ec": { + "simStepIds": [ + "700f35e0-b39d-417c-b0a8-0612feade977" + ] + }, + "c441312f-f3c6-48c6-b1b9-7c12749d91bb": { + "simStepIds": [ + "10f80ada-2d0a-4e98-bd5d-057e7d4fe6de" + ] + }, + "3530244f-2689-444e-bcbf-611db2e3c000": { + "simStepIds": [ + "16d87608-4458-430c-b459-cd9b1d413fa6" + ] + }, + "2ee1c4e8-90fb-4470-ad9f-6ffed5420853": { + "simStepIds": [ + "6c024d4c-178b-45fc-8bca-4fc6c00549a4" + ] + }, + "bdd52e6f-ddf7-4966-b3af-957f9553082f": { + "simStepIds": [ + "c796f8ad-d548-45f3-8d62-67f59c6c1aa4" + ] + }, + "52732a37-82b8-40a7-b12d-dc1b82380b2d": { + "simStepIds": [ + "6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9" + ] + } + }, + "isAIGenerated": true, + "keywords": "isAllowed, RoleProtectedRoute, Team", + "generationPrompt": "User and Team Management with Role-Based Access", + "generationKeywords": "isAllowed, RoleProtectedRoute, Team" + }, + "Bulk Monitor Management": { + "name": "Bulk Monitor Management", + "simSteps": [ + { + "simStepId": "a29da01e-8c5a-4e6d-8d3c-e49af528e4fa", + "diagramNodeId": "c4caeba8-ed9a-4cbc-9888-38cc8ca55362", + "simStepLabel": "Flow 1: CSV Import - Navigate to Bulk Import Page", + "simStepDescription": "The user navigates to the \"/uptime/bulk-import\" URL, which is routed to the BulkImport component for rendering the UI.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Routes/index.jsx", + "startLine": "88", + "endLine": "91", + "relevantVariables": [ + "Route", + "BulkImport" + ] + }, + "inputDataExample": "{\"url\": \"/uptime/bulk-import\"}", + "outputDataExample": "{\"component\": \"BulkImport\"}" + }, + { + "simStepId": "ef0bbcff-9535-4c0e-a5ca-96c457f89139", + "diagramNodeId": "c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "simStepLabel": "Flow 1: CSV Import - Render UI", + "simStepDescription": "The BulkImport component is rendered, displaying the file upload interface to the user.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "startLine": "16", + "endLine": "50", + "relevantVariables": [ + "BulkImport" + ] + }, + "inputDataExample": "{\"props\": {}}", + "outputDataExample": "{\"props\": {}}" + }, + { + "simStepId": "bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb", + "diagramNodeId": "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8", + "simStepLabel": "Flow 1: CSV Import - User Selects File", + "simStepDescription": "The user clicks the 'Select File' button, triggering the file input. When a file is chosen, the `handleFileChange` function validates that it's a CSV file and updates the component's state.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "startLine": "23", + "endLine": "31", + "relevantVariables": [ + "handleFileChange", + "selectedFile", + "setError", + "setFile" + ] + }, + "inputDataExample": "{\"event\": {\"target\": {\"files\": [{\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}]}}}", + "outputDataExample": "{\"stateUpdate\": {\"selectedFile\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}}" + }, + { + "simStepId": "610120d2-0521-4444-b245-feb97239a389", + "diagramNodeId": "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "simStepLabel": "Flow 1: CSV Import - File State Update", + "simStepDescription": "The selected file object is passed from the UploadFile component to the parent BulkImport component via the `onFileSelect` prop, updating its `selectedFile` state.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "startLine": "22", + "endLine": "22", + "relevantVariables": [ + "setSelectedFile" + ] + }, + "inputDataExample": "{\"selectedFile\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}", + "outputDataExample": "{\"selectedFile\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}" + }, + { + "simStepId": "0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c", + "diagramNodeId": "dd2f9356-fad3-47d4-81d5-a5e038298e2b", + "simStepLabel": "Flow 1: CSV Import - Submit Upload", + "simStepDescription": "The user clicks the submit button, which executes the `handleSubmit` function. This function calls the `createBulkMonitors` function provided by the `useCreateBulkMonitors` custom hook.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "startLine": "32", + "endLine": "45", + "relevantVariables": [ + "handleSubmit", + "createBulkMonitors", + "selectedFile", + "user" + ] + }, + "inputDataExample": "{\"selectedFile\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}", + "outputDataExample": "null" + }, + { + "simStepId": "a365d018-e80c-419c-80b3-5ffc8f14dc5c", + "diagramNodeId": "b8c0292d-afbb-4648-b484-1c27be96d52f", + "simStepLabel": "Flow 1: CSV Import - Prepare Form Data", + "simStepDescription": "The `useCreateBulkMonitors` hook creates a `FormData` object and appends the selected CSV file to it, preparing it for the network request.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/monitorHooks.js", + "startLine": "490", + "endLine": "493", + "relevantVariables": [ + "createBulkMonitors", + "formData" + ] + }, + "inputDataExample": "{\"file\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}", + "outputDataExample": "{\"formData\": {\"csvFile\": \"\"}}" + }, + { + "simStepId": "2fc087b3-033d-4cf5-81f2-e35b99a14bce", + "diagramNodeId": "5967e576-f542-4c0b-a3f4-9446ae992721", + "simStepLabel": "Flow 1: CSV Import - Send API Request", + "simStepDescription": "The NetworkService sends the `FormData` containing the CSV file to the backend via a POST request to the `/monitors/bulk` endpoint. The `Content-Type` is set to `multipart/form-data`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "978", + "endLine": "983", + "relevantVariables": [ + "createBulkMonitors", + "this.axiosInstance.post" + ] + }, + "inputDataExample": "{\"url\": \"/monitors/bulk\", \"formData\": {\"csvFile\": \"\"}}", + "outputDataExample": "{\"promise\": \"\"}" + }, + { + "simStepId": "e6dec1dc-33cb-4669-a3b0-7a3c0b233185", + "diagramNodeId": "23715cce-c83a-44c0-9853-4c95b85e5ecd", + "simStepLabel": "Flow 1: CSV Import - Transmit Data to Server", + "simStepDescription": "The HTTP POST request containing the multipart form data is transmitted from the client to the server's API endpoint.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "46", + "endLine": "46", + "relevantVariables": [ + "this.router.post", + "upload.single", + "this.monitorController.createBulkMonitors" + ] + }, + "inputDataExample": "{\"request\": {\"method\": \"POST\", \"endpoint\": \"/api/v1/monitors/bulk\", \"headers\": {\"Content-Type\": \"multipart/form-data\"}, \"body\": \"---boundary\\nContent-Disposition: form-data; name=\\\"csvFile\\\"; filename=\\\"monitors.csv\\\"\\n...\"}}", + "outputDataExample": "{\"request\": {\"method\": \"POST\", \"endpoint\": \"/api/v1/monitors/bulk\", \"headers\": {\"Content-Type\": \"multipart/form-data\"}, \"body\": \"---boundary\\nContent-Disposition: form-data; name=\\\"csvFile\\\"; filename=\\\"monitors.csv\\\"\\n...\"}}" + }, + { + "simStepId": "07bf3d83-047d-4e51-9f96-8c9c64848500", + "diagramNodeId": "c1ac3c26-996a-43ef-9c34-4c2776188784", + "simStepLabel": "Flow 1: CSV Import - Server Receives Request", + "simStepDescription": "The server's monitor route receives the request. The `multer` middleware processes the file upload, making the file available in `req.file`. The request is then passed to the `createBulkMonitors` controller method.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "46", + "endLine": "46", + "relevantVariables": [ + "this.router.post", + "upload.single", + "this.monitorController.createBulkMonitors" + ] + }, + "inputDataExample": "{\"request\": {\"endpoint\": \"/monitors/bulk\"}}", + "outputDataExample": "{\"req.file\": {\"fieldname\": \"csvFile\", \"originalname\": \"monitors.csv\", \"buffer\": \"\"}}" + }, + { + "simStepId": "c53d3f62-73fe-4fad-9835-ef2c8cf52894", + "diagramNodeId": "20d0e690-a569-4c6f-8238-916cac920553", + "simStepLabel": "Flow 1: CSV Import - Route to Controller", + "simStepDescription": "The request object, now populated with the uploaded file buffer by the middleware, is passed from the routing layer to the monitor controller for business logic processing.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "206", + "endLine": "212", + "relevantVariables": [ + "createBulkMonitors", + "req.file" + ] + }, + "inputDataExample": "{\"req\": {\"file\": {\"buffer\": \"\"}, \"user\": {\"_id\": \"user_123\", \"teamId\": \"team_abc\"}}}", + "outputDataExample": "{\"req\": {\"file\": {\"buffer\": \"\"}, \"user\": {\"_id\": \"user_123\", \"teamId\": \"team_abc\"}}}" + }, + { + "simStepId": "a3799570-64bf-481e-9ffe-31dbad8a36e4", + "diagramNodeId": "0e520a6d-34bf-467b-a894-e73979364478", + "simStepLabel": "Flow 1: CSV Import - Controller Processes Data", + "simStepDescription": "The controller validates the presence of the file and extracts the file data (buffer), `userId`, and `teamId`. It then calls the `monitorService.createBulkMonitors` method to handle the core logic.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "228", + "endLine": "232", + "relevantVariables": [ + "fileData", + "monitors", + "this.monitorService.createBulkMonitors" + ] + }, + "inputDataExample": "{\"file\": {\"buffer\": \"\"}, \"user\": {\"_id\": \"user_123\", \"teamId\": \"team_abc\"}}", + "outputDataExample": "null" + }, + { + "simStepId": "6038422c-ab83-4fc6-a58b-c75734276118", + "diagramNodeId": "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "simStepLabel": "Flow 1: CSV Import - Controller to Service", + "simStepDescription": "The file buffer and user/team identifiers are passed from the controller to the monitor service layer.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "84", + "endLine": "84", + "relevantVariables": [ + "createBulkMonitors" + ] + }, + "inputDataExample": "{\"fileData\": \"\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}", + "outputDataExample": "{\"fileData\": \"\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}" + }, + { + "simStepId": "2c7c3082-e4b6-4525-860f-2ffcd9e86db1", + "diagramNodeId": "6139e487-1dd5-405a-ba7d-ededec4b282e", + "simStepLabel": "Flow 1: CSV Import - Service Parses CSV", + "simStepDescription": "The monitor service uses the `papaparse` library to parse the CSV file buffer into a JSON object array. In the `complete` callback, it enriches each record with `userId` and `teamId`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "85", + "endLine": "134", + "relevantVariables": [ + "papaparse.parse", + "enrichedData", + "this.db.monitorModule.createBulkMonitors" + ] + }, + "inputDataExample": "{\"fileData\": \"name,type,url\\nGoogle,http,https://google.com\\n\"}", + "outputDataExample": "{\"enrichedData\": [{\"name\": \"Google\", \"type\": \"http\", \"url\": \"https://google.com\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}]}" + }, + { + "simStepId": "0115fb69-4959-4b32-bd90-1dcd3c2231f6", + "diagramNodeId": "b55a0dc6-f596-4e5f-9265-63588b46b519", + "simStepLabel": "Flow 1: CSV Import - Service to Database Module", + "simStepDescription": "The array of parsed and enriched monitor objects is passed to the database module for persistence.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "126", + "endLine": "126", + "relevantVariables": [ + "this.db.monitorModule.createBulkMonitors" + ] + }, + "inputDataExample": "{\"enrichedData\": [{\"name\": \"Google\", \"type\": \"http\", \"url\": \"https://google.com\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}]}", + "outputDataExample": "{\"enrichedData\": [{\"name\": \"Google\", \"type\": \"http\", \"url\": \"https://google.com\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}]}" + }, + { + "simStepId": "70f18c21-2918-402e-9c35-1bdce8bc82d4", + "diagramNodeId": "d9d519af-5c4d-4494-929e-a869581c1dc4", + "simStepLabel": "Flow 1: CSV Import - Database Bulk Save", + "simStepDescription": "The `monitorModule` uses `Monitor.bulkSave` to efficiently insert all the new monitor documents into the MongoDB database in a single operation.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModule.js", + "startLine": "465", + "endLine": "468", + "relevantVariables": [ + "createBulkMonitors", + "this.Monitor.bulkSave" + ] + }, + "inputDataExample": "[{\"name\": \"Google\", \"type\": \"http\", \"url\": \"https://google.com\"}]", + "outputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]" + }, + { + "simStepId": "2ee7bd94-d131-4dd8-bdf8-68433d00472d", + "diagramNodeId": "f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "simStepLabel": "Flow 1: CSV Import - Database to Service", + "simStepDescription": "The newly created monitor documents, now with database-generated `_id`s, are returned from the database module back to the service layer.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModule.js", + "startLine": "467", + "endLine": "467", + "relevantVariables": [ + "monitors" + ] + }, + "inputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]", + "outputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]" + }, + { + "simStepId": "66fbf913-e662-4bd9-ada6-077be9e37f07", + "diagramNodeId": "1e1d975f-7f0f-4132-bd71-2563b35e99b3", + "simStepLabel": "Flow 1: CSV Import - Enqueue Monitor Jobs", + "simStepDescription": "After the monitors are saved, the service iterates through them and adds a new job to the `jobQueue` for each one, scheduling them for their first check. It then resolves the promise, returning the data up the call stack.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "128", + "endLine": "132", + "relevantVariables": [ + "Promise.all", + "this.jobQueue.addJob" + ] + }, + "inputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]", + "outputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]" + }, + { + "simStepId": "94d5772d-3f46-4d43-b764-3493db5c4971", + "diagramNodeId": "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "simStepLabel": "Flow 1: CSV Import - Service to Controller", + "simStepDescription": "The final list of created monitors is passed back to the controller.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "230", + "endLine": "230", + "relevantVariables": [ + "monitors" + ] + }, + "inputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]", + "outputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]" + }, + { + "simStepId": "f5377571-156f-4c5c-8bab-b6dacdf0fea6", + "diagramNodeId": "72b55fa1-d944-4e46-ac98-f6a6cb201afa", + "simStepLabel": "Flow 1: CSV Import - Send Success Response", + "simStepDescription": "The controller constructs a successful JSON response containing a message and the data for the newly created monitors, and sends it back to the client.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "232", + "endLine": "235", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]", + "outputDataExample": "{\"msg\": \"Monitors created successfully.\", \"data\": [{\"_id\": \"monitor_xyz\"}]}" + }, + { + "simStepId": "c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0", + "diagramNodeId": "926a330c-a9c5-4983-a4b5-70359813e49a", + "simStepLabel": "Flow 1: CSV Import - Transmit Success to Client", + "simStepDescription": "The server sends the successful HTTP response back to the client's browser.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/monitorHooks.js", + "startLine": "494", + "endLine": "494", + "relevantVariables": [ + "response" + ] + }, + "inputDataExample": "{\"status\": 200, \"body\": {\"msg\": \"Monitors created successfully!\", \"data\": [{\"_id\": \"monitor_xyz\"}]}}", + "outputDataExample": "{\"status\": 200, \"body\": {\"msg\": \"Monitors created successfully!\", \"data\": [{\"_id\": \"monitor_xyz\"}]}}" + }, + { + "simStepId": "72b3f16d-290d-4a61-8748-2ad90d63ec68", + "diagramNodeId": "ae3b7e23-ed64-40aa-9760-389f0a869f8d", + "simStepLabel": "Flow 1: CSV Import - Client Handles Success", + "simStepDescription": "The client-side promise resolves. The `handleSubmit` function in the `BulkImport` component receives the success status, displays a success toast message, and navigates the user to the main uptime monitoring page.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "startLine": "37", + "endLine": "41", + "relevantVariables": [ + "createToast", + "navigate" + ] + }, + "inputDataExample": "{\"success\": true, \"data\": [{\"_id\": \"monitor_xyz\"}]}", + "outputDataExample": "{\"navigation\": \"/uptime\"}" + }, + { + "simStepId": "63d8d81f-f436-4594-a6eb-8c0fd2110bc1", + "diagramNodeId": "26d76516-fd7e-4ae8-956a-d077de4257fa", + "simStepLabel": "Flow 2: CSV Export - Initiate Export", + "simStepDescription": "An admin user clicks an 'Export to CSV' button in the UI. This action triggers a call to the `/monitors/export` API endpoint to begin the export process.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "45", + "endLine": "45", + "relevantVariables": [ + "this.router.get", + "this.monitorController.exportMonitorsToCSV" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"request\": {\"method\": \"GET\", \"endpoint\": \"/api/v1/monitors/export\"}}" + }, + { + "simStepId": "53cebb8e-cf45-49eb-a7d3-98fd87af21b4", + "diagramNodeId": "febea87e-5033-479c-ba7a-eb35d98db899", + "simStepLabel": "Flow 2: CSV Export - Transmit Request to Server", + "simStepDescription": "An HTTP GET request is sent from the client to the server to fetch the monitor data as a CSV file.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "45", + "endLine": "45", + "relevantVariables": [ + "this.router.get", + "this.monitorController.exportMonitorsToCSV" + ] + }, + "inputDataExample": "{\"request\": {\"method\": \"GET\", \"endpoint\": \"/api/v1/monitors/export\"}}", + "outputDataExample": "{\"request\": {\"method\": \"GET\", \"endpoint\": \"/api/v1/monitors/export\"}}" + }, + { + "simStepId": "d9db3f40-8add-4551-9323-274348b9800f", + "diagramNodeId": "a3106388-6a3d-4b68-a3fc-3761a0d54f98", + "simStepLabel": "Flow 2: CSV Export - Controller Handles Request", + "simStepDescription": "The `exportMonitorsToCSV` method in the monitor controller is invoked. It retrieves the `teamId` from the authenticated user's session and calls the monitor service to perform the export.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "427", + "endLine": "434", + "relevantVariables": [ + "exportMonitorsToCSV", + "teamId", + "this.monitorService.exportMonitorsToCSV" + ] + }, + "inputDataExample": "{\"req\": {\"user\": {\"teamId\": \"team_abc\"}}}", + "outputDataExample": "{\"teamId\": \"team_abc\"}" + }, + { + "simStepId": "74e95fb5-863c-4137-a41b-d7a32efa04a5", + "diagramNodeId": "3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "simStepLabel": "Flow 2: CSV Export - Controller to Service", + "simStepDescription": "The `teamId` is passed from the controller to the monitor service to ensure only monitors belonging to the correct team are exported.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "245", + "endLine": "245", + "relevantVariables": [ + "exportMonitorsToCSV", + "teamId" + ] + }, + "inputDataExample": "{\"teamId\": \"team_abc\"}", + "outputDataExample": "{\"teamId\": \"team_abc\"}" + }, + { + "simStepId": "85cd1d5d-7ffb-49c5-aacb-907048003fc5", + "diagramNodeId": "ab9cb862-0ac7-45a3-9888-f64e87bdda54", + "simStepLabel": "Flow 2: CSV Export - Generate CSV", + "simStepDescription": "The monitor service fetches all monitors for the given `teamId` from the database. It then maps the data into the required format and uses `papaparse` to convert the JSON array into a CSV-formatted string.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "245", + "endLine": "266", + "relevantVariables": [ + "getMonitorsByTeamId", + "csvData", + "this.papaparse.unparse" + ] + }, + "inputDataExample": "{\"teamId\": \"team_abc\"}", + "outputDataExample": "{\"csv\": \"name,description,type,url,interval,port,ignoreTlsErrors,isActive\\nMy Website,,http,https://example.com,60,,false,true\"}" + }, + { + "simStepId": "4deb5e47-2ac4-4638-9ee6-48295d027bcd", + "diagramNodeId": "ad300b1c-6748-40f9-a7c9-5895e0915991", + "simStepLabel": "Flow 2: CSV Export - Service to Controller", + "simStepDescription": "The generated CSV string is returned from the service layer to the controller.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "432", + "endLine": "432", + "relevantVariables": [ + "csv" + ] + }, + "inputDataExample": "{\"csv\": \"name,description,type,url,...\\nMy Website,,http,https://example.com,...\"}", + "outputDataExample": "{\"csv\": \"name,description,type,url,...\\nMy Website,,http,https://example.com,...\"}" + }, + { + "simStepId": "256095b8-5e3a-4c20-abd9-be9e6a9d0bad", + "diagramNodeId": "f151f89d-6399-48f0-aecd-5eb1572b48cc", + "simStepLabel": "Flow 2: CSV Export - Send File Response", + "simStepDescription": "The controller receives the CSV string and uses the `res.file` method to send it back to the client as a file. It sets the `Content-Type` to `text/csv` and `Content-Disposition` to trigger a download with the filename `monitors.csv`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "434", + "endLine": "440", + "relevantVariables": [ + "res.file", + "csv" + ] + }, + "inputDataExample": "{\"csv\": \"name,description,type,url,...\\nMy Website,,http,https://example.com,...\"}", + "outputDataExample": "{\"responseHeaders\": {\"Content-Type\": \"text/csv\", \"Content-Disposition\": \"attachment; filename=\\\"monitors.csv\\\"\"}}" + }, + { + "simStepId": "605f4cbd-1f7f-4e89-b028-f18737ac7fb1", + "diagramNodeId": "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "simStepLabel": "Flow 2: CSV Export - Transmit File to Client", + "simStepDescription": "The server sends the HTTP response, which contains the CSV data and headers, to the client's browser.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "434", + "endLine": "440", + "relevantVariables": [ + "res.file" + ] + }, + "inputDataExample": "{\"httpResponse\": {\"headers\": {\"Content-Disposition\": \"attachment; filename=\\\"monitors.csv\\\"\"}, \"body\": \"name,description,...\\n...\"}}", + "outputDataExample": "{\"httpResponse\": {\"headers\": {\"Content-Disposition\": \"attachment; filename=\\\"monitors.csv\\\"\"}, \"body\": \"name,description,...\\n...\"}}" + }, + { + "simStepId": "60abdb27-4078-4678-a010-0f6938d4be48", + "diagramNodeId": "d8877474-7203-484f-bd47-fc69e3c7c482", + "simStepLabel": "Flow 2: CSV Export - Browser Prompts Download", + "simStepDescription": "The browser receives the server's response. The `Content-Disposition` header instructs the browser to prompt the user to save the received data as a file named `monitors.csv`, completing the export process.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "434", + "endLine": "440", + "relevantVariables": [ + "res.file" + ] + }, + "inputDataExample": "{\"httpResponse\": {\"headers\": {\"Content-Disposition\": \"attachment; filename=\\\"monitors.csv\\\"\"}}}", + "outputDataExample": "{\"userAction\": \"File Save Dialog\"}" + } + ], + "description": "
  • Provides efficiency tools for managing a large number of monitors
  • - Users can import multiple monitors at once by uploading a CSV file, saving significant time during initial setup
  • - A template CSV is provided to guide the user on the required format and available fields
  • - All existing monitors can be exported to a CSV file, useful for backups or migrations
  • - Superadmins have the ability to delete all monitors for a team in a single action
", + "simulationNodesAndEdges": { + "c4caeba8-ed9a-4cbc-9888-38cc8ca55362": { + "simStepIds": [ + "a29da01e-8c5a-4e6d-8d3c-e49af528e4fa" + ] + }, + "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8": { + "simStepIds": [ + "bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb" + ] + }, + "dd2f9356-fad3-47d4-81d5-a5e038298e2b": { + "simStepIds": [ + "0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c" + ] + }, + "5967e576-f542-4c0b-a3f4-9446ae992721": { + "simStepIds": [ + "2fc087b3-033d-4cf5-81f2-e35b99a14bce" + ] + }, + "c1ac3c26-996a-43ef-9c34-4c2776188784": { + "simStepIds": [ + "07bf3d83-047d-4e51-9f96-8c9c64848500" + ] + }, + "0e520a6d-34bf-467b-a894-e73979364478": { + "simStepIds": [ + "a3799570-64bf-481e-9ffe-31dbad8a36e4" + ] + }, + "6139e487-1dd5-405a-ba7d-ededec4b282e": { + "simStepIds": [ + "2c7c3082-e4b6-4525-860f-2ffcd9e86db1" + ] + }, + "d9d519af-5c4d-4494-929e-a869581c1dc4": { + "simStepIds": [ + "70f18c21-2918-402e-9c35-1bdce8bc82d4" + ] + }, + "1e1d975f-7f0f-4132-bd71-2563b35e99b3": { + "simStepIds": [ + "66fbf913-e662-4bd9-ada6-077be9e37f07" + ] + }, + "72b55fa1-d944-4e46-ac98-f6a6cb201afa": { + "simStepIds": [ + "f5377571-156f-4c5c-8bab-b6dacdf0fea6" + ] + }, + "ae3b7e23-ed64-40aa-9760-389f0a869f8d": { + "simStepIds": [ + "72b3f16d-290d-4a61-8748-2ad90d63ec68" + ] + }, + "26d76516-fd7e-4ae8-956a-d077de4257fa": { + "simStepIds": [ + "63d8d81f-f436-4594-a6eb-8c0fd2110bc1" + ] + }, + "a3106388-6a3d-4b68-a3fc-3761a0d54f98": { + "simStepIds": [ + "d9db3f40-8add-4551-9323-274348b9800f" + ] + }, + "ab9cb862-0ac7-45a3-9888-f64e87bdda54": { + "simStepIds": [ + "85cd1d5d-7ffb-49c5-aacb-907048003fc5" + ] + }, + "f151f89d-6399-48f0-aecd-5eb1572b48cc": { + "simStepIds": [ + "256095b8-5e3a-4c20-abd9-be9e6a9d0bad" + ] + }, + "d8877474-7203-484f-bd47-fc69e3c7c482": { + "simStepIds": [ + "60abdb27-4078-4678-a010-0f6938d4be48" + ] + }, + "c9a012bc-d53f-494f-82f2-1ee3f40ce646": { + "simStepIds": [ + "ef0bbcff-9535-4c0e-a5ca-96c457f89139" + ] + }, + "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48": { + "simStepIds": [ + "610120d2-0521-4444-b245-feb97239a389" + ] + }, + "b8c0292d-afbb-4648-b484-1c27be96d52f": { + "simStepIds": [ + "a365d018-e80c-419c-80b3-5ffc8f14dc5c" + ] + }, + "23715cce-c83a-44c0-9853-4c95b85e5ecd": { + "simStepIds": [ + "e6dec1dc-33cb-4669-a3b0-7a3c0b233185" + ] + }, + "20d0e690-a569-4c6f-8238-916cac920553": { + "simStepIds": [ + "c53d3f62-73fe-4fad-9835-ef2c8cf52894" + ] + }, + "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d": { + "simStepIds": [ + "6038422c-ab83-4fc6-a58b-c75734276118" + ] + }, + "b55a0dc6-f596-4e5f-9265-63588b46b519": { + "simStepIds": [ + "0115fb69-4959-4b32-bd90-1dcd3c2231f6" + ] + }, + "f27c60ef-fb40-43e2-9750-c2617bdeaf43": { + "simStepIds": [ + "2ee7bd94-d131-4dd8-bdf8-68433d00472d" + ] + }, + "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9": { + "simStepIds": [ + "94d5772d-3f46-4d43-b764-3493db5c4971" + ] + }, + "926a330c-a9c5-4983-a4b5-70359813e49a": { + "simStepIds": [ + "c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0" + ] + }, + "febea87e-5033-479c-ba7a-eb35d98db899": { + "simStepIds": [ + "53cebb8e-cf45-49eb-a7d3-98fd87af21b4" + ] + }, + "3e0f0966-5c2f-41af-9bcc-e8f615e03a00": { + "simStepIds": [ + "74e95fb5-863c-4137-a41b-d7a32efa04a5" + ] + }, + "ad300b1c-6748-40f9-a7c9-5895e0915991": { + "simStepIds": [ + "4deb5e47-2ac4-4638-9ee6-48295d027bcd" + ] + }, + "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03": { + "simStepIds": [ + "605f4cbd-1f7f-4e89-b028-f18737ac7fb1" + ] + } + }, + "isAIGenerated": true, + "keywords": "bulkImport, createBulkMonitors, exportMonitorsToCSV", + "generationPrompt": "Bulk Monitor Management", + "generationKeywords": "bulkImport, createBulkMonitors, exportMonitorsToCSV" + }, + "System Administration and Configuration": { + "name": "System Administration and Configuration", + "simSteps": [ + { + "simStepId": "64e9f1af-13b7-4361-a2a9-3f45760e316d", + "diagramNodeId": "97908daf-d071-4df5-8366-1f5f4d8d534a", + "simStepLabel": "Flow 1: Render Settings Page", + "simStepDescription": "The user navigates to the settings page. The main `Settings` component fetches existing settings using the `useFetchSettings` hook and renders various sub-components for different configuration areas like TimeZone, UI, PageSpeed, Email, etc.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Settings/index.jsx", + "startLine": "25", + "endLine": "52", + "relevantVariables": [ + "useFetchSettings", + "settingsData", + "setSettingsData", + "isApiKeySet", + "isEmailPasswordSet" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"settingsData\": {}, \"errors\": {}, \"isApiKeySet\": false, \"isEmailPasswordSet\": false}" + }, + { + "simStepId": "dca4a33c-b666-47b8-896b-370ed9d6f9c5", + "diagramNodeId": "f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "simStepLabel": "API Call: Fetch Application Settings", + "simStepDescription": "The `useFetchSettings` hook triggers a GET request to the `/api/v1/settings` endpoint to retrieve the current application configuration.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "692", + "endLine": "698", + "relevantVariables": [ + "getAppSettings" + ] + }, + "inputDataExample": "{\"method\": \"GET\", \"url\": \"/api/v1/settings\"}", + "outputDataExample": "{\"method\": \"GET\", \"url\": \"/api/v1/settings\"}" + }, + { + "simStepId": "441b0c45-5f27-478d-8ca8-58fbd5f460ea", + "diagramNodeId": "83596bd1-dfca-4196-b52e-955f6824fafb", + "simStepLabel": "Backend: Retrieve Settings from Database", + "simStepDescription": "The `settingsController` handles the GET request. It calls the `settingsService` to fetch settings from the database. Sensitive data like passwords is sanitized, and flags like `emailPasswordSet` are added before sending the data to the client.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/settingsController.js", + "startLine": "40", + "endLine": "49", + "relevantVariables": [ + "getAppSettings", + "settingsService.getDBSettings", + "buildAppSettings" + ] + }, + "inputDataExample": "{\"request\": \"GET /api/v1/settings\"}", + "outputDataExample": "{\"msg\": \"Got app settings successfully\", \"data\": {\"emailPasswordSet\": false, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\"}}}" + }, + { + "simStepId": "aacc9302-6ca8-45c6-acb8-956d8d0242c2", + "diagramNodeId": "195ad532-9858-493e-876a-6ec457811d1d", + "simStepLabel": "API Response: Send Settings to Client", + "simStepDescription": "The backend responds to the GET request with a JSON object containing the application settings, which will be used to populate the UI.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/settingsController.js", + "startLine": "45", + "endLine": "48", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"msg\": \"Got app settings successfully\", \"data\": {\"emailPasswordSet\": false, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\", \"systemEmailHost\": \"\", \"systemEmailPort\": \"\", \"systemEmailSecure\": false, \"systemEmailUser\": \"\", \"systemEmailAddress\": \"\"}}}", + "outputDataExample": "{\"msg\": \"Got app settings successfully\", \"data\": {\"emailPasswordSet\": false, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\", \"systemEmailHost\": \"\", \"systemEmailPort\": \"\", \"systemEmailSecure\": false, \"systemEmailUser\": \"\", \"systemEmailAddress\": \"\"}}}" + }, + { + "simStepId": "ae5c5146-4dbb-4612-9d23-086e57f72474", + "diagramNodeId": "09cb194a-0ea4-46e2-b32b-36b62fa87f40", + "simStepLabel": "User Interaction: Modify Email Settings", + "simStepDescription": "The user interacts with the `SettingsEmail` component, filling in SMTP server details. The component's state is updated via the `handleChange` function passed down from the parent `Settings` component, which prepares the new configuration data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "startLine": "119", + "endLine": "167", + "relevantVariables": [ + "handleChange", + "handlePasswordChange", + "TextInput", + "systemEmailHost", + "systemEmailPort", + "systemEmailUser", + "systemEmailPassword" + ] + }, + "inputDataExample": "{\"settingsData\": {\"settings\": {\"systemEmailHost\": \"\"}}}", + "outputDataExample": "{\"settingsData\": {\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}}" + }, + { + "simStepId": "e7e82a97-1409-4bc5-ae27-b4ff5ba746df", + "diagramNodeId": "8c107d79-f796-49eb-ab88-f553dd6f9d95", + "simStepLabel": "User Action: Save Settings", + "simStepDescription": "The user clicks the 'Save' button. This action triggers the `handleSave` function, which validates the new settings and prepares to send them to the backend.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Settings/index.jsx", + "startLine": "258", + "endLine": "261", + "relevantVariables": [ + "handleSave" + ] + }, + "inputDataExample": "{\"settingsData\": {\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}}", + "outputDataExample": "{\"settingsData\": {\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}}" + }, + { + "simStepId": "d7098072-9a83-4369-8176-9646ba34e186", + "diagramNodeId": "1b2cf182-9ee3-4218-a7ec-b74e427ccddf", + "simStepLabel": "API Call: Update Application Settings", + "simStepDescription": "The `handleSave` function calls `saveSettings` from the `useSaveSettings` hook. This triggers a PUT request to `/api/v1/settings` with the new configuration in the request body.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/settingsHooks.js", + "startLine": "41", + "endLine": "67", + "relevantVariables": [ + "saveSettings", + "networkService.updateAppSettings" + ] + }, + "inputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}", + "outputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}" + }, + { + "simStepId": "383387e5-4623-44ab-b2c4-d9ea046f4547", + "diagramNodeId": "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "simStepLabel": "Data Transmission: Send Updated Settings to Server", + "simStepDescription": "The PUT request containing the new settings is sent to the backend API endpoint for processing and persistence.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "711", + "endLine": "717", + "relevantVariables": [ + "updateAppSettings", + "this.axiosInstance.put" + ] + }, + "inputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}", + "outputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}" + }, + { + "simStepId": "7632e8d2-4086-4169-ae3f-bfbf8d657987", + "diagramNodeId": "cce3b9c8-f3b7-434b-b3c6-d8d164714f86", + "simStepLabel": "Backend: Persist Updated Settings", + "simStepDescription": "The `settingsController` receives the PUT request, validates the body, and calls the `db.settingsModule.updateAppSettings` method to update the settings document in the database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/settingsController.js", + "startLine": "54", + "endLine": "64", + "relevantVariables": [ + "updateAppSettings", + "updateAppSettingsBodyValidation", + "db.settingsModule.updateAppSettings" + ] + }, + "inputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}", + "outputDataExample": "{\"msg\": \"Updated app settings successfully\", \"data\": {\"emailPasswordSet\": true, \"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", ...}}}" + }, + { + "simStepId": "0afcde35-7d51-4939-aae8-bac5ac83c49d", + "diagramNodeId": "fbe9b160-307e-4f51-8f72-713fda5373c8", + "simStepLabel": "API Response: Confirm Settings Update", + "simStepDescription": "The backend sends a success response to the client, including the newly saved (and sanitized) settings, confirming the update.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/settingsController.js", + "startLine": "60", + "endLine": "63", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"msg\": \"Updated app settings successfully\", \"data\": {\"emailPasswordSet\": true, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\", \"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": 465, \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\"}}}", + "outputDataExample": "{\"msg\": \"Updated app settings successfully\", \"data\": {\"emailPasswordSet\": true, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\", \"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": 465, \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\"}}}" + }, + { + "simStepId": "a157bd07-b969-44f8-914c-3d8393c1605a", + "diagramNodeId": "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3", + "simStepLabel": "UI Update: Display Success Confirmation", + "simStepDescription": "The frontend network service receives the successful API response. The `saveSettings` hook then calls `createToast` with a success message, which is displayed to the user.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/settingsHooks.js", + "startLine": "58", + "endLine": "58", + "relevantVariables": [ + "createToast", + "t(\"settingsSuccessSaved\")" + ] + }, + "inputDataExample": "{\"body\": \"Settings saved successfully\"}", + "outputDataExample": "{}" + }, + { + "simStepId": "a9b24b5f-534b-4baf-a185-bac7c32a1c42", + "diagramNodeId": "704e2ec8-9f2f-47e5-852f-c6ada67a02e0", + "simStepLabel": "Flow 2: Navigate to Diagnostics Page", + "simStepDescription": "The user navigates to the 'Logs' page and selects the 'Diagnostics' tab. The `Diagnostics` component renders and immediately calls the `useFetchDiagnostics` hook to retrieve system health data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "startLine": "14", + "endLine": "15", + "relevantVariables": [ + "useFetchDiagnostics", + "diagnostics" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{}" + }, + { + "simStepId": "23b8ad7d-8967-4e48-814c-1a7bf5b4dfac", + "diagramNodeId": "499dfe1f-77d4-4b26-a12c-45240691109e", + "simStepLabel": "API Call: Fetch System Diagnostics", + "simStepDescription": "The `useFetchDiagnostics` hook triggers a GET request to the `/api/v1/diagnostic/system` endpoint to fetch real-time system metrics.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "1113", + "endLine": "1119", + "relevantVariables": [ + "getDiagnostics", + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\"method\": \"GET\", \"url\": \"/api/v1/diagnostic/system\"}", + "outputDataExample": "{\"method\": \"GET\", \"url\": \"/api/v1/diagnostic/system\"}" + }, + { + "simStepId": "8fb704ca-67d9-437c-90d8-01302634d568", + "diagramNodeId": "09f07e80-185e-4547-aab4-edaf8ae7501e", + "simStepLabel": "Backend: Gather System Statistics", + "simStepDescription": "The `diagnosticController` receives the request and calls `diagnosticService.getSystemStats()`. This service uses native Node.js modules like `os` and `v8` to collect metrics on memory, CPU, heap statistics, and process uptime.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/diagnosticService.js", + "startLine": "39", + "endLine": "95", + "relevantVariables": [ + "getSystemStats", + "os", + "process.memoryUsage", + "process.cpuUsage", + "v8.getHeapStatistics", + "process.uptime" + ] + }, + "inputDataExample": "{\"request\": \"GET /api/v1/diagnostic/system\"}", + "outputDataExample": "{\"osStats\": {\"totalMemoryBytes\": 16777216000, \"freeMemoryBytes\": 1572864000}, \"memoryUsage\": {\"rss\": 123456789}, \"cpuUsage\": {\"usagePercentage\": 12.5}, \"v8HeapStats\": {\"usedHeapSizeBytes\": 54321098}, \"uptimeMs\": 86400000}" + }, + { + "simStepId": "44a4372f-a903-4c43-a9a5-c7549c875e11", + "diagramNodeId": "d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "simStepLabel": "API Response: Send Diagnostics Data to Client", + "simStepDescription": "The backend responds with a JSON object containing the comprehensive system diagnostics data.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/diagnosticController.js", + "startLine": "52", + "endLine": "55", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"msg\": \"OK\", \"data\": {\"osStats\": {\"totalMemoryBytes\": 16777216000, \"freeMemoryBytes\": 1572864000}, \"memoryUsage\": {\"rss\": 123456789}, \"cpuUsage\": {\"usagePercentage\": 12.5}, \"v8HeapStats\": {\"usedHeapSizeBytes\": 54321098}, \"uptimeMs\": 86400000}}", + "outputDataExample": "{\"msg\": \"OK\", \"data\": {\"osStats\": {\"totalMemoryBytes\": 16777216000, \"freeMemoryBytes\": 1572864000}, \"memoryUsage\": {\"rss\": 123456789}, \"cpuUsage\": {\"usagePercentage\": 12.5}, \"v8HeapStats\": {\"usedHeapSizeBytes\": 54321098}, \"uptimeMs\": 86400000}}" + }, + { + "simStepId": "e6c94f52-a599-4568-a06f-2db18f963817", + "diagramNodeId": "4b418736-6b72-47e0-917a-d86d4bed9ecd", + "simStepLabel": "UI Update: Display System Health", + "simStepDescription": "The frontend receives the diagnostic data. The `Diagnostics` component passes this data to its children, `Gauges` and `StatusBoxes`, which then render the information as interactive gauges and statistical boxes.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "startLine": "59", + "endLine": "63", + "relevantVariables": [ + "Gauges", + "StatusBoxes", + "diagnostics" + ] + }, + "inputDataExample": "{\"diagnostics\": {\"osStats\": {\"totalMemoryBytes\": 16777216000, \"freeMemoryBytes\": 1572864000}, \"cpuUsage\": {\"usagePercentage\": 12.5}, \"v8HeapStats\": {\"usedHeapSizeBytes\": 54321098}, \"uptimeMs\": 86400000}}", + "outputDataExample": "{}" + } + ], + "description": "
  • Provides centralized settings for administrators to configure and maintain the Checkmate instance
  • - Admins can configure global application settings, including email server (SMTP) details for sending alerts
  • - UI settings like theme (light/dark) and default timezone can be customized
  • - Data retention policies, such as the Time-To-Live (TTL) for check history, can be adjusted
  • - A diagnostics page provides system information and logs for troubleshooting and monitoring the health of the Checkmate application itself
", + "simulationNodesAndEdges": { + "97908daf-d071-4df5-8366-1f5f4d8d534a": { + "simStepIds": [ + "64e9f1af-13b7-4361-a2a9-3f45760e316d" + ] + }, + "83596bd1-dfca-4196-b52e-955f6824fafb": { + "simStepIds": [ + "441b0c45-5f27-478d-8ca8-58fbd5f460ea" + ] + }, + "09cb194a-0ea4-46e2-b32b-36b62fa87f40": { + "simStepIds": [ + "ae5c5146-4dbb-4612-9d23-086e57f72474" + ] + }, + "1b2cf182-9ee3-4218-a7ec-b74e427ccddf": { + "simStepIds": [ + "d7098072-9a83-4369-8176-9646ba34e186" + ] + }, + "cce3b9c8-f3b7-434b-b3c6-d8d164714f86": { + "simStepIds": [ + "7632e8d2-4086-4169-ae3f-bfbf8d657987" + ] + }, + "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3": { + "simStepIds": [ + "a157bd07-b969-44f8-914c-3d8393c1605a" + ] + }, + "704e2ec8-9f2f-47e5-852f-c6ada67a02e0": { + "simStepIds": [ + "a9b24b5f-534b-4baf-a185-bac7c32a1c42" + ] + }, + "09f07e80-185e-4547-aab4-edaf8ae7501e": { + "simStepIds": [ + "8fb704ca-67d9-437c-90d8-01302634d568" + ] + }, + "4b418736-6b72-47e0-917a-d86d4bed9ecd": { + "simStepIds": [ + "e6c94f52-a599-4568-a06f-2db18f963817" + ] + }, + "f6d80f09-ae83-4c95-aba0-4efffeda38e0": { + "simStepIds": [ + "dca4a33c-b666-47b8-896b-370ed9d6f9c5" + ] + }, + "195ad532-9858-493e-876a-6ec457811d1d": { + "simStepIds": [ + "aacc9302-6ca8-45c6-acb8-956d8d0242c2" + ] + }, + "8c107d79-f796-49eb-ab88-f553dd6f9d95": { + "simStepIds": [ + "e7e82a97-1409-4bc5-ae27-b4ff5ba746df" + ] + }, + "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812": { + "simStepIds": [ + "383387e5-4623-44ab-b2c4-d9ea046f4547" + ] + }, + "fbe9b160-307e-4f51-8f72-713fda5373c8": { + "simStepIds": [ + "0afcde35-7d51-4939-aae8-bac5ac83c49d" + ] + }, + "499dfe1f-77d4-4b26-a12c-45240691109e": { + "simStepIds": [ + "23b8ad7d-8967-4e48-814c-1a7bf5b4dfac" + ] + }, + "d0f92604-a6fa-4d85-9716-ea8f32db78e0": { + "simStepIds": [ + "44a4372f-a903-4c43-a9a5-c7549c875e11" + ] + } + }, + "isAIGenerated": true, + "keywords": "Settings, updateAppSettings, diagnostic", + "generationPrompt": "System Administration and Configuration", + "generationKeywords": "Settings, updateAppSettings, diagnostic" + } + }, + "cellToPath": { + "0b23ea66-3e37-4643-b3b9-acb87ea02bae": "client", + "98af25c7-efc2-43d2-a273-c8da4e1038ad": "server", + "d3264577-10f0-42fe-9411-7e9df0754d1e": "client/src", + "35864a2c-7bd0-4428-84fc-c0aa90c7f1be": "server/src", + "f0feab85-316a-41b5-a2f1-629220018846": "client/src/Pages", + "4295a047-076f-4c07-95dd-b3123b6614f7": "client/src/Hooks", + "02b07589-ec21-4366-b7d2-9071e8f3bad8": "client/src/Utils", + "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b": "server/src/routes", + "478e649c-1d1b-41d2-9b7b-6927e1dec7e1": "server/src/controllers", + "99e9b746-7809-41f4-a433-1de7cc90574b": "server/src/service", + "15ee5d81-a0cf-4614-80b8-7a1123ab20b8": "server/src/db", + "1a083b4a-787c-4eb4-8029-6700de987679": "client/src/Pages/v1", + "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961": "client/src/Hooks/v1", + "16731e9d-3f73-4055-9d79-5ca9be679f68": "client/src/Utils/NetworkService.js", + "d3375a20-2776-4771-b806-649bbeb541bd": "server/src/routes/v1", + "fc591f7a-d4ac-4565-949d-a87a58a79bbc": "server/src/controllers/v1", + "bbbe53d6-f595-498a-ae45-e08611f39c3d": "server/src/service/v1", + "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf": "server/src/db/v1", + "7090fcdb-09be-4c85-86fd-e73553bbb2ff": "client/src/Pages/v1/Uptime", + "595cf0e8-2f86-4543-b51c-b2aeac307f4b": "client/src/Hooks/v1/monitorHooks.js", + "10611636-ec73-4a26-87a5-654ae64a4843": "server/src/routes/v1/monitorRoute.js", + "341f2784-67ee-4477-9445-a8c240ee6261": "server/src/controllers/v1/monitorController.js", + "87d3b101-9443-4ed4-9c4f-f759d41677eb": "server/src/service/v1/business", + "1670815f-860d-4ea4-92e1-b238a799d3dc": "server/src/service/v1/infrastructure", + "82cb3dbe-c922-438a-a912-255376f4b380": "server/src/db/v1/modules", + "3dba5acc-cf25-40b7-ad21-9cd715454cd5": "client/src/Pages/v1/Uptime/Create", + "b4e479df-9b21-4430-9383-84831f503c2e": "server/src/service/v1/business/monitorService.js", + "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b": "server/src/service/v1/infrastructure/SuperSimpleQueue", + "00406786-9663-440a-96b6-919d544732c0": "server/src/service/v1/infrastructure/networkService.js", + "fbd7d244-2f37-453c-8c83-a23a260b26cc": "server/src/service/v1/infrastructure/statusService.js", + "2ff1f030-2080-48da-bbbe-90302fdfc2f7": "server/src/service/v1/infrastructure/notificationService.js", + "630b3ece-2648-48d5-99b5-ef9611de2ad5": "server/src/db/v1/modules/monitorModule.js", + "391ad99e-3114-4f54-a6ca-fd89b33c7e06": "client/src/Pages/v1/Uptime/Create/index.jsx", + "d9c0127e-31aa-4509-b5c9-f7017d10d69a": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "04c12e9a-e025-457c-a634-5f7472a42985": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "7a2da928-a65c-49d8-b0a3-c159a6df2bc5": "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-ab5de0b9-c70d-492c-98d9-8ddee66284ad", + "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c": "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-e02ddf57-270e-4f71-b4c4-d5183bb7879a", + "a30ac765-c546-4993-988e-8c8cfdb7532f": "client/src/Utils/NetworkService.js-simstep-14e9986b-c6ce-475e-97ed-da1fa2b6968d", + "f0409948-75dc-457f-a02d-6b9774258e48": "server/src/controllers/v1/monitorController.js-simstep-38fd069d-9c44-4078-954b-3a0899e9acf4", + "2d407bfd-e4b0-46b8-99e7-121c507d525f": "server/src/db/v1/modules/monitorModule.js-simstep-ca3ad309-323a-418d-a171-c16f56fae23c", + "51e8db5f-2fdd-4ddc-9605-43981db9d940": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js-simstep-2702bfca-7611-4562-8d3e-a05af8257871", + "18c13391-0cf7-4303-8180-0a40cfd64b58": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e22a708a-a348-4775-8ee3-f32a35446e24", + "5c900373-e6ba-44d5-b341-676a32d659c8": "server/src/service/v1/infrastructure/networkService.js-simstep-84f82fea-e306-4e0b-8222-e9705803d51a", + "45e38f3a-a7c3-4de7-b3dc-589361d793fd": "server/src/service/v1/infrastructure/statusService.js-simstep-c66a04c6-113e-4391-ab71-93fc612279ef", + "9f3ea7fc-16d3-4d36-97eb-66f8808425cc": "server/src/service/v1/infrastructure/notificationService.js-simstep-1a260a75-cf50-4f7a-90b0-c280b512a5cb", + "0e09aa33-d7c7-4c89-9da8-f0937658f4d3": "generated-edge-simstep-888df086-41bb-469d-9c0a-daf74bdecb9f-0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "1f6f04a9-0123-4729-b06a-8f3da6d1b58c": "generated-edge-simstep-3fe0afaa-c673-4b52-a0c4-02a3ffd3a075-1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "b6d963d5-c613-407a-9ce9-60a04048dea7": "generated-edge-simstep-13dd0564-44a5-43ea-a163-ce1148cc48e7-b6d963d5-c613-407a-9ce9-60a04048dea7", + "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7": "generated-edge-simstep-a6f4c967-b1a8-4804-bc2e-67f47a7b68f9-a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "de320ac1-0949-4297-be7c-b9d7a14e0fec": "generated-edge-simstep-71ec03b6-1166-4c5d-9ada-0875a0866e30-de320ac1-0949-4297-be7c-b9d7a14e0fec", + "38ba9b22-5b26-432d-a856-0eceb792c737": "generated-edge-simstep-1d7580f2-f107-4008-a207-b8ec07998721-38ba9b22-5b26-432d-a856-0eceb792c737", + "5e80734c-7b72-41be-9068-30cda3ffb425": "generated-edge-simstep-9b0ada30-d3e6-4b2c-b3ff-f50615139588-5e80734c-7b72-41be-9068-30cda3ffb425", + "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c": "generated-edge-simstep-9896c387-4fca-47ad-82eb-7301286decb7-0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "3f26b33e-477f-41df-a78a-3e3e4f4209ae": "client/src/Pages/v1/Infrastructure", + "74019e8a-3148-4a32-aae2-5afd16f3eb9b": "client/src/Pages/v1/Infrastructure/Create", + "591128f9-0994-4e01-a1ab-f23357f97aff": "client/src/Pages/v1/Infrastructure/Details", + "4940977f-933d-4bbc-b19e-749647b44d25": "server/src/service/v1/infrastructure/notificationUtils.js", + "a0b22a5a-59e1-47e3-8218-a4ad083b1b71": "server/src/db/v1/modules/monitorModuleQueries.js", + "ceded1fc-87a9-43c8-a756-746be3d4ad8d": "client/src/Pages/v1/Infrastructure/Create/Components", + "e292c552-b3df-4bac-bd9a-8f44c260a5aa": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "e97d4e61-483e-4ac5-85a1-0ff72fcdd449": "client/src/Pages/v1/Infrastructure/Details/Components", + "83800be3-2a0e-4419-b379-3f995f4050e7": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "6a7c7720-a399-4d4d-8225-36e0b2f59731": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes", + "8fe3d890-3605-407d-b06c-1d890a354e46": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx", + "9651d084-5c16-4417-89c1-f52ae6572a95": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx-simstep-80dd889a-1fc9-4d56-b9cf-5b45bf1110b1", + "4c47b5f2-5363-4816-9745-30e61d184c7a": "server/src/service/v1/infrastructure/networkService.js-simstep-ed6e7faf-69dd-4d0b-bda8-137739ed288b", + "c1bf10c9-8c1a-44f4-8e23-39708f0301e8": "server/src/service/v1/infrastructure/statusService.js-simstep-e5b90cf2-9369-4741-9803-c835a81c009b", + "d4f48f1a-61de-4cde-87d2-4b5215464899": "server/src/service/v1/infrastructure/notificationUtils.js-simstep-07fe8489-1d94-4afb-83f8-7cebb0dff49a", + "801fe70a-1a19-406c-8f0d-770b0cb74409": "client/src/Pages/v1/Infrastructure/Details/index.jsx-simstep-c27365db-9db6-4cc9-96f0-4f046fd38562", + "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03": "server/src/db/v1/modules/monitorModule.js-simstep-8502ae1f-f029-446e-9dde-59c2aba87a8a", + "9ea2e32d-1ea8-42f2-9a17-613844723ae6": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx-simstep-6eedce82-b9c8-4022-95e5-3a6e00628105", + "1c5c9857-bf32-40e4-921b-957c252dac2a": "generated-edge-simstep-815f96af-c28e-4f80-b4ab-18a6fb23eee0-1c5c9857-bf32-40e4-921b-957c252dac2a", + "a64547c9-d9e6-4941-8e5e-f49add06516b": "generated-edge-simstep-4434dca1-2048-4458-a70b-b70d569e378d-a64547c9-d9e6-4941-8e5e-f49add06516b", + "7a250d09-50c6-4935-b10c-626b0ebe1fba": "generated-edge-simstep-b019a6d0-a23a-4a1d-bd21-644f01996f14-7a250d09-50c6-4935-b10c-626b0ebe1fba", + "3d357de8-9fd2-4d0b-9623-f36fce4cf1de": "generated-edge-simstep-c691b3b9-88b3-4c88-a399-ff9fd5866194-3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4": "generated-edge-simstep-a5d20172-1c95-4c9e-af39-f86a498734bb-00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "60308696-6f8a-43c8-b9a1-da199ef500cd": "generated-edge-simstep-627fcb0c-0282-4b4d-8c55-aceb636e4da0-60308696-6f8a-43c8-b9a1-da199ef500cd", + "feefae96-61b8-4ac5-a37c-592ed4b2330e": "server/src/service/v2", + "b3575a92-df07-4cf7-810f-8e5e0672d666": "client/src/Pages/v1/Notifications", + "cd2428fd-6545-48dd-b685-5261311b7532": "client/src/Hooks/v1/useNotifications.js", + "96250197-23af-4059-9904-fd6751f70ad5": "server/src/routes/v1/notificationRoute.js", + "8e5941e1-e577-455f-bbf2-83a418ad5908": "server/src/controllers/v1/notificationController.js", + "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f": "server/src/service/v2/infrastructure", + "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb": "client/src/Pages/v1/Notifications/create", + "62daeac4-9402-448a-972c-cb8cb760671c": "server/src/db/v1/modules/notificationModule.js", + "159e569d-31d8-4883-b443-4ec08b21a044": "server/src/service/v2/infrastructure/JobGenerator.ts", + "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa": "server/src/service/v2/infrastructure/NotificationService.ts", + "b71b02fb-d08e-4fcf-b959-de1b9547d9c2": "client/src/Pages/v1/Notifications/create/index.jsx", + "15486629-84b3-463a-962b-06083f69e4bf": "client/src/Pages/v1/Notifications/create/index.jsx-simstep-e924ec15-1bea-4e3b-befb-a8bf30cbf740", + "a07b805e-e55f-46cb-b54b-765b7813b0ca": "client/src/Hooks/v1/useNotifications.js-simstep-383c6be8-aa51-4398-9ae3-21035413f73a", + "d0a4737f-715f-4f50-b6d5-5e4b31f86f93": "server/src/routes/v1/notificationRoute.js-simstep-2f9266c9-4cbc-4b50-a280-cf8b5800d540", + "4a796ccf-40a9-4446-a109-6a6eed52e898": "server/src/db/v1/modules/notificationModule.js-simstep-d29ab4ab-be50-4f17-af6e-2c61cec9d0ab", + "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39": "server/src/routes/v1/notificationRoute.js-simstep-c1b17b64-90e9-48bc-8927-25d7ebfc3036", + "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709": "server/src/service/v1/infrastructure/notificationService.js-simstep-f29f9be1-a0cd-43a3-b259-e5105a25ce41", + "28f1eabb-4f24-4944-8a0a-04f41ceb8589": "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-157cec87-9b76-4a1c-9d85-0a6d3decbaa3", + "31fe5a28-0640-4b52-841b-dd02e2ae646e": "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-55ad645b-118a-478e-a00c-09ef82357008", + "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1": "server/src/service/v2/infrastructure/NotificationService.ts-simstep-8d1176f3-c755-401d-9ca8-234689b4b837", + "54497b9a-4910-4df4-b016-480b9ac250a7": "generated-edge-simstep-71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f-54497b9a-4910-4df4-b016-480b9ac250a7", + "dc135d8d-0515-403d-ae58-f3c04647b2ae": "generated-edge-simstep-162d8d2f-6cd1-45fa-8711-1ee85c5f86ef-dc135d8d-0515-403d-ae58-f3c04647b2ae", + "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc": "generated-edge-simstep-73e63d2f-2b43-488d-9e86-170f511673f5-7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "279155cd-ff31-41a2-8f5f-a538f01bfa66": "generated-edge-simstep-d390ef45-f2fe-4806-a45a-cc9c84b48a5a-279155cd-ff31-41a2-8f5f-a538f01bfa66", + "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f": "generated-edge-simstep-6f2bf027-50d9-472e-9569-808a64fce7cf-57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "50797935-1447-47a4-891b-9e886e5601fa": "generated-edge-simstep-b7ec7415-fcae-4077-8c19-302a015b00b5-50797935-1447-47a4-891b-9e886e5601fa", + "c778c6b9-c12e-45c0-8682-2c9e892867b0": "generated-edge-simstep-b7259d77-c8df-4060-ac03-58728ae3eb54-c778c6b9-c12e-45c0-8682-2c9e892867b0", + "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc": "client/src/Routes", + "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562": "client/src/Routes/index.jsx", + "9d662180-8b65-4426-ad37-cfed4bc01fb4": "client/src/Pages/v1/StatusPage", + "773e1065-b0a5-46a9-b0cc-6b963f69f5cc": "server/src/routes/v1/statusPageRoute.js", + "17a4f435-5a8e-4433-84a6-9ac17362709a": "server/src/controllers/v1/statusPageController.js", + "08602a28-f073-484e-9403-7eb798126dd4": "client/src/Pages/v1/StatusPage/Create", + "8ef70566-37d9-48fb-b5e8-37e5b68768a2": "client/src/Pages/v1/StatusPage/Status", + "f8541fe3-4bcf-4833-a226-66b5301adf58": "server/src/db/v1/modules/statusPageModule.js", + "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "56f6775e-9843-4ca2-bc8c-1c263b6c35e9": "client/src/Pages/v1/StatusPage/Create/Hooks", + "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "79da0721-d810-4f7f-9cf9-3ea462bf0bff": "client/src/Pages/v1/StatusPage/Status/Hooks", + "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "5b2cdab8-66f1-40a1-9621-0efaf54e04ae": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "00289d11-0015-4b2a-9c5b-dc9fd0f32b80": "client/src/Pages/v1/StatusPage/Create/index.jsx-simstep-de7b5d1f-b370-42a4-a410-4178f1e2d3a6", + "aa5d6d07-05f0-45f3-b19a-199af50e35f1": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx-simstep-3c99ae6a-b671-4f47-bed0-ddccd8ff5d35", + "3efddcad-588f-4492-9808-b0c81c823c96": "server/src/routes/v1/statusPageRoute.js-simstep-84f11567-b3c4-42a2-aa7f-d78c1412099a", + "f15ef77e-380f-4768-98cb-7f0dd4113841": "server/src/controllers/v1/statusPageController.js-simstep-01694730-8a75-4949-8815-bb4d6e3e2b42", + "e234917e-a950-48f5-b864-cd957b2216e1": "server/src/db/v1/modules/statusPageModule.js-simstep-b6fa93cd-6fca-4d9e-969a-5400e2437ee8", + "058ded1f-83ce-40a7-a99d-d178c81fe557": "server/src/controllers/v1/statusPageController.js-simstep-2a115421-5175-4c8f-8cdd-62f3fe148af5", + "30aaee38-f509-4e19-a917-e6d2f4bba61c": "client/src/Routes/index.jsx-simstep-132354d7-df7a-4e03-9d00-16d54f836a1b", + "ecfb2d04-2681-4f16-af75-632f69e1a868": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx-simstep-b57bb249-9066-454e-aa8c-89108fdfcc2e", + "2e26cd2f-1413-4c16-8e63-ea7808ab6c52": "server/src/routes/v1/statusPageRoute.js-simstep-477c6821-b29b-4b38-8930-ced21b8a75e5", + "9f217674-4cd7-4ef8-b088-d5620042a7ba": "server/src/controllers/v1/statusPageController.js-simstep-5bdbd179-ce8f-4171-a073-af72a99b4468", + "26da60f9-dde4-44ec-be89-91858cc14f90": "server/src/db/v1/modules/statusPageModule.js-simstep-ceb044e9-6545-4d04-bf79-ec8146eb8ec9", + "d76e70e0-41f9-46cc-b775-b2027c5a0d5b": "server/src/controllers/v1/statusPageController.js-simstep-83224406-2f1d-4030-9beb-d65c2baf544d", + "f59b91de-5196-4a43-9d57-3f28402c0672": "client/src/Pages/v1/StatusPage/Status/index.jsx-simstep-3d51e811-a659-442e-97e0-00798cd15647", + "de23eee2-b60a-4236-a890-3c5b78388bb3": "generated-edge-simstep-d419f7b6-7b9b-4494-9fad-83e3ce208ca4-de23eee2-b60a-4236-a890-3c5b78388bb3", + "d7faa713-4204-4a97-a172-d621da36c287": "generated-edge-simstep-8a2ab372-73db-4b9f-bc0e-6a20f15a4d79-d7faa713-4204-4a97-a172-d621da36c287", + "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0": "generated-edge-simstep-0923a533-c572-4da3-ad87-cdfff111f3bb-a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "9735611c-924b-49aa-92e8-10d8a86ac288": "generated-edge-simstep-67603f03-c088-4ed0-9f18-072435134cdd-9735611c-924b-49aa-92e8-10d8a86ac288", + "40cb7d7b-2888-44de-bb7d-7735af70ba34": "generated-edge-simstep-ea4b3532-baae-4890-a73e-2ca385d0319e-40cb7d7b-2888-44de-bb7d-7735af70ba34", + "c3f83811-8674-4dbb-9f84-79499f97fb1f": "generated-edge-simstep-210acc07-81c3-4394-b566-41f6031e8eeb-c3f83811-8674-4dbb-9f84-79499f97fb1f", + "0575bf87-5362-4988-b895-7e514148d420": "generated-edge-simstep-2ba49e28-1cac-47f5-9118-ab930788736a-0575bf87-5362-4988-b895-7e514148d420", + "ae1c9341-0e39-4d91-a9ac-5cc8a627c247": "generated-edge-simstep-de0714d2-bf1e-4a1a-a389-60793de8b772-ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "5735f30c-b55f-444a-9236-f3d75f68ed27": "generated-edge-simstep-b79b8922-4de6-477d-8648-37b4bd9a6767-5735f30c-b55f-444a-9236-f3d75f68ed27", + "dace45d6-fb25-4dae-b940-49ef7d77bbed": "generated-edge-simstep-788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8-dace45d6-fb25-4dae-b940-49ef7d77bbed", + "137a5ab7-1e49-42ec-869d-58f7f2380cb3": "generated-edge-simstep-4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4-137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "04a33e4f-1dfe-4c5a-8b30-01ed0591c380": "generated-edge-simstep-d5f3c300-9777-4e09-bb05-f1281c2ff29a-04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "933a6cdc-349c-4725-b7ae-14cf780bfde8": "client/src/Hooks/v1/checkHooks.js", + "d18dbe1d-2f2c-4336-bd21-47c1670f7bab": "server/src/routes/v1/checkRoute.js", + "d983e54f-2fbe-496a-932d-a827e1124804": "server/src/controllers/v1/checkController.js", + "273f3009-69d0-46fe-af57-3cd94a24b55b": "client/src/Pages/v1/Uptime/Details", + "f90db867-e1e7-4554-b446-fa78fc6a8578": "server/src/service/v1/business/checkService.js", + "860b5eec-a1ce-4e1f-a316-e1586f03af96": "server/src/db/v1/modules/checkModule.js", + "1807d5b9-54be-430b-8997-cada4dd9b521": "client/src/Pages/v1/Uptime/Details/index.jsx", + "6aed78bb-62f1-4ac1-9b9a-9d5977e87404": "client/src/Pages/v1/Uptime/Details/Components", + "5ac0878f-d238-4763-8464-a85b4885a40f": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable", + "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx", + "78a8a7c5-41d3-4453-a7f7-ac632bbef288": "client/src/Pages/v1/Uptime/Details/index.jsx-simstep-35195328-9847-4678-b014-9331553f0d19", + "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4": "client/src/Hooks/v1/checkHooks.js-simstep-8dc4347c-3e9e-40d7-ae6f-54684f2e6956", + "cdd090be-0b62-45b4-baa0-fec01fc3ffd1": "server/src/routes/v1/checkRoute.js-simstep-c5fd192f-ea28-48a0-b3e7-5bc04f521192", + "499b46a2-f2a2-4ccd-aecd-012eade2bd23": "server/src/service/v1/business/checkService.js-simstep-83fea05d-8306-4a73-a43f-882cdd14f50f", + "14df9f01-1358-442d-96f5-eab9a4c4abbf": "server/src/controllers/v1/checkController.js-simstep-5768fdc5-045c-48ef-b0c9-79769403648f", + "ceb9cda3-968d-44aa-b223-49902adb93b4": "client/src/Hooks/v1/checkHooks.js-simstep-faba07f3-7460-47ae-924d-f92f5b2e821d", + "c996f7ca-9384-4252-b343-7bfc049d14bb": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx-simstep-2b1651cd-2e70-4cc6-b2bb-23f94fa73d66", + "e90e4943-6108-46e3-8a2c-a9a1c7361279": "generated-edge-simstep-49979250-1a32-4da5-95e0-58652fe218f9-e90e4943-6108-46e3-8a2c-a9a1c7361279", + "2b8ae59e-ed8d-4837-9a9b-64eb480f982c": "generated-edge-simstep-336b534d-8d6a-4639-af8e-889b38f51ab1-2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "839ba9f6-8a34-4919-a04a-13fba1795c61": "generated-edge-simstep-4f73f0fe-b390-4fe1-a354-eb0857835126-839ba9f6-8a34-4919-a04a-13fba1795c61", + "42d3bc70-701a-4be0-96b4-d6c2f0519b49": "generated-edge-simstep-65da1a9e-041c-4656-9c5b-0a92b761a9be-42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "6c5b028c-356c-43c8-a35a-b47448cd8695": "generated-edge-simstep-2c1bdec8-d0e6-4163-a99c-f27899b987c5-6c5b028c-356c-43c8-a35a-b47448cd8695", + "74b0dd89-9fea-4219-af1f-9571316a9d5a": "generated-edge-simstep-9405dce1-0565-4dfa-9539-a048472acda9-74b0dd89-9fea-4219-af1f-9571316a9d5a", + "e2f99143-54b7-44dc-b566-1c2fb901bd16": "server/src/db/v2", + "043d3dac-d4bb-449b-a3a7-270d53b66dac": "client/src/Pages/v1/PageSpeed", + "7ef79284-534a-466f-a1f1-4e3a2f56be9d": "server/src/service/v2/business", + "b53384d7-b989-4ac7-bd68-1c3621174ef4": "server/src/db/v2/models", + "414c500b-7b68-42ba-8110-e855efbee750": "client/src/Pages/v1/PageSpeed/Create", + "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b": "client/src/Pages/v1/PageSpeed/Details", + "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7": "server/src/service/v2/infrastructure/NetworkService.ts", + "f0ebd5da-21bb-4d96-8ff9-ca10409acc80": "server/src/service/v2/business/CheckService.ts", + "7a3824cd-24c8-4b32-8b1d-e0bf357262c4": "server/src/service/v2/business/MonitorService.ts", + "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc": "server/src/db/v2/models/checks", + "5ffe0baa-054a-4e5b-8b29-1254f07ea975": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "c4a7bb58-1d9c-4690-b6b8-460c1cc33702": "client/src/Pages/v1/PageSpeed/Details/index.jsx", + "1425ec43-f0f4-4c8b-a140-176c079219d6": "server/src/db/v2/models/checks/Check.ts", + "0622b212-3a4a-484d-9b7a-125864d785cd": "client/src/Pages/v1/PageSpeed/Create/index.jsx-simstep-b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12", + "6527fb47-157e-4bba-9aed-adde061c5689": "server/src/service/v2/infrastructure/NetworkService.ts-simstep-53968267-0c6e-42f4-88b3-cd90f44532ae", + "f89cbd87-c961-4f8b-92b6-714da57718d5": "server/src/service/v2/business/CheckService.ts-simstep-01206383-2db8-4b18-84d0-be2a0289c196", + "ca6ee27b-1fbf-49de-978a-d6de259cc3d5": "server/src/service/v2/business/MonitorService.ts-simstep-5616ac6d-c281-4879-82f1-6590c721c8f7", + "9ef9e218-4742-487a-9a88-5c70e788ccdc": "client/src/Pages/v1/PageSpeed/Details/index.jsx-simstep-0ec0eb06-05eb-4204-b077-9e47eb2cc316", + "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438": "generated-edge-simstep-f8df1cad-6051-466e-a13e-47d710ba1c14-b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "0d3c8091-3d91-4e42-9b46-7e35ed726a7a": "generated-edge-simstep-0b380571-3896-4365-b5dc-187494f02e0d-0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "27c7191b-80a2-475b-9371-e106108aff1c": "generated-edge-simstep-a629a102-0df1-4798-a7a2-45c8f6cdad29-27c7191b-80a2-475b-9371-e106108aff1c", + "7be432cd-2587-4eab-abdc-f7b9c1923597": "generated-edge-simstep-4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08-7be432cd-2587-4eab-abdc-f7b9c1923597", + "fe47576c-c186-4950-82a5-b2fd24eb2561": "server/src/config", + "cb53d612-9b89-47da-911c-165d0382a7d8": "server/src/config/routes.js", + "8021b55d-2d3a-4416-b598-e134e76c9434": "client/src/Pages/v1/Maintenance", + "5f21f109-1967-416f-a649-9b48e78b90f6": "server/src/routes/v1/maintenanceWindowRoute.js", + "a0cae18c-3c0e-40d3-ab03-eef7eace65d6": "server/src/controllers/v1/maintenanceWindowController.js", + "6f7119ac-d65a-4fdf-a6f2-31052e0f5549": "client/src/Pages/v1/Maintenance/CreateMaintenance", + "1eae6263-aaa2-4206-b92e-1e67f529c1c9": "client/src/Pages/v1/Maintenance/index.jsx", + "5ad4d33f-0932-479c-bb57-22ce6405758f": "client/src/Pages/v1/Maintenance/MaintenanceTable", + "26b8daff-aa48-4baa-9d6c-05f771bf0990": "server/src/service/v1/business/maintenanceWindowService.js", + "95ed6c41-ebeb-4c08-98c3-bab8b426789e": "server/src/db/v1/modules/maintenanceWindowModule.js", + "92c244d2-e6a7-4804-a643-1e8d36baf9c6": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "015a17fc-3fcf-45e0-8936-19f5e2ca86a3": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks", + "e7ec5ca9-6d25-4bf2-9695-639556f7d141": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx", + "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "20335f29-a2ac-4e0c-aba6-28707748ecd4": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-2401ef7a-8d8e-4056-868a-547ab213daf1", + "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx-simstep-5e0d7085-099b-478a-afaa-47ad4dd07043", + "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8": "server/src/config/routes.js-simstep-3e93ef65-8e2c-405b-8dc1-56d8e93937b3", + "0fae6e75-38ea-444d-a1bf-0f820ba443b9": "server/src/controllers/v1/maintenanceWindowController.js-simstep-95fc1919-339d-4659-98cb-3dc50f662075", + "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f": "server/src/service/v1/business/maintenanceWindowService.js-simstep-6e2cf739-1514-4c37-9874-10b68a47010e", + "3890b98d-a133-4279-a961-5d2facc6a17a": "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-a3c47fd4-c701-46c2-9aff-94a667b3e368", + "a5a84b08-8c70-4023-8d08-b74e23aab4d7": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc", + "d145bb4c-e1e0-473a-b6bc-decc932b738b": "client/src/Pages/v1/Maintenance/index.jsx-simstep-82b5ea14-bb00-4511-aba4-76196029fd81", + "c35a065c-2808-402c-877e-9055d956ce0d": "server/src/controllers/v1/maintenanceWindowController.js-simstep-1251da83-5949-4e24-a1d2-fc74ff785aa9", + "27d9160a-4f8d-4dff-a09b-1678bc28cf2c": "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-9bfbae5a-f487-40ef-86f5-04a1cc3c7662", + "970e2002-7c0e-40ef-80d8-e5e6d0a60735": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx-simstep-acf97639-e291-4ca6-81a6-d0487397f42a", + "23630a1f-7452-4373-a864-7ec7821137f2": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-a47affaf-b952-46d6-9709-6120a77690e1", + "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-2127e749-80b4-499a-ae14-8dc2dcc9db9b", + "0fe32416-db43-49b9-9438-3796ba1a0add": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-6ba6b9d1-5fc3-454e-8744-2022b5192ed9", + "01e042c3-40b4-416a-8d9e-6d6fd323d60b": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e59fe64e-f06b-4855-b631-923bb5a68540", + "d3bdc6ff-875e-472e-88c8-eb10b20c526e": "generated-edge-simstep-f881435b-3bc2-453d-b6bf-56ad3a12f120-d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af": "generated-edge-simstep-0660b48b-139f-4ecf-b7d9-f12f544e0ab5-fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "e2588541-3ffc-47c6-8f8a-41d30ea7fc72": "generated-edge-simstep-e4cc5ce8-6da9-4281-9204-29c061ab13ce-e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f": "generated-edge-simstep-c3d1a95c-e93d-499a-8830-a6d870902414-e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "12d0ab8a-392f-48fa-9efa-06ffe1f3a252": "generated-edge-simstep-80aed28e-68b6-47bf-8bc9-ededb4824066-12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "fcf4eef4-394c-4872-9124-f7a0b32cdc3b": "generated-edge-simstep-69fdc2a9-837f-466c-9562-a12319019580-fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "7f921125-500f-4902-abc3-936ecac033d8": "generated-edge-simstep-7e20cd6c-d24e-4540-86d6-d6a9fa087314-7f921125-500f-4902-abc3-936ecac033d8", + "c3127c74-43f1-42d7-8da6-b0a494c3658f": "generated-edge-simstep-feb8bd4f-339f-4132-a709-89c63c4efa9c-c3127c74-43f1-42d7-8da6-b0a494c3658f", + "6c9979cd-1cc1-4227-b535-2ec3a10ca27e": "generated-edge-simstep-eda86dca-dea6-4671-a079-7d212f4f18c4-6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "c99291bb-7316-440e-9a7f-323ee39e26b8": "generated-edge-simstep-87583096-f1ec-4160-ae52-5238f32ed0ab-c99291bb-7316-440e-9a7f-323ee39e26b8", + "8a13e585-2310-420b-9e92-211b6d65f201": "generated-edge-simstep-e6542e14-556d-482a-8c35-f7b03997be49-8a13e585-2310-420b-9e92-211b6d65f201", + "40f4d9df-4c0f-4384-901c-8d7117e3b538": "generated-edge-simstep-a715d98f-60bf-4f32-aae6-21ee4e09f4da-40f4d9df-4c0f-4384-901c-8d7117e3b538", + "a968db5d-8f6d-4fd7-9df0-8b332b521703": "server/src/middleware", + "09082f73-e215-465f-9206-0a963fb4c7cb": "server/src/middleware/v1", + "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19": "client/src/Pages/v1/Account", + "9edafb8a-3661-4a7e-8510-a47f3cbc7564": "server/src/routes/v1/inviteRoute.js", + "001fa2e4-eadd-4474-b2ec-79d7ea43465f": "server/src/middleware/v1/isAllowed.js", + "6a186805-9d50-4bc3-9d56-e627a7ec2eb8": "server/src/controllers/v1/inviteController.js", + "33960806-1b9e-4d61-a163-e4bebf82d87a": "client/src/Pages/v1/Account/index.jsx", + "425a4c15-043f-4d05-8832-da00e18f97b9": "client/src/Pages/v1/Account/components", + "570278b7-9b72-4299-9fd9-46b253b48e9b": "server/src/service/v1/business/inviteService.js", + "08853f8a-f596-4df0-9190-dad04f54caef": "server/src/db/v1/modules/inviteModule.js", + "1145f351-2257-411d-86f7-30bf8c26ad68": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "3de2d413-5e9b-49f4-9409-74be32c52e0f": "client/src/Pages/v1/Account/index.jsx-simstep-2d5a952a-ce62-4948-9309-96b28ad74e59", + "b27f1bba-c655-40c6-a1dc-71e78c590c92": "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-eebaca25-d588-49c4-a35c-9abc82fde580", + "8b3cb6c2-ed57-4510-be15-decb8e3c89e1": "server/src/middleware/v1/isAllowed.js-simstep-50dd7316-7d01-43f7-9036-d97d51a9c178", + "ae858158-8db6-4917-8fda-0966a953f6ed": "server/src/service/v1/business/inviteService.js-simstep-55fe9963-30d2-47f8-859f-b485a77064c4", + "a7be2d0b-0227-4228-a0a6-074f7324fd89": "server/src/service/v1/business/inviteService.js-simstep-7d1f86f2-c8d5-48fb-a8c1-b337dc56271f", + "02d87257-883c-4822-a421-823c983532ec": "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-700f35e0-b39d-417c-b0a8-0612feade977", + "c441312f-f3c6-48c6-b1b9-7c12749d91bb": "generated-edge-simstep-10f80ada-2d0a-4e98-bd5d-057e7d4fe6de-c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "3530244f-2689-444e-bcbf-611db2e3c000": "generated-edge-simstep-16d87608-4458-430c-b459-cd9b1d413fa6-3530244f-2689-444e-bcbf-611db2e3c000", + "2ee1c4e8-90fb-4470-ad9f-6ffed5420853": "generated-edge-simstep-6c024d4c-178b-45fc-8bca-4fc6c00549a4-2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "bdd52e6f-ddf7-4966-b3af-957f9553082f": "generated-edge-simstep-c796f8ad-d548-45f3-8d62-67f59c6c1aa4-bdd52e6f-ddf7-4966-b3af-957f9553082f", + "52732a37-82b8-40a7-b12d-dc1b82380b2d": "generated-edge-simstep-6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9-52732a37-82b8-40a7-b12d-dc1b82380b2d", + "56dae13e-5735-4089-a9a4-e18be315aff7": "client/src/Pages/v1/Uptime/BulkImport", + "29b6ad3c-9538-47bc-8796-aab942dc4b71": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "cd84476a-1c8b-4781-811f-58fb9944365b": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "c4caeba8-ed9a-4cbc-9888-38cc8ca55362": "client/src/Routes/index.jsx-simstep-a29da01e-8c5a-4e6d-8d3c-e49af528e4fa", + "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx-simstep-bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb", + "dd2f9356-fad3-47d4-81d5-a5e038298e2b": "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c", + "5967e576-f542-4c0b-a3f4-9446ae992721": "client/src/Utils/NetworkService.js-simstep-2fc087b3-033d-4cf5-81f2-e35b99a14bce", + "c1ac3c26-996a-43ef-9c34-4c2776188784": "server/src/routes/v1/monitorRoute.js-simstep-07bf3d83-047d-4e51-9f96-8c9c64848500", + "0e520a6d-34bf-467b-a894-e73979364478": "server/src/controllers/v1/monitorController.js-simstep-a3799570-64bf-481e-9ffe-31dbad8a36e4", + "6139e487-1dd5-405a-ba7d-ededec4b282e": "server/src/service/v1/business/monitorService.js-simstep-2c7c3082-e4b6-4525-860f-2ffcd9e86db1", + "d9d519af-5c4d-4494-929e-a869581c1dc4": "server/src/db/v1/modules/monitorModule.js-simstep-70f18c21-2918-402e-9c35-1bdce8bc82d4", + "1e1d975f-7f0f-4132-bd71-2563b35e99b3": "server/src/service/v1/business/monitorService.js-simstep-66fbf913-e662-4bd9-ada6-077be9e37f07", + "72b55fa1-d944-4e46-ac98-f6a6cb201afa": "server/src/controllers/v1/monitorController.js-simstep-f5377571-156f-4c5c-8bab-b6dacdf0fea6", + "ae3b7e23-ed64-40aa-9760-389f0a869f8d": "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-72b3f16d-290d-4a61-8748-2ad90d63ec68", + "26d76516-fd7e-4ae8-956a-d077de4257fa": "server/src/routes/v1/monitorRoute.js-simstep-63d8d81f-f436-4594-a6eb-8c0fd2110bc1", + "a3106388-6a3d-4b68-a3fc-3761a0d54f98": "server/src/controllers/v1/monitorController.js-simstep-d9db3f40-8add-4551-9323-274348b9800f", + "ab9cb862-0ac7-45a3-9888-f64e87bdda54": "server/src/service/v1/business/monitorService.js-simstep-85cd1d5d-7ffb-49c5-aacb-907048003fc5", + "f151f89d-6399-48f0-aecd-5eb1572b48cc": "server/src/controllers/v1/monitorController.js-simstep-256095b8-5e3a-4c20-abd9-be9e6a9d0bad", + "d8877474-7203-484f-bd47-fc69e3c7c482": "server/src/controllers/v1/monitorController.js-simstep-60abdb27-4078-4678-a010-0f6938d4be48", + "c9a012bc-d53f-494f-82f2-1ee3f40ce646": "generated-edge-simstep-ef0bbcff-9535-4c0e-a5ca-96c457f89139-c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48": "generated-edge-simstep-610120d2-0521-4444-b245-feb97239a389-e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "b8c0292d-afbb-4648-b484-1c27be96d52f": "generated-edge-simstep-a365d018-e80c-419c-80b3-5ffc8f14dc5c-b8c0292d-afbb-4648-b484-1c27be96d52f", + "23715cce-c83a-44c0-9853-4c95b85e5ecd": "generated-edge-simstep-e6dec1dc-33cb-4669-a3b0-7a3c0b233185-23715cce-c83a-44c0-9853-4c95b85e5ecd", + "20d0e690-a569-4c6f-8238-916cac920553": "generated-edge-simstep-c53d3f62-73fe-4fad-9835-ef2c8cf52894-20d0e690-a569-4c6f-8238-916cac920553", + "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d": "generated-edge-simstep-6038422c-ab83-4fc6-a58b-c75734276118-7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "b55a0dc6-f596-4e5f-9265-63588b46b519": "generated-edge-simstep-0115fb69-4959-4b32-bd90-1dcd3c2231f6-b55a0dc6-f596-4e5f-9265-63588b46b519", + "f27c60ef-fb40-43e2-9750-c2617bdeaf43": "generated-edge-simstep-2ee7bd94-d131-4dd8-bdf8-68433d00472d-f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9": "generated-edge-simstep-94d5772d-3f46-4d43-b764-3493db5c4971-c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "926a330c-a9c5-4983-a4b5-70359813e49a": "generated-edge-simstep-c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0-926a330c-a9c5-4983-a4b5-70359813e49a", + "febea87e-5033-479c-ba7a-eb35d98db899": "generated-edge-simstep-53cebb8e-cf45-49eb-a7d3-98fd87af21b4-febea87e-5033-479c-ba7a-eb35d98db899", + "3e0f0966-5c2f-41af-9bcc-e8f615e03a00": "generated-edge-simstep-74e95fb5-863c-4137-a41b-d7a32efa04a5-3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "ad300b1c-6748-40f9-a7c9-5895e0915991": "generated-edge-simstep-4deb5e47-2ac4-4638-9ee6-48295d027bcd-ad300b1c-6748-40f9-a7c9-5895e0915991", + "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03": "generated-edge-simstep-605f4cbd-1f7f-4e89-b028-f18737ac7fb1-3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "50f77798-f41b-4f5b-b096-7afc921a0419": "client/src/Pages/v1/Settings", + "1716ec41-8b42-4386-9850-55b8a607638e": "client/src/Pages/v1/Logs", + "8ee5d524-e61d-4e39-a729-babf54929def": "client/src/Hooks/v1/settingsHooks.js", + "a1fea5b0-fb7c-4189-9484-9d3312660767": "server/src/controllers/v1/settingsController.js", + "5882419e-8a92-4edd-a71a-1252983f9af7": "server/src/controllers/v1/diagnosticController.js", + "d4b34195-7e1e-47a0-9303-603987c81f95": "client/src/Pages/v1/Settings/index.jsx", + "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "e0cb598c-e300-4fd3-8554-db7ffd6201f1": "client/src/Pages/v1/Logs/Diagnostics", + "643b26c1-3bfb-4c41-8179-96dfc237d99e": "server/src/service/v1/business/diagnosticService.js", + "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "97908daf-d071-4df5-8366-1f5f4d8d534a": "client/src/Pages/v1/Settings/index.jsx-simstep-64e9f1af-13b7-4361-a2a9-3f45760e316d", + "83596bd1-dfca-4196-b52e-955f6824fafb": "server/src/controllers/v1/settingsController.js-simstep-441b0c45-5f27-478d-8ca8-58fbd5f460ea", + "09cb194a-0ea4-46e2-b32b-36b62fa87f40": "client/src/Pages/v1/Settings/SettingsEmail.jsx-simstep-ae5c5146-4dbb-4612-9d23-086e57f72474", + "1b2cf182-9ee3-4218-a7ec-b74e427ccddf": "client/src/Hooks/v1/settingsHooks.js-simstep-d7098072-9a83-4369-8176-9646ba34e186", + "cce3b9c8-f3b7-434b-b3c6-d8d164714f86": "server/src/controllers/v1/settingsController.js-simstep-7632e8d2-4086-4169-ae3f-bfbf8d657987", + "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3": "client/src/Hooks/v1/settingsHooks.js-simstep-a157bd07-b969-44f8-914c-3d8393c1605a", + "704e2ec8-9f2f-47e5-852f-c6ada67a02e0": "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-a9b24b5f-534b-4baf-a185-bac7c32a1c42", + "09f07e80-185e-4547-aab4-edaf8ae7501e": "server/src/service/v1/business/diagnosticService.js-simstep-8fb704ca-67d9-437c-90d8-01302634d568", + "4b418736-6b72-47e0-917a-d86d4bed9ecd": "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-e6c94f52-a599-4568-a06f-2db18f963817", + "f6d80f09-ae83-4c95-aba0-4efffeda38e0": "generated-edge-simstep-dca4a33c-b666-47b8-896b-370ed9d6f9c5-f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "195ad532-9858-493e-876a-6ec457811d1d": "generated-edge-simstep-aacc9302-6ca8-45c6-acb8-956d8d0242c2-195ad532-9858-493e-876a-6ec457811d1d", + "8c107d79-f796-49eb-ab88-f553dd6f9d95": "generated-edge-simstep-e7e82a97-1409-4bc5-ae27-b4ff5ba746df-8c107d79-f796-49eb-ab88-f553dd6f9d95", + "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812": "generated-edge-simstep-383387e5-4623-44ab-b2c4-d9ea046f4547-ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "fbe9b160-307e-4f51-8f72-713fda5373c8": "generated-edge-simstep-0afcde35-7d51-4939-aae8-bac5ac83c49d-fbe9b160-307e-4f51-8f72-713fda5373c8", + "499dfe1f-77d4-4b26-a12c-45240691109e": "generated-edge-simstep-23b8ad7d-8967-4e48-814c-1a7bf5b4dfac-499dfe1f-77d4-4b26-a12c-45240691109e", + "d0f92604-a6fa-4d85-9716-ea8f32db78e0": "generated-edge-simstep-44a4372f-a903-4c43-a9a5-c7549c875e11-d0f92604-a6fa-4d85-9716-ea8f32db78e0" + } +} \ No newline at end of file diff --git a/README.md b/README.md index b93794d4f..129953fb9 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). -Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. +Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. ### Using a Custom CA @@ -91,6 +91,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you! - Website monitoring - Page speed monitoring - Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent + - Selective disk monitoring with mountpoint selection - Docker monitoring - Ping monitoring - SSL monitoring @@ -162,6 +163,7 @@ Here's how you can contribute: 4. Open an issue if you believe you've encountered a bug. 5. Check for good-first-issue's if you are a newcomer. 6. Make a pull request to add new features/make quality-of-life improvements/fix bugs. +7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). diff --git a/charts/helm/checkmate/INSTALLATION.md b/charts/helm/checkmate/INSTALLATION.md index 9ca321ad3..06cf9f7b9 100644 --- a/charts/helm/checkmate/INSTALLATION.md +++ b/charts/helm/checkmate/INSTALLATION.md @@ -21,7 +21,12 @@ cd checkmate/charts/helm/checkmate Edit `values.yaml` to update: - `client.ingress.host` and `server.ingress.host` with your domain names - `server.protocol` (usually http or https) +- **If upgrading**: Migrate persistence settings from flat structure to nested: + - Old: `persistence.mongodbSize` → New: `persistence.mongo.size` + - Old: `persistence.redisSize` → New: `persistence.redis.size` + - Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default) - Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values +- **For TLS/HTTPS**: Configure ingress TLS settings (see section below) ### 3. Deploy the Helm chart ```bash @@ -37,3 +42,75 @@ kubectl get svc ``` Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts. + +## Enabling TLS/HTTPS with cert-manager + +If you have [cert-manager](https://cert-manager.io/) installed in your cluster, you can enable automatic TLS certificate provisioning using Let's Encrypt or other certificate issuers. + +### Prerequisites +- cert-manager installed in your cluster +- A ClusterIssuer or Issuer configured (e.g., `letsencrypt-prod`) + +### Configuration + +Edit `values.yaml` to enable TLS (and update protocols to https): + +```yaml +client: + protocol: https + ingress: + enabled: true + host: checkmate.example.com + className: nginx + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + tls: + enabled: true + secretName: checkmate-client-tls + +server: + protocol: https + ingress: + enabled: true + host: checkmate.example.com + className: nginx + annotations: + cert-manager.io/cluster-issuer: "letsencrypt-prod" + tls: + enabled: true + secretName: checkmate-server-tls +``` + +### Alternative: Using --set flags + +You can also enable TLS during installation using Helm's `--set` flags: + +```bash +helm install checkmate ./charts/helm/checkmate \ + --set client.protocol=https \ + --set server.protocol=https \ + --set client.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \ + --set client.ingress.tls.enabled=true \ + --set client.ingress.tls.secretName=checkmate-client-tls \ + --set server.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \ + --set server.ingress.tls.enabled=true \ + --set server.ingress.tls.secretName=checkmate-server-tls +``` + +### Verification + +After deployment, cert-manager will automatically create the TLS secrets. You can verify the certificate status: + +```bash +# Check certificates +kubectl get certificates + +# Check certificate details +kubectl describe certificate checkmate-client-tls +kubectl describe certificate checkmate-server-tls + +# Verify the secrets were created +kubectl get secrets | grep checkmate-tls +``` + +The ingress will automatically use these secrets to enable HTTPS access to your Checkmate instance. diff --git a/charts/helm/checkmate/templates/_prechecks.yaml b/charts/helm/checkmate/templates/_prechecks.yaml deleted file mode 100644 index 870ab5c77..000000000 --- a/charts/helm/checkmate/templates/_prechecks.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if eq .Values.client.ingress.host "change_me" }} - {{- fail "client.ingress.host must be overridden and not set to 'change_me'" }} -{{- end }} - -{{- if eq .Values.server.ingress.host "change_me" }} - {{- fail "server.ingress.host must be overridden and not set to 'change_me'" }} -{{- end }} - -{{- $protocol := .Values.server.protocol }} -{{- if not (or (eq $protocol "http") (eq $protocol "https")) }} - {{- fail "server.protocol must be either 'http' or 'https'" }} -{{- end }} diff --git a/charts/helm/checkmate/templates/client-deployment.yaml b/charts/helm/checkmate/templates/client-deployment.yaml index 6c1f70fd7..381631ef5 100644 --- a/charts/helm/checkmate/templates/client-deployment.yaml +++ b/charts/helm/checkmate/templates/client-deployment.yaml @@ -1,17 +1,25 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: client + name: checkmate-client spec: replicas: 1 selector: matchLabels: - app: client + app: checkmate-client template: metadata: labels: - app: client + app: checkmate-client spec: + {{- with .Values.client.affinity }} + affinity: + {{- tpl ( . | toYaml) $ | nindent 8 }} + {{- end }} + {{- with .Values.client.tolerations }} + tolerations: + {{- tpl ( . | toYaml) $ | nindent 8 }} + {{- end }} containers: - name: client image: {{ .Values.client.image }} @@ -20,3 +28,17 @@ spec: env: - name: UPTIME_APP_API_BASE_URL value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1" + - name: UPTIME_APP_CLIENT_HOST + value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}" + volumeMounts: + - name: config-volume + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + {{- with .Values.client.resources }} + resources: + {{- tpl ( . | toYaml) $ | nindent 12 }} + {{- end }} + volumes: + - name: config-volume + configMap: + name: checkmate-server-nginx-cm diff --git a/charts/helm/checkmate/templates/client-ingress.yaml b/charts/helm/checkmate/templates/client-ingress.yaml index da059a863..4fe126ea6 100644 --- a/charts/helm/checkmate/templates/client-ingress.yaml +++ b/charts/helm/checkmate/templates/client-ingress.yaml @@ -2,10 +2,21 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: client-ingress + name: checkmate-client-ingress + {{- if .Values.client.ingress.annotations }} annotations: - nginx.ingress.kubernetes.io/rewrite-target: / + {{- range $key, $value := .Values.client.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} spec: + ingressClassName: {{ .Values.client.ingress.className }} + {{- if .Values.client.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.client.ingress.host }} + secretName: {{ default (printf "%s-client-tls" .Release.Name) .Values.client.ingress.tls.secretName }} + {{- end }} rules: - host: {{ .Values.client.ingress.host }} http: @@ -14,7 +25,7 @@ spec: pathType: Prefix backend: service: - name: client + name: checkmate-client port: number: {{ .Values.client.port }} {{- end }} diff --git a/charts/helm/checkmate/templates/client-service.yaml b/charts/helm/checkmate/templates/client-service.yaml index afb88188c..b56eaeedf 100644 --- a/charts/helm/checkmate/templates/client-service.yaml +++ b/charts/helm/checkmate/templates/client-service.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: client + name: checkmate-client spec: selector: - app: client + app: checkmate-client ports: - port: {{ .Values.client.port }} diff --git a/charts/helm/checkmate/templates/mongodb-deployment.yaml b/charts/helm/checkmate/templates/mongodb-deployment.yaml deleted file mode 100644 index 4c906fe09..000000000 --- a/charts/helm/checkmate/templates/mongodb-deployment.yaml +++ /dev/null @@ -1,27 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: mongodb -spec: - replicas: 1 - selector: - matchLabels: - app: mongodb - template: - metadata: - labels: - app: mongodb - spec: - containers: - - name: mongodb - image: {{ .Values.mongodb.image }} - ports: - - containerPort: {{ .Values.mongodb.port }} - command: ["mongod", "--quiet", "--bind_ip_all"] - volumeMounts: - - name: mongodb-data - mountPath: /data/db - volumes: - - name: mongodb-data - persistentVolumeClaim: - claimName: mongodb-pvc \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-pvc.yaml b/charts/helm/checkmate/templates/mongodb-pvc.yaml deleted file mode 100644 index 435b662e6..000000000 --- a/charts/helm/checkmate/templates/mongodb-pvc.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: mongodb-pvc -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.persistence.mongodbSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-service.yaml b/charts/helm/checkmate/templates/mongodb-service.yaml index 2911dc3eb..b96fe1a82 100644 --- a/charts/helm/checkmate/templates/mongodb-service.yaml +++ b/charts/helm/checkmate/templates/mongodb-service.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: mongodb + name: checkmate-mongodb spec: selector: - app: mongodb + app: checkmate-mongodb ports: - port: {{ .Values.mongodb.port }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-statefulsets.yaml b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml new file mode 100644 index 000000000..8149b77e2 --- /dev/null +++ b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: checkmate-mongodb +spec: + replicas: 1 + selector: + matchLabels: + app: checkmate-mongodb + template: + metadata: + labels: + app: checkmate-mongodb + spec: + {{- with .Values.mongodb.affinity }} + affinity: + {{- tpl ( . | toYaml) $ | nindent 8 }} + {{- end }} + {{- with .Values.mongodb.tolerations }} + tolerations: + {{- tpl ( . | toYaml) $ | nindent 8 }} + {{- end }} + containers: + - name: mongodb + image: {{ .Values.mongodb.image }} + ports: + - containerPort: {{ .Values.mongodb.port }} + command: ["mongod", "--quiet", "--bind_ip_all"] + volumeMounts: + - name: checkmate-mongo-persistent-storage + mountPath: /data/db + {{- with .Values.mongodb.resources }} + resources: + {{- tpl ( . | toYaml) $ | nindent 12 }} + {{- end }} + volumeClaimTemplates: + - metadata: + name: checkmate-mongo-persistent-storage + spec: + storageClassName: {{ .Values.persistence.mongo.storageClass | quote }} + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.persistence.mongo.size | default "5Gi" | quote }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/prechecks.yaml b/charts/helm/checkmate/templates/prechecks.yaml new file mode 100644 index 000000000..e2b9ec5c2 --- /dev/null +++ b/charts/helm/checkmate/templates/prechecks.yaml @@ -0,0 +1,48 @@ +{{- if eq .Values.client.ingress.host "change_me" }} + {{- fail "client.ingress.host must be overridden and not set to 'change_me'" }} +{{- end }} + +{{- if eq .Values.server.ingress.host "change_me" }} + {{- fail "server.ingress.host must be overridden and not set to 'change_me'" }} +{{- end }} + +{{- $serverProtocol := .Values.server.protocol }} +{{- if not (or (eq $serverProtocol "http") (eq $serverProtocol "https")) }} + {{- fail "server.protocol must be either 'http' or 'https'" }} +{{- end }} + +{{- $clientProtocol := .Values.client.protocol }} +{{- if not (or (eq $clientProtocol "http") (eq $clientProtocol "https")) }} + {{- fail "client.protocol must be either 'http' or 'https'" }} +{{- end }} + +{{/* Enforce protocol when TLS is enabled to avoid mixed-content */}} +{{- if and .Values.client.ingress.tls.enabled (ne $clientProtocol "https") }} + {{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }} +{{- end }} + +{{- if and .Values.server.ingress.tls.enabled (ne $serverProtocol "https") }} + {{- fail "server.ingress.tls.enabled is true but server.protocol is not 'https'. Set server.protocol: https to ensure correct API base URL." }} +{{- end }} + +{{/* If client runs on https, API must also be https to avoid mixed content */}} +{{- if and (eq $clientProtocol "https") (ne $serverProtocol "https") }} + {{- fail "client.protocol is 'https' but server.protocol is not. Set server.protocol: https to prevent browser mixed-content issues." }} +{{- end }} + +{{/* Fail early if TLS enabled without cert-manager annotations (cluster-issuer or issuer) */}} +{{- $cAnn := .Values.client.ingress.annotations | default dict }} +{{- $sAnn := .Values.server.ingress.annotations | default dict }} + +{{- $clientHasIssuer := or (hasKey $cAnn "cert-manager.io/cluster-issuer") (hasKey $cAnn "cert-manager.io/issuer") }} +{{- $serverHasIssuer := or (hasKey $sAnn "cert-manager.io/cluster-issuer") (hasKey $sAnn "cert-manager.io/issuer") }} + +{{- if and .Values.client.ingress.tls.enabled (not $clientHasIssuer) }} + {{- fail "client.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }} +{{- end }} + +{{- if and .Values.server.ingress.tls.enabled (not $serverHasIssuer) }} + {{- fail "server.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }} +{{- end }} + +{{/* Secret name can be omitted; we default to -client|server-tls in templates */}} diff --git a/charts/helm/checkmate/templates/redis-deployment.yaml b/charts/helm/checkmate/templates/redis-deployment.yaml deleted file mode 100644 index cc9ffdaf9..000000000 --- a/charts/helm/checkmate/templates/redis-deployment.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis -spec: - replicas: 1 - selector: - matchLabels: - app: redis - template: - metadata: - labels: - app: redis - spec: - containers: - - name: redis - image: {{ .Values.redis.image }} - ports: - - containerPort: {{ .Values.redis.port }} - volumeMounts: - - name: redis-data - mountPath: /data - volumes: - - name: redis-data - persistentVolumeClaim: - claimName: redis-pvc diff --git a/charts/helm/checkmate/templates/redis-pvc.yaml b/charts/helm/checkmate/templates/redis-pvc.yaml deleted file mode 100644 index 2f0d81960..000000000 --- a/charts/helm/checkmate/templates/redis-pvc.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: redis-pvc -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.persistence.redisSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-service.yaml b/charts/helm/checkmate/templates/redis-service.yaml index f29fa46de..2720350da 100644 --- a/charts/helm/checkmate/templates/redis-service.yaml +++ b/charts/helm/checkmate/templates/redis-service.yaml @@ -1,9 +1,11 @@ +{{- if .Values.redis.enabled }} apiVersion: v1 kind: Service metadata: - name: redis + name: checkmate-redis spec: selector: - app: redis + app: checkmate-redis ports: - port: {{ .Values.redis.port }} +{{- end }} diff --git a/charts/helm/checkmate/templates/redis-statefulsets.yaml b/charts/helm/checkmate/templates/redis-statefulsets.yaml new file mode 100644 index 000000000..acf7c85a8 --- /dev/null +++ b/charts/helm/checkmate/templates/redis-statefulsets.yaml @@ -0,0 +1,33 @@ +{{- if .Values.redis.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: checkmate-redis +spec: + replicas: 1 + selector: + matchLabels: + app: checkmate-redis + template: + metadata: + labels: + app: checkmate-redis + spec: + containers: + - name: redis + image: {{ .Values.redis.image }} + ports: + - containerPort: {{ .Values.redis.port }} + volumeMounts: + - name: redis-data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: checkmate-redis-persistent-storage + spec: + storageClassName: {{ .Values.persistence.redis.storageClass | quote }} + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.persistence.redis.size | default "1Gi" | quote }} +{{- end }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/secrets.yaml b/charts/helm/checkmate/templates/secrets.yaml index 0a3f1ace5..af104e7d1 100644 --- a/charts/helm/checkmate/templates/secrets.yaml +++ b/charts/helm/checkmate/templates/secrets.yaml @@ -4,10 +4,6 @@ {{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }} {{- end }} -{{- if or (not $secrets.REFRESH_TOKEN_SECRET) (eq $secrets.REFRESH_TOKEN_SECRET "change_me") }} - {{- fail "secrets.REFRESH_TOKEN_SECRET must be overridden and cannot be 'change_me'" }} -{{- end }} - apiVersion: v1 kind: Secret metadata: diff --git a/charts/helm/checkmate/templates/server-deployment.yaml b/charts/helm/checkmate/templates/server-deployment.yaml index 6f21f86f3..393964ccd 100644 --- a/charts/helm/checkmate/templates/server-deployment.yaml +++ b/charts/helm/checkmate/templates/server-deployment.yaml @@ -1,17 +1,25 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: server + name: checkmate-server spec: replicas: 1 selector: matchLabels: - app: server + app: checkmate-server template: metadata: labels: - app: server + app: checkmate-server spec: + {{- with .Values.server.affinity }} + affinity: + {{- tpl ( . | toYaml) $ | nindent 8 }} + {{- end }} + {{- with .Values.server.tolerations }} + tolerations: + {{- tpl ( . | toYaml) $ | nindent 8 }} + {{- end }} containers: - name: server image: {{ .Values.server.image }} @@ -20,3 +28,10 @@ spec: envFrom: - secretRef: name: checkmate-secrets + {{- with .Values.server.resources }} + resources: + {{- tpl ( . | toYaml) $ | nindent 12 }} + {{- end }} + env: + - name: CLIENT_HOST + value: "{{ .Values.client.protocol }}://{{ .Values.client.ingress.host }}" diff --git a/charts/helm/checkmate/templates/server-ingress.yaml b/charts/helm/checkmate/templates/server-ingress.yaml index c1982d451..34c9ac0dc 100644 --- a/charts/helm/checkmate/templates/server-ingress.yaml +++ b/charts/helm/checkmate/templates/server-ingress.yaml @@ -2,24 +2,37 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: server-ingress + name: checkmate-server-ingress + {{- if .Values.server.ingress.annotations }} annotations: - nginx.ingress.kubernetes.io/rewrite-target: / - nginx.ingress.kubernetes.io/enable-cors: "true" - nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}" - nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS" - nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" - nginx.ingress.kubernetes.io/cors-allow-credentials: "true" + {{- range $key, $value := .Values.server.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + {{/*#annotations: + #nginx.ingress.kubernetes.io/rewrite-target: / + #nginx.ingress.kubernetes.io/enable-cors: "true" + #nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}" + #nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS" + #nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" + #nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}} spec: + ingressClassName: {{ .Values.server.ingress.className }} + {{- if .Values.server.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.server.ingress.host }} + secretName: {{ default (printf "%s-server-tls" .Release.Name) .Values.server.ingress.tls.secretName }} + {{- end }} rules: - host: {{ .Values.server.ingress.host }} http: paths: - - path: / + - path: /api/v1 pathType: Prefix backend: service: - name: server + name: checkmate-server port: number: {{ .Values.server.port }} {{- end }} diff --git a/charts/helm/checkmate/templates/server-nginx-cm.yaml b/charts/helm/checkmate/templates/server-nginx-cm.yaml new file mode 100644 index 000000000..84da55299 --- /dev/null +++ b/charts/helm/checkmate/templates/server-nginx-cm.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: checkmate-server-nginx-cm +data: + default.conf: | + server { + listen 80; + listen [::]:80; + + server_name checkmate-demo.bluewavelabs.ca; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + # location /api/ { + # proxy_pass http://{{ .Values.server.ingress.host }}:5000/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; + # } + + location /api-docs/ { + proxy_pass http://{{ .Values.server.ingress.host }}:5000/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/charts/helm/checkmate/templates/server-service.yaml b/charts/helm/checkmate/templates/server-service.yaml index 64494ff90..d8a7fbe86 100644 --- a/charts/helm/checkmate/templates/server-service.yaml +++ b/charts/helm/checkmate/templates/server-service.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: server + name: checkmate-server spec: selector: - app: server + app: checkmate-server ports: - port: {{ .Values.server.port }} diff --git a/charts/helm/checkmate/values.yaml b/charts/helm/checkmate/values.yaml index 79c77a49e..9a586dd54 100644 --- a/charts/helm/checkmate/values.yaml +++ b/charts/helm/checkmate/values.yaml @@ -1,41 +1,70 @@ client: - image: ghcr.io/bluewave-labs/checkmate-client:v2.3 + image: ghcr.io/bluewave-labs/checkmate-client:v3.2.0 port: 80 + protocol: http ingress: enabled: true host: change_me + className: nginx + annotations: {} + # Example annotations for cert-manager: + # annotations: + # cert-manager.io/cluster-issuer: "letsencrypt-prod" + tls: + enabled: false + # secretName: {{ .Release.Name }}-client-tls # Optional; defaults to -client-tls if omitted + # Note: when enabling TLS, also set client.protocol: https and add + # a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod"). + # The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation server: - image: ghcr.io/bluewave-labs/checkmate-backend:v2.3 + image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0 port: 52345 - protocol: change_me + protocol: http ingress: enabled: true host: change_me + className: nginx + annotations: {} + # Example annotations for cert-manager: + # annotations: + # cert-manager.io/cluster-issuer: "letsencrypt-prod" + tls: + enabled: false + # secretName: {{ .Release.Name }}-server-tls # Optional; defaults to -server-tls if omitted + # Note: when enabling TLS, also set server.protocol: https and add + # a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod"). + # The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation redis: + enabled: false image: redis:7.2 port: 6379 mongodb: - image: mongo:7.0 + image: ghcr.io/bluewave-labs/checkmate-mongo:v3.2.0 port: 27017 secrets: JWT_SECRET: change_me - REFRESH_TOKEN_SECRET: change_me - SYSTEM_EMAIL_ADDRESS: test@example.com - SYSTEM_EMAIL_PASSWORD: change_me - SYSTEM_EMAIL_HOST: smtp.example.com - SYSTEM_EMAIL_PORT: "587" - PAGESPEED_API_KEY: change_me - DB_CONNECTION_STRING: mongodb://mongodb:27017/uptime_db - REDIS_HOST: redis - REDIS_PORT: "6379" - DB_TYPE: MongoDB - TOKEN_TTL: 99d - REFRESH_TOKEN_TTL: 99d +# REFRESH_TOKEN_SECRET: change_me +# SYSTEM_EMAIL_ADDRESS: test@example.com +# SYSTEM_EMAIL_PASSWORD: change_me +# SYSTEM_EMAIL_HOST: smtp.example.com +# SYSTEM_EMAIL_PORT: "587" +# PAGESPEED_API_KEY: change_me + DB_CONNECTION_STRING: mongodb://checkmate-mongodb.namespace.svc:27017/uptime_db + CLIENT_HOST: change_me +# REDIS_HOST: redis +# REDIS_PORT: "6379" +# DB_TYPE: MongoDB +# TOKEN_TTL: 99d +# REFRESH_TOKEN_TTL: 99d persistence: - mongodbSize: 5Gi - redisSize: 1Gi \ No newline at end of file + mongo: + size: 5Gi + storageClass: "" + redis: + size: 1Gi + storageClass: "" \ No newline at end of file diff --git a/client/.env.production b/client/.env.production index 7a11848c0..25849c7b9 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,3 +1,4 @@ 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 diff --git a/client/.gitignore b/client/.gitignore index 83f6fe9ed..23b6e49af 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -28,4 +28,5 @@ dist-ssr # Scripts *.sh -!env.sh \ No newline at end of file +!env.sh +.env.development \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index ee1ab1bc1..b4ad9f73c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,7 +11,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^18.0.0", - "@mui/icons-material": "6.4.11", + "@hookform/resolvers": "5.2.2", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "6.4.11", "@mui/x-charts": "^7.5.1", @@ -21,11 +21,15 @@ "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", + "lucide-react": "^0.562.0", "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", @@ -34,9 +38,12 @@ "react-toastify": "^10.0.5", "recharts": "2.15.2", "redux-persist": "6.0.0", - "vite-plugin-svgr": "^4.2.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", @@ -45,6 +52,7 @@ "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" } }, @@ -89,6 +97,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -400,6 +409,7 @@ "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", @@ -443,6 +453,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -488,6 +499,262 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", @@ -504,6 +771,134 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "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", @@ -653,6 +1048,18 @@ "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", @@ -782,32 +1189,6 @@ "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", @@ -969,6 +1350,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz", "integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@mui/core-downloads-tracker": "^6.4.11", @@ -1133,7 +1515,6 @@ "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", @@ -1161,7 +1542,6 @@ "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", @@ -1192,7 +1572,6 @@ "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", @@ -1268,7 +1647,6 @@ "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", @@ -1646,9 +2024,9 @@ } }, "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==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -1676,6 +2054,123 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "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", @@ -1689,6 +2184,71 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "cpu": [ + "s390x" + ], + "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", @@ -1715,6 +2275,45 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -2107,6 +2706,17 @@ "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", + "peer": true, + "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", @@ -2124,6 +2734,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2187,6 +2798,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2493,6 +3105,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -2905,7 +3518,8 @@ "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.1", @@ -2991,6 +3605,15 @@ "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", @@ -3303,6 +3926,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4082,6 +4706,15 @@ "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", @@ -4101,6 +4734,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -4637,9 +5271,9 @@ "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4804,6 +5438,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "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", @@ -4916,6 +5559,12 @@ "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", @@ -5131,6 +5780,18 @@ "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", @@ -5258,6 +5919,21 @@ "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", @@ -5335,6 +6011,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5343,6 +6020,23 @@ "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", + "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": "15.5.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", @@ -5418,12 +6112,12 @@ } }, "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==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -5433,13 +6127,13 @@ } }, "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==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -5535,7 +6229,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-persist": { "version": "6.0.0", @@ -5673,6 +6368,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -6141,6 +6837,19 @@ "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", @@ -6289,6 +6998,21 @@ "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", + "peer": true, + "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", @@ -6308,6 +7032,13 @@ "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", @@ -6393,6 +7124,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6613,20 +7345,6 @@ "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", @@ -6639,6 +7357,15 @@ "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" + } } } } diff --git a/client/package.json b/client/package.json index 3108f7121..a5c17e343 100644 --- a/client/package.json +++ b/client/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", - "build-dev": "vite build --mode development", + "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 .", @@ -16,7 +16,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^18.0.0", - "@mui/icons-material": "6.4.11", + "@hookform/resolvers": "5.2.2", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "6.4.11", "@mui/x-charts": "^7.5.1", @@ -26,11 +26,15 @@ "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", + "lucide-react": "^0.562.0", "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", @@ -39,17 +43,12 @@ "react-toastify": "^10.0.5", "recharts": "2.15.2", "redux-persist": "6.0.0", - "vite-plugin-svgr": "^4.2.0" - }, - "unusedDepencies": { - "@solana/wallet-adapter-base": "0.9.25", - "@solana/wallet-adapter-material-ui": "0.16.35", - "@solana/wallet-adapter-react": "0.15.37", - "@solana/wallet-adapter-react-ui": "0.9.37", - "@solana/wallet-adapter-wallets": "0.19.34", - "@solana/web3.js": "1.98.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", @@ -58,6 +57,7 @@ "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" } } diff --git a/client/src/App.jsx b/client/src/App.jsx index b599890f1..da83f9061 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -9,8 +9,7 @@ import { CssBaseline, GlobalStyles } from "@mui/material"; import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; import { Routes } from "./Routes"; -import WalletProvider from "./Components/WalletProvider"; -import AppLayout from "./Components/Layouts/AppLayout"; +import AppLayout from "@/Components/v1/Layouts/AppLayout"; function App() { const mode = useSelector((state) => state.ui.mode); @@ -23,17 +22,29 @@ function App() { }; }, []); - return ( - /* Extract Themeprovider, baseline and global styles to Styles */ - - - + const theme = mode === "light" ? lightTheme : darkTheme; - - - - - + return ( + + + + + + + ); } diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx index 9b46204e3..fdda73f4a 100644 --- a/client/src/Components/LanguageSelector.jsx +++ b/client/src/Components/LanguageSelector.jsx @@ -16,7 +16,7 @@ const langMap = { const LanguageSelector = () => { const { i18n } = useTranslation(); const theme = useTheme(); - const { language } = useSelector((state) => state.ui); + const { language = "en" } = useSelector((state) => state.ui); const dispatch = useDispatch(); const handleChange = (event) => { const newLang = event.target.value; diff --git a/client/src/Components/Layouts/HomeLayout/index.jsx b/client/src/Components/Layouts/HomeLayout/index.jsx deleted file mode 100644 index 55b612522..000000000 --- a/client/src/Components/Layouts/HomeLayout/index.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import Sidebar from "../../Sidebar"; -import { Outlet } from "react-router"; -import { Stack } from "@mui/material"; - -import "./index.css"; - -const HomeLayout = () => { - return ( - - - - - - - ); -}; - -export default HomeLayout; diff --git a/client/src/Components/ProgressStepper/index.css b/client/src/Components/ProgressStepper/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Components/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx similarity index 90% rename from client/src/Components/ActionsMenu/index.jsx rename to client/src/Components/v1/ActionsMenu/index.jsx index 1b590a471..ea0d029b8 100644 --- a/client/src/Components/ActionsMenu/index.jsx +++ b/client/src/Components/v1/ActionsMenu/index.jsx @@ -2,17 +2,17 @@ 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 "../../Components/Dialog"; +import Icon from "../Icon"; +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"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import PropTypes from "prop-types"; -import { usePauseMonitor, useDeleteMonitor } from "../../Hooks/monitorHooks"; +import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/monitorHooks.js"; const ActionsMenu = ({ monitor, @@ -31,7 +31,7 @@ const ActionsMenu = ({ const handleRemove = async (event) => { event.preventDefault(); event.stopPropagation(); - let monitor = { _id: actions.id }; + let monitor = { id: actions.id }; await deleteMonitor({ monitor }); updateRowCallback(); }; @@ -39,7 +39,7 @@ const ActionsMenu = ({ const handlePause = async () => { try { setIsLoading(true); - await pauseMonitor({ monitorId: monitor._id }); + await pauseMonitor({ monitorId: monitor.id }); pauseCallback(); } catch (error) { createToast({ body: "Failed to pause monitor." }); @@ -72,7 +72,7 @@ const ActionsMenu = ({ aria-label="monitor actions" onClick={(event) => { event.stopPropagation(); - openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url); + openMenu(event, monitor.id, monitor.type === "ping" ? null : monitor.url); }} sx={{ "&:focus": { @@ -83,7 +83,10 @@ const ActionsMenu = ({ }, }} > - + { + console.log(e); e.stopPropagation(); handleRemove(e); }} @@ -210,7 +214,7 @@ const ActionsMenu = ({ ActionsMenu.propTypes = { monitor: PropTypes.shape({ - _id: PropTypes.string, + id: PropTypes.string, url: PropTypes.string, type: PropTypes.string, isActive: PropTypes.bool, diff --git a/client/src/Components/Alert/index.css b/client/src/Components/v1/Alert/index.css similarity index 100% rename from client/src/Components/Alert/index.css rename to client/src/Components/v1/Alert/index.css diff --git a/client/src/Components/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx similarity index 86% rename from client/src/Components/Alert/index.jsx rename to client/src/Components/v1/Alert/index.jsx index 921d4fd6e..dd3b2f9f2 100644 --- a/client/src/Components/Alert/index.jsx +++ b/client/src/Components/v1/Alert/index.jsx @@ -1,10 +1,7 @@ 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 Icon from "../Icon"; import "./index.css"; /** @@ -13,9 +10,24 @@ import "./index.css"; */ const icons = { - info: , - error: , - warning: , + info: ( + + ), + error: ( + + ), + warning: ( + + ), }; /** @@ -97,10 +109,9 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { }, }} > - )} diff --git a/client/src/Components/Animated/PulseDot.jsx b/client/src/Components/v1/Animated/PulseDot.jsx similarity index 100% rename from client/src/Components/Animated/PulseDot.jsx rename to client/src/Components/v1/Animated/PulseDot.jsx diff --git a/client/src/Components/ArrowLeft/index.jsx b/client/src/Components/v1/ArrowLeft/index.jsx similarity index 55% rename from client/src/Components/ArrowLeft/index.jsx rename to client/src/Components/v1/ArrowLeft/index.jsx index 7909441f3..e18a7f2d5 100644 --- a/client/src/Components/ArrowLeft/index.jsx +++ b/client/src/Components/v1/ArrowLeft/index.jsx @@ -1,27 +1,28 @@ -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 Icon from "../Icon"; import PropTypes from "prop-types"; const ArrowLeft = ({ type, color = "#667085", ...props }) => { if (type === "double") { return ( - ); } else if (type === "long") { return ( - ); } else { return ( - ); diff --git a/client/src/Components/ArrowRight/index.jsx b/client/src/Components/v1/ArrowRight/index.jsx similarity index 60% rename from client/src/Components/ArrowRight/index.jsx rename to client/src/Components/v1/ArrowRight/index.jsx index 335bc19f2..3ae64a580 100644 --- a/client/src/Components/ArrowRight/index.jsx +++ b/client/src/Components/v1/ArrowRight/index.jsx @@ -1,19 +1,20 @@ -import RightArrow from "../../assets/icons/right-arrow.svg?react"; -import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react"; +import Icon from "../Icon"; import PropTypes from "prop-types"; const ArrowRight = ({ type, color = "#667085", ...props }) => { if (type === "double") { return ( - ); } else { return ( - ); diff --git a/client/src/Components/Avatar/index.css b/client/src/Components/v1/Avatar/index.css similarity index 100% rename from client/src/Components/Avatar/index.css rename to client/src/Components/v1/Avatar/index.css diff --git a/client/src/Components/Avatar/index.jsx b/client/src/Components/v1/Avatar/index.jsx similarity index 100% rename from client/src/Components/Avatar/index.jsx rename to client/src/Components/v1/Avatar/index.jsx diff --git a/client/src/Components/Breadcrumbs/index.css b/client/src/Components/v1/Breadcrumbs/index.css similarity index 100% rename from client/src/Components/Breadcrumbs/index.css rename to client/src/Components/v1/Breadcrumbs/index.css diff --git a/client/src/Components/Breadcrumbs/index.jsx b/client/src/Components/v1/Breadcrumbs/index.jsx similarity index 97% rename from client/src/Components/Breadcrumbs/index.jsx rename to client/src/Components/v1/Breadcrumbs/index.jsx index f1af89790..34e5d5e95 100644 --- a/client/src/Components/Breadcrumbs/index.jsx +++ b/client/src/Components/v1/Breadcrumbs/index.jsx @@ -2,7 +2,7 @@ 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"; +import ArrowRight from "../ArrowRight/index.jsx"; import "./index.css"; /** diff --git a/client/src/Components/Buttons/RoundGradientButton.jsx b/client/src/Components/v1/Buttons/RoundGradientButton.jsx similarity index 100% rename from client/src/Components/Buttons/RoundGradientButton.jsx rename to client/src/Components/v1/Buttons/RoundGradientButton.jsx diff --git a/client/src/Components/Charts/AreaChart/index.jsx b/client/src/Components/v1/Charts/AreaChart/index.jsx similarity index 98% rename from client/src/Components/Charts/AreaChart/index.jsx rename to client/src/Components/v1/Charts/AreaChart/index.jsx index e1f1832e5..dd5a900d6 100644 --- a/client/src/Components/Charts/AreaChart/index.jsx +++ b/client/src/Components/v1/Charts/AreaChart/index.jsx @@ -61,7 +61,7 @@ import { Tooltip, ResponsiveContainer, } from "recharts"; -import { createGradient } from "../Utils/gradientUtils"; +import { createGradient } from "../Utils/gradientUtils.jsx"; import PropTypes from "prop-types"; import { useTheme } from "@mui/material"; import { useId } from "react"; diff --git a/client/src/Components/Charts/BarChart/index.css b/client/src/Components/v1/Charts/BarChart/index.css similarity index 100% rename from client/src/Components/Charts/BarChart/index.css rename to client/src/Components/v1/Charts/BarChart/index.css diff --git a/client/src/Components/Charts/BarChart/index.jsx b/client/src/Components/v1/Charts/BarChart/index.jsx similarity index 96% rename from client/src/Components/Charts/BarChart/index.jsx rename to client/src/Components/v1/Charts/BarChart/index.jsx index 375773b94..77b836cae 100644 --- a/client/src/Components/Charts/BarChart/index.jsx +++ b/client/src/Components/v1/Charts/BarChart/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useEffect, useState } from "react"; import "./index.css"; import { useSelector } from "react-redux"; @@ -103,7 +103,7 @@ const BarChart = ({ checks = [] }) => { } placement="top" - key={`check-${check?._id}`} + key={`check-${check?.id}`} slotProps={{ popper: { className: "bar-tooltip", @@ -145,7 +145,7 @@ const BarChart = ({ checks = [] }) => { position="relative" width="9px" height="100%" - backgroundColor={theme.palette.primary.lowContrast} // CAIO_REVIEW + backgroundColor={theme.palette.primary.lowContrast} sx={{ borderRadius: theme.spacing(1.5), /* diff --git a/client/src/Components/Charts/ChartBox/EmptyView.jsx b/client/src/Components/v1/Charts/ChartBox/EmptyView.jsx similarity index 98% rename from client/src/Components/Charts/ChartBox/EmptyView.jsx rename to client/src/Components/v1/Charts/ChartBox/EmptyView.jsx index 296197a8c..c1b60632d 100644 --- a/client/src/Components/Charts/ChartBox/EmptyView.jsx +++ b/client/src/Components/v1/Charts/ChartBox/EmptyView.jsx @@ -2,7 +2,7 @@ import { Typography, Stack } from "@mui/material"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox"; +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. diff --git a/client/src/Components/Charts/ChartBox/index.jsx b/client/src/Components/v1/Charts/ChartBox/index.jsx similarity index 95% rename from client/src/Components/Charts/ChartBox/index.jsx rename to client/src/Components/v1/Charts/ChartBox/index.jsx index 2c13ece87..be2faf8ed 100644 --- a/client/src/Components/Charts/ChartBox/index.jsx +++ b/client/src/Components/v1/Charts/ChartBox/index.jsx @@ -1,7 +1,7 @@ import { Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox"; -import EmptyView from "./EmptyView"; +import IconBox from "../../IconBox/index.jsx"; +import EmptyView from "./EmptyView.jsx"; import PropTypes from "prop-types"; const ChartBox = ({ diff --git a/client/src/Components/Charts/CustomGauge/index.css b/client/src/Components/v1/Charts/CustomGauge/index.css similarity index 100% rename from client/src/Components/Charts/CustomGauge/index.css rename to client/src/Components/v1/Charts/CustomGauge/index.css diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/v1/Charts/CustomGauge/index.jsx similarity index 100% rename from client/src/Components/Charts/CustomGauge/index.jsx rename to client/src/Components/v1/Charts/CustomGauge/index.jsx diff --git a/client/src/Components/Charts/DePINStatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx similarity index 98% rename from client/src/Components/Charts/DePINStatusPageBarChart/index.jsx rename to client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx index 65c6278f1..8b6c2b165 100644 --- a/client/src/Components/Charts/DePINStatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx @@ -5,7 +5,7 @@ import { Stack, Box, Tooltip, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; const PlaceholderCheck = ({ daysToShow }) => { diff --git a/client/src/Components/Charts/LegendBox/index.jsx b/client/src/Components/v1/Charts/LegendBox/index.jsx similarity index 95% rename from client/src/Components/Charts/LegendBox/index.jsx rename to client/src/Components/v1/Charts/LegendBox/index.jsx index ac14669e3..f19e09105 100644 --- a/client/src/Components/Charts/LegendBox/index.jsx +++ b/client/src/Components/v1/Charts/LegendBox/index.jsx @@ -1,6 +1,6 @@ import { Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox"; +import IconBox from "../../IconBox/index.jsx"; import PropTypes from "prop-types"; const LegendBox = ({ children, icon, header, sx }) => { diff --git a/client/src/Components/Charts/MonitorDetailsAreaChart/index.css b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.css similarity index 100% rename from client/src/Components/Charts/MonitorDetailsAreaChart/index.css rename to client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.css diff --git a/client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx similarity index 97% rename from client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx rename to client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx index a1ead3cf9..dafca7b40 100644 --- a/client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx @@ -12,11 +12,11 @@ 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"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { tooltipDateFormatLookup, tickDateFormatLookup, -} from "../Utils/chartUtilFunctions"; +} from "../Utils/chartUtilFunctions.js"; import "./index.css"; const CustomToolTip = ({ active, payload, label, dateRange }) => { diff --git a/client/src/Components/Charts/StatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx similarity index 98% rename from client/src/Components/Charts/StatusPageBarChart/index.jsx rename to client/src/Components/v1/Charts/StatusPageBarChart/index.jsx index eab0c7d1d..2dc5e61f0 100644 --- a/client/src/Components/Charts/StatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useEffect, useState, forwardRef } from "react"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; @@ -148,7 +148,7 @@ const StatusPageBarChart = ({ checks = [] }) => { } placement="top" - key={`check-${check?._id}`} + key={`check-${check?.id}`} slotProps={{ popper: { className: "bar-tooltip", diff --git a/client/src/Components/Charts/Utils/chartUtilFunctions.js b/client/src/Components/v1/Charts/Utils/chartUtilFunctions.js similarity index 100% rename from client/src/Components/Charts/Utils/chartUtilFunctions.js rename to client/src/Components/v1/Charts/Utils/chartUtilFunctions.js diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/v1/Charts/Utils/chartUtils.jsx similarity index 98% rename from client/src/Components/Charts/Utils/chartUtils.jsx rename to client/src/Components/v1/Charts/Utils/chartUtils.jsx index e24881c1c..7797ba462 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/v1/Charts/Utils/chartUtils.jsx @@ -2,9 +2,9 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; import { useTheme } from "@mui/material"; import { Text } from "recharts"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { Box, Stack, Typography } from "@mui/material"; -import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions"; +import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js"; /** * Custom tick component for rendering time with timezone. * diff --git a/client/src/Components/Charts/Utils/gradientUtils.jsx b/client/src/Components/v1/Charts/Utils/gradientUtils.jsx similarity index 100% rename from client/src/Components/Charts/Utils/gradientUtils.jsx rename to client/src/Components/v1/Charts/Utils/gradientUtils.jsx diff --git a/client/src/Components/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx similarity index 81% rename from client/src/Components/Check/Check.jsx rename to client/src/Components/v1/Check/Check.jsx index 02b9fd78a..c40f3de3e 100644 --- a/client/src/Components/Check/Check.jsx +++ b/client/src/Components/v1/Check/Check.jsx @@ -1,7 +1,6 @@ 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 Icon from "../Icon"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; @@ -35,16 +34,16 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => alignItems="center" > {outlined ? ( - + ) : ( - path": { fill: colors[variant] }, - }} - > - - + )} fontWeight: 450, }} > - {noHighlightText && {noHighlightText}}{" "} + {noHighlightText && ( + + {noHighlightText} + + )}{" "} {text} diff --git a/client/src/Components/Check/check.css b/client/src/Components/v1/Check/check.css similarity index 100% rename from client/src/Components/Check/check.css rename to client/src/Components/v1/Check/check.css diff --git a/client/src/Components/CircularCount/index.jsx b/client/src/Components/v1/CircularCount/index.jsx similarity index 100% rename from client/src/Components/CircularCount/index.jsx rename to client/src/Components/v1/CircularCount/index.jsx diff --git a/client/src/Components/Common/AppBar.jsx b/client/src/Components/v1/Common/AppBar.jsx similarity index 94% rename from client/src/Components/Common/AppBar.jsx rename to client/src/Components/v1/Common/AppBar.jsx index de5f0992e..5cd3db5c1 100644 --- a/client/src/Components/Common/AppBar.jsx +++ b/client/src/Components/v1/Common/AppBar.jsx @@ -8,9 +8,8 @@ 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"; +import Icon from "../Icon"; +import ThemeSwitch from "../ThemeSwitch/index.jsx"; import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; @@ -153,7 +152,11 @@ const AppAppBar = () => { aria-label="Menu button" onClick={toggleDrawer(true)} > - + { }} > - + diff --git a/client/src/Components/Common/Footer.jsx b/client/src/Components/v1/Common/Footer.jsx similarity index 100% rename from client/src/Components/Common/Footer.jsx rename to client/src/Components/v1/Common/Footer.jsx diff --git a/client/src/Components/ConfigBox/index.jsx b/client/src/Components/v1/ConfigBox/index.jsx similarity index 100% rename from client/src/Components/ConfigBox/index.jsx rename to client/src/Components/v1/ConfigBox/index.jsx diff --git a/client/src/Components/ConfigRow/index.jsx b/client/src/Components/v1/ConfigRow/index.jsx similarity index 96% rename from client/src/Components/ConfigRow/index.jsx rename to client/src/Components/v1/ConfigRow/index.jsx index 217085b6d..12fea923e 100644 --- a/client/src/Components/ConfigRow/index.jsx +++ b/client/src/Components/v1/ConfigRow/index.jsx @@ -1,7 +1,7 @@ import { Box, Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import ConfigBox from "../ConfigBox"; +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. diff --git a/client/src/Components/Dialog/genericDialog.jsx b/client/src/Components/v1/Dialog/genericDialog.jsx similarity index 100% rename from client/src/Components/Dialog/genericDialog.jsx rename to client/src/Components/v1/Dialog/genericDialog.jsx diff --git a/client/src/Components/Dialog/index.jsx b/client/src/Components/v1/Dialog/index.jsx similarity index 95% rename from client/src/Components/Dialog/index.jsx rename to client/src/Components/v1/Dialog/index.jsx index 678eb8721..b8cf07439 100644 --- a/client/src/Components/Dialog/index.jsx +++ b/client/src/Components/v1/Dialog/index.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { Button, Stack } from "@mui/material"; -import { GenericDialog } from "./genericDialog"; +import { GenericDialog } from "./genericDialog.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Components/Dot/index.jsx b/client/src/Components/v1/Dot/index.jsx similarity index 100% rename from client/src/Components/Dot/index.jsx rename to client/src/Components/v1/Dot/index.jsx diff --git a/client/src/Components/Fallback/FallBackActionButtons.jsx b/client/src/Components/v1/Fallback/FallBackActionButtons.jsx similarity index 100% rename from client/src/Components/Fallback/FallBackActionButtons.jsx rename to client/src/Components/v1/Fallback/FallBackActionButtons.jsx diff --git a/client/src/Components/Fallback/FallbackBackground.jsx b/client/src/Components/v1/Fallback/FallbackBackground.jsx similarity index 78% rename from client/src/Components/Fallback/FallbackBackground.jsx rename to client/src/Components/v1/Fallback/FallbackBackground.jsx index fe46f4064..2182082fd 100644 --- a/client/src/Components/Fallback/FallbackBackground.jsx +++ b/client/src/Components/v1/Fallback/FallbackBackground.jsx @@ -1,8 +1,8 @@ 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 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(); diff --git a/client/src/Components/Fallback/FallbackCheckList.jsx b/client/src/Components/v1/Fallback/FallbackCheckList.jsx similarity index 95% rename from client/src/Components/Fallback/FallbackCheckList.jsx rename to client/src/Components/v1/Fallback/FallbackCheckList.jsx index 4d13dead4..befb95185 100644 --- a/client/src/Components/Fallback/FallbackCheckList.jsx +++ b/client/src/Components/v1/Fallback/FallbackCheckList.jsx @@ -2,7 +2,7 @@ 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"; +import Check from "../Check/Check.jsx"; const FallbackCheckList = ({ checks, type }) => { const theme = useTheme(); return ( diff --git a/client/src/Components/Fallback/FallbackContainer.jsx b/client/src/Components/v1/Fallback/FallbackContainer.jsx similarity index 100% rename from client/src/Components/Fallback/FallbackContainer.jsx rename to client/src/Components/v1/Fallback/FallbackContainer.jsx diff --git a/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx b/client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx similarity index 97% rename from client/src/Components/Fallback/FallbackPageSpeedWarning.jsx rename to client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx index bcec7300d..2cf52bd88 100644 --- a/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx +++ b/client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx @@ -3,7 +3,7 @@ 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"; +import Alert from "../Alert/index.jsx"; import PropTypes from "prop-types"; const renderWarningMessage = (t) => { diff --git a/client/src/Components/Fallback/FallbackTitle.jsx b/client/src/Components/v1/Fallback/FallbackTitle.jsx similarity index 100% rename from client/src/Components/Fallback/FallbackTitle.jsx rename to client/src/Components/v1/Fallback/FallbackTitle.jsx diff --git a/client/src/Components/Fallback/index.css b/client/src/Components/v1/Fallback/index.css similarity index 100% rename from client/src/Components/Fallback/index.css rename to client/src/Components/v1/Fallback/index.css diff --git a/client/src/Components/Fallback/index.jsx b/client/src/Components/v1/Fallback/index.jsx similarity index 87% rename from client/src/Components/Fallback/index.jsx rename to client/src/Components/v1/Fallback/index.jsx index 5fbb958e7..40113f882 100644 --- a/client/src/Components/Fallback/index.jsx +++ b/client/src/Components/v1/Fallback/index.jsx @@ -2,11 +2,11 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack } from "@mui/material"; import "./index.css"; -import FallbackTitle from "./FallbackTitle"; -import FallbackCheckList from "./FallbackCheckList"; -import FallbackActionButtons from "./FallBackActionButtons"; -import FallbackBackground from "./FallbackBackground"; -import FallbackContainer from "./FallbackContainer"; +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. * diff --git a/client/src/Components/FilterHeader/index.jsx b/client/src/Components/v1/FilterHeader/index.jsx similarity index 95% rename from client/src/Components/FilterHeader/index.jsx rename to client/src/Components/v1/FilterHeader/index.jsx index 36dd04081..de2c7f0e9 100644 --- a/client/src/Components/FilterHeader/index.jsx +++ b/client/src/Components/v1/FilterHeader/index.jsx @@ -1,7 +1,7 @@ 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"; +import Icon from "../Icon"; /** * A reusable filter header component that displays a dropdown menu with selectable options. @@ -29,9 +29,10 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) => setSelectedTypes(e.target.value as MonitorType[])} + > + {types.map((type) => ( + + {type} + + ))} + + + + {isFilterActive && ( + + )} + + ); +}; diff --git a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx new file mode 100644 index 000000000..65be06f3b --- /dev/null +++ b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx @@ -0,0 +1,69 @@ +import Stack from "@mui/material/Stack"; +import { StatBox } from "@/Components/v2/design-elements"; + +import { useTheme } from "@mui/material/styles"; +import type { MonitorStats, Monitor } from "@/Types/Monitor"; +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; +import { getStatusPalette } from "@/Utils/MonitorUtils"; +import { useTranslation } from "react-i18next"; + +interface MonitorStatBoxesProps { + monitor?: Monitor; + monitorStats: MonitorStats | null; + certificateExpiry?: string; +} + +export const MonitorStatBoxes = ({ + monitor, + monitorStats, + certificateExpiry, +}: MonitorStatBoxesProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + if (!monitorStats || !monitor) { + return null; + } + + const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0; + const timeSinceLastFailure = + timeOfLastFailure > 0 + ? Date.now() - timeOfLastFailure + : Date.now() - new Date(monitorStats?.createdAt || 0).getTime(); + + // Determine time since last check + const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0; + const timeSinceLastCheck = Date.now() - timeOfLastCheck; + + const streakTime = getHumanReadableDuration(timeSinceLastFailure); + + const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck); + const palette = getStatusPalette(monitor?.status); + + return ( + + + + + + {monitor?.type === "http" && ( + + )} + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramDetails.tsx b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx new file mode 100644 index 000000000..8858a9784 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx @@ -0,0 +1,163 @@ +import { BaseChart, BaseBox } from "@/Components/v2/design-elements"; +import { Clock } from "lucide-react"; +import { + AreaChart, + Area, + XAxis, + Tooltip, + CartesianGrid, + ResponsiveContainer, + Text, +} from "recharts"; +import Typography from "@mui/material/Typography"; + +import { useSelector } from "react-redux"; +import { + formatDateWithTz, + tickDateFormatLookup, + tooltipDateFormatLookup, +} from "@/Utils/TimeUtils"; +import { useTheme } from "@mui/material/styles"; +import type { GroupedCheck } from "@/Types/Check"; +import type { RootState } from "@/Types/state"; + +type XTickProps = { + x: number; + y: number; + payload: { value: any }; + range: string; +}; + +export const XTick = ({ x, y, payload, range }: XTickProps) => { + const format = tickDateFormatLookup(range); + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + return ( + + {formatDateWithTz(payload?.value, format, uiTimezone)} + + ); +}; + +type ResponseTimeToolTipProps = { + active?: boolean | undefined; + payload?: any[]; + label?: string | number; + range: string; + theme: any; + uiTimezone: string; +}; + +const ResponseTimeToolTip = ({ + active, + payload, + label, + range, + theme, + uiTimezone, +}: ResponseTimeToolTipProps) => { + if (!label) return null; + if (!payload) return null; + if (!active) return null; + + const format = tooltipDateFormatLookup(range); + const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0); + return ( + + {formatDateWithTz(String(label), format, uiTimezone)} + Response time: {responseTime} ms + + ); +}; + +export const HistogramDetails = ({ + checks, + range, +}: { + checks: GroupedCheck[]; + range: string; +}) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + return ( + + } + title="Response times" + > + + + + + + + + + + ( + + )} + /> + + ( + + )} + /> + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx new file mode 100644 index 000000000..1bcceebd1 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx @@ -0,0 +1,106 @@ +import Stack from "@mui/material/Stack"; +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; +import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip"; +import { useTheme } from "@mui/material/styles"; +import type { Check } from "@/Types/Check"; +import type { MonitorStatus } from "@/Types/Monitor"; + +import { getStatusColor } from "@/Utils/MonitorUtils"; + +const processData = (checks: Check[]) => { + if (checks.length === 0) return []; + const formattedData: { score: number; date: string }[] = []; + + const calculateScore = (entry: Check) => { + const accessibility = entry?.accessibility || 0; + const bestPractices = entry?.bestPractices || 0; + const performance = entry?.performance || 0; + const seo = entry?.seo || 0; + return Math.floor((accessibility + bestPractices + performance + seo) / 4); + }; + + checks.forEach((entry) => { + const score = calculateScore(entry); + const date = entry.createdAt; + formattedData.push({ score, date }); + }); + + return formattedData; +}; + +export const HistogramPageSpeed = ({ + checks, + status, +}: { + checks: Check[]; + status: MonitorStatus; +}) => { + const theme = useTheme(); + const scores = processData(checks ?? []); + const color = getStatusColor(status, theme); + return ( + event.stopPropagation()} + overflow={"hidden"} + sx={{ + cursor: "default", + position: "relative", + }} + > + + + + } + /> + + + + + + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetails.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetails.tsx new file mode 100644 index 000000000..8fc39e16c --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetails.tsx @@ -0,0 +1,144 @@ +import { BaseChart } from "@/Components/v2/design-elements"; +import { TrendingUp } from "lucide-react"; +import { XTick } from "@/Components/v2/monitors/"; +import { + XAxis, + AreaChart, + Area, + Tooltip, + CartesianGrid, + ResponsiveContainer, +} from "recharts"; +import { HistogramPageSpeedScoresTooltip } from "@/Components/v2/monitors"; + +import { useTheme } from "@mui/material/styles"; +import type { Check } from "@/Types/Check"; +import type { Palette } from "@mui/material/styles"; +type PaletteColorKey = Extract< + keyof Palette, + "primary" | "error" | "success" | "warning" +>; + +export interface ConfigItem { + id: string; + text: string; + palette: PaletteColorKey; +} + +const config: Record = { + seo: { + id: "seo", + text: "SEO", + palette: "primary", + }, + performance: { + id: "performance", + text: "performance", + palette: "success", + }, + bestPractices: { + id: "bestPractices", + text: "best practices", + palette: "warning", + }, + accessibility: { + id: "accessibility", + text: "accessibility", + palette: "error", + }, +}; + +export const HistogramPageSpeedDetails = ({ + checks, + range, +}: { + checks: Check[]; + range: string; +}) => { + const theme = useTheme(); + return ( + + } + title="Score history" + > + + + ( + + )} + /> + + } + /> + + {Object.values(config).map(({ id, palette }) => { + const startColor = theme.palette[palette].main; + const endColor = theme.palette[palette].light; + + return ( + + + + + ); + })} + + {Object.keys(config).map((key) => { + const { palette } = config[key]; + const strokeColor = theme.palette[palette].main; + const bgColor = theme.palette.primary.main; + + return ( + + ); + })} + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx new file mode 100644 index 000000000..7219ba821 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx @@ -0,0 +1,78 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { ConfigItem } from "@/Components/v2/monitors"; +import type { TooltipProps } from "recharts"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; + +interface HistogramPageSpeedScoresTooltipProps + extends Partial> { + config: Record; +} +export const HistogramPageSpeedScoresTooltip = ({ + active, + payload, + label, + config, +}: HistogramPageSpeedScoresTooltipProps) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + if (active && payload && payload.length) { + return ( + + + {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)} + + {Object.keys(config) + .reverse() + .map((key) => { + const { palette } = config[key]; + const dotColor = theme.palette[palette].main; + + return ( + + + + {config[key].text} + + {Math.floor(payload[0].payload[key])} + + ); + })} + + ); + } + return null; +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx new file mode 100644 index 000000000..28d43a66e --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx @@ -0,0 +1,39 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { TooltipProps } from "recharts"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; + +export const HistogramPageSpeedTooltip = ({ + active, + payload, +}: TooltipProps) => { + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const theme = useTheme(); + + if (!active || !payload || payload.length === 0) { + return null; + } + + const data = payload[0]?.payload; + + return ( + + + {formatDateWithTz(data?.date, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} + + Score: {data?.score} + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx new file mode 100644 index 000000000..1c25c339e --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -0,0 +1,166 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BaseChart } from "@/Components/v2/design-elements"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy"; +import { useTranslation } from "react-i18next"; +import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts"; +import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import type { GroupedCheck } from "@/Types/Check"; +import type { RootState } from "@/Types/state"; + +interface XLabelProps { + p1: GroupedCheck; + p2: GroupedCheck; + range: string; +} + +const XLabel = ({ p1, p2, range }: XLabelProps) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const dateFormat = range === "day" ? "MMM D, h:mm A" : "MMM D"; + return ( + <> + + {formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)} + + + {formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)} + + + ); +}; + +interface HistogramStatusProps { + title: string; + icon: React.ReactNode; + checks?: GroupedCheck[]; + range: string; +} + +export const HistogramStatus = ({ + title, + icon, + checks = [], + range, +}: HistogramStatusProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const CustomTooltip = ({ + active, + payload, + }: { + active?: boolean; + payload?: Array<{ payload: GroupedCheck }>; + }) => { + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + if (!active || !payload?.length) return null; + const d = payload[0]?.payload; + const avg = d?.avgResponseTime ?? 0; + const titleText = t("common.charts.labels.averageResponseTime"); + const fmt = range === "30d" ? "MMM D, YYYY" : "ddd, MMM D, YYYY, h:mm A"; + let dateLabel = ""; + if (d?.bucketDate) { + const base = new Date(d.bucketDate); + const midBucket = + range === "30d" ? new Date(base.getTime() + 12 * 60 * 60 * 1000) : base; + dateLabel = formatDateWithTz(midBucket.toISOString(), fmt, uiTimezone); + } + return ( + + {dateLabel ? ( + + {dateLabel} + + ) : null} + {titleText} + {Math.floor(avg)} ms + + ); + }; + + const totalChecks = checks.reduce((acc, check) => acc + (check.totalChecks || 0), 0); + + return ( + + + + + Total checks: {totalChecks} + + + + + + } + /> + } + /> + + {checks?.map((groupedCheck, index) => { + const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime); + return ( + + ); + })} + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx b/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx new file mode 100644 index 000000000..1c58bb3f7 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx @@ -0,0 +1,161 @@ +import { BaseChart } from "@/Components/v2/design-elements"; +import { FileText } from "lucide-react"; +import type { Check, CheckAudits } from "@/Types/Check"; +import { Pie, PieChart, ResponsiveContainer, Label } from "recharts"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import { getPageSpeedPalette } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; +import { alpha } from "@mui/material/styles"; +import { useState } from "react"; +import Tooltip from "@mui/material/Tooltip"; +import { useTranslation } from "react-i18next"; + +const CenterLabel = ({ viewBox, value }: any) => { + const { cx, cy } = viewBox; + return ( + + + {value} + + + ); +}; + +export const PiePageSpeed = ({ latestCheck }: { latestCheck?: Check }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const [hoverTitle, setHoverTitle] = useState(null); + + if (!latestCheck) return null; + const LABELS: Record = { + FCP: t("pages.pageSpeed.charts.common.fcp"), + SI: t("pages.pageSpeed.charts.common.si"), + LCP: t("pages.pageSpeed.charts.common.lcp"), + TBT: t("pages.pageSpeed.charts.common.tbt"), + CLS: t("pages.pageSpeed.charts.common.cls"), + }; + const metrics: { key: keyof CheckAudits; color: string; weight: number }[] = [ + { key: "fcp", color: alpha("#1DE9B6", 0.6), weight: 0.1 }, + { key: "si", color: alpha("#7C4DFF", 0.6), weight: 0.1 }, + { key: "lcp", color: alpha("#FFB200", 0.6), weight: 0.25 }, + { key: "tbt", color: alpha("#00AFFE", 0.6), weight: 0.3 }, + { key: "cls", color: alpha("#FF4181", 0.6), weight: 0.25 }, + ]; + + const scores = metrics.flatMap(({ key, color, weight }) => { + const audit = latestCheck?.audits?.[key]; + const val = Math.floor((audit?.score ?? 0) * 100); + const inverse = 100 - val; + return [ + { + name: `${key.toUpperCase()} Inverse`, + value: inverse * weight, + fill: "transparent", + stroke: color, + weight, + }, + { + name: key.toUpperCase(), + value: val * weight, + fill: color, + stroke: color, + weight, + }, + ]; + }); + + const totalScore = + (latestCheck.audits?.fcp?.score || 0) * 0.1 + + (latestCheck.audits?.si?.score || 0) * 0.1 + + (latestCheck.audits?.lcp?.score || 0) * 0.25 + + (latestCheck.audits?.tbt?.score || 0) * 0.3 + + (latestCheck.audits?.cls?.score || 0) * 0.25; + const pageSpeedPalette = getPageSpeedPalette(Math.floor(totalScore * 100)); + + const score = [ + { + value: 100, + fill: alpha(theme.palette[pageSpeedPalette].light || "#ffffff", 0.6), + stroke: "none", + }, + ]; + + return ( + + } + title={t("pages.pageSpeed.charts.pie.title")} + > + + + + + + + `${name}: ${Math.round(value)}`} + dataKey="value" + stroke="none" + onMouseEnter={(_, index: number) => { + const d = scores[index]; + if (!d) return; + const isInverse = String(d.name).includes("Inverse"); + const pair = isInverse && scores[index + 1] ? scores[index + 1] : d; + const base = String(pair.name).replace(" Inverse", ""); + const full = LABELS[base] ?? base; + const displayVal = Math.round(Number(pair.value) || 0); + setHoverTitle(`${full}: ${displayVal}`); + }} + onMouseLeave={() => setHoverTitle(null)} + /> + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx b/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx new file mode 100644 index 000000000..5d220ae4e --- /dev/null +++ b/client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx @@ -0,0 +1,105 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import { BarChart3 } from "lucide-react"; +import { BaseChart } from "@/Components/v2/design-elements"; +import Typography from "@mui/material/Typography"; + +import type { Check } from "@/Types/Check"; +import { useTranslation } from "react-i18next"; +import { getPageSpeedPalette } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; + +const MetricBox = ({ + label, + value, + weight, +}: { + label: string; + value: number; + weight: number; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const palette = getPageSpeedPalette(value); + return ( + + + {label} + + {`${value}%`} + {`${t("pages.pageSpeed.charts.legend.weight")}: ${weight}%`} + + + + + ); +}; + +export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: Check }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + if (!latestCheck) { + return null; + } + return ( + + } + title={t("pages.pageSpeed.charts.legend.title")} + > + + + + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx b/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx new file mode 100644 index 000000000..b54bfc764 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/RadialAvgResponse.tsx @@ -0,0 +1,91 @@ +import { BaseChart } from "@/Components/v2/design-elements"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Gauge } from "lucide-react"; +import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts"; + +import { useTranslation } from "react-i18next"; +import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; + +export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const chartData = [ + { name: "max", value: max - avg, color: "transparent" }, + { name: "avg", value: avg, color: "red" }, + ]; + + const palette = getResponseTimeColor(avg); + const msg: Record = { + success: "Excellent", + warning: "Average", + danger: "Poor", + }; + + return ( + + } + title={t("common.charts.labels.averageResponseTime")} + > + + + + + + + + + + + {t("common.charts.labels.low")} + {t("common.charts.labels.high")} + + + + {msg[palette]} + + {`${avg?.toFixed()}ms`} + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx new file mode 100644 index 000000000..4efaae65c --- /dev/null +++ b/client/src/Components/v2/monitors/index.tsx @@ -0,0 +1,11 @@ +export * from "./ControlsFilter"; +export * from "./MonitorStatBoxes"; +export * from "./charts/HistogramStatus"; +export * from "./charts/RadialAvgResponse"; +export * from "./charts/HistogramDetails"; +export * from "./charts/HistogramPageSpeed"; +export * from "./charts/HistogramPageSpeedTooltip"; +export * from "./charts/PiePageSpeed"; +export * from "./charts/PiePageSpeedLegend"; +export * from "./charts/HistogramPageSpeedDetails"; +export * from "./charts/HistogramPageSpeedDetailsTooltip"; diff --git a/client/src/Features/Auth/authSlice.js b/client/src/Features/Auth/authSlice.js index 5ee31944a..b78a8b47e 100644 --- a/client/src/Features/Auth/authSlice.js +++ b/client/src/Features/Auth/authSlice.js @@ -10,21 +10,24 @@ const initialState = { msg: null, }; -export const register = createAsyncThunk("auth/register", async (form, thunkApi) => { - try { - const res = await networkService.registerUser(form); - return res.data; - } catch (error) { - if (error.response.data) { - return thunkApi.rejectWithValue(error.response.data); +export const register = createAsyncThunk( + "auth/register", + async ({ user, token }, thunkApi) => { + try { + const res = await networkService.registerUser(user, token); + return res.data; + } catch (error) { + if (error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); } -}); +); export const login = createAsyncThunk("auth/login", async (form, thunkApi) => { try { diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index 45ba267b0..65aff99a8 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -29,6 +29,7 @@ const initialState = { distributedUptimeEnabled: false, language: "en", starPromptOpen: true, + chartType: "histogram", }; const uiSlice = createSlice({ @@ -47,6 +48,10 @@ const uiSlice = createSlice({ toggleSidebar: (state) => { state.sidebar.collapsed = !state.sidebar.collapsed; }, + setCollapsed: (state, action) => { + const { collapsed } = action.payload; + state.sidebar.collapsed = collapsed; + }, setMode: (state, action) => { state.mode = action.payload; }, @@ -54,6 +59,9 @@ const uiSlice = createSlice({ state.showURL = action.payload; }, setGreeting(state, action) { + if (!state.greeting) { + state.greeting = { index: 0, lastUpdate: null }; + } state.greeting.index = action.payload.index; state.greeting.lastUpdate = action.payload.lastUpdate; }, @@ -66,6 +74,9 @@ const uiSlice = createSlice({ setStarPromptOpen: (state, action) => { state.starPromptOpen = action.payload; }, + setChartType: (state, action) => { + state.chartType = action.payload; + }, }, }); @@ -73,6 +84,7 @@ export default uiSlice.reducer; export const { setRowsPerPage, toggleSidebar, + setCollapsed, setMode, setShowURL, setGreeting, @@ -80,4 +92,5 @@ export const { setDistributedUptimeEnabled, setLanguage, setStarPromptOpen, + setChartType, } = uiSlice.actions; diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts new file mode 100644 index 000000000..aa71f01af --- /dev/null +++ b/client/src/Hooks/UseApi.ts @@ -0,0 +1,144 @@ +import useSWR from "swr"; +import type { SWRConfiguration } from "swr"; +import type { AxiosRequestConfig } from "axios"; +import { get, patch, post, deleteOp } from "@/Utils/ApiClient"; +import { useState } from "react"; +import { useToast } from "@/Hooks/UseToast"; + +interface ApiResponse { + success: boolean; + msg: string; + data: T; +} + +const fetcher = async (url: string, config?: AxiosRequestConfig) => { + const res = await get>(url, config); + return res.data; +}; + +export const useGet = ( + url: string | null, + axiosConfig?: AxiosRequestConfig, + swrConfig?: SWRConfiguration +) => { + const { data, error, isLoading, mutate } = useSWR>( + url, + (url: string) => fetcher(url, axiosConfig), + swrConfig + ); + + return { + data: data?.data ?? null, + isLoading, + error, + refetch: mutate, + }; +}; + +export const usePost = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError, toastSuccess } = 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, + }, + }); + toastSuccess(res.data?.msg || "Operation successful"); + + return res.data; + } catch (err: any) { + const errMsg = err?.response?.data?.msg || err.message || "An error occurred"; + console.error(err); + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { post: postFn, loading, error }; +}; + +export const usePatch = () => { + 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, + }, + }); + toastSuccess(res.data?.msg || "Operation successful"); + return res.data; + } catch (err: any) { + console.error(err); + 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 [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, + }, + }); + toastSuccess(res.data?.msg || "Operation successful"); + return res.data; + } catch (err: any) { + console.error(err); + const errMsg = err?.response?.data?.message || 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..8823aa675 --- /dev/null +++ b/client/src/Hooks/UseToast.ts @@ -0,0 +1,24 @@ +import { toast, type ToastOptions } from "react-toastify"; + +export const useToast = () => { + const showToast = (message: string, options?: ToastOptions) => { + toast.dismiss(); + const baseStyle: React.CSSProperties = { + whiteSpace: "pre-line", + wordBreak: "break-word", + }; + toast(message, { + ...options, + style: { ...baseStyle, ...(options?.style || {}) }, + }); + }; + + 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/checkHooks.js b/client/src/Hooks/checkHooks.js index bdb1a73b7..49a9b6ba3 100644 --- a/client/src/Hooks/checkHooks.js +++ b/client/src/Hooks/checkHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchChecksTeam = ({ @@ -185,36 +185,9 @@ const useResolveIncident = () => { return [resolveIncident, isLoading]; }; -const useAcknowledgeChecks = () => { - const [isLoading, setIsLoading] = useState(false); - const { t } = useTranslation(); - - const acknowledge = async (setUpdateTrigger, monitorId = null) => { - setIsLoading(true); - try { - if (monitorId) { - await networkService.updateMonitorChecksStatus({ monitorId, ack: true }); - } else { - await networkService.updateAllChecksStatus({ ack: true }); - } - setUpdateTrigger((prev) => !prev); - } catch (error) { - const toastMessage = monitorId - ? t("checkHooks.failureResolveMonitor") - : t("checkHooks.failureResolveAll"); - createToast({ body: toastMessage }); - } finally { - setIsLoading(false); - } - }; - - return { acknowledge, isLoading }; -}; - export { useFetchChecksByMonitor, useFetchChecksTeam, useFetchChecksSummaryByTeamId, useResolveIncident, - useAcknowledgeChecks, }; diff --git a/client/src/Hooks/inviteHooks.js b/client/src/Hooks/inviteHooks.js index fb577272a..c862eec31 100644 --- a/client/src/Hooks/inviteHooks.js +++ b/client/src/Hooks/inviteHooks.js @@ -1,5 +1,5 @@ import { useState } from "react"; -import { networkService } from "../main"; +import { networkService } from "../main.jsx"; import { useTranslation } from "react-i18next"; const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST; diff --git a/client/src/Hooks/logHooks.js b/client/src/Hooks/logHooks.js index 989870eea..d5162de5a 100644 --- a/client/src/Hooks/logHooks.js +++ b/client/src/Hooks/logHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchLogs = () => { diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index eb1e7acdc..9d46e9365 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -1,12 +1,12 @@ import { useEffect, useState } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "./useMonitorUtils"; +import { useMonitorUtils } from "./useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => { +export const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => { const [isLoading, setIsLoading] = useState(false); const [monitors, setMonitors] = useState(undefined); const [monitorsSummary, setMonitorsSummary] = useState(undefined); @@ -37,7 +37,7 @@ const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => { return [monitors, monitorsSummary, isLoading, networkError]; }; -const useFetchMonitorsWithChecks = ({ +export const useFetchMonitorsWithChecks = ({ types, limit, page, @@ -50,6 +50,7 @@ const useFetchMonitorsWithChecks = ({ const [isLoading, setIsLoading] = useState(false); const [count, setCount] = useState(undefined); const [monitors, setMonitors] = useState(undefined); + const [summary, setSummary] = useState(undefined); const [networkError, setNetworkError] = useState(false); const theme = useTheme(); @@ -67,12 +68,14 @@ const useFetchMonitorsWithChecks = ({ field, order, }); - const { count, monitors } = res?.data?.data ?? {}; + + const { count, monitors, summary } = res?.data?.data ?? {}; const mappedMonitors = monitors.map((monitor) => getMonitorWithPercentage(monitor, theme) ); + setSummary(summary); setMonitors(mappedMonitors); - setCount(count?.monitorsCount ?? 0); + setCount(count || 0); } catch (error) { console.error(error); setNetworkError(true); @@ -96,25 +99,12 @@ const useFetchMonitorsWithChecks = ({ types, monitorUpdateTrigger, ]); - return [monitors, count, isLoading, networkError]; + return [summary, monitors, count, isLoading, networkError]; }; -const useFetchMonitorsByTeamId = ({ - types, - limit, - page, - rowsPerPage, - filter, - field, - order, - checkOrder, - normalize, - status, - updateTrigger, -}) => { +export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { const [isLoading, setIsLoading] = useState(false); const [monitors, setMonitors] = useState(undefined); - const [summary, setSummary] = useState(undefined); const [networkError, setNetworkError] = useState(false); useEffect(() => { @@ -122,20 +112,11 @@ const useFetchMonitorsByTeamId = ({ try { setIsLoading(true); const res = await networkService.getMonitorsByTeamId({ - limit, types, - page, - rowsPerPage, filter, - field, - order, - checkOrder, - status, - normalize, }); - if (res?.data?.data?.filteredMonitors) { - setMonitors(res.data.data.filteredMonitors); - setSummary(res.data.data.summary); + if (res?.data?.data) { + setMonitors(res.data.data); } } catch (error) { setNetworkError(true); @@ -147,23 +128,11 @@ const useFetchMonitorsByTeamId = ({ } }; fetchMonitors(); - }, [ - types, - limit, - page, - rowsPerPage, - filter, - field, - order, - updateTrigger, - checkOrder, - normalize, - status, - ]); - return [monitors, summary, isLoading, networkError]; + }, [types, filter, updateTrigger]); + return [monitors, isLoading, networkError]; }; -const useFetchStatsByMonitorId = ({ +export const useFetchStatsByMonitorId = ({ monitorId, sortOrder, limit, @@ -202,7 +171,7 @@ const useFetchStatsByMonitorId = ({ return [monitor, audits, isLoading, networkError]; }; -const useFetchMonitorGames = ({ setGames, updateTrigger }) => { +export const useFetchMonitorGames = ({ setGames, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchGames = async () => { @@ -221,7 +190,7 @@ const useFetchMonitorGames = ({ setGames, updateTrigger }) => { return [isLoading]; }; -const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { +export const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (typeof monitorId === "undefined") { @@ -244,7 +213,7 @@ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { return [isLoading]; }; -const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => { +export const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); const [networkError, setNetworkError] = useState(false); const [monitor, setMonitor] = useState(undefined); @@ -270,8 +239,36 @@ const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => }, [monitorId, dateRange, updateTrigger]); return [monitor, isLoading, networkError]; }; +export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigger }) => { + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + const [monitor, setMonitor] = useState(undefined); + const [monitorStats, setMonitorStats] = useState(undefined); -const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { + useEffect(() => { + const fetchMonitor = async () => { + try { + if (!monitorId) { + return { monitor: undefined, isLoading: false, networkError: undefined }; + } + const response = await networkService.getPageSpeedDetailsByMonitorId({ + monitorId: monitorId, + dateRange: dateRange, + }); + setMonitor(response.data.data.monitor); + setMonitorStats(response.data.data.monitorStats); + } catch (error) { + setNetworkError(true); + } finally { + setIsLoading(false); + } + }; + fetchMonitor(); + }, [monitorId, dateRange, updateTrigger]); + return [monitor, monitorStats, isLoading, networkError]; +}; + +export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { const [networkError, setNetworkError] = useState(false); const [isLoading, setIsLoading] = useState(true); const [monitor, setMonitor] = useState(undefined); @@ -299,7 +296,7 @@ const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { return [monitor, monitorStats, isLoading, networkError]; }; -const useCreateMonitor = () => { +export const useCreateMonitor = () => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const createMonitor = async ({ monitor, redirect }) => { @@ -319,7 +316,7 @@ const useCreateMonitor = () => { return [createMonitor, isLoading]; }; -const useFetchGlobalSettings = () => { +export const useFetchGlobalSettings = () => { const [isLoading, setIsLoading] = useState(true); const [globalSettings, setGlobalSettings] = useState(undefined); useEffect(() => { @@ -341,13 +338,13 @@ const useFetchGlobalSettings = () => { return [globalSettings, isLoading]; }; -const useDeleteMonitor = () => { +export const useDeleteMonitor = () => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const deleteMonitor = async ({ monitor, redirect }) => { try { setIsLoading(true); - await networkService.deleteMonitorById({ monitorId: monitor._id }); + await networkService.deleteMonitorById({ monitorId: monitor.id }); createToast({ body: "Monitor deleted successfully!" }); if (redirect) { navigate(redirect); @@ -361,7 +358,7 @@ const useDeleteMonitor = () => { return [deleteMonitor, isLoading]; }; -const useUpdateMonitor = () => { +export const useUpdateMonitor = () => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const updateMonitor = async ({ monitor, redirect }) => { @@ -387,10 +384,11 @@ const useUpdateMonitor = () => { ...(monitor.type === "hardware" && { thresholds: monitor.thresholds, secret: monitor.secret, + selectedDisks: monitor.selectedDisks, }), }; await networkService.updateMonitor({ - monitorId: monitor._id, + monitorId: monitor.id, updatedFields, }); @@ -407,7 +405,7 @@ const useUpdateMonitor = () => { return [updateMonitor, isLoading]; }; -const usePauseMonitor = () => { +export const usePauseMonitor = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); const pauseMonitor = async ({ monitorId, triggerUpdate }) => { @@ -430,7 +428,7 @@ const usePauseMonitor = () => { return [pauseMonitor, isLoading, error]; }; -const useAddDemoMonitors = () => { +export const useAddDemoMonitors = () => { const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const addDemoMonitors = async () => { @@ -447,7 +445,7 @@ const useAddDemoMonitors = () => { return [addDemoMonitors, isLoading]; }; -const useDeleteAllMonitors = () => { +export const useDeleteAllMonitors = () => { const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const deleteAllMonitors = async () => { @@ -464,7 +462,7 @@ const useDeleteAllMonitors = () => { return [deleteAllMonitors, isLoading]; }; -const useDeleteMonitorStats = () => { +export const useDeleteMonitorStats = () => { const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const deleteMonitorStats = async () => { @@ -482,7 +480,7 @@ const useDeleteMonitorStats = () => { return [deleteMonitorStats, isLoading]; }; -const useCreateBulkMonitors = () => { +export const useCreateBulkMonitors = () => { const [isLoading, setIsLoading] = useState(false); const createBulkMonitors = async (file, user) => { @@ -505,7 +503,7 @@ const useCreateBulkMonitors = () => { return [createBulkMonitors, isLoading]; }; -const useExportMonitors = () => { +export const useExportMonitors = () => { const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); @@ -538,23 +536,19 @@ const useExportMonitors = () => { return [exportMonitors, isLoading]; }; -export { - useFetchMonitorsWithSummary, - useFetchMonitorsWithChecks, - useFetchMonitorsByTeamId, - useFetchStatsByMonitorId, - useFetchMonitorById, - useFetchUptimeMonitorById, - useFetchHardwareMonitorById, - useCreateMonitor, - useFetchGlobalSettings, - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, - useAddDemoMonitors, - useDeleteAllMonitors, - useDeleteMonitorStats, - useCreateBulkMonitors, - useExportMonitors, - useFetchMonitorGames, +export const useFetchJson = () => { + const [isLoading, setIsLoading] = useState(false); + const fetchJson = async () => { + try { + setIsLoading(true); + const res = await networkService.fetchJson(); + createToast({ body: "JSON fetched successfully" }); + return res?.data?.data ?? []; + } catch (error) { + createToast({ body: "Failed to create monitor." }); + } finally { + setIsLoading(false); + } + }; + return [fetchJson, isLoading]; }; diff --git a/client/src/Hooks/settingsHooks.js b/client/src/Hooks/settingsHooks.js index 98a21c1fa..4693d8daa 100644 --- a/client/src/Hooks/settingsHooks.js +++ b/client/src/Hooks/settingsHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => { @@ -42,11 +42,11 @@ const useSaveSettings = ({ setIsLoading(true); try { const settingsResponse = await networkService.updateAppSettings({ settings }); - if (settings.checkTTL) { - await networkService.updateChecksTTL({ - ttl: settings.checkTTL, - }); - } + // if (settings.checkTTL) { + // await networkService.updateChecksTTL({ + // ttl: settings.checkTTL, + // }); + // } setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet); setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet); if (settingsResponse.data.data.pagespeedKeySet === true) { @@ -56,6 +56,7 @@ const useSaveSettings = ({ setEmailPasswordHasBeenReset(false); } setSettingsData(settingsResponse.data.data); + createToast({ body: t("settingsSuccessSaved") }); } catch (error) { createToast({ body: t("settingsFailedToSave") }); diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 539725958..6c5919fac 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback } from "react"; -import { createToast } from "../Utils/toastUtils"; -import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils.jsx"; +import { networkService } from "../main.jsx"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils"; +import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils.js"; const useCreateNotification = () => { const navigate = useNavigate(); @@ -102,7 +102,7 @@ const useGetNotificationById = (id, setNotification) => { const notificationData = { address: notification?.address, notificationName: notification?.notificationName, - type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id, + type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?.id, }; setNotification(notificationData); diff --git a/client/src/Hooks/useSendTestEmail.js b/client/src/Hooks/useSendTestEmail.js index e280b1f99..7223a489a 100644 --- a/client/src/Hooks/useSendTestEmail.js +++ b/client/src/Hooks/useSendTestEmail.js @@ -1,7 +1,7 @@ import { useState } from "react"; -import { networkService } from "../main"; +import { networkService } from "../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../Utils/toastUtils"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useSendTestEmail = () => { diff --git a/client/src/Hooks/useSidebar.js b/client/src/Hooks/useSidebar.js new file mode 100644 index 000000000..97724b417 --- /dev/null +++ b/client/src/Hooks/useSidebar.js @@ -0,0 +1,29 @@ +import { useSelector } from "react-redux"; + +// CSS variable names for sidebar widths +const SIDEBAR_WIDTH_VAR = "var(--env-var-side-bar-width)"; +const SIDEBAR_COLLAPSED_WIDTH_VAR = "var(--env-var-side-bar-collapsed-width)"; + +// Transition timing for sidebar width changes +const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)"; + +/** + * Hook to get sidebar state and computed width + * Centralizes sidebar width logic to avoid duplication between Sidebar and HomeLayout + * + * @returns {Object} Sidebar state and styles + * @returns {boolean} collapsed - Whether the sidebar is collapsed + * @returns {string} width - CSS width value based on collapsed state + * @returns {string} transition - CSS transition for width changes + */ +export const useSidebar = () => { + const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + + return { + collapsed, + width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR, + transition: SIDEBAR_TRANSITION, + }; +}; + +export default useSidebar; diff --git a/client/src/Hooks/userHooks.js b/client/src/Hooks/userHooks.js index 8be32a082..3d822f979 100644 --- a/client/src/Hooks/userHooks.js +++ b/client/src/Hooks/userHooks.js @@ -1,13 +1,12 @@ import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; export const useGetUser = (userId) => { const [user, setUser] = useState(undefined); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const fetchUser = useCallback(async () => { try { setIsLoading(true); @@ -54,8 +53,29 @@ export const useEditUser = (userId) => { setIsLoading(false); } }, - [userId] + [userId, t] ); - return [editUser, isLoading, error]; + const changePassword = useCallback( + async (passwordForm) => { + try { + setIsLoading(true); + + await networkService.changePasswordByAdmin({ userId, passwordForm }); + createToast({ + body: t("teamPanel.changeTeamPassword.success"), + }); + } catch (error) { + createToast({ + body: error.message, + }); + setError(error); + } finally { + setIsLoading(false); + } + }, + [userId, t] + ); + + return [editUser, isLoading, error, changePassword]; }; diff --git a/client/src/Pages/About/index.jsx b/client/src/Pages/About/index.jsx index 97040b2e3..ff7bb6c6b 100644 --- a/client/src/Pages/About/index.jsx +++ b/client/src/Pages/About/index.jsx @@ -1,6 +1,6 @@ import React from "react"; -import AppBar from "../../Components/Common/AppBar"; -import Footer from "../../Components/Common/Footer"; +import AppBar from "@/Components/v1/Common/AppBar.jsx"; +import Footer from "@/Components/v1/Common/Footer.jsx"; import { useTranslation } from "react-i18next"; const AboutUs = () => { diff --git a/client/src/Pages/Account/EditUser/hooks/editUser.js b/client/src/Pages/Account/EditUser/hooks/editUser.js index 2ffa914a0..aeb50ffff 100644 --- a/client/src/Pages/Account/EditUser/hooks/editUser.js +++ b/client/src/Pages/Account/EditUser/hooks/editUser.js @@ -1,8 +1,8 @@ import { useState, useEffect } from "react"; -import { ROLES } from "../../../../Utils/roleUtils"; -import { editUserValidation } from "../../../../Validation/validation"; +import { ROLES } from "../../../../Utils/roleUtils.js"; +import { editUserValidation } from "../../../../Validation/validation.js"; import Joi from "joi"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; export const useEditUserForm = (user) => { const [searchInput, setSearchInput] = useState(""); diff --git a/client/src/Pages/Account/EditUser/index.jsx b/client/src/Pages/Account/EditUser/index.jsx index e1eeead9d..2b2ac5128 100644 --- a/client/src/Pages/Account/EditUser/index.jsx +++ b/client/src/Pages/Account/EditUser/index.jsx @@ -1,23 +1,25 @@ // Components import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import TextInput from "../../../Components/Inputs/TextInput"; -import Search from "../../../Components/Inputs/Search"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; import Button from "@mui/material/Button"; -import RoleTable from "../components/RoleTable"; - +import RoleTable from "../components/RoleTable/index.jsx"; +import ChangePasswordModal from "@/Pages/Account/components/ChangePasswordModal/index.jsx"; // Utils import { useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useGetUser, useEditUser } from "../../../Hooks/userHooks"; -import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils"; -import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser"; +import { useGetUser, useEditUser } from "../../../Hooks/userHooks.js"; +import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils.js"; +import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser.js"; +import { useSelector } from "react-redux"; const EditUser = () => { + const { user } = useSelector((state) => state.auth); const { userId } = useParams(); + const isSameUser = user?._id === userId; const theme = useTheme(); const { t } = useTranslation(); const BREADCRUMBS = [ @@ -25,8 +27,8 @@ const EditUser = () => { { name: t("editUserPage.title"), path: "" }, ]; - const [user, isLoading, error] = useGetUser(userId); - const [editUser, isSaving, saveError] = useEditUser(userId); + const [userToEdit, isLoading, error] = useGetUser(userId); + const [editUser, isSaving, saveError, changePassword] = useEditUser(userId); const [ form, setForm, @@ -34,7 +36,7 @@ const EditUser = () => { handleDeleteRole, searchInput, handleSearchInput, - ] = useEditUserForm(user); + ] = useEditUserForm(userToEdit); const [errors, validateForm, validateField] = useValidateEditUserForm(); const onChange = (e) => { @@ -104,7 +106,12 @@ const EditUser = () => { roles={form?.role} handleDeleteRole={handleDeleteRole} /> - + - + {!isSameUser && ( + + )} + ); diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/Account/components/AddMemberMenu/index.jsx index ab1ba599d..96bea3522 100644 --- a/client/src/Pages/Account/components/AddMemberMenu/index.jsx +++ b/client/src/Pages/Account/components/AddMemberMenu/index.jsx @@ -3,7 +3,7 @@ import Button from "@mui/material/Button"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import { useTheme } from "@emotion/react"; -import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; import Proptypes from "prop-types"; @@ -24,7 +24,13 @@ const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => { + + + } + sx={{ mb: theme.spacing(5) }} + /> + + } + sx={{ mb: theme.spacing(5) }} + /> + + + + + + + + ); +}; + +ChangePasswordModal.propTypes = { + isSaving: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, + changePassword: PropTypes.func.isRequired, +}; + +export default ChangePasswordModal; diff --git a/client/src/Pages/Account/components/PasswordPanel.jsx b/client/src/Pages/Account/components/PasswordPanel.jsx index dabd67706..e889db96c 100644 --- a/client/src/Pages/Account/components/PasswordPanel.jsx +++ b/client/src/Pages/Account/components/PasswordPanel.jsx @@ -2,14 +2,14 @@ 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/Inputs/TextInput/Adornments"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { newOrChangedCredentials } from "../../../Validation/validation"; -import Alert from "../../../Components/Alert"; -import { update } from "../../../Features/Auth/authSlice"; +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"; -import { getTouchedFieldErrors } from "../../../Validation/error"; +import { createToast } from "../../../Utils/toastUtils.jsx"; +import { getTouchedFieldErrors } from "../../../Validation/error.js"; import { useTranslation } from "react-i18next"; const defaultPasswordsState = { @@ -167,7 +167,11 @@ const PasswordPanel = () => { value={localData.password} onChange={handleChange} error={errors[idToName["edit-current-password"]] ? true : false} - helperText={errors[idToName["edit-current-password"]]} + helperText={ + errors[idToName["edit-current-password"]] + ? t(errors[idToName["edit-current-password"]]) + : "" + } endAdornment={} flex={1} /> @@ -193,7 +197,11 @@ const PasswordPanel = () => { value={localData.newPassword} onChange={handleChange} error={errors[idToName["edit-new-password"]] ? true : false} - helperText={errors[idToName["edit-new-password"]]} + helperText={ + errors[idToName["edit-new-password"]] + ? t(errors[idToName["edit-new-password"]]) + : "" + } endAdornment={} flex={1} /> @@ -219,7 +227,11 @@ const PasswordPanel = () => { value={localData.confirm} onChange={handleChange} error={errors[idToName["edit-confirm-password"]] ? true : false} - helperText={errors[idToName["edit-confirm-password"]]} + helperText={ + errors[idToName["edit-confirm-password"]] + ? t(errors[idToName["edit-confirm-password"]]) + : "" + } endAdornment={} flex={1} /> diff --git a/client/src/Pages/Account/components/ProfilePanel.jsx b/client/src/Pages/Account/components/ProfilePanel.jsx index 7c73e7aa3..dfe508879 100644 --- a/client/src/Pages/Account/components/ProfilePanel.jsx +++ b/client/src/Pages/Account/components/ProfilePanel.jsx @@ -2,16 +2,16 @@ 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/Avatar"; -import TextInput from "../../../Components/Inputs/TextInput"; -import ImageUpload from "../../../Components/Inputs/ImageUpload"; -import { newOrChangedCredentials } from "../../../Validation/validation"; +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"; -import { createToast } from "../../../Utils/toastUtils"; -import { logger } from "../../../Utils/Logger"; -import { GenericDialog } from "../../../Components/Dialog/genericDialog"; -import Dialog from "../../../Components/Dialog"; +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"; /** @@ -210,7 +210,11 @@ const ProfilePanel = () => { autoComplete="given-name" onChange={handleChange} error={errors[idToName["edit-first-name"]] ? true : false} - helperText={errors[idToName["edit-first-name"]]} + helperText={ + errors[idToName["edit-first-name"]] + ? t(errors[idToName["edit-first-name"]]) + : "" + } flex={1} /> @@ -228,7 +232,11 @@ const ProfilePanel = () => { value={localData.lastName} onChange={handleChange} error={errors[idToName["edit-last-name"]] ? true : false} - helperText={errors[idToName["edit-last-name"]]} + helperText={ + errors[idToName["edit-last-name"]] + ? t(errors[idToName["edit-last-name"]]) + : "" + } flex={1} /> diff --git a/client/src/Pages/Account/components/RoleTable/index.jsx b/client/src/Pages/Account/components/RoleTable/index.jsx index 9d6ba3a16..90a30dcd4 100644 --- a/client/src/Pages/Account/components/RoleTable/index.jsx +++ b/client/src/Pages/Account/components/RoleTable/index.jsx @@ -1,9 +1,9 @@ import Typography from "@mui/material/Typography"; -import DataTable from "../../../../Components/Table"; -import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; -import { ROLES } from "../../../../Utils/roleUtils"; +import { ROLES } from "../../../../Utils/roleUtils.js"; const RoleTable = ({ roles, handleDeleteRole }) => { const { t } = useTranslation(); @@ -21,11 +21,13 @@ const RoleTable = ({ roles, handleDeleteRole }) => { render: (row) => { if (row === ROLES.SUPERADMIN) return null; return ( - { handleDeleteRole(row); }} - sx={{ cursor: "pointer" }} + style={{ cursor: "pointer" }} /> ); }, diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx index fc1768642..bb40a5e4f 100644 --- a/client/src/Pages/Account/components/TeamPanel.jsx +++ b/client/src/Pages/Account/components/TeamPanel.jsx @@ -3,18 +3,18 @@ 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/Inputs/TextInput"; -import { newOrChangedCredentials } from "../../../Validation/validation"; -import { networkService } from "../../../main"; -import { createToast } from "../../../Utils/toastUtils"; -import Select from "../../../Components/Inputs/Select"; -import { GenericDialog } from "../../../Components/Dialog/genericDialog"; -import AddTeamMember from "../components/AddTeamMember"; -import DataTable from "../../../Components/Table"; -import { useGetInviteToken } from "../../../Hooks/inviteHooks"; +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/inviteHooks.js"; import { useNavigate } from "react-router-dom"; -import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin"; -import AddMemberMenu from "./AddMemberMenu"; +import { useIsSuperAdmin } from "@/Hooks/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, @@ -104,7 +104,7 @@ const TeamPanel = () => { team = team.map((member) => ({ ...member, - id: member._id, + id: member.id, role: member.role.map((role) => ROLE_MAP[role]).join(","), })); setData(team); @@ -268,6 +268,7 @@ const TeamPanel = () => { cursor: isSuperAdmin ? "pointer" : "default", }, onRowClick: (row) => { + console.log(row); if (isSuperAdmin) { navigate(`/account/team/${row.id}`); } diff --git a/client/src/Pages/Account/index.jsx b/client/src/Pages/Account/index.jsx index 37c64aca6..ae16ff9d7 100644 --- a/client/src/Pages/Account/index.jsx +++ b/client/src/Pages/Account/index.jsx @@ -3,11 +3,11 @@ import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { useSelector } from "react-redux"; import { Box, Tab, useTheme } from "@mui/material"; -import CustomTabList from "../../Components/Tab"; +import CustomTabList from "@/Components/v1/Tab/index.jsx"; import TabContext from "@mui/lab/TabContext"; -import ProfilePanel from "./components/ProfilePanel"; -import PasswordPanel from "./components/PasswordPanel"; -import TeamPanel from "./components/TeamPanel"; +import ProfilePanel from "./components/ProfilePanel.jsx"; +import PasswordPanel from "./components/PasswordPanel.jsx"; +import TeamPanel from "./components/TeamPanel.jsx"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/Auth/CheckEmail.jsx b/client/src/Pages/Auth/CheckEmail.jsx index e1f5e43ed..95261a768 100644 --- a/client/src/Pages/Auth/CheckEmail.jsx +++ b/client/src/Pages/Auth/CheckEmail.jsx @@ -3,13 +3,13 @@ import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../Utils/toastUtils"; -import { forgotPassword } from "../../Features/Auth/authSlice"; +import { createToast } from "../../Utils/toastUtils.jsx"; +import { forgotPassword } from "../../Features/Auth/authSlice.js"; import { Trans, useTranslation } from "react-i18next"; -import Background from "../../assets/Images/background-grid.svg?react"; -import EmailIcon from "../../assets/icons/email.svg?react"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import IconBox from "../../Components/IconBox"; +import Background from "@/assets/Images/background-grid.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; +import Icon from "@/Components/v1/Icon"; import "./index.css"; const CheckEmail = () => { @@ -160,7 +160,7 @@ const CheckEmail = () => { svgHeight={24} mb={theme.spacing(4)} > - + {t("auth.forgotPassword.heading")} diff --git a/client/src/Pages/Auth/ForgotPassword.jsx b/client/src/Pages/Auth/ForgotPassword.jsx index 2a887756e..e6ba4c1f7 100644 --- a/client/src/Pages/Auth/ForgotPassword.jsx +++ b/client/src/Pages/Auth/ForgotPassword.jsx @@ -1,16 +1,16 @@ import { Box, Stack, Typography, Button } from "@mui/material"; import { useTheme } from "@emotion/react"; -import { createToast } from "../../Utils/toastUtils"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useDispatch, useSelector } from "react-redux"; -import { forgotPassword } from "../../Features/Auth/authSlice"; +import { forgotPassword } from "../../Features/Auth/authSlice.js"; import { useEffect, useState } from "react"; -import { newOrChangedCredentials } from "../../Validation/validation"; +import { newOrChangedCredentials } from "../../Validation/validation.js"; import { useNavigate } from "react-router-dom"; -import TextInput from "../../Components/Inputs/TextInput"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import Key from "../../assets/icons/key.svg?react"; -import Background from "../../assets/Images/background-grid.svg?react"; -import IconBox from "../../Components/IconBox"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; +import Icon from "@/Components/v1/Icon"; import { Trans, useTranslation } from "react-i18next"; import "./index.css"; @@ -165,7 +165,7 @@ const ForgotPassword = () => { svgHeight={24} mb={theme.spacing(4)} > - + {t("auth.forgotPassword.heading")} diff --git a/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx b/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx index 6bc5716ff..b009793d5 100644 --- a/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx +++ b/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx @@ -1,7 +1,7 @@ import { useDispatch } from "react-redux"; -import { login } from "../../../../Features/Auth/authSlice"; +import { login } from "../../../../Features/Auth/authSlice.js"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useLoginSubmit = () => { diff --git a/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx b/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx index e5d045be8..0872213ad 100644 --- a/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx +++ b/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { loginCredentials } from "../../../../Validation/validation"; +import { loginCredentials } from "../../../../Validation/validation.js"; const useValidateLoginForm = () => { const [errors, setErrors] = useState({ diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx index d06ecef6d..7f5dcb5e4 100644 --- a/client/src/Pages/Auth/Login/index.jsx +++ b/client/src/Pages/Auth/Login/index.jsx @@ -1,17 +1,17 @@ // Components import Stack from "@mui/material/Stack"; import Button from "@mui/material/Button"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import TextLink from "../../../Components/TextLink"; -import AuthPageWrapper from "../components/AuthPageWrapper"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; +import TextLink from "@/Components/v1/TextLink/index.jsx"; +import AuthPageWrapper from "../components/AuthPageWrapper.jsx"; // Utils import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import useLoginForm from "./hooks/useLoginForm"; -import useValidateLoginForm from "./hooks/useValidateLoginForm"; -import useLoginSubmit from "./hooks/useLoginSubmit"; -import useLoadingSubmit from "./hooks/useLoadingSubmit"; +import useLoginForm from "./hooks/useLoginForm.jsx"; +import useValidateLoginForm from "./hooks/useValidateLoginForm.jsx"; +import useLoginSubmit from "./hooks/useLoginSubmit.jsx"; +import useLoadingSubmit from "./hooks/useLoadingSubmit.jsx"; const Login = () => { // Hooks diff --git a/client/src/Pages/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/Auth/NewPasswordConfirmed.jsx index 7ace77abc..c92e52552 100644 --- a/client/src/Pages/Auth/NewPasswordConfirmed.jsx +++ b/client/src/Pages/Auth/NewPasswordConfirmed.jsx @@ -2,11 +2,11 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { useDispatch } from "react-redux"; -import { clearAuthState } from "../../Features/Auth/authSlice"; -import Background from "../../assets/Images/background-grid.svg?react"; -import ConfirmIcon from "../../assets/icons/check-outlined.svg?react"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import IconBox from "../../Components/IconBox"; +import { clearAuthState } from "../../Features/Auth/authSlice.js"; +import Background from "@/assets/Images/background-grid.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; +import Icon from "@/Components/v1/Icon"; import { Trans, useTranslation } from "react-i18next"; import "./index.css"; @@ -94,7 +94,7 @@ const NewPasswordConfirmed = () => { svgHeight={24} mb={theme.spacing(4)} > - + {t("auth.forgotPassword.heading")} diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx index 2b45d2d15..821c56427 100644 --- a/client/src/Pages/Auth/Register/index.jsx +++ b/client/src/Pages/Auth/Register/index.jsx @@ -1,10 +1,10 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import TextInput from "../../../Components/Inputs/TextInput"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; -import PasswordTooltip from "../components/PasswordTooltip"; +import PasswordTooltip from "../components/PasswordTooltip.jsx"; // Utils import { useTheme } from "@emotion/react"; @@ -13,11 +13,11 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; -import { networkService } from "../../../main"; -import { newOrChangedCredentials } from "../../../Validation/validation"; -import { register } from "../../../Features/Auth/authSlice"; -import AuthPageWrapper from "../components/AuthPageWrapper"; -import { createToast } from "../../../Utils/toastUtils"; +import { networkService } from "../../../main.jsx"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; +import { register } from "../../../Features/Auth/authSlice.js"; +import AuthPageWrapper from "../components/AuthPageWrapper.jsx"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import PropTypes from "prop-types"; const getFeedbackStatus = (form, errors, field, criteria) => { @@ -176,7 +176,7 @@ const Register = ({ superAdminExists }) => { delete toSubmit.confirm; - const action = await dispatch(register(toSubmit)); + const action = await dispatch(register({ user: toSubmit, token })); if (action.payload.success) { navigate("/uptime"); createToast({ diff --git a/client/src/Pages/Auth/SetNewPassword.jsx b/client/src/Pages/Auth/SetNewPassword.jsx index b4899f47c..ada5d6246 100644 --- a/client/src/Pages/Auth/SetNewPassword.jsx +++ b/client/src/Pages/Auth/SetNewPassword.jsx @@ -4,18 +4,18 @@ import { useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; -import { setNewPassword } from "../../Features/Auth/authSlice"; -import { createToast } from "../../Utils/toastUtils"; -import { newOrChangedCredentials } from "../../Validation/validation"; -import Check from "../../Components/Check/Check"; -import TextInput from "../../Components/Inputs/TextInput"; -import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; -import IconBox from "../../Components/IconBox"; -import LockIcon from "../../assets/icons/lock.svg?react"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import Background from "../../assets/Images/background-grid.svg?react"; +import { setNewPassword } from "../../Features/Auth/authSlice.js"; +import { createToast } from "../../Utils/toastUtils.jsx"; +import { newOrChangedCredentials } from "../../Validation/validation.js"; +import Check from "@/Components/v1/Check/Check.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; +import Icon from "@/Components/v1/Icon"; import "./index.css"; -import { useValidatePassword } from "./hooks/useValidatePassword"; +import { useValidatePassword } from "./hooks/useValidatePassword.jsx"; import { Trans, useTranslation } from "react-i18next"; const SetNewPassword = () => { @@ -140,7 +140,7 @@ const SetNewPassword = () => { svgHeight={24} mb={theme.spacing(4)} > - + {t("auth.forgotPassword.heading")} diff --git a/client/src/Pages/Auth/components/AuthHeader.jsx b/client/src/Pages/Auth/components/AuthHeader.jsx index ce454c065..56b69ef14 100644 --- a/client/src/Pages/Auth/components/AuthHeader.jsx +++ b/client/src/Pages/Auth/components/AuthHeader.jsx @@ -1,9 +1,9 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import LanguageSelector from "../../../Components/LanguageSelector"; -import ThemeSwitch from "../../../Components/ThemeSwitch"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; +import LanguageSelector from "@/Components/LanguageSelector.jsx"; +import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; // Utils import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Pages/Auth/components/AuthPageWrapper.jsx b/client/src/Pages/Auth/components/AuthPageWrapper.jsx index b83be19c5..a5fbe311b 100644 --- a/client/src/Pages/Auth/components/AuthPageWrapper.jsx +++ b/client/src/Pages/Auth/components/AuthPageWrapper.jsx @@ -1,10 +1,10 @@ -import Background from "../../../assets/Images/background-grid.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import AuthHeader from "../components/AuthHeader"; +import AuthHeader from "./AuthHeader.jsx"; import { useTheme } from "@mui/material/styles"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; import PropTypes from "prop-types"; const AuthPageWrapper = ({ children, heading, welcome }) => { diff --git a/client/src/Pages/Auth/components/PasswordTooltip.jsx b/client/src/Pages/Auth/components/PasswordTooltip.jsx index bc2f28ec7..361850aca 100644 --- a/client/src/Pages/Auth/components/PasswordTooltip.jsx +++ b/client/src/Pages/Auth/components/PasswordTooltip.jsx @@ -1,4 +1,4 @@ -import Check from "../../../Components/Check/Check"; +import Check from "@/Components/v1/Check/Check.jsx"; import Stack from "@mui/material/Stack"; import { Tooltip, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; @@ -75,7 +75,7 @@ const PasswordTooltip = ({ feedback, form, children }) => { slotProps={{ tooltip: { sx: { - backgroundColor: theme.palette.tertiary.background, + backgroundColor: theme.palette.tertiary.main, border: `0.5px solid ${theme.palette.primary.lowContrast}90`, borderRadius: theme.spacing(4), color: theme.palette.primary.contrastText, @@ -88,7 +88,7 @@ const PasswordTooltip = ({ feedback, form, children }) => { }, arrow: { sx: { - color: theme.palette.tertiary.background, + color: theme.palette.tertiary.main, }, }, }} diff --git a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx index 4159e6166..73e8fb738 100644 --- a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx +++ b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { newOrChangedCredentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; const usePasswordFeedback = () => { const [feedback, setFeedback] = useState({}); diff --git a/client/src/Pages/Auth/hooks/useValidatePassword.jsx b/client/src/Pages/Auth/hooks/useValidatePassword.jsx index 102d8d73e..014804d35 100644 --- a/client/src/Pages/Auth/hooks/useValidatePassword.jsx +++ b/client/src/Pages/Auth/hooks/useValidatePassword.jsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import { newOrChangedCredentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; const getFeedbackStatus = (form, errors, field, criteria) => { const fieldErrors = errors[field]; diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx new file mode 100644 index 000000000..857f31bff --- /dev/null +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -0,0 +1,233 @@ +//Components +import Stack from "@mui/material/Stack"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableRow from "@mui/material/TableRow"; +import TableCell from "@mui/material/TableCell"; +import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; + +//Utils +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js"; +import { useSelector } from "react-redux"; +import { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { useFetchChecksTeam, useFetchChecksByMonitor } from "@/Hooks/checkHooks.js"; +import { Button, Typography, useTheme } from "@mui/material"; +import { lighten } from "@mui/material/styles"; + +const GetTooltip = (row) => { + const theme = useTheme(); + const phases = row?.timings?.phases; + + const phaseKeyFormattingMap = { + firstByte: "first byte", + }; + return ( + + {`Status code: ${row?.statusCode}`} + {`Response time: ${row?.responseTime} ms`} + {phases && ( + <> + {`Request timing: `} + + + {Object.keys(phases)?.map((phaseKey) => ( + + + + {`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`} + + + + {`${phases[phaseKey]} ms`} + + + ))} + +
+ + )} +
+ ); +}; + +const IncidentTable = ({ + isLoading, + monitors, + selectedMonitor, + filter, + dateRange, + updateTrigger, +}) => { + //Redux state + const uiTimezone = useSelector((state) => state.ui.timezone); + + //Local state + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const selectedMonitorDetails = monitors?.[selectedMonitor]; + const selectedMonitorType = selectedMonitorDetails?.type; + + //Hooks + const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = + useFetchChecksByMonitor({ + monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, + type: selectedMonitorType, + status: false, + sortOrder: "desc", + limit: null, + dateRange, + filter: filter === "resolved" ? "all" : filter, + ack: filter === "resolved" ? true : false, + page: page, + rowsPerPage: rowsPerPage, + enabled: selectedMonitor !== "0", + updateTrigger, + }); + + const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] = + useFetchChecksTeam({ + status: false, + sortOrder: "desc", + limit: null, + dateRange, + filter: filter === "resolved" ? "all" : filter, + ack: filter === "resolved" ? true : false, + page: page, + rowsPerPage: rowsPerPage, + enabled: selectedMonitor === "0", + updateTrigger, + }); + + const checks = selectedMonitor === "0" ? checksTeam : checksMonitor; + const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor; + isLoading = isLoadingTeam || isLoadingMonitor; + const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor; + + const { t } = useTranslation(); + + //Handlers + const handleChangePage = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); + }; + + const headers = [ + { + id: "monitorName", + content: t("incidentsTableMonitorName"), + render: (row) => { + return monitors?.[row.metadata?.monitorId]?.name || "N/A"; + }, + }, + { + id: "status", + content: t("incidentsTableStatus"), + render: (row) => { + const status = row.status === true ? "up" : "down"; + return ( + + ); + }, + }, + { + id: "dateTime", + content: t("incidentsTableDateTime"), + render: (row) => { + const formattedDate = formatDateWithTz( + row.createdAt, + "YYYY-MM-DD HH:mm:ss A", + uiTimezone + ); + return formattedDate; + }, + }, + { + id: "statusCode", + content: t("incidentsTableStatusCode"), + render: (row) => , + }, + { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, + ]; + + if (isLoading) return ; + + if (networkError) { + return ( + + + + ); + } + + if (!isLoading && typeof checksCount === "undefined") { + return {t("incidentsTableNoIncidents")}; + } + + return ( + <> + + + + ); +}; + +IncidentTable.propTypes = { + isLoading: PropTypes.bool, + monitors: PropTypes.object, + selectedMonitor: PropTypes.string, + filter: PropTypes.string, + dateRange: PropTypes.string, + updateTrigger: PropTypes.bool, + setUpdateTrigger: PropTypes.func, +}; +export default IncidentTable; diff --git a/client/src/Pages/Checks/Components/OptionsHeader/index.jsx b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx new file mode 100644 index 000000000..74bea4711 --- /dev/null +++ b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx @@ -0,0 +1,140 @@ +// Components +import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; +import PropTypes from "prop-types"; + +//Utils +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton.jsx"; +import { useTranslation } from "react-i18next"; + +const OptionsHeader = ({ + shouldRender, + selectedMonitor = 0, + setSelectedMonitor, + monitors, + filter = "all", + setFilter, + dateRange = "hour", + setDateRange, +}) => { + const theme = useTheme(); + const { t } = useTranslation(); + console.log(monitors); + const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; + const filterOptions = [ + { id: "all", name: t("incidentsOptionsHeaderFilterAll") }, + { id: "down", name: t("incidentsOptionsHeaderFilterDown") }, + { id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, + { id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, + ]; + + // The stacks below which are three in number have the same style so + const stackStyles = { + direction: "row", + alignItems: "center", + gap: theme.spacing(6), + }; + + if (!shouldRender) return ; + + return ( + + + + {t("incidentsOptionsHeader")} + + setFilter(e.target.value)} + items={filterOptions} + sx={{ + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastTextSecondary, + }} + /> + + + + {t("incidentsOptionsHeaderShow")} + + + + + + + + + + ); +}; + +OptionsHeader.propTypes = { + shouldRender: PropTypes.bool, + selectedMonitor: PropTypes.string, + setSelectedMonitor: PropTypes.func, + monitors: PropTypes.object, + filter: PropTypes.string, + setFilter: PropTypes.func, + dateRange: PropTypes.string, + setDateRange: PropTypes.func, +}; + +export default OptionsHeader; diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/skeleton.jsx b/client/src/Pages/Checks/Components/OptionsHeader/skeleton.jsx similarity index 100% rename from client/src/Pages/Incidents/Components/OptionsHeader/skeleton.jsx rename to client/src/Pages/Checks/Components/OptionsHeader/skeleton.jsx diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx similarity index 84% rename from client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx rename to client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx index 7b20e3d51..d7f02ada3 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx +++ b/client/src/Pages/Checks/Components/StatusBoxes/StatusBox.jsx @@ -1,11 +1,8 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Background from "../../../../assets/Images/background-grid.svg?react"; -import AlertIcon from "../../../../assets/icons/alert-icon.svg?react"; -import CheckIcon from "../../../../assets/icons/check-icon.svg?react"; -import CloseIcon from "../../../../assets/icons/close-icon.svg?react"; -import WarningIcon from "../../../../assets/icons/warning-icon.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; +import Icon from "@/Components/v1/Icon"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); @@ -26,28 +23,40 @@ const StatusBox = ({ title, value, status }) => { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } else { color = theme.palette.accent.main; icon = ( - + ); } diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx b/client/src/Pages/Checks/Components/StatusBoxes/index.jsx similarity index 87% rename from client/src/Pages/Incidents/Components/StatusBoxes/index.jsx rename to client/src/Pages/Checks/Components/StatusBoxes/index.jsx index 6ba2f1820..bb993111a 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/Checks/Components/StatusBoxes/index.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; import { Stack } from "@mui/material"; -import StatusBox from "./StatusBox"; +import StatusBox from "./StatusBox.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; const StatusBoxes = ({ isLoading, summary }) => { const theme = useTheme(); @@ -17,7 +17,7 @@ const StatusBoxes = ({ isLoading, summary }) => { > { + // Redux state + const { t } = useTranslation(); + + const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }]; + + // Local state + const [selectedMonitor, setSelectedMonitor] = useState("0"); + const [filter, setFilter] = useState(undefined); + const [dateRange, setDateRange] = useState(undefined); + const [monitorLookup, setMonitorLookup] = useState(undefined); + const [updateTrigger, setUpdateTrigger] = useState(false); + + //Hooks + + //Utils + const theme = useTheme(); + const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); + const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ + updateTrigger, + }); + const { monitorId } = useParams(); + + useEffect(() => { + if (monitorId) { + setSelectedMonitor(monitorId); + } + }, [monitorId]); + + useEffect(() => { + const monitorLookup = monitors?.reduce((acc, monitor) => { + acc[monitor.id] = { + id: monitor.id, + name: monitor.name, + type: monitor.type, + }; + return acc; + }, {}); + setMonitorLookup(monitorLookup); + }, [monitors]); + + if (networkError || networkErrorSummary) { + return ( + + + + ); + } + + return ( + + + + + + + + ); +}; + +export default Checks; diff --git a/client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx b/client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx new file mode 100644 index 000000000..fda257695 --- /dev/null +++ b/client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx @@ -0,0 +1,134 @@ +import PropTypes from "prop-types"; +import { Box, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx"; +import Icon from "@/Components/v1/Icon"; +import SummaryCard from "../SummaryCard/index.jsx"; + +/** + * ActiveIncidentsPanel Component + * + * Displays a quick overview of currently active incidents. + * Shows only essential information: monitor name, status badge, and duration. + * + * @param {Object} props + * @param {Array} props.incidents - Array of active incidents + * @param {number} props.totalCount - Total count of active incidents (for badge) + * @param {boolean} props.isLoading - Loading state + * @param {Object} props.error - Error object if any + * @param {Function} props.onIncidentClick - Callback when incident is clicked (optional) + */ +const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + if (isLoading) { + return ; + } + + if (error) { + return ( + + + {t("incidentsPage.incidentsActivePanelError")} + + + ); + } + + if (!totalCount || totalCount === 0) { + return ( + + + + + + + + {t("incidentsPage.allSystemsAreOperational")} + + + + ); + } + + return ( + + + + + + + + {totalCount} + + + + ); +}; + +ActiveIncidentsPanel.propTypes = { + totalCount: PropTypes.number, + isLoading: PropTypes.bool, + error: PropTypes.object, + onIncidentClick: PropTypes.func, +}; + +export default ActiveIncidentsPanel; diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx new file mode 100644 index 000000000..bfca9e93f --- /dev/null +++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx @@ -0,0 +1,415 @@ +// Components +import { Stack, Typography, Grid, Box, Button, Divider } from "@mui/material"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import SummaryCard from "../SummaryCard/index.jsx"; +import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; + +// Utils +import { useEffect, useState } from "react"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy.js"; +import { useSelector } from "react-redux"; +import PropTypes from "prop-types"; + +// Hooks +import useFetchIncidents from "../../hooks/useFetchIncidents.js"; +import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration.js"; + +const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { fetchIncidentById, resolveIncident } = useFetchIncidents(); + const [incidentData, setIncidentData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false); + const uiTimezone = useSelector((state) => state.ui.timezone); + const isActive = incidentData?.incident?.status === true; + const duration = useGetIncidentsDuration(incidentData?.incident || null, isActive); + + const statusColor = isActive ? theme.palette.error.main : theme.palette.success.main; + const toCapitalLetter = (text) => + typeof text === "string" && text.length > 0 + ? text.charAt(0).toUpperCase() + text.slice(1) + : text; + + const KeyValueRow = ({ label, value, sx = {} }) => ( + + + + {label} + + + + + {value ?? "-"} + + + + ); + + useEffect(() => { + const loadIncident = async () => { + if (!incidentId || !open) { + setIncidentData(null); + setIsLoading(false); + return; + } + + setIsLoading(true); + try { + const incidentData = await fetchIncidentById(incidentId); + setIncidentData(incidentData); + } catch (error) { + console.error("Error fetching incident:", error); + setIncidentData(null); + } finally { + setIsLoading(false); + } + }; + + loadIncident(); + }, [incidentId, open, fetchIncidentById]); + + const openResolveDialog = () => { + setIsResolveDialogOpen(true); + }; + + const closeResolveDialog = () => { + setIsResolveDialogOpen(false); + }; + + const handleAfterResolve = async () => { + if (!incidentId) return; + const updatedIncident = await fetchIncidentById(incidentId); + if (updatedIncident) { + setIncidentData(updatedIncident); + } + }; + + const renderContent = () => { + console.log(incidentData); + + if (isLoading) { + return ( + + ); + } + + if (!incidentData) { + return ( + + ); + } + + return ( + + + + + + + + {isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")} + + + + + + {t("incidentsPage.incidentItemMonitor")} + + + {incidentData?.monitor?.name || t("incidentsPage.unknownMonitor")} + + + + + {t("incidentsPage.URL")} + + + {incidentData?.monitor?.url || "-"} + + + + + + + + + + + + + + {t("incidentsPage.timeline")} + + + + + {!isActive && ( + + )} + + + + + + + + + + + {isActive + ? t("incidentsPage.errorDetails") + : t("incidentsPage.resolutionDetails")} + + + + + + + + + {!isActive && ( + <> + + {incidentData?.incident?.resolutionType === "manual" && ( + + )} + + )} + + + + + + + + ); + }; + + return ( + + + {renderContent()} + {!isActive && + incidentData?.incident?.resolutionType === "manual" && + incidentData?.user?.email && ( + + {t("incidentsPage.resolvedBy")}: {incidentData.user.email} + + )} + + {isActive && ( + + )} + + + + + + + ); +}; + +IncidentDetailsModal.propTypes = { + open: PropTypes.bool.isRequired, + incidentId: PropTypes.string, + onClose: PropTypes.func.isRequired, + onResolved: PropTypes.func, +}; + +export default IncidentDetailsModal; diff --git a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx new file mode 100644 index 000000000..d125a9fdb --- /dev/null +++ b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx @@ -0,0 +1,156 @@ +// Components +import IconButton from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Icon from "@/Components/v1/Icon/index.jsx"; +import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; + +// Utils +import { useState } from "react"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { TypeToPathMap } from "@/Utils/monitorUtilsLegacy.js"; +import { useNavigate } from "react-router-dom"; +import PropTypes from "prop-types"; + +const IncidentActionsMenu = ({ incident, monitor, onResolve, onOpenDetails }) => { + const [anchorEl, setAnchorEl] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const theme = useTheme(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const openMenu = (event) => { + event.preventDefault(); + event.stopPropagation(); + setAnchorEl(event.currentTarget); + }; + + const closeMenu = (e) => { + if (e) { + e.stopPropagation(); + } + setAnchorEl(null); + }; + + const openResolveDialog = (e) => { + closeMenu(e); + setIsDialogOpen(true); + }; + + const closeDialog = (e) => { + if (e) { + e.stopPropagation(); + } + setIsDialogOpen(false); + }; + + const isActive = incident.status === true; + const handleGoToDetails = (e) => { + e.stopPropagation(); + closeMenu(e); + if (onOpenDetails) { + onOpenDetails(incident.id); + } + }; + + const handleGoToMonitor = async (e) => { + e.stopPropagation(); + closeMenu(e); + try { + if (monitor) { + const path = TypeToPathMap[monitor.type]; + if (path && monitor.id) { + navigate(`/${path}/${monitor.id}`); + } else { + console.error(`Monitor type not found`); + } + } else { + console.error("Monitor information not found in incident data"); + } + } catch (error) { + console.error("Error fetching incident details:", error); + } + }; + + return ( + <> + { + event.stopPropagation(); + openMenu(event); + }} + sx={{ + "&:focus": { + outline: "none", + }, + "& svg path": { + stroke: theme.palette.primary.contrastTextTertiary, + }, + }} + > + + + + + + {t("incidentsPage.incidentsTableActionDetails", "Details")} + + + {t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")} + + {isActive && ( + { + e.stopPropagation(); + openResolveDialog(e); + }} + > + {t("incidentsPage.incidentsTableActionResolveManually")} + + )} + + + + ); +}; + +IncidentActionsMenu.propTypes = { + incident: PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.bool.isRequired, + }).isRequired, + monitor: PropTypes.shape({ + id: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + }).isRequired, + onResolve: PropTypes.func.isRequired, + onOpenDetails: PropTypes.func, +}; + +export default IncidentActionsMenu; diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx index df9174290..2a03a8822 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -1,223 +1,144 @@ //Components -import Stack from "@mui/material/Stack"; -import DataTable from "../../../../Components/Table"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableRow from "@mui/material/TableRow"; -import TableCell from "@mui/material/TableCell"; -import TableSkeleton from "../../../../Components/Table/skeleton"; -import Pagination from "../../../../Components/Table/TablePagination"; -import { StatusLabel } from "../../../../Components/Label"; -import { HttpStatusLabel } from "../../../../Components/HttpStatusLabel"; -import GenericFallback from "../../../../Components/GenericFallback"; -import NetworkError from "../../../../Components/GenericFallback/NetworkError"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; +import IncidentActionsMenu from "./IncidentActionsMenu.jsx"; //Utils -import { formatDateWithTz } from "../../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; import { useSelector } from "react-redux"; -import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { useFetchChecksTeam } from "../../../../Hooks/checkHooks"; -import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks"; -import { useResolveIncident } from "../../../../Hooks/checkHooks"; -import { Button, Typography, useTheme } from "@mui/material"; -import { lighten } from "@mui/material/styles"; - -const GetTooltip = (row) => { - const theme = useTheme(); - const phases = row?.timings?.phases; - - const phaseKeyFormattingMap = { - firstByte: "first byte", - }; - return ( - - {`Status code: ${row?.statusCode}`} - {`Response time: ${row?.responseTime} ms`} - {phases && ( - <> - {`Request timing: `} - - - {Object.keys(phases)?.map((phaseKey) => ( - - - - {`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`} - - - - {`${phases[phaseKey]} ms`} - - - ))} - -
- - )} -
- ); -}; +import { Typography, useTheme } from "@mui/material"; const IncidentTable = ({ - isLoading, - monitors, - selectedMonitor, - filter, - dateRange, - updateTrigger, - setUpdateTrigger, + monitors = [], + incidents = [], + incidentsCount = 0, + isLoading = false, + networkError = false, + page = 0, + rowsPerPage = 10, + handleChangePage, + handleChangeRowsPerPage, + resolveIncident, + handleUpdateTrigger, + onOpenDetails, }) => { - //Redux state const uiTimezone = useSelector((state) => state.ui.timezone); - //Local state - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const selectedMonitorDetails = monitors?.[selectedMonitor]; - const selectedMonitorType = selectedMonitorDetails?.type; - - //Hooks - const [resolveIncident, resolveLoading] = useResolveIncident(); - - const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = - useFetchChecksByMonitor({ - monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, - type: selectedMonitorType, - status: false, - sortOrder: "desc", - limit: null, - dateRange, - filter: filter === "resolved" ? "all" : filter, - ack: filter === "resolved" ? true : false, - page: page, - rowsPerPage: rowsPerPage, - enabled: selectedMonitor !== "0", - updateTrigger, - }); - - const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] = - useFetchChecksTeam({ - status: false, - sortOrder: "desc", - limit: null, - dateRange, - filter: filter === "resolved" ? "all" : filter, - ack: filter === "resolved" ? true : false, - page: page, - rowsPerPage: rowsPerPage, - enabled: selectedMonitor === "0", - updateTrigger, - }); - - const checks = selectedMonitor === "0" ? checksTeam : checksMonitor; - const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor; - isLoading = isLoadingTeam || isLoadingMonitor; - const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor; - const { t } = useTranslation(); + const theme = useTheme(); - //Handlers - const handleChangePage = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(event.target.value); - }; - - const handleResolveIncident = (checkId) => { - resolveIncident(checkId, setUpdateTrigger); + const handleResolveIncident = async (incidentId, options = {}) => { + try { + await resolveIncident(incidentId, options); + handleUpdateTrigger(); + } catch (error) { + console.error(t("incidentsPage.errorResolvingIncident"), error); + } }; const headers = [ { id: "monitorName", content: t("incidentsTableMonitorName"), - render: (row) => monitors[row.monitorId]?.name ?? "N/A", + render: (row) => { + const monitor = monitors.find((monitor) => monitor.id === row.monitorId); + return monitor ? monitor.name : "N/A"; + }, }, { id: "status", content: t("incidentsTableStatus"), render: (row) => { - const status = row.status === true ? "up" : "down"; + const status = row.status === true ? "down" : "up"; + const statusText = + row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved"); return ( ); }, }, { - id: "dateTime", - content: t("incidentsTableDateTime"), + id: "startTime", + content: t("incidentsPage.startTime"), render: (row) => { const formattedDate = formatDateWithTz( - row.createdAt, + row.startTime || row.createdAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone ); return formattedDate; }, }, + { + id: "endTime", + content: t("incidentsPage.endTime"), + render: (row) => { + if (row.endTime) { + return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone); + } + return "-"; + }, + }, + { + id: "resolutionType", + content: t("incidentsPage.resolutionType"), + render: (row) => { + if (row.resolutionType) { + return ( + + {row.resolutionType} + + ); + } + return "-"; + }, + }, { id: "statusCode", content: t("incidentsTableStatusCode"), render: (row) => , }, - { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, + { + id: "message", + content: t("incidentsTableMessage"), + render: (row) => row.message || "-", + }, { id: "action", content: t("actions"), render: (row) => { - return row.ack === false ? ( - - ) : ( - - {t("incidentsTableResolvedAt")}{" "} - {formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)} - + return ( + monitor.id === row.monitorId)} + onResolve={handleResolveIncident} + onOpenDetails={onOpenDetails} + /> ); }, }, ]; - if (isLoading || resolveLoading) return ; + if (isLoading) return ; if (networkError) { return ( @@ -227,20 +148,25 @@ const IncidentTable = ({ ); } - if (!isLoading && typeof checksCount === "undefined") { - return {t("incidentsTableNoIncidents")}; + if (!isLoading && !networkError && incidents?.length === 0) { + return ( + + {t("incidentsTableNoIncidents", "No incidents found")} + + ); } + const incidentsData = Array.isArray(incidents) ? incidents : []; + return ( <> { + const { fetchIncidentSummary } = useFetchIncidents(); + const [summary, setSummary] = useState({ + totalActive: 0, + avgResolutionTimeHours: 0, + total: 0, + topMonitor: null, + totalManualResolutions: 0, + totalAutomaticResolutions: 0, + latestIncidents: [], + }); + + const [isLoadingSummary, setIsLoadingSummary] = useState(false); + const [summaryError, setSummaryError] = useState(null); + + useEffect(() => { + const fetchSummary = async () => { + setIsLoadingSummary(true); + setSummaryError(null); + + try { + const summaryData = await fetchIncidentSummary({ + limit: 3, + }); + + if (summaryData) { + setSummary(summaryData); + } else { + setSummaryError(new Error("Failed to fetch summary")); + } + } catch (error) { + console.error("Error fetching incident summary:", error); + setSummaryError(error); + } finally { + setIsLoadingSummary(false); + } + }; + + fetchSummary(); + }, [fetchIncidentSummary, updateTrigger]); + + return ( + <> + + + + + + + + + + + + + + ); +}; + +IncidentsSummaryPanel.propTypes = { + updateTrigger: PropTypes.bool.isRequired, +}; +export default IncidentsSummaryPanel; diff --git a/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/skeleton.jsx b/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/skeleton.jsx new file mode 100644 index 000000000..153cd7c25 --- /dev/null +++ b/client/src/Pages/Incidents/Components/IncidentsSummaryPanel/skeleton.jsx @@ -0,0 +1,20 @@ +import { Skeleton } from "@mui/material"; +import SummaryCard from "../SummaryCard/index.jsx"; + +/** + * Simple skeleton loader for summary panels + * Reusable for ActiveIncidentsPanel, LatestIncidentsPanel, and StatisticsPanel + */ +const PanelSkeleton = () => { + return ( + + + + ); +}; + +export default PanelSkeleton; diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx new file mode 100644 index 000000000..ca5bb640f --- /dev/null +++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/IncidentItem.jsx @@ -0,0 +1,96 @@ +import PropTypes from "prop-types"; +import { Box, Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; +import Icon from "@/Components/v1/Icon"; +import { useTranslation } from "react-i18next"; +import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration"; + +const IncidentItem = ({ incident }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isActive = incident.status === true; + const duration = useGetIncidentsDuration(incident, isActive); + const iconWrapperStyle = { + display: "flex", + justifyContent: "center", + mx: theme.spacing(2), + color: theme.palette.primary.contrastTextTertiary, + "& svg": { + width: 18, + height: 18, + }, + "& svg path": { + stroke: "currentColor", + strokeWidth: 1.5, + }, + }; + + return ( + + + + + + + {incident.monitorName || t("incidentsPage.unknownMonitor")} + + + + + + + + + {duration} + + + ); +}; + +IncidentItem.propTypes = { + incident: PropTypes.shape({ + _id: PropTypes.string, + id: PropTypes.string, + monitorId: PropTypes.string, + monitorName: PropTypes.string, + status: PropTypes.bool, + startTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), + endTime: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]), + resolutionType: PropTypes.oneOf(["automatic", "manual"]), + }), + onClick: PropTypes.func, +}; + +export default IncidentItem; diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx new file mode 100644 index 000000000..ff1ef9417 --- /dev/null +++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx @@ -0,0 +1,114 @@ +import PropTypes from "prop-types"; +import { Box, Stack, Typography, Divider } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx"; +import IncidentItem from "./IncidentItem.jsx"; +import SummaryCard from "../SummaryCard/index.jsx"; +import Icon from "@/Components/v1/Icon"; + +/** + * LatestIncidentsPanel Component + * + * Displays a quick overview of recent incidents (both active and resolved). + * Shows only essential information: monitor name, status, duration, and resolution type. + * + * @param {Object} props + * @param {Array} props.incidents - Array of recent incidents + * @param {boolean} props.isLoading - Loading state + * @param {Object} props.error - Error object if any + */ +const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + if (isLoading) { + return ; + } + + if (error) { + return ( + + + + {t("incidentsPage.incidentsLatestPanelError")} + + + + ); + } + + return ( + + {!incidents || incidents.length === 0 ? ( + + + + + + + {t("incidentsPage.incidentsLatestPanelEmpty")} + + + ) : ( + + {incidents.map((incident, index) => ( + + + {index < incidents.length - 1 && } + + ))} + + )} + + ); +}; + +LatestIncidentsPanel.propTypes = { + incidents: PropTypes.arrayOf( + PropTypes.shape({ + _id: PropTypes.string, + monitorId: PropTypes.string, + monitorName: PropTypes.string, + status: PropTypes.bool, + duration: PropTypes.string, + resolutionType: PropTypes.oneOf(["automatic", "manual"]), + }) + ), + isLoading: PropTypes.bool, + error: PropTypes.object, + onIncidentClick: PropTypes.func, +}; + +export default LatestIncidentsPanel; diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx index 259880223..8d7439576 100644 --- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx @@ -1,11 +1,10 @@ // Components import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import Select from "../../../../Components/Inputs/Select"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; import PropTypes from "prop-types"; //Utils import { useTheme } from "@emotion/react"; -import SkeletonLayout from "./skeleton"; import { useTranslation } from "react-i18next"; const OptionsHeader = ({ @@ -15,27 +14,28 @@ const OptionsHeader = ({ monitors, filter = "all", setFilter, - dateRange = "hour", + dateRange = "all", setDateRange, }) => { const theme = useTheme(); const { t } = useTranslation(); const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; + const filterOptions = [ - { _id: "all", name: t("incidentsOptionsHeaderFilterAll") }, - { _id: "down", name: t("incidentsOptionsHeaderFilterDown") }, - { _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, - { _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, + { _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") }, + { _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") }, + { _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") }, + { _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") }, + { _id: "automatic", name: t("incidentsPage.incidentsOptionsHeaderFilterAutomatic") }, ]; - // The stacks below which are three in number have the same style so const stackStyles = { direction: "row", alignItems: "center", gap: theme.spacing(6), }; - if (!shouldRender) return ; + if (!shouldRender) return; return ( { + const { t } = useTranslation(); + const theme = useTheme(); + const [comment, setComment] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (open) { + setComment(""); + } + }, [open]); + + const handleConfirm = async () => { + if (!incidentId) return; + const trimmed = comment.trim(); + try { + setIsSubmitting(true); + await onResolve(incidentId, trimmed ? { comment: trimmed } : {}); + if (onAfterResolve) { + await onAfterResolve(); + } + onResolved?.(); + onClose(); + } catch (error) { + console.error("Error resolving incident:", error); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + setComment(e.target.value)} + maxWidth="100%" + /> + + + + + + + ); +}; + +ResolveIncidentDialog.propTypes = { + open: PropTypes.bool.isRequired, + incidentId: PropTypes.string, + onClose: PropTypes.func.isRequired, + onResolve: PropTypes.func.isRequired, + onAfterResolve: PropTypes.func, + isLoading: PropTypes.bool, + onResolved: PropTypes.func, +}; + +export default ResolveIncidentDialog; diff --git a/client/src/Pages/Incidents/Components/StatisticsPanel/index.jsx b/client/src/Pages/Incidents/Components/StatisticsPanel/index.jsx new file mode 100644 index 000000000..7f07f688f --- /dev/null +++ b/client/src/Pages/Incidents/Components/StatisticsPanel/index.jsx @@ -0,0 +1,168 @@ +import PropTypes from "prop-types"; +import { Box, Stack, Typography, Divider } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx"; +import { useTranslation } from "react-i18next"; +import Icon from "@/Components/v1/Icon"; +import SummaryCard from "../SummaryCard/index.jsx"; + +/** + * StatisticsPanel Component + * + * Displays key metrics and statistics about incidents. + * + * @param {Object} props + * @param {Object} props.statistics - Statistics data object + * @param {number} props.statistics.total - Total incidents count + * @param {number} props.statistics.totalTrend - Trend percentage for total (optional) + * @param {string} props.statistics.mttr - Mean Time To Resolve (formatted string, e.g., "2h 15m") + * @param {number} props.statistics.mttrTrend - Trend percentage for MTTR (optional) + * @param {Object} props.statistics.topMonitor - Top monitor with most incidents + * @param {string} props.statistics.topMonitor.name - Monitor name + * @param {number} props.statistics.topMonitor.count - Incident count + * @param {number} props.statistics.topMonitor.percentage - Percentage of total + * @param {string} props.statistics.uptimeAffected - Total downtime (formatted string, e.g., "12h 45m") + * @param {number} props.statistics.availability - Availability percentage + * @param {boolean} props.isLoading - Loading state + * @param {Object} props.error - Error object if any + */ +const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const totalResolutions = + (summary?.totalManualResolutions || 0) + (summary?.totalAutomaticResolutions || 0); + + const iconWrapperStyle = { + display: "flex", + justifyContent: "center", + mx: theme.spacing(2), + color: theme.palette.primary.contrastTextTertiary, + "& svg": { + width: 18, + height: 18, + }, + "& svg path": { + stroke: "currentColor", + strokeWidth: 1.5, + }, + }; + if (isLoading) { + return ; + } + + if (error) { + return ( + + + + {t("incidentsPage.incidentsStatisticsPanelError")} + + + + ); + } + + const getMostAffectedMonitor = () => { + if (!summary.total || summary.total === 0) { + return t("incidentsPage.none"); + } + return summary.topMonitor?.monitorName || t("incidentsPage.none"); + }; + + const rowStyle = { + py: theme.spacing(0.5), + minHeight: 32, + }; + + return ( + + + + + + + + + {t("incidentsPage.totalIncidents")}: {summary.total || 0} + + + + + + + + + + + {t("incidentsPage.mostAffectedMonitor")}: {getMostAffectedMonitor()} + + + + + + + + + + {t("incidentsPage.avgResolutionTime")}:{" "} + {summary.total > 0 + ? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}` + : "N/A"} + + + + + ); +}; + +StatisticsPanel.propTypes = { + summary: PropTypes.object, + isLoading: PropTypes.bool, + error: PropTypes.object, +}; + +export default StatisticsPanel; diff --git a/client/src/Pages/Incidents/Components/SummaryCard/index.jsx b/client/src/Pages/Incidents/Components/SummaryCard/index.jsx new file mode 100644 index 000000000..3a8589dc1 --- /dev/null +++ b/client/src/Pages/Incidents/Components/SummaryCard/index.jsx @@ -0,0 +1,64 @@ +import { Paper } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import PropTypes from "prop-types"; +import { Box, Stack, Typography, Divider } from "@mui/material"; +const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null }) => { + const theme = useTheme(); + return ( + + {title && ( + + + + {title} + + + + + )} + + {children} + + + ); +}; +SummaryCard.propTypes = { + children: PropTypes.node, + isHighPriority: PropTypes.bool, + sx: PropTypes.object, + title: PropTypes.string, +}; +export default SummaryCard; diff --git a/client/src/Pages/Incidents/hooks/useFetchIncidents.js b/client/src/Pages/Incidents/hooks/useFetchIncidents.js new file mode 100644 index 000000000..e308c9319 --- /dev/null +++ b/client/src/Pages/Incidents/hooks/useFetchIncidents.js @@ -0,0 +1,215 @@ +import { useState, useCallback } from "react"; +import { networkService } from "../../../main.jsx"; +import { createToast } from "../../../Utils/toastUtils.jsx"; +import { useTranslation } from "react-i18next"; +/** + * Hook to fetch and manage incidents + * + * Provides multiple functions to fetch incidents with different filters. + * + * @returns {Object} { + * incidents, + * incidentsCount, + * isLoading, + * networkError, + * fetchIncidents, + * fetchActiveIncidents, + * fetchResolvedIncidents, + * fetchIncidentsByTeam, + * fetchIncidentById, + * resolveIncident, + * fetchIncidentSummary + * } + */ +const useFetchIncidents = () => { + const [incidents, setIncidents] = useState(null); + const [incidentsCount, setIncidentsCount] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [networkError, setNetworkError] = useState(false); + const { t } = useTranslation(); + /** + * Generic function to fetch incidents by team with custom config + * + * @param {Object} config - Configuration object + * @param {string} [config.monitorId] - Filter by monitor ID + * @param {boolean} [config.status] - Filter by status (true=active, false=resolved) + * @param {string} [config.resolutionType] - Filter by resolution type (automatic/manual) + * @param {string} [config.sortOrder] - Sort order (asc/desc) + * @param {string} [config.dateRange] - Date range filter + * @param {number} [config.page] - Page number + * @param {number} [config.rowsPerPage] - Rows per page + */ + const fetchIncidentsByTeam = useCallback(async (config = {}) => { + try { + setIsLoading(true); + setNetworkError(false); + + const res = await networkService.getIncidentsByTeam(config); + setIncidents(res.data?.data?.incidents || []); + setIncidentsCount(res.data?.data?.count || 0); + } catch (error) { + setNetworkError(true); + console.error(t("incidentsPage.errorFetchingIncidents"), error); + createToast({ body: error.message || t("incidentsPage.errorFetchingIncidents") }); + } finally { + setIsLoading(false); + } + }, []); + + /** + * Fetch active (open) incidents - Default function + * + * @param {Object} config - Additional configuration + */ + const fetchActiveIncidents = useCallback( + async (config = {}) => { + await fetchIncidentsByTeam({ + status: true, + sortOrder: "desc", + ...config, + }); + }, + [fetchIncidentsByTeam] + ); + + const fetchIncidentsByResolutionType = useCallback( + async (resolutionType, config = {}) => { + await fetchIncidentsByTeam({ + resolutionType, + sortOrder: "desc", + ...config, + }); + }, + [fetchIncidentsByTeam] + ); + /** + * Fetch resolved incidents + * + * @param {Object} config - Additional configuration + */ + const fetchResolvedIncidents = useCallback( + async (config = {}) => { + await fetchIncidentsByTeam({ + status: false, + sortOrder: "desc", + ...config, + }); + }, + [fetchIncidentsByTeam] + ); + + /** + * Fetch incidents with custom filters (alias for fetchIncidentsByTeam) + * + * @param {Object} config - Configuration object + */ + const fetchIncidents = useCallback( + async (config = {}) => { + await fetchIncidentsByTeam(config); + }, + [fetchIncidentsByTeam] + ); + + /** + * Fetch a single incident by ID + * + * @param {string} incidentId - The ID of the incident to fetch + * @returns {Promise} The incident object or null if not found + */ + const fetchIncidentById = useCallback(async (incidentId) => { + if (!incidentId) { + console.error(t("incidentsPage.noIncidentIdProvided")); + return null; + } + + try { + setIsLoading(true); + setNetworkError(false); + + const res = await networkService.getIncidentById(incidentId); + return res.data?.data || null; + } catch (error) { + setNetworkError(true); + console.error(t("incidentsPage.errorFetchingIncident"), error); + createToast({ body: error.message || t("incidentsPage.errorFetchingIncident") }); + return null; + } finally { + setIsLoading(false); + } + }, []); + + /** + * Resolve an incident manually + * + * @param {string} incidentId - The ID of the incident to resolve + * @param {Object} options - Resolution options + * @param {string} [options.comment] - Optional comment about the resolution + * @param {Function} [onSuccess] - Callback function to call on success + * @param {Function} [onError] - Callback function to call on error + */ + const resolveIncident = useCallback(async (incidentId, options = {}) => { + if (!incidentId) { + console.error(t("incidentsPage.noIncidentIdProvided")); + return; + } + + try { + setIsLoading(true); + setNetworkError(false); + + const res = await networkService.resolveIncidentManually(incidentId, options); + if (res.data?.success) { + createToast({ body: t("incidentsPage.incidentResolvedSuccessfully") }); + } else { + createToast({ body: t("incidentsPage.errorResolvingIncident") }); + } + } catch (error) { + setNetworkError(true); + console.error(t("incidentsPage.errorResolvingIncident"), error); + const errorMessage = + error.response?.data?.message || + error.message || + t("incidentsPage.errorResolvingIncident"); + createToast({ body: errorMessage }); + } finally { + setIsLoading(false); + } + }, []); + + /** + * Fetch incident summary + * + * @param {Object} config - Configuration object + * @param {number} [config.limit=10] - Number of latest incidents to return + * @returns {Promise} The summary object or null if error + */ + const fetchIncidentSummary = useCallback(async (config = {}) => { + try { + const res = await networkService.getIncidentSummary(config); + return res.data?.data || null; + } catch (error) { + console.error(t("incidentsPage.errorFetchingIncidentSummary"), error); + createToast({ + body: error.message || t("incidentsPage.errorFetchingIncidentSummary"), + }); + return null; + } + }, []); + + return { + incidents, + incidentsCount, + isLoading, + networkError, + fetchIncidents, + fetchActiveIncidents, + fetchResolvedIncidents, + fetchIncidentsByTeam, + fetchIncidentById, + resolveIncident, + fetchIncidentSummary, + fetchIncidentsByResolutionType, + }; +}; + +export default useFetchIncidents; diff --git a/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js new file mode 100644 index 000000000..a4f434a80 --- /dev/null +++ b/client/src/Pages/Incidents/hooks/useGetIncidentsDuration.js @@ -0,0 +1,25 @@ +import { getHumanReadableDuration } from "@/Utils/timeUtilsLegacy.js"; +const useGetIncidentsDuration = (incident, isActive) => { + if (!incident?.startTime) { + return "-"; + } + const startTime = new Date(incident?.startTime); + const endTime = isActive + ? new Date() + : incident?.endTime + ? new Date(incident.endTime) + : null; + + if (!endTime) { + return "-"; + } + + const durationMs = endTime.getTime() - startTime.getTime(); + + if (durationMs < 0) { + return "-"; + } + + return getHumanReadableDuration(durationMs); +}; +export default useGetIncidentsDuration; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 7696c453b..0377405b7 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -1,73 +1,113 @@ // Components import { Stack } from "@mui/material"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import GenericFallback from "../../Components/GenericFallback"; -import IncidentTable from "./Components/IncidentTable"; -import OptionsHeader from "./Components/OptionsHeader"; -import StatusBoxes from "./Components/StatusBoxes"; -import { Box, Button } from "@mui/material"; - +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import IncidentTable from "./Components/IncidentTable/index.jsx"; +import OptionsHeader from "./Components/OptionsHeader/index.jsx"; +import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx"; +import IncidentDetailsModal from "./Components/IncidentDetailsModal/index.jsx"; //Utils import { useTheme } from "@emotion/react"; -import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks"; -import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks"; -import { useAcknowledgeChecks } from "../../Hooks/checkHooks"; import { useState, useEffect } from "react"; -import NetworkError from "../../Components/GenericFallback/NetworkError"; +import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; -//Constants -const Incidents = () => { - // Redux state +// Hooks +import useFetchIncidents from "./hooks/useFetchIncidents.js"; +import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js"; + +const Incidents2 = () => { + const { monitorId } = useParams(); const { t } = useTranslation(); const BREADCRUMBS = [ { name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }, ]; - // Local state - const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState(undefined); - const [dateRange, setDateRange] = useState(undefined); + const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); + const [filter, setFilter] = useState("all"); + const [dateRange, setDateRange] = useState("all"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); const [monitorLookup, setMonitorLookup] = useState(undefined); const [updateTrigger, setUpdateTrigger] = useState(false); + const [isDetailsOpen, setIsDetailsOpen] = useState(false); + const [selectedIncidentId, setSelectedIncidentId] = useState(null); + const handleUpdateTrigger = () => { + setUpdateTrigger((prev) => !prev); + }; - //Hooks - const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks(); + const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId( + {} + ); + + const { + incidents, + incidentsCount, + isLoading: isLoadingIncidents, + networkError: incidentsNetworkError, + fetchIncidents, + fetchActiveIncidents, + fetchResolvedIncidents, + resolveIncident, + fetchIncidentsByResolutionType, + } = useFetchIncidents(); + + const networkError = monitorsNetworkError || incidentsNetworkError; - //Utils const theme = useTheme(); - const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); - const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ - updateTrigger, - }); - const { monitorId } = useParams(); useEffect(() => { - if (monitorId) { - setSelectedMonitor(monitorId); + setPage(0); + }, [selectedMonitor, filter, dateRange]); + + useEffect(() => { + const config = { + monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined, + sortOrder: "desc", + dateRange, + page, + rowsPerPage, + }; + + if (filter === "active") { + fetchActiveIncidents(config); + } else if (filter === "resolved") { + fetchResolvedIncidents(config); + } else if (filter === "manual") { + fetchIncidentsByResolutionType("manual", config); + } else if (filter === "automatic") { + fetchIncidentsByResolutionType("automatic", config); + } else { + fetchIncidents(config); } - }, [monitorId]); + }, [ + selectedMonitor, + filter, + dateRange, + page, + rowsPerPage, + updateTrigger, + fetchActiveIncidents, + fetchResolvedIncidents, + fetchIncidents, + fetchIncidentsByResolutionType, + ]); useEffect(() => { - const monitorLookup = monitors?.reduce((acc, monitor) => { - acc[monitor._id] = { - _id: monitor._id, + const lookup = monitors?.reduce((acc, monitor) => { + acc[monitor.id] = { + id: monitor.id, name: monitor.name, type: monitor.type, }; return acc; }, {}); - setMonitorLookup(monitorLookup); + setMonitorLookup(lookup); }, [monitors]); - const handleAcknowledge = () => { - const monitorId = selectedMonitor === "0" ? null : selectedMonitor; - acknowledge(setUpdateTrigger, monitorId); - }; - - if (networkError || networkErrorSummary) { + if (networkError) { return ( @@ -75,27 +115,33 @@ const Incidents = () => { ); } + const handleChangePage = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + const handleOpenDetails = (id) => { + if (!id) return; + setSelectedIncidentId(id); + setIsDetailsOpen(true); + }; + + const handleCloseDetails = () => { + setIsDetailsOpen(false); + setSelectedIncidentId(null); + }; + return ( - - - - + + + { dateRange={dateRange} setDateRange={setDateRange} /> + + ); }; -export default Incidents; +export default Incidents2; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx index 5a6f5e981..64f82cec2 100644 --- a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx +++ b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx @@ -1,7 +1,7 @@ -import ConfigBox from "../../../../Components/ConfigBox"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import { Box, Stack, Typography } from "@mui/material"; -import { CustomThreshold } from "../Components/CustomThreshold"; -import { capitalizeFirstLetter } from "../../../../Utils/stringUtils"; +import { CustomThreshold } from "./CustomThreshold/index.jsx"; +import { capitalizeFirstLetter } from "../../../../Utils/stringUtils.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx index 865698f44..d783f7985 100644 --- a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx +++ b/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx @@ -1,6 +1,6 @@ import { Box, Stack, Typography } from "@mui/material"; -import TextInput from "../../../../../Components/Inputs/TextInput"; -import Checkbox from "../../../../../Components/Inputs/Checkbox"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx b/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx new file mode 100644 index 000000000..50c4ed980 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx @@ -0,0 +1,90 @@ +import React from "react"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import { Box, Stack, Typography } from "@mui/material"; +import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; + +const DiskSelection = ({ availableDisks, selectedDisks, onChange }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const handleDiskChange = (event, mountpoint) => { + const isChecked = event.target.checked; + let newSelectedDisks = []; + + if (isChecked) { + newSelectedDisks = [...selectedDisks, mountpoint]; + } else { + newSelectedDisks = selectedDisks.filter((disk) => disk !== mountpoint); + } + + onChange(newSelectedDisks); + }; + + return ( + + + + {t("v1.infrastructure.disk_selection_title")} + + + {t("v1.infrastructure.disk_selection_description")} + + + + + {!availableDisks || availableDisks.length === 0 ? ( + + {t("v1.infrastructure.disk_selection_info")} + + ) : ( + availableDisks.map((disk) => { + const identifier = disk.mountpoint || disk.device; + return ( + + + handleDiskChange(e, identifier)} + /> + + + ); + }) + )} + + + ); +}; + +DiskSelection.propTypes = { + availableDisks: PropTypes.arrayOf( + PropTypes.shape({ + mountpoint: PropTypes.string.isRequired, + }) + ), + selectedDisks: PropTypes.arrayOf(PropTypes.string).isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default DiskSelection; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx index becda3e3a..2bde5f321 100644 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx +++ b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx @@ -2,9 +2,8 @@ import { useState } from "react"; import { Box, Button } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; -import Dialog from "../../../../Components/Dialog"; +import Icon from "@/Components/v1/Icon"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; import PropTypes from "prop-types"; const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => { @@ -36,12 +35,18 @@ const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => > {monitor?.isActive ? ( <> - + {t("pause")} ) : ( <> - + {t("resume")} )} diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx index 2ed48bf64..8fa6f4ff1 100644 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx +++ b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx @@ -1,8 +1,8 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import PulseDot from "../../../../Components/Animated/PulseDot"; +import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; import PropTypes from "prop-types"; const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => { const theme = useTheme(); diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx index f0a701517..a38ac9fc6 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx @@ -17,6 +17,7 @@ const useInfrastructureMonitorForm = () => { temperature: false, usage_temperature: "", secret: "", + selectedDisks: [], }); const onChangeForm = (name, value) => { @@ -48,6 +49,7 @@ const useInfrastructureMonitorForm = () => { temperature: gt.temperature !== undefined, usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", secret: "", + selectedDisks: [], })); }, []); @@ -81,6 +83,7 @@ const useInfrastructureMonitorForm = () => { ? (thresholds.usage_temperature * 100).toString() : "", secret: monitor.secret || "", + selectedDisks: monitor.selectedDisks || [], })); }, []); return { diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx index 9b3ee0d04..04279cac4 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -1,4 +1,4 @@ -import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks"; +import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks.js"; const useInfrastructureSubmit = () => { const [createMonitor, isCreating] = useCreateMonitor(); const [updateMonitor, isUpdating] = useUpdateMonitor(); @@ -26,11 +26,11 @@ const useInfrastructureSubmit = () => { ...(infrastructureMonitor.disk ? { usage_disk: infrastructureMonitor.usage_disk } : {}), - temperature: infrastructureMonitor.temperature, ...(infrastructureMonitor.temperature ? { usage_temperature: infrastructureMonitor.usage_temperature } : {}), secret: infrastructureMonitor.secret, + selectedDisks: infrastructureMonitor.selectedDisks, }; return form; }; @@ -49,6 +49,7 @@ const useInfrastructureSubmit = () => { usage_disk, temperature, usage_temperature, + selectedDisks, ...rest } = form; @@ -60,11 +61,12 @@ const useInfrastructureSubmit = () => { }; const finalForm = { - ...(isCreate ? {} : { _id: monitorId }), + ...(isCreate ? {} : { id: monitorId }), ...rest, description: form.name, type: "hardware", notifications: infrastructureMonitor.notifications, + selectedDisks, thresholds, }; // Handle create or update diff --git a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx index 080f441c4..f7665e65f 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { infrastructureMonitorValidation } from "../../../../Validation/validation"; -import { createToast } from "../../../../Utils/toastUtils"; +import { infrastructureMonitorValidation } from "../../../../Validation/validation.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; const useValidateInfrastructureForm = () => { const [errors, setErrors] = useState({}); diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index a9d16c926..0a09105b8 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -1,18 +1,20 @@ //Components -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import ConfigBox from "../../../Components/ConfigBox"; -import FieldWrapper from "../../../Components/Inputs/FieldWrapper"; -import Link from "../../../Components/Link"; -import Select from "../../../Components/Inputs/Select"; -import TextInput from "../../../Components/Inputs/TextInput"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx"; +import Link from "@/Components/v1/Link/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import MonitorStatusHeader from "./Components/MonitorStatusHeader"; -import MonitorActionButtons from "./Components/MonitorActionButtons"; -import CustomAlertsSection from "./Components/CustomAlertsSection"; +import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; +import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx"; +import MonitorActionButtons from "./Components/MonitorActionButtons.jsx"; +import CustomAlertsSection from "./Components/CustomAlertsSection.jsx"; +import DiskSelection from "./Components/DiskSelection.jsx"; // Utils -import NotificationsConfig from "../../../Components/NotificationConfig"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; +import { networkService } from "../../../Utils/NetworkService.js"; import { useParams } from "react-router-dom"; import { useState, useEffect } from "react"; import { useTheme } from "@emotion/react"; @@ -20,12 +22,12 @@ import { useTranslation } from "react-i18next"; import { useDeleteMonitor, useFetchGlobalSettings, - useFetchHardwareMonitorById, + useFetchMonitorById, usePauseMonitor, -} from "../../../Hooks/monitorHooks"; -import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm"; -import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm"; -import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit"; +} from "@/Hooks/monitorHooks.js"; +import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx"; +import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx"; +import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; const CreateInfrastructureMonitor = () => { const { monitorId } = useParams(); @@ -35,13 +37,16 @@ const CreateInfrastructureMonitor = () => { const { t } = useTranslation(); // State + const [monitor, setMonitor] = useState(null); const [https, setHttps] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false); + const [availableDisks, setAvailableDisks] = useState([]); // Fetch monitor details if editing - const [monitor, isLoading] = useFetchHardwareMonitorById({ + const [isLoading] = useFetchMonitorById({ monitorId, - updateTrigger, + setMonitor, + updateTrigger: true, }); const [deleteMonitor, isDeleting] = useDeleteMonitor(); const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); @@ -85,6 +90,31 @@ const CreateInfrastructureMonitor = () => { } else if (monitor) { setHttps(monitor.url.startsWith("https")); initializeInfrastructureMonitorForUpdate(monitor); + const fetchLastCheck = async () => { + try { + const { stats } = monitor ?? {}; + let latestCheck = stats?.aggregateData?.latestCheck; + let disks = latestCheck?.disk || []; + + if (disks.length > 0) { + setAvailableDisks(disks); + return; + } + + const response = await networkService.getChecksByMonitor({ + monitorId, + dateRange: "all", + rowsPerPage: 1, + sortOrder: "desc", + }); + disks = response?.data?.data?.checks?.[0]?.disk || []; + setAvailableDisks(disks); + } catch (error) { + setAvailableDisks([]); + } + }; + + fetchLastCheck(); } }, [ isCreate, @@ -99,7 +129,9 @@ const CreateInfrastructureMonitor = () => { const onSubmit = async (event) => { event.preventDefault(); const form = buildForm(infrastructureMonitor, https); - const error = validateForm(form); + // When editing, exclude URL from validation since it's disabled and can't be changed + const formToValidate = isCreate ? form : { ...form, url: monitor.url }; + const error = validateForm(formToValidate); if (error) { return; } @@ -175,14 +207,14 @@ const CreateInfrastructureMonitor = () => { <> )} - {!isCreate && ( + {!isCreate && monitor && ( )} - {!isCreate && ( + {!isCreate && monitor && ( { infrastructureMonitor={infrastructureMonitor} handleCheckboxChange={handleCheckboxChange} /> + + {monitorId && ( + + onChangeForm("selectedDisks", newSelectedDisks) + } + /> + )} + { const theme = useTheme(); const { getDimensions } = useHardwareUtils(); diff --git a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx index 0eb3aa36d..3465683d9 100644 --- a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx @@ -1,7 +1,7 @@ // Components import { Stack } from "@mui/material"; -import InfraAreaChart from "./InfraAreaChart"; -import SkeletonLayout from "./skeleton"; +import InfraAreaChart from "./InfraAreaChart.jsx"; +import SkeletonLayout from "./skeleton.jsx"; // Utils import { @@ -9,9 +9,9 @@ import { TzTick, InfrastructureTooltip, TemperatureTooltip, -} from "../../../../../Components/Charts/Utils/chartUtils"; +} from "@/Components/v1/Charts/Utils/chartUtils.jsx"; import { useTheme } from "@emotion/react"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import { useTranslation } from "react-i18next"; const AreaChartBoxes = ({ shouldRender, monitor, dateRange }) => { const theme = useTheme(); diff --git a/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx b/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx index 246769751..9c0fe1574 100644 --- a/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx @@ -11,10 +11,10 @@ import { Box } from "@mui/material"; // Utils import { useTheme } from "@emotion/react"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import PropTypes from "prop-types"; -const BaseContainer = ({ children, sx = {} }) => { +const BaseContainer = ({ children, sx = {}, shouldExpand = false }) => { const theme = useTheme(); const { getDimensions } = useHardwareUtils(); return ( @@ -22,7 +22,7 @@ const BaseContainer = ({ children, sx = {} }) => { sx={{ padding: `${theme.spacing(getDimensions().baseBoxPaddingVertical)} ${theme.spacing(getDimensions().baseBoxPaddingHorizontal)}`, minWidth: 200, - width: 225, + width: shouldExpand ? "100%" : 225, backgroundColor: theme.palette.primary.main, border: 1, borderStyle: "solid", @@ -38,6 +38,7 @@ const BaseContainer = ({ children, sx = {} }) => { BaseContainer.propTypes = { children: PropTypes.node.isRequired, sx: PropTypes.object, + shouldExpand: PropTypes.bool, }; export default BaseContainer; diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx index 70d36bd00..c762eeb32 100644 --- a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx @@ -1,26 +1,42 @@ // Components -import CustomGauge from "../../../../../Components/Charts/CustomGauge"; -import BaseContainer from "../BaseContainer"; +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 Gauge = ({ + value, + heading, + metricOne, + valueOne, + metricTwo, + valueTwo, + metricThree, + valueThree, + metricFour, + valueFour, + shouldExpand = false, +}) => { const theme = useTheme(); const valueStyle = { borderRadius: theme.spacing(2), backgroundColor: theme.palette.tertiary.main, - width: "40%", + minWidth: "40%", + maxWidth: "60%", mb: theme.spacing(2), mt: theme.spacing(2), pr: theme.spacing(2), textAlign: "right", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", }; return ( - + {metricTwo} {valueTwo} + + {metricThree} + {valueThree} + + + {metricFour} + {valueFour} + @@ -83,6 +117,11 @@ Gauge.propTypes = { valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), metricTwo: PropTypes.string, valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + metricThree: PropTypes.string, + valueThree: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + metricFour: PropTypes.string, + valueFour: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + shouldExpand: PropTypes.bool, }; export default Gauge; diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx index 3c1c68f8b..7693fd32e 100644 --- a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx @@ -1,16 +1,17 @@ // Components -import { Stack } from "@mui/material"; -import Gauge from "./Gauge"; -import SkeletonLayout from "./skeleton"; +import { Box } from "@mui/material"; +import Gauge from "./Gauge.jsx"; +import SkeletonLayout from "./skeleton.jsx"; import PropTypes from "prop-types"; // Utils -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +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 { decimalToPercentage, formatBytes, formatDeviceName, formatMountpoint } = + useHardwareUtils(); const theme = useTheme(); const { t } = useTranslation(); @@ -46,23 +47,41 @@ const Gauges = ({ isLoading = false, monitor }) => { 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), - })), + ...(latestCheck?.disk ?? []) + .filter((disk) => { + if (!monitor?.selectedDisks || monitor.selectedDisks.length === 0) { + return true; + } + return monitor.selectedDisks.includes(disk.mountpoint || disk.device); + }) + .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), + metricThree: t("device"), + valueThree: formatDeviceName(disk.device), + metricFour: t("mountedOn"), + valueFour: formatMountpoint(disk.mountpoint), + })), ]; + // Only expand gauges to fill row when there are 4 or more + const shouldExpand = gauges.length >= 4; + return ( - {gauges.map((gauge) => { return ( @@ -74,10 +93,15 @@ const Gauges = ({ isLoading = false, monitor }) => { valueOne={gauge.valueOne} metricTwo={gauge.metricTwo} valueTwo={gauge.valueTwo} + metricThree={gauge.metricThree} + valueThree={gauge.valueThree} + metricFour={gauge.metricFour} + valueFour={gauge.valueFour} + shouldExpand={shouldExpand} /> ); })} - + ); }; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 9157a3432..d9f93311c 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -1,15 +1,15 @@ import PropTypes from "prop-types"; import { Stack, Typography } from "@mui/material"; -import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart"; +import InfraAreaChart from "../AreaChartBoxes/InfraAreaChart.jsx"; import { TzTick, InfrastructureTooltip, NetworkTick, -} from "../../../../../Components/Charts/Utils/chartUtils"; +} from "@/Components/v1/Charts/Utils/chartUtils.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; const NetworkCharts = ({ ethernetData, dateRange }) => { const theme = useTheme(); diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx index 98ba90dae..0747dfcd1 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; -import StatusBoxes from "../../../../../Components/StatusBoxes"; -import StatBox from "../../../../../Components/StatBox"; +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"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; function formatNumber(num) { return num != null ? num.toLocaleString() : "0"; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index 41d2b0a87..78db333e6 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -3,10 +3,10 @@ import { useState, useEffect } from "react"; import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; -import Select from "../../../../../Components/Inputs/Select"; -import NetworkStatBoxes from "./NetworkStatBoxes"; -import NetworkCharts from "./NetworkCharts"; -import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; +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); diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx index 6c11e7c82..ef91709cf 100644 --- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx @@ -1,11 +1,11 @@ // Components import { Stack, Typography } from "@mui/material"; -import StatusBoxes from "../../../../../Components/StatusBoxes"; -import StatBox from "../../../../../Components/StatBox"; +import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; +import StatBox from "@/Components/v1/StatBox/index.jsx"; //Utils -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import { useTranslation } from "react-i18next"; const InfraStatBoxes = ({ shouldRender, monitor }) => { diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx index 8edb6fb87..8938847a4 100644 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx +++ b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx @@ -1,4 +1,4 @@ -import { Typography } from "@mui/material"; +import { Typography, Tooltip } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; @@ -120,6 +120,85 @@ const useHardwareUtils = () => { } }; + const formatDeviceName = (device) => { + const deviceStr = String(device || ""); + + // Show full device path + return ( + + + {deviceStr} + + + ); + }; + + const formatMountpoint = (mountpoint) => { + const mountpointStr = String(mountpoint || ""); + + if (!mountpointStr) { + return ( + + + N/A + + + ); + } + + // Show full mountpoint path + return ( + + + {mountpointStr} + + + ); + }; + /** * Converts a decimal value to a percentage * @@ -178,6 +257,8 @@ const useHardwareUtils = () => { return { formatBytes, + formatDeviceName, + formatMountpoint, decimalToPercentage, buildTemps, getDimensions, diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index a16f3906b..62a5c2642 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -1,20 +1,20 @@ // Components import { Stack, Typography, Tab } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; -import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; -import StatusBoxes from "./Components/StatusBoxes"; -import GaugeBoxes from "./Components/GaugeBoxes"; -import AreaChartBoxes from "./Components/AreaChartBoxes"; -import GenericFallback from "../../../Components/GenericFallback"; -import NetworkStats from "./Components/NetworkStats"; -import CustomTabList from "../../../Components/Tab"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; +import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; +import StatusBoxes from "./Components/StatusBoxes/index.jsx"; +import GaugeBoxes from "./Components/GaugeBoxes/index.jsx"; +import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import NetworkStats from "./Components/NetworkStats/index.jsx"; +import CustomTabList from "@/Components/v1/Tab/index.jsx"; import TabContext from "@mui/lab/TabContext"; // Utils import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; +import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx index 3c2511c2f..01e56c68a 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx @@ -1,9 +1,9 @@ import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import FilterHeader from "../../../../../Components/FilterHeader"; +import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; import { useMemo } from "react"; import { Box, Button } from "@mui/material"; -import ClearIcon from "@mui/icons-material/Clear"; +import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; /** @@ -68,7 +68,12 @@ const Filter = ({ color={theme.palette.primary.contrastText} onClick={handleReset} variant="contained" - endIcon={} + endIcon={ + + } sx={{ visibility: isFilterActive ? "visible" : "hidden", }} diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx index 7ee603ef7..292eb9714 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx @@ -1,18 +1,18 @@ // Components import { Box } from "@mui/material"; -import DataTable from "../../../../../Components/Table"; -import Host from "../../../../../Components/Host"; -import { StatusLabel } from "../../../../../Components/Label"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Host from "@/Components/v1/Host/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { Stack } from "@mui/material"; -import { InfrastructureMenu } from "../MonitorsTableMenu"; -import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner"; +import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx"; +import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx"; // Assets -import CPUChipIcon from "../../../../../assets/icons/cpu-chip.svg?react"; -import CustomGauge from "../../../../../Components/Charts/CustomGauge"; +import Icon from "@/Components/v1/Icon"; +import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; @@ -67,9 +67,9 @@ const MonitorsTable = ({ alignItems={"center"} gap=".25rem" > - {row.processor} @@ -115,7 +115,7 @@ const MonitorsTable = ({ : theme.palette.success.main; return { - id: monitor._id, + id: monitor.id, name: monitor.name, url: monitor.url, processor, diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx index 147f1c297..50af0aaa2 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx @@ -3,13 +3,13 @@ import { useRef, useState } from "react"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../../../Utils/toastUtils"; +import { createToast } from "@/Utils/toastUtils.jsx"; import { IconButton, Menu, MenuItem } from "@mui/material"; -import Settings from "../../../../../assets/icons/settings-bold.svg?react"; +import Icon from "@/Components/v1/Icon"; import PropTypes from "prop-types"; -import Dialog from "../../../../../Components/Dialog"; -import { networkService } from "../../../../../Utils/NetworkService.js"; -import { usePauseMonitor } from "../../../../../Hooks/monitorHooks"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; +import { networkService } from "@/Utils/NetworkService.js"; +import { usePauseMonitor } from "@/Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; /** @@ -100,7 +100,10 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => { }} ref={anchor} > - + { const field = toFilterStatus !== undefined ? "status" : undefined; - const [monitors, summary, isLoading, networkError] = useFetchMonitorsByTeamId({ - limit: 1, + const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ types: TYPES, - page, + limit: 1, + page: page, field: field, filter: toFilterStatus ?? search, - rowsPerPage, - updateTrigger, + rowsPerPage: rowsPerPage, + monitorUpdateTrigger: updateTrigger, }); return ( @@ -99,7 +100,7 @@ const InfrastructureMonitors = () => { { isSearching={isSearching} /> { const theme = useTheme() return( - {children} @@ -99,7 +102,7 @@ const Gauges = ({ diagnostics, isLoading }) => { return ( { diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx index 72ee47f48..194e13821 100644 --- a/client/src/Pages/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -1,14 +1,14 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; -import Gauges from "./components/gauges"; +import Gauges from "./components/gauges/index.jsx"; import Button from "@mui/material/Button"; -import StatBox from "../../../Components/StatBox"; -import StatusBoxes from "../../../Components/StatusBoxes"; +import StatBox from "@/Components/v1/StatBox/index.jsx"; +import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useFetchDiagnostics } from "../../../Hooks/logHooks"; -import { getHumanReadableDuration } from "../../../Utils/timeUtils"; -import { formatBytes, getPercentage } from "./utils/utils"; +import { useFetchDiagnostics } from "../../../Hooks/logHooks.js"; +import { getHumanReadableDuration } from "../../../Utils/timeUtilsLegacy.js"; +import { formatBytes, getPercentage } from "./utils/utils.js"; const Diagnostics = () => { // Local state @@ -26,12 +26,12 @@ const Diagnostics = () => { status="up" heading={t("status")} subHeading={ - error + error ? t("logsPage.logLevelSelect.values.error") - : isLoading + : isLoading ? t("commonSaving") - : diagnostics - ? t("diagnosticsPage.diagnosticDescription") + : diagnostics + ? t("diagnosticsPage.diagnosticDescription") : t("general.noOptionsFound", { unit: "data" }) } /> diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index b44b3fd1a..d0319a387 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -1,15 +1,18 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; -import Select from "../../../Components/Inputs/Select"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; import Typography from "@mui/material/Typography"; -import Divider from "@mui/material/Divider"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import { useFetchLogs } from "../../../Hooks/logHooks"; +import { useFetchLogs } from "../../../Hooks/logHooks.js"; import { useTheme } from "@emotion/react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -const formatLog = (theme, log, idx) => { +const LevelBadge = ({ level }) => { + const theme = useTheme(); + const levelColors = { info: theme.palette.success.main, warn: theme.palette.warning.main, @@ -17,31 +20,38 @@ const formatLog = (theme, log, idx) => { debug: theme.palette.accent.main, }; - const color = levelColors[log.level] || theme.palette.primary.contrastText; + const color = levelColors[level] || theme.palette.primary.contrastText; return ( - - [{log.timestamp}]{" "} - {log.level.toUpperCase()} - {": "} - {`(${log.service})`} - {`(${log.method})`} - {": "} - {log.message} -
-
+ + {level} + ); }; -const Logs = () => { - // Local state - const [logLevel, setLogLevel] = useState("all"); +const formatTimestamp = (timestamp) => { + if (!timestamp) return "-"; + const date = new Date(timestamp); + return date.toLocaleString(); +}; + +const Logs = () => { + const [logLevel, setLogLevel] = useState("all"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(15); - // Hooks const theme = useTheme(); const { t } = useTranslation(); const [logs, isLoading, error] = useFetchLogs(); - // Setup + const LOG_LEVELS = [ { _id: "all", name: t("logsPage.logLevelSelect.values.all") }, { _id: "info", name: t("logsPage.logLevelSelect.values.info") }, @@ -49,53 +59,116 @@ const Logs = () => { { _id: "error", name: t("logsPage.logLevelSelect.values.error") }, { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") }, ]; + + const headers = [ + { + id: "timestamp", + content: t("logsPage.table.timestamp"), + render: (row) => ( + + {formatTimestamp(row.timestamp)} + + ), + }, + { + id: "level", + content: t("logsPage.table.level"), + render: (row) => , + }, + { + id: "service", + content: t("logsPage.table.service"), + render: (row) => ( + {row.service || "-"} + ), + }, + { + id: "method", + content: t("logsPage.table.method"), + render: (row) => ( + {row.method || "-"} + ), + }, + { + id: "message", + content: t("logsPage.table.message"), + render: (row) => ( + + {row.message || "-"} + + ), + }, + ]; + + const filteredLogs = logs + ?.filter((log) => { + if (logLevel === "all") return true; + return log.level === logLevel; + }) + .reverse() + .map((log, idx) => ({ ...log, id: idx })); + + const paginatedLogs = filteredLogs?.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleLogLevelChange = (e) => { + setLogLevel(e.target.value); + setPage(0); + }; + return ( - {t("logsPage.logLevelSelect.title")} + + + + ); @@ -54,6 +71,7 @@ SettingsUI.propTypes = { handleChange: PropTypes.func, mode: PropTypes.string, language: PropTypes.string, + chartType: PropTypes.string, }; export default SettingsUI; diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/Settings/SettingsURL.jsx index 24c49cad1..c52dcf317 100644 --- a/client/src/Pages/Settings/SettingsURL.jsx +++ b/client/src/Pages/Settings/SettingsURL.jsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../Components/ConfigBox"; -import Select from "../../Components/Inputs/Select"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 92f58b4c0..bd3513a2c 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -1,37 +1,51 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import SettingsTimeZone from "./SettingsTimeZone"; -import SettingsUI from "./SettingsUI"; -import SettingsURL from "./SettingsURL"; -import SettingsPagespeed from "./SettingsPagespeed"; -import SettingsDemoMonitors from "./SettingsDemoMonitors"; -import SettingsAbout from "./SettingsAbout"; -import SettingsEmail from "./SettingsEmail"; -import SettingsGlobalThresholds from "./SettingsGlobalThresholds"; +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 SettingsExport from "./SettingsExport.jsx"; import Button from "@mui/material/Button"; // Utils -import { settingsValidation } from "../../Validation/validation"; +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"; -import SettingsStats from "./SettingsStats"; +import { + setTimezone, + setMode, + setLanguage, + setShowURL, + setChartType, +} from "@/Features/UI/uiSlice.js"; +import SettingsStats from "./SettingsStats.jsx"; -import { useFetchSettings, useSaveSettings } from "../../Hooks/settingsHooks"; -import { useIsAdmin } from "../../Hooks/useIsAdmin"; +import { useFetchSettings, useSaveSettings } from "@/Hooks/settingsHooks.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useAddDemoMonitors, useDeleteAllMonitors, useDeleteMonitorStats, -} from "../../Hooks/monitorHooks"; + useFetchJson, +} from "@/Hooks/monitorHooks.js"; // Constants const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; const Settings = () => { // Redux state - const { mode, language, timezone, showURL } = useSelector((state) => state.ui); + const { + mode, + language = "en", + timezone, + showURL, + chartType = "histogram", + } = useSelector((state) => state.ui); // Local state const [settingsData, setSettingsData] = useState({}); @@ -61,6 +75,7 @@ const Settings = () => { }); const [deleteAllMonitors, isDeletingMonitors] = useDeleteAllMonitors(); const [deleteMonitorStats, isDeletingMonitorStats] = useDeleteMonitorStats(); + const [fetchJson, isFetchingJson] = useFetchJson(); // Setup const isAdmin = useIsAdmin(); @@ -72,11 +87,6 @@ const Settings = () => { 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" || @@ -87,6 +97,12 @@ const Settings = () => { ) { newValue = checked; } + + // Ensure showURL is a proper boolean + if (name === "showURL") { + newValue = value === true || value === "true"; + } + // Build next state early const newSettingsData = { ...settingsData, @@ -108,6 +124,11 @@ const Settings = () => { return; } + if (name === "chartType") { + dispatch(setChartType(value)); + return; + } + if (name === "deleteStats") { await deleteMonitorStats(); return; @@ -123,38 +144,72 @@ const Settings = () => { 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; + if (name === "export") { + const json = await fetchJson(); + if (!json || json.length === 0) { + return; + } + + const blob = new Blob([JSON.stringify(json, null, 2)], { + type: "application/json", }); - setErrors(newErrors); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = "monitors.json"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + return; } setSettingsData(newSettingsData); + + // Update Redux immediately for UI feedback + if (name === "showURL") { + dispatch(setShowURL(newValue)); + } }; const handleSave = () => { - const { error } = settingsValidation.validate(settingsData.settings, { + // Validate] + + const toSubmit = { + checkTTL: settingsData.settings.checkTTL, + pagespeedApiKey: settingsData.settings.pagespeedApiKey, + language: settingsData.settings.language, + timezone: settingsData.settings.timezone, + systemEmailHost: settingsData.settings.systemEmailHost, + systemEmailPort: settingsData.settings.systemEmailPort, + systemEmailSecure: settingsData.settings.systemEmailSecure, + systemEmailPool: settingsData.settings.systemEmailPool, + systemEmailAddress: settingsData.settings.systemEmailAddress, + systemEmailPassword: settingsData.settings.systemEmailPassword, + systemEmailUser: settingsData.settings.systemEmailUser, + systemEmailConnectionHost: settingsData.settings.systemEmailConnectionHost, + systemEmailTLSServername: settingsData.settings.systemEmailTLSServername, + systemEmailIgnoreTLS: settingsData.settings.systemEmailIgnoreTLS, + systemEmailRequireTLS: settingsData.settings.systemEmailRequireTLS, + systemEmailRejectUnauthorized: settingsData.settings.systemEmailRejectUnauthorized, + showURL: settingsData.settings.showURL, + globalThresholds: settingsData.settings.globalThresholds, + }; + + const { error } = settingsValidation.validate(toSubmit, { abortEarly: false, }); if (!error || error.details.length === 0) { setErrors({}); + saveSettings(toSubmit); } else { const newErrors = {}; error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); - setErrors(newErrors); } - saveSettings(settingsData?.settings); }; return ( @@ -171,6 +226,7 @@ const Settings = () => { handleChange={handleChange} mode={mode} language={language} + chartType={chartType} /> { setEmailPasswordHasBeenReset={setEmailPasswordHasBeenReset} /> + - + {monitor.name} - { onDelete(monitor); }} @@ -43,7 +47,7 @@ const MonitorListItem = ({ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { const onDelete = (monitorToDelete) => { const newMonitors = selectedMonitors.filter( - (monitor) => monitor._id !== monitorToDelete._id + (monitor) => monitor.id !== monitorToDelete.id ); setSelectedMonitors(newMonitors); }; @@ -80,8 +84,8 @@ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { > {selectedMonitors?.map((monitor, index) => ( {(provided, snapshot) => ( diff --git a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx b/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx index 14c4ce0f5..594fd9709 100644 --- a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx @@ -1,14 +1,19 @@ import { Button, Box } from "@mui/material"; -import ProgressUpload from "../../../../../Components/ProgressBars"; -import ImageIcon from "@mui/icons-material/Image"; +import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx"; +import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; -import { formatBytes } from "../../../../../Utils/fileUtils"; +import { formatBytes } from "../../../../../Utils/fileUtils.js"; const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => { const { t } = useTranslation(); if (isLoading) { return ( } + icon={ + + } label={logo?.name} size={formatBytes(logo?.size)} progress={progressValue} diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx index e313f0e85..f31c456ef 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx @@ -1,5 +1,5 @@ import { Stack, Typography } from "@mui/material"; -import ConfigBox from "../../../../../Components/ConfigBox"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx index 327a2fb0a..e84de16dc 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx @@ -1,14 +1,14 @@ // Components import { Stack, Typography } from "@mui/material"; import { TabPanel } from "@mui/lab"; -import MonitorList from "../MonitorList"; -import Search from "../../../../../Components/Inputs/Search"; -import Checkbox from "../../../../../Components/Inputs/Checkbox"; +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"; +import ConfigStack from "./ConfigStack.jsx"; const Content = ({ tabValue, form, @@ -24,7 +24,7 @@ const Content = ({ // Handlers const handleMonitorsChange = (selectedMonitors) => { handleFormChange({ - target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor._id) }, + target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor.id) }, }); setSelectedMonitors(selectedMonitors); }; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx index dd4345158..d4c583d3a 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -1,13 +1,13 @@ // Components import { Stack, Typography } from "@mui/material"; import { TabPanel } from "@mui/lab"; -import ConfigBox from "../../../../../Components/ConfigBox"; -import Checkbox from "../../../../../Components/Inputs/Checkbox"; -import TextInput from "../../../../../Components/Inputs/TextInput"; -import Search from "../../../../../Components/Inputs/Search"; -import ImageUpload from "../../../../../Components/Inputs/ImageUpload"; -import ColorPicker from "../../../../../Components/Inputs/ColorPicker"; -import Progress from "../Progress"; +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"; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx index 60d86e1c9..e8669c474 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx @@ -1,12 +1,12 @@ // Components import { TabContext } from "@mui/lab"; import { Tab } from "@mui/material"; -import Settings from "./Settings"; -import Content from "./Content"; +import Settings from "./Settings.jsx"; +import Content from "./Content.jsx"; // Utils import PropTypes from "prop-types"; -import CustomTabList from "../../../../../Components/Tab"; +import CustomTabList from "@/Components/v1/Tab/index.jsx"; const Tabs = ({ isCreate, diff --git a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx index a2ac5f55d..256fa4be9 100644 --- a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx +++ b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx @@ -1,14 +1,15 @@ import { useState } from "react"; -import { networkService } from "../../../../main"; -import { createToast } from "../../../../Utils/toastUtils"; +import { networkService } from "../../../../main.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; -const useCreateStatusPage = (isCreate, url) => { +const useCreateStatusPage = (isCreate) => { const [isLoading, setIsLoading] = useState(false); const [networkError, setNetworkError] = useState(false); - const createStatusPage = async ({ form }) => { + const createStatusPage = async ({ form, id }) => { + console.log(id); setIsLoading(true); try { - await networkService.createStatusPage({ form, isCreate, url }); + await networkService.createStatusPage({ form, isCreate, id }); return true; } catch (error) { setNetworkError(true); diff --git a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx index f380bed9a..03ebb07bf 100644 --- a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx +++ b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { networkService } from "../../../../main"; +import { networkService } from "../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; const useMonitorsFetch = () => { const { user } = useSelector((state) => state.auth); @@ -14,9 +14,9 @@ const useMonitorsFetch = () => { try { const response = await networkService.getMonitorsByTeamId({ limit: null, // donot return any checks for the monitors - types: ["http", "ping", "port"], // status page is available for uptime, ping, and port monitors + types: ["http", "ping", "port", "game"], // include game servers in status page monitor selection }); - setMonitors(response.data.data.monitors); + setMonitors(response.data.data); } catch (error) { setNetworkError(true); createToast({ body: error.message }); diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/index.jsx index d4cbc0fd5..4eb171232 100644 --- a/client/src/Pages/StatusPage/Create/index.jsx +++ b/client/src/Pages/StatusPage/Create/index.jsx @@ -1,23 +1,23 @@ // Components import { Stack, Button, Typography } from "@mui/material"; -import Tabs from "./Components/Tabs"; -import GenericFallback from "../../../Components/GenericFallback"; -import SkeletonLayout from "./Components/Skeleton"; -import Dialog from "../../../Components/Dialog"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; +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"; -import { buildErrors } from "../../../Validation/error"; -import { useMonitorsFetch } from "./Hooks/useMonitorsFetch"; -import { useCreateStatusPage } from "./Hooks/useCreateStatusPage"; -import { createToast } from "../../../Utils/toastUtils"; +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"; +import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete"; +import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete.jsx"; //Constants const ERROR_TAB_MAPPING = [ ["companyName", "url", "timezone", "color", "isPublished", "logo"], @@ -54,15 +54,14 @@ const CreateStatusPage = () => { //Utils const theme = useTheme(); const [monitors, isLoading, networkError] = useMonitorsFetch(); - const [createStatusPage] = useCreateStatusPage(isCreate); const navigate = useNavigate(); const { t } = useTranslation(); + const [createStatusPage] = useCreateStatusPage(isCreate); 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; @@ -144,12 +143,13 @@ const CreateStatusPage = () => { ...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 }); + const success = await createStatusPage({ form, id: statusPage?.id }); if (success) { createToast({ body: isCreate ? t("statusPage.createSuccess") : t("statusPage.updateSuccess"), @@ -205,7 +205,7 @@ const CreateStatusPage = () => { companyName: statusPage?.companyName, isPublished: statusPage?.isPublished, timezone: statusPage?.timezone, - monitors: statusPageMonitors.map((monitor) => monitor._id), + monitors: statusPageMonitors.map((monitor) => monitor.id), color: statusPage?.color, logo: newLogo, showCharts: statusPage?.showCharts ?? true, diff --git a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx b/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx index beac3b0d2..fe1c4c852 100644 --- a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx @@ -1,9 +1,8 @@ // Components import { Box, Stack, Typography, Button } from "@mui/material"; -import Image from "../../../../../Components/Image"; -import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react"; -import ThemeSwitch from "../../../../../Components/ThemeSwitch"; -import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; +import Image from "@/Components/v1/Image/index.jsx"; +import Icon from "@/Components/v1/Icon"; +import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; //Utils import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; @@ -46,7 +45,12 @@ const Controls = ({ url, type }) => { }, }} > - {t("configure")} + {" "} + {t("configure")} @@ -111,7 +115,10 @@ const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => { }} > {t("publicLink")} - + )}
diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx index 545974c57..33f771233 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx @@ -1,12 +1,12 @@ // Components import { Stack, Box } from "@mui/material"; -import Host from "../../../../../Components/Host"; -import StatusPageBarChart from "../../../../../Components/Charts/StatusPageBarChart"; -import { StatusLabel } from "../../../../../Components/Label"; +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/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; import PropTypes from "prop-types"; import { useSelector } from "react-redux"; @@ -28,35 +28,31 @@ const MonitorsList = ({ const status = determineState(monitor); return ( {statusPage.showCharts !== false && ( )} - - - ); diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx index 1898ff3d7..7b9460068 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx @@ -1,7 +1,6 @@ // Components import { Stack, Typography } from "@mui/material"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; -import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import Icon from "@/Components/v1/Icon"; // Utils import { useTheme } from "@mui/material/styles"; @@ -10,8 +9,10 @@ import PropTypes from "prop-types"; const getMonitorStatus = (monitors, theme) => { const monitorsStatus = { icon: ( - ), }; @@ -19,8 +20,10 @@ const getMonitorStatus = (monitors, theme) => { monitorsStatus.msg = "All systems operational"; monitorsStatus.color = theme.palette.success.lowContrast; monitorsStatus.icon = ( - ); } diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx index 2c39c70b3..73b9a8a5f 100644 --- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx +++ b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { networkService } from "../../../../Utils/NetworkService"; -import { createToast } from "../../../../Utils/toastUtils"; +import { networkService } from "../../../../Utils/NetworkService.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx index ae9c7b579..7db6b6c5e 100644 --- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx +++ b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx @@ -1,9 +1,9 @@ import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../../../../main"; +import { networkService } from "../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; const useStatusPageFetch = (isCreate = false, url) => { const [isLoading, setIsLoading] = useState(true); diff --git a/client/src/Pages/StatusPage/Status/index.jsx b/client/src/Pages/StatusPage/Status/index.jsx index a77f2a380..adcd9c2c7 100644 --- a/client/src/Pages/StatusPage/Status/index.jsx +++ b/client/src/Pages/StatusPage/Status/index.jsx @@ -1,18 +1,18 @@ // Components import { Typography, Stack } from "@mui/material"; -import GenericFallback from "../../../Components/GenericFallback"; -import AdminLink from "./Components/AdminLink"; -import ControlsHeader from "./Components/ControlsHeader"; -import SkeletonLayout from "./Components/Skeleton"; -import StatusBar from "./Components/StatusBar"; -import MonitorsList from "./Components/MonitorsList"; -import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; -import TextLink from "../../../Components/TextLink"; +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"; +import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx"; import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useLocation } from "react-router-dom"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx index 04d7a6cfb..9882239bd 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -1,8 +1,8 @@ -import DataTable from "../../../../../Components/Table"; +import DataTable from "@/Components/v1/Table/index.jsx"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { StatusLabel } from "../../../../../Components/Label"; -import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import Icon from "@/Components/v1/Icon"; import { Stack, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; const StatusPagesTable = ({ data }) => { @@ -48,7 +48,12 @@ const StatusPagesTable = ({ data }) => { }} > {content} - {row.isPublished && } + {row.isPublished && ( + + )}
); }, diff --git a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx index b236368c8..9336e5d31 100644 --- a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx +++ b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; -import { networkService } from "../../../../main"; +import { networkService } from "../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; const useStatusPagesFetch = () => { const { user } = useSelector((state) => state.auth); @@ -14,7 +14,7 @@ const useStatusPagesFetch = () => { const fetchStatusPages = async () => { try { const res = await networkService.getStatusPagesByTeamId(); - setStatusPages(res?.data?.data); + setStatusPages(res?.data?.data || []); } catch (error) { setNetworkError(true); createToast({ diff --git a/client/src/Pages/StatusPage/StatusPages/index.jsx b/client/src/Pages/StatusPage/StatusPages/index.jsx index fa26c2b81..7137d01ad 100644 --- a/client/src/Pages/StatusPage/StatusPages/index.jsx +++ b/client/src/Pages/StatusPage/StatusPages/index.jsx @@ -1,13 +1,13 @@ // Components import { Stack } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; -import StatusPagesTable from "./Components/StatusPagesTable"; -import PageStateWrapper from "../../../Components/PageStateWrapper"; +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"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; const BREADCRUMBS = [{ name: `Status Pages`, path: "" }]; const StatusPages = () => { diff --git a/client/src/Pages/Uptime/BulkImport/index.jsx b/client/src/Pages/Uptime/BulkImport/index.jsx index 9490b4eab..6f2f7bcb5 100644 --- a/client/src/Pages/Uptime/BulkImport/index.jsx +++ b/client/src/Pages/Uptime/BulkImport/index.jsx @@ -5,14 +5,14 @@ import { useState } from "react"; import { Box, Stack, Typography, Button, Link } from "@mui/material"; //Components -import { createToast } from "../../../Utils/toastUtils"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import ConfigBox from "../../../Components/ConfigBox"; -import UploadFile from "./Upload"; +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/monitorHooks"; +import { useCreateBulkMonitors } from "../../../Hooks/monitorHooks.js"; const BulkImport = () => { const theme = useTheme(); diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 3f905fa11..01fc9eb7a 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -9,31 +9,28 @@ import { Tooltip, Typography, } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import Radio from "../../../Components/Inputs/Radio"; -import Select from "../../../Components/Inputs/Select"; -import ConfigBox from "../../../Components/ConfigBox"; -import NotificationsConfig from "../../../Components/NotificationConfig"; -import Checkbox from "../../../Components/Inputs/Checkbox"; -import Dialog from "../../../Components/Dialog"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import SkeletonLayout from "./skeleton"; +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"; -import { createToast } from "../../../Utils/toastUtils"; -import { - PauseOutlined as PauseOutlinedIcon, - PlayArrowOutlined as PlayArrowOutlinedIcon, -} from "@mui/icons-material"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import { monitorValidation } from "../../../Validation/validation.js"; +import { createToast } from "../../../Utils/toastUtils.jsx"; +import Icon from "@/Components/v1/Icon"; +import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; import { useParams } from "react-router-dom"; import { useCreateMonitor, @@ -42,7 +39,7 @@ import { usePauseMonitor, useFetchMonitorById, useFetchMonitorGames, -} from "../../../Hooks/monitorHooks"; +} from "../../../Hooks/monitorHooks.js"; /** * Create page renders monitor creation or configuration views. @@ -83,13 +80,13 @@ const UptimeCreate = ({ isClone = false }) => { const [isFetchingMonitor] = useFetchMonitorById({ monitorId, setMonitor, - updateTrigger: true, + updateTrigger, }); // Fetch games const [isFetchingGames] = useFetchMonitorGames({ setGames, - triggerUpdate: true, + updateTrigger, }); // Combine the loading states @@ -107,11 +104,16 @@ const UptimeCreate = ({ isClone = false }) => { // Constants const MS_PER_MINUTE = 60000; const FREQUENCIES = [ + { _id: 0.25, name: t("time.fifteenSeconds") }, + { _id: 0.5, name: t("time.thirtySeconds") }, { _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") }, + { _id: 10, name: t("time.tenMinutes") }, + { _id: 15, name: t("time.fifteenMinutes") }, + { _id: 30, name: t("time.thirtyMinutes") }, ]; const GAMELIST = Object.entries(games).map(([key, value]) => ({ @@ -194,7 +196,7 @@ const UptimeCreate = ({ isClone = false }) => { }; } else { form = { - _id: monitor._id, + id: monitor.id, url: monitor.url, name: monitor.name || monitor.url.substring(0, 50), statusWindowSize: monitor.statusWindowSize, @@ -281,13 +283,10 @@ const UptimeCreate = ({ isClone = false }) => { })); }; - const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - }; - const handleRemove = async (event) => { event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/uptime" }); + const TEMP_MONITOR = { id: monitor.id }; + await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" }); }; const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; @@ -423,9 +422,24 @@ const UptimeCreate = ({ isClone = false }) => { color="secondary" loading={isBusy} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? ( + + ) : ( + + ) } - onClick={handlePause} + onClick={() => { + pauseMonitor({ + monitorId: monitor?.id, + triggerUpdate, + }); + }} > {monitor?.isActive ? t("pause") : t("resume")} diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx deleted file mode 100644 index b60a4dd04..000000000 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ /dev/null @@ -1,147 +0,0 @@ -// Components -import { Stack, Typography, Box } from "@mui/material"; -import ChartBox from "../../../../../Components/Charts/ChartBox"; -import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react"; -import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react"; -import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react"; -import UpBarChart from "../Charts/UpBarChart"; -import DownBarChart from "../Charts/DownBarChart"; -import ResponseGaugeChart from "../Charts/ResponseGaugeChart"; -import SkeletonLayout from "./skeleton"; -// Utils -import { formatDateWithTz } from "../../../../../Utils/timeUtils"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { useState } from "react"; - -const ChartBoxes = ({ - isLoading = false, - monitorData, - dateRange, - uiTimezone, - dateFormat, -}) => { - // Local state - const [hoveredUptimeData, setHoveredUptimeData] = useState(null); - const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); - const theme = useTheme(); - if (isLoading) { - return ; - } - - const noIncidentsMessage = "Great. No Incidents, yet!"; - - return ( - - } - header="Uptime" - isEmpty={monitorData?.groupedUpChecks?.length === 0} - > - - - Total Checks - - {hoveredUptimeData !== null - ? hoveredUptimeData.totalChecks - : (monitorData?.groupedUpChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredUptimeData !== null && hoveredUptimeData.time !== null && ( - - {formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)} - - )} - - - - {hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"} - - - {hoveredUptimeData !== null - ? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0) - : Math.floor((monitorData?.groupedUptimePercentage ?? 0) * 100)} - - {hoveredUptimeData !== null ? " ms" : " %"} - - - - - - - } - header="Incidents" - noDataMessage={noIncidentsMessage} - isEmpty={monitorData?.groupedDownChecks?.length === 0} - > - - - - {hoveredIncidentsData !== null - ? hoveredIncidentsData.totalChecks - : (monitorData?.groupedDownChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && ( - - {formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)} - - )} - - - - - } - header="Average Response Time" - > - - - - ); -}; - -export default ChartBoxes; - -ChartBoxes.propTypes = { - isLoading: PropTypes.bool, - monitorData: PropTypes.object, - dateRange: PropTypes.string.isRequired, - uiTimezone: PropTypes.string.isRequired, - dateFormat: PropTypes.string.isRequired, - hoveredUptimeData: PropTypes.object, - setHoveredUptimeData: PropTypes.func, - hoveredIncidentsData: PropTypes.object, - setHoveredIncidentsData: PropTypes.func, -}; diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx deleted file mode 100644 index 750b3b20a..000000000 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx b/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx deleted file mode 100644 index 8df33953f..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils"; - -const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D"; - - return ( - <> - - {formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)} - - - {formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)} - - - ); -}; - -CustomLabels.propTypes = { - x: PropTypes.number.isRequired, - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - firstDataPoint: PropTypes.object, - lastDataPoint: PropTypes.object, - type: PropTypes.string.isRequired, -}; - -export default CustomLabels; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx deleted file mode 100644 index 662abc831..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import { memo, useState } from "react"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; -import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels"; - -const DownBarChart = memo(({ groupedDownChecks = [], type, onBarHover }) => { - const theme = useTheme(); - - const [chartHovered, setChartHovered] = useState(false); - const [hoveredBarIndex, setHoveredBarIndex] = useState(null); - - return ( - - { - setChartHovered(true); - onBarHover({ time: null, totalChecks: 0 }); - }} - onMouseLeave={() => { - setChartHovered(false); - setHoveredBarIndex(null); - onBarHover(null); - }} - > - - } - /> - - {groupedDownChecks?.map((entry, index) => { - return ( - { - setHoveredBarIndex(index); - onBarHover(entry); - }} - onMouseLeave={() => { - setHoveredBarIndex(null); - onBarHover({ time: null, totalChecks: 0 }); - }} - /> - ); - })} - - - - ); -}); - -DownBarChart.displayName = "DownBarChart"; -DownBarChart.propTypes = { - monitor: PropTypes.shape({ - groupedDownChecks: PropTypes.arrayOf(PropTypes.object), - }), - type: PropTypes.string, - onBarHover: PropTypes.func, -}; -export default DownBarChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx deleted file mode 100644 index 084a711c3..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts"; -import { useTranslation } from "react-i18next"; - -const ResponseGaugeChart = ({ avgResponseTime }) => { - const theme = useTheme(); - const { t } = useTranslation(); - let max = 1000; // max ms - - const data = [ - { response: max, fill: "transparent", background: false }, - { response: avgResponseTime, background: true }, - ]; - let responseTime = Math.floor(avgResponseTime); - let responseProps = - responseTime <= 200 - ? { - category: "Excellent", - main: theme.palette.success.main, - bg: theme.palette.success.lowContrast, - } - : responseTime <= 500 - ? { - category: "Fair", - main: theme.palette.success.main, - bg: theme.palette.success.lowContrast, - } - : responseTime <= 600 - ? { - category: "Acceptable", - main: theme.palette.warning.main, - bg: theme.palette.warning.lowContrast, - } - : { - category: "Poor", - main: theme.palette.error.main, - bg: theme.palette.error.lowContrast, - }; - - return ( - - - - {t("low")} - - - {t("high")} - - - {responseProps.category} - - - {responseTime} ms - - - - - - - - ); -}; - -ResponseGaugeChart.propTypes = { - avgResponseTime: PropTypes.number.isRequired, -}; - -export default ResponseGaugeChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx deleted file mode 100644 index e2d8dea80..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import ChartBox from "../../../../../Components/Charts/ChartBox"; -import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart"; -import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react"; -import SkeletonLayout from "./ResponseTimeChartSkeleton"; -import PropTypes from "prop-types"; - -const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) => { - if (isLoading) { - return ; - } - - return ( - } - header="Response Times" - > - - - ); -}; - -ResponseTImeChart.propTypes = { - isLoading: PropTypes.bool, - groupedChecks: PropTypes.array, - dateRange: PropTypes.string, -}; - -export default ResponseTImeChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx deleted file mode 100644 index a70757e36..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Skeleton } from "@mui/material"; -const ResponseTimeChartSkeleton = () => { - return ( - - ); -}; - -export default ResponseTimeChartSkeleton; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx deleted file mode 100644 index 317147b74..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { memo, useState } from "react"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; -import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels"; - -const getThemeColor = (responseTime) => { - if (responseTime < 200) { - return "success"; - } else if (responseTime < 300) { - return "warning"; - } else { - return "error"; - } -}; - -const UpBarChart = memo(({ groupedUpChecks = [], type, onBarHover }) => { - const theme = useTheme(); - const [chartHovered, setChartHovered] = useState(false); - const [hoveredBarIndex, setHoveredBarIndex] = useState(null); - - return ( - - { - setChartHovered(true); - onBarHover({ time: null, totalChecks: 0, originalAvgResponseTime: 0 }); - }} - onMouseLeave={() => { - setChartHovered(false); - setHoveredBarIndex(null); - onBarHover(null); - }} - > - - } - /> - - {groupedUpChecks?.map((entry, index) => { - const themeColor = getThemeColor(entry.originalAvgResponseTime); - return ( - { - setHoveredBarIndex(index); - onBarHover(entry); - }} - onMouseLeave={() => { - setHoveredBarIndex(null); - onBarHover({ - time: null, - totalChecks: 0, - groupUptimePercentage: 0, - }); - }} - /> - ); - })} - - - - ); -}); - -// Add display name for the component -UpBarChart.displayName = "UpBarChart"; - -// Validate props using PropTypes -UpBarChart.propTypes = { - monitor: PropTypes.shape({ - groupedUpChecks: PropTypes.array, - }), - type: PropTypes.string, - onBarHover: PropTypes.func, -}; -export default UpBarChart; diff --git a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx new file mode 100644 index 000000000..2cfdc1f15 --- /dev/null +++ b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx @@ -0,0 +1,100 @@ +import { Table, Pagination, StatusLabel } from "@/Components/v2/design-elements"; +import Box from "@mui/material/Box"; +import type { Header } from "@/Components/v2/design-elements/"; +import type { Check } from "@/Types/Check"; + +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { RootState } from "@/Types/state"; +import { useSelector } from "react-redux"; + +const getHeaders = (t: Function, uiTimezone: string) => { + const headers: Header[] = [ + { + id: "status", + content: t("common.table.headers.status"), + render: (row) => { + return ; + }, + }, + { + id: "date", + content: t("pages.checks.table.headers.dateTime"), + render: (row) => { + return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone); + }, + }, + { + id: "message", + content: t("common.table.headers.message"), + render: (row) => { + return row.message || "N/A"; + }, + }, + { + id: "statusCode", + content: t("pages.checks.table.headers.statusCode"), + render: (row) => { + return row.statusCode || "N/A"; + }, + }, + ]; + return headers; +}; + +export const ChecksTable = ({ + checks, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + checks: Check[]; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const navigate = useNavigate(); + const { t } = useTranslation(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + 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 ( + + { + navigate(`/checks/${row.id}`); + }} + /> + + + ); +}; diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx deleted file mode 100644 index 4d482aa07..000000000 --- a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import ChartBox from "../../../../../Components/Charts/ChartBox"; -import PropTypes from "prop-types"; -import HistoryIcon from "../../../../../assets/icons/history-icon.svg?react"; -import Table from "../../../../../Components/Table"; -import TablePagination from "../../../../../Components/Table/TablePagination"; -import { StatusLabel } from "../../../../../Components/Label"; -import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils"; -import SkeletonLayout from "./skeleton"; -const ResponseTable = ({ - isLoading = false, - checks = [], - checksCount, - uiTimezone, - page, - setPage, - rowsPerPage, - setRowsPerPage, -}) => { - const { t } = useTranslation(); - if (isLoading) { - return ; - } - - const headers = [ - { - id: "status", - content: t("status"), - render: (row) => { - const status = row.status === true ? "up" : "down"; - - return ( - - ); - }, - }, - { - id: "date", - content: t("date&Time"), - render: (row) => - formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone), - }, - { - id: "statusCode", - content: t("statusCode"), - render: (row) => (row.statusCode ? row.statusCode : "N/A"), - }, - { - id: "message", - content: t("message"), - render: (row) => row.message, - }, - ]; - - return ( - } - header="Response Times" - height="100%" - > -
- - - ); -}; - -ResponseTable.propTypes = { - isLoading: PropTypes.bool, - checks: PropTypes.array, - checksCount: PropTypes.number, - uiTimezone: PropTypes.string.isRequired, - page: PropTypes.number.isRequired, - setPage: PropTypes.func.isRequired, - rowsPerPage: PropTypes.number.isRequired, - setRowsPerPage: PropTypes.func.isRequired, -}; - -export default ResponseTable; diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx deleted file mode 100644 index 2665ffaf1..000000000 --- a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx deleted file mode 100644 index abeb251fe..000000000 --- a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx +++ /dev/null @@ -1,81 +0,0 @@ -import StatusBoxes from "../../../../../Components/StatusBoxes"; -import StatBox from "../../../../../Components/StatBox"; - -import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; -import { useTheme } from "@mui/material/styles"; -import { Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; - -const UptimeStatusBoxes = ({ - isLoading = false, - monitor, - monitorStats, - certificateExpiry, -}) => { - const theme = useTheme(); - const { determineState } = useMonitorUtils(); - - // Determine time since last failure - const timeOfLastFailure = monitorStats?.timeOfLastFailure; - const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure : 0; - - // Determine time since last check - const timeOfLastCheck = monitorStats?.lastCheckTimestamp; - const timeSinceLastCheck = Date.now() - timeOfLastCheck; - - const streakTime = getHumanReadableDuration(timeSinceLastFailure); - - const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck); - return ( - - - - {lastCheckTime} - {"ago"} - - } - /> - - {monitorStats?.lastResponseTime} - {"ms"} - - } - /> - {monitor?.type === "http" && ( - - {certificateExpiry} - - } - /> - )} - - ); -}; - -UptimeStatusBoxes.propTypes = { - isLoading: PropTypes.bool, - monitor: PropTypes.object, - monitorStats: PropTypes.object, - certificateExpiry: PropTypes.string, -}; - -export default UptimeStatusBoxes; diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx index ad0db6f70..8912438d8 100644 --- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx +++ b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx @@ -1,7 +1,7 @@ -import { logger } from "../../../../Utils/Logger"; +import { logger } from "../../../../Utils/Logger.js"; import { useEffect, useState } from "react"; -import { networkService } from "../../../../main"; -import { formatDateWithTz } from "../../../../Utils/timeUtils"; +import { networkService } from "../../../../main.jsx"; +import { formatDateWithTz } from "../../../../Utils/timeUtilsLegacy.js"; const useCertificateFetch = ({ monitor, diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx new file mode 100644 index 000000000..1a6177ddc --- /dev/null +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -0,0 +1,166 @@ +import { BasePage } from "@/Components/v2/design-elements"; +import { HeaderMonitorControls, HeaderTimeRange } from "@/Components/v2/common"; +import Stack from "@mui/material/Stack"; +import { + HistogramStatus, + RadialAvgResponse, + HistogramDetails, +} from "@/Components/v2/monitors"; +import { TrendingUp, AlertTriangle } from "lucide-react"; +import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable"; + +import { useTheme } from "@mui/material/styles"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import { useState, useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useGet } from "@/Hooks/UseApi"; +import type { MonitorDetailsResponse } from "@/Types/Monitor"; +import type { ChecksResponse } from "@/Types/Check"; +import type { RootState } from "@/Types/state"; +import { MonitorStatBoxes } from "@/Components/v2/monitors"; +import { formatDateWithTz } from "@/Utils/timeUtilsLegacy"; +import { t } from "i18next"; + +const certificateDateFormat = "MMM D, YYYY h A"; + +interface CertificateResponse { + certificateDate: string; +} + +const UptimeDetailsPage = () => { + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const { monitorId } = useParams<{ monitorId: string }>(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [dateRange, setDateRange] = useState("recent"); + + const monitorDetailsUrl = useMemo(() => { + if (!monitorId) { + return null; + } + const params = new URLSearchParams(); + params.append("dateRange", dateRange); + params.append("normalize", "true"); + return `/monitors/uptime/details/${monitorId}?${params.toString()}`; + }, [monitorId, dateRange]); + + const { + data: monitorDetailsData, + isLoading: monitorIsLoading, + refetch: refetchMonitor, + } = useGet( + monitorDetailsUrl, + {}, + { refreshInterval: 10000, keepPreviousData: true } + ); + + const monitorData = monitorDetailsData?.monitorData; + const monitor = monitorData?.monitor; + const monitorStats = monitorDetailsData?.monitorStats ?? null; + + // Certificate fetch - only for HTTP monitors + const certificateUrl = useMemo(() => { + if (!monitorId || monitor?.type !== "http") { + return null; + } + return `/monitors/certificate/${monitorId}`; + }, [monitorId, monitor?.type]); + + const { data: certificateData } = useGet(certificateUrl); + + const certificateExpiry = useMemo(() => { + if (!certificateData?.certificateDate) { + return undefined; + } + return ( + formatDateWithTz( + certificateData.certificateDate, + certificateDateFormat, + uiTimezone + ) ?? "N/A" + ); + }, [certificateData, uiTimezone]); + + const checksUrl = useMemo(() => { + if (!monitorId || !monitor?.type) { + return null; + } + const params = new URLSearchParams(); + params.append("type", monitor.type); + params.append("sortOrder", "desc"); + params.append("dateRange", dateRange); + params.append("page", String(page)); + params.append("rowsPerPage", String(rowsPerPage)); + return `/checks/${monitorId}?${params.toString()}`; + }, [monitorId, monitor?.type, dateRange, page, rowsPerPage]); + + const { data: checksData, isLoading: checksIsLoading } = useGet( + checksUrl, + {}, + { keepPreviousData: true } + ); + + const checks = checksData?.checks ?? []; + const checksCount = checksData?.checksCount ?? 0; + + return ( + + + + + + } + checks={monitorData?.groupedUpChecks ?? []} + range={dateRange} + /> + } + checks={monitorData?.groupedDownChecks ?? []} + range={dateRange} + /> + + + + + + ); +}; + +export default UptimeDetailsPage; diff --git a/client/src/Pages/Uptime/Details/index.jsx b/client/src/Pages/Uptime/Details/old.jsx similarity index 87% rename from client/src/Pages/Uptime/Details/index.jsx rename to client/src/Pages/Uptime/Details/old.jsx index c72a5756f..144ceb4dc 100644 --- a/client/src/Pages/Uptime/Details/index.jsx +++ b/client/src/Pages/Uptime/Details/old.jsx @@ -1,12 +1,12 @@ // Components -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; -import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; -import ChartBoxes from "./Components/ChartBoxes"; -import ResponseTimeChart from "./Components/Charts/ResponseTimeChart"; -import ResponseTable from "./Components/ResponseTable"; -import UptimeStatusBoxes from "./Components/UptimeStatusBoxes"; -import GenericFallback from "../../../Components/GenericFallback"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; +import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; +import ChartBoxes from "./Components/ChartBoxes/index.jsx"; +import ResponseTimeChart from "./Components/Charts/ResponseTimeChart.jsx"; +import ResponseTable from "./Components/ResponseTable/index.jsx"; +import UptimeStatusBoxes from "./Components/UptimeStatusBoxes/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; @@ -15,10 +15,10 @@ import { useState } from "react"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks"; -import useCertificateFetch from "./Hooks/useCertificateFetch"; -import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; +import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks.js"; +import useCertificateFetch from "./Hooks/useCertificateFetch.jsx"; +import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks.js"; import { useTranslation } from "react-i18next"; // Constants diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx index 0efb06b6c..62ba0be13 100644 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx @@ -1,9 +1,9 @@ import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import FilterHeader from "../../../../../Components/FilterHeader"; +import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; import { useMemo } from "react"; import { Box, Button } from "@mui/material"; -import ClearIcon from "@mui/icons-material/Clear"; +import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; /** @@ -131,7 +131,12 @@ const Filter = ({ color={theme.palette.primary.contrastText} onClick={handleReset} variant="contained" - endIcon={} + endIcon={ + + } sx={{ visibility: isFilterActive ? "visible" : "hidden", }} diff --git a/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx b/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx index 0c6eec854..64c546a8a 100644 --- a/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import Search from "../../../../../Components/Inputs/Search"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; import { Box } from "@mui/material"; -import useDebounce from "../../Hooks/useDebounce"; +import useDebounce from "../../Hooks/useDebounce.jsx"; import { useEffect, useRef } from "react"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx index 9cd29c4ca..5cba9e3b1 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; import { Stack } from "@mui/material"; -import StatusBox from "./statusBox"; +import StatusBox from "./statusBox.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; const StatusBoxes = ({ shouldRender, monitorsSummary }) => { const theme = useTheme(); diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx index 78d555409..136c3575d 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx @@ -1,9 +1,8 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react"; -import Background from "../../../../../assets/Images/background-grid.svg?react"; -import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react"; +import Icon from "@/Components/v1/Icon"; +import Background from "@/assets/Images/background-grid.svg?react"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); @@ -20,21 +19,30 @@ const StatusBox = ({ title, value, status }) => { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx index 63a40f85a..8c3239dd3 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -1,19 +1,18 @@ // Components import { Box, Stack } from "@mui/material"; -import DataTable from "../../../../../Components/Table"; -import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; -import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; -import Host from "../../../../../Components/Host"; -import { StatusLabel } from "../../../../../Components/Label"; -import BarChart from "../../../../../Components/Charts/BarChart"; -import ActionsMenu from "../../../../../Components/ActionsMenu"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Icon from "@/Components/v1/Icon"; +import Host from "@/Components/v1/Host/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import BarChart from "@/Components/v1/Charts/BarChart/index.jsx"; +import ActionsMenu from "@/Components/v1/ActionsMenu/index.jsx"; -import LoadingSpinner from "../LoadingSpinner"; -import TableSkeleton from "../../../../../Components/Table/skeleton"; +import LoadingSpinner from "../LoadingSpinner/index.jsx"; +import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; @@ -24,13 +23,13 @@ import { useTranslation } from "react-i18next"; * @param {boolean} props.isAdmin - Whether the current user has admin privileges * @param {boolean} props.isLoading - Loading state of the table * @param {Array<{ - * _id: string, + * id: string, * url: string, * title: string, * percentage: number, * percentageColor: string, * monitor: { - * _id: string, + * id: string, * type: string, * checks: Array * } @@ -92,16 +91,22 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} ), render: (row) => ( {sort?.order === "asc" ? ( - + ) : ( - + )} @@ -200,7 +211,7 @@ const UptimeDataTable = ({ }, }, onRowClick: (row) => { - navigate(`/uptime/${row._id}`); + navigate(`/uptime/${row.id}`); }, emptyView: "No monitors found", }} diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx new file mode 100644 index 000000000..c7f5c4c7f --- /dev/null +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -0,0 +1,259 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { + Table, + type Header, + Pagination, + StatusLabel, +} from "@/Components/v2/design-elements"; +import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/v2/common"; +import { ActionsMenu } from "@/Components/v2/actions-menu"; +import { ArrowDown, ArrowUp } from "lucide-react"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { usePost } from "@/Hooks/UseApi"; +import { useSelector } from "react-redux"; + +import type { Monitor } from "@/Types/Monitor"; +import type { ActionMenuItem } from "@/Components/v2/actions-menu"; +import type { RootState } from "@/Types/state"; + +export const MonitorTable = ({ + monitors, + refetch, + setSelectedMonitor, + sortField, + setSortField, + sortOrder, + setSortOrder, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: Monitor[]; + refetch: Function; + setSelectedMonitor: (monitor: Monitor | null) => void; + sortField: string; + setSortField: (field: string) => void; + sortOrder: "asc" | "desc"; + setSortOrder: (order: "asc" | "desc") => void; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + const chartType = useSelector((state: RootState) => state.ui?.chartType ?? "histogram"); + const { + post, + // loading: isPosting, + // error: postError, + } = usePost(); + + const handleSort = (e: any, field: string) => { + e.preventDefault(); + e.stopPropagation(); + if (sortField === field) { + const newOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newOrder); + } else { + setSortField(field); + setSortOrder("asc"); + } + refetch(); + }; + + const getActions = (monitor: Monitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.openSite"), + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: t("pages.common.monitors.actions.details"), + action: () => { + navigate(`${monitor.id}`); + }, + }, + { + id: 3, + label: t("pages.common.monitors.actions.incidents"), + action: () => { + navigate(`/incidents?monitorId=${monitor.id}`); + }, + }, + { + id: 4, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/uptime/configure/${monitor.id}`); + }, + }, + // { + // id: 5, + // label: "Clone", + // action: () => { + + // }, + // }, + { + id: 6, + label: + monitor.isActive === false + ? t("pages.common.monitors.actions.resume") + : t("pages.common.monitors.actions.pause"), + action: async () => { + await post(`/monitors/pause/${monitor.id}`, {}); + refetch(); + }, + closeMenu: true, + }, + { + id: 7, + label: ( + + {t("pages.common.monitors.actions.delete")} + + ), + action: () => setSelectedMonitor(monitor), + closeMenu: true, + }, + ]; + }; + + const getHeaders = (chartType: string) => { + const renderSortIcon = (isActive: boolean) => ( + + {isActive ? ( + sortOrder === "asc" ? ( + + ) : ( + + ) + ) : null} + + ); + const headers: Header[] = [ + { + id: "name", + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.name")} + {renderSortIcon(sortField === "name")} + + ), + + render: (row) => { + return row?.name; + }, + }, + { + id: "status", + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.status")} + {renderSortIcon(sortField === "status")} + + ), + render: (row) => { + return ( + + ); + }, + }, + { + id: "histogram", + content: t("pages.uptime.table.headers.responseTime"), + render: (row) => { + if (chartType === "histogram") { + return ; + } else { + return ; + } + }, + }, + { + id: "type", + content: ( + handleSort(e, "type")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.type")} + {renderSortIcon(sortField === "type")} + + ), + render: (row) => { + return row.type; + }, + }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(chartType); + + return ( + +
{ + navigate(`/uptime/${row.id}`); + }} + /> + setPage(newPage)} + onRowsPerPageChange={(e) => setRowsPerPage(Number(e.target.value))} + /> + + ); +}; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx new file mode 100644 index 000000000..5c3828d07 --- /dev/null +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -0,0 +1,216 @@ +import { ControlsFilter } from "@/Components/v2/monitors"; +import { + MonitorBasePageWithStates, + UpStatusBox, + DownStatusBox, + PausedStatusBox, +} from "@/Components/v2/design-elements"; +import { TextField, Dialog } from "@/Components/v2/inputs"; +import Stack from "@mui/material/Stack"; +import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; +import { HeaderCreate } from "@/Components/v2/common"; + +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useGet, useDelete } from "@/Hooks/UseApi"; +import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor"; +import { useState, useMemo, useCallback } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { setRowsPerPage } from "@/Features/UI/uiSlice.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import type { RootState } from "@/Types/state"; +import { useTheme } from "@mui/material"; + +const UptimeMonitorsPage = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const dispatch = useDispatch(); + const isAdmin = useIsAdmin(); + // Redux state + const rowsPerPage = useSelector( + (state: RootState) => state.ui?.monitors?.rowsPerPage ?? 10 + ); + + // Local state + const [selectedTypes, setSelectedTypes] = useState([]); + const [selectedStatus, setSelectedStatus] = useState(""); + const [selectedState, setSelectedState] = useState(""); + const [page, setPage] = useState(0); + const [search, setSearch] = useState(""); + const [sortField, setSortField] = useState(""); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); + + // Convert filter selections to API filter values + // Status: "up" -> true, "down" -> false + // State: "active" -> true, "paused" -> false + const toFilterStatus = useMemo(() => { + if (selectedStatus === "up") return "true"; + if (selectedStatus === "down") return "false"; + return undefined; + }, [selectedStatus]); + + const toFilterActive = useMemo(() => { + if (selectedState === "active") return "true"; + if (selectedState === "paused") return "false"; + return undefined; + }, [selectedState]); + + // Determine field and filter for the API request + // Priority: status > isActive > search > sort + const filterLookup = new Map([ + [toFilterStatus, "status"], + [toFilterActive, "isActive"], + ]); + const activeFilter = [...filterLookup].find(([key]) => key !== undefined); + const field = activeFilter?.[1] || (search ? "name" : sortField || undefined); + const filter = activeFilter?.[0] || search; + + // Default to all types when none selected + const effectiveTypes = + selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port"]; + + // Build URL for monitors with checks + const monitorsWithChecksUrl = useMemo(() => { + const params = new URLSearchParams(); + effectiveTypes.forEach((type) => params.append("type", type)); + params.append("limit", "25"); + if (page !== undefined) params.append("page", String(page)); + if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage)); + if (filter) params.append("filter", filter); + if (field) params.append("field", field); + if (sortOrder) params.append("order", sortOrder); + return `/monitors/team/with-checks?${params.toString()}`; + }, [effectiveTypes, page, rowsPerPage, filter, field, sortOrder]); + + // Data fetching + const { + data: monitors, + isLoading: monitorsLoading, + error, + refetch: refetchMonitorsAndSummary, + } = useGet( + "/monitors/team?type=http&type=ping&type=port&type=docker", + {}, + { keepPreviousData: true } + ); + + const { + data: monitorsWithChecksData, + isLoading: monitorsWithChecksLoading, + error: monitorsWithChecksError, + refetch, + } = useGet( + monitorsWithChecksUrl, + {}, + { refreshInterval: 5000, keepPreviousData: true } + ); + + const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; + + // Delete hook + const { deleteFn, loading: isDeleting } = useDelete(); + + // Handlers + const handleClearFilters = useCallback(() => { + setSelectedTypes([]); + setSelectedStatus(""); + setSelectedState(""); + setSearch(""); + }, []); + + const handleConfirm = async () => { + if (!selectedMonitor) return; + await deleteFn(`/monitors/${selectedMonitor.id}`); + setSelectedMonitor(null); + refetch(); + refetchMonitorsAndSummary(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + + const isLoading = monitorsLoading || monitorsWithChecksLoading; + + return ( + + + + + + + + + + + { + setSearch(event.target.value); + }} + /> + + { + dispatch( + setRowsPerPage({ + value: rowsPerPage, + table: "monitors", + }) + ); + setPage(0); + }} + /> + + + ); +}; + +export default UptimeMonitorsPage; diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/old.jsx similarity index 79% rename from client/src/Pages/Uptime/Monitors/index.jsx rename to client/src/Pages/Uptime/Monitors/old.jsx index 7f28c1cd7..67019362f 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/old.jsx @@ -4,32 +4,32 @@ // 2a.List of monitors must have the total number of monitors that match. // Components -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import Greeting from "../../../Utils/greeting"; -import StatusBoxes from "./Components/StatusBoxes"; -import UptimeDataTable from "./Components/UptimeDataTable"; -import Pagination from "../../../Components/Table/TablePagination"; -import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; -import SearchComponent from "./Components/SearchComponent"; -import Filter from "./Components/Filter"; -import PageStateWrapper from "../../../Components/PageStateWrapper"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import Greeting from "../../../Utils/greeting.jsx"; +import StatusBoxes from "./Components/StatusBoxes/index.jsx"; +import UptimeDataTable from "./Components/UptimeDataTable/index.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; +import SearchComponent from "./Components/SearchComponent/index.jsx"; +import Filter from "./Components/Filter/index.jsx"; +import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; -import MonitorCountHeader from "../../../Components/MonitorCountHeader"; +import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; // MUI Components import { Stack, Box, Button } from "@mui/material"; // Utils import { useState, useCallback, useEffect } from "react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; import { - useFetchMonitorsWithSummary, useFetchMonitorsWithChecks, -} from "../../../Hooks/monitorHooks"; + useFetchMonitorsByTeamId, +} from "@/Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; const TYPES = ["http", "ping", "docker", "port", "game"]; @@ -103,12 +103,6 @@ const UptimeMonitors = () => { setMonitorUpdateTrigger((prev) => !prev); }, []); - const [monitors, monitorsSummary, monitorsWithSummaryIsLoading, networkError] = - useFetchMonitorsWithSummary({ - types: TYPES, - monitorUpdateTrigger, - }); - const handleReset = () => { setSelectedState(undefined); setSelectedTypes(undefined); @@ -123,16 +117,17 @@ const UptimeMonitors = () => { ]); const activeFilter = [...filterLookup].find(([key]) => key !== undefined); - const field = activeFilter?.[1] || sort?.field; + const field = activeFilter?.[1] || (search ? "name" : sort?.field); const filter = activeFilter?.[0] || search; const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; const [ + summary, monitorsWithChecks, monitorsWithChecksCount, monitorsWithChecksIsLoading, - monitorsWithChecksNetworkError, + networkError, ] = useFetchMonitorsWithChecks({ types: effectiveTypes, limit: 25, @@ -144,13 +139,17 @@ const UptimeMonitors = () => { monitorUpdateTrigger, }); + const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({ + type: ["http", "ping", "docker", "port", "game"], + }); + useEffect(() => { if (isSearching) { setPage(undefined); } }, [isSearching]); - const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading; + const isLoading = monitorsWithChecksIsLoading; return ( <> @@ -174,14 +173,14 @@ const UptimeMonitors = () => { /> { + const mode = useSelector((state) => state.ui.mode); const AdminCheckedRegister = withAdminCheck(AuthRegister); + const v2theme = mode === "light" ? lightTheme : darkTheme; return ( { /> } + element={ + <> + + + + + } /> { /> } + element={ + <> + + + + + } /> { } + element={ + <> + + + + + } /> { /> } + element={ + <> + + + + + } /> { path="infrastructure/:monitorId" element={} /> + } + /> } diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts new file mode 100644 index 000000000..0aa953dbb --- /dev/null +++ b/client/src/Types/Check.ts @@ -0,0 +1,146 @@ +export interface CheckMetadata { + monitorId: string; + teamId: string; + type: string; +} + +export interface CheckCpuInfo { + physical_core?: number; + logical_core?: number; + frequency?: number; + temperature?: number[]; + free_percent?: number; + usage_percent?: number; +} + +export interface CheckMemoryInfo { + total_bytes?: number; + available_bytes?: number; + used_bytes?: number; + usage_percent?: number; +} + +export interface CheckHostInfo { + os?: string; + platform?: string; + kernel_version?: string; +} + +export interface CheckCaptureInfo { + version?: string; + mode?: string; +} + +export interface CheckDiskInfo { + device?: string; + mountpoint?: string; + read_speed_bytes?: number; + write_speed_bytes?: number; + total_bytes?: number; + free_bytes?: number; + usage_percent?: number; +} + +export interface CheckErrorInfo { + metric: string[]; + err: string; +} + +export interface CheckNetworkInterfaceInfo { + 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 CheckAudits { + cls?: ILighthouseAudit; + si?: ILighthouseAudit; + fcp?: ILighthouseAudit; + lcp?: ILighthouseAudit; + tbt?: ILighthouseAudit; +} + +export interface ILighthouseAudit { + id?: string; + title?: string; + score?: number | null; + displayValue?: string; + numericValue?: number; + numericUnit?: string; +} + +export interface CheckTimings { + start?: number; + socket?: number; + lookup?: number; + connect?: number; + secureConnect?: number; + response?: number; + end?: number; + abort?: number; + error?: number; + phases?: { + wait?: number; + dns?: number; + tcp?: number; + tls?: number; + request?: number; + firstByte?: number; + download?: number; + total?: number; + }; +} + +export interface Check { + id: string; + metadata: CheckMetadata; + status: boolean; + originalResponseTime?: number; + responseTime: number; + timings?: CheckTimings; + statusCode: number; + message: string; + ack: boolean; + ackAt?: string | null; + expiry: string; + cpu?: CheckCpuInfo; + memory?: CheckMemoryInfo; + disk?: CheckDiskInfo[]; + host?: CheckHostInfo; + errors?: CheckErrorInfo[]; + capture?: CheckCaptureInfo; + net?: CheckNetworkInterfaceInfo[]; + accessibility?: number; + bestPractices?: number; + seo?: number; + performance?: number; + audits?: CheckAudits; + createdAt: string; + updatedAt: string; +} + +export interface GroupedCheck { + bucketDate: string; + avgResponseTime: number; + totalChecks: number; +} + +export interface LatestCheck { + status: boolean; + responseTime: number; + checkedAt: string; + id: string; +} + +export interface ChecksResponse { + checks: Check[]; + checksCount: number; +} diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts new file mode 100644 index 000000000..6baca2845 --- /dev/null +++ b/client/src/Types/Monitor.ts @@ -0,0 +1,83 @@ +import type { Check, GroupedCheck } from "@/Types/Check"; +export type MonitorStatus = boolean | undefined; + +export const MonitorTypes = [ + "http", + "ping", + "pagespeed", + "hardware", + "docker", + "port", + "game", + "unknown", +] as const; +export type MonitorType = (typeof MonitorTypes)[number]; + +export interface Monitor { + checks: Check[]; + createdAt: string; + createdBy: string; + interval: number; + isActive: boolean; + latestChecks: Check[]; + n: number; + name: string; + status: MonitorStatus; + type: string; + updatedAt: string; + updatedBy: string; + url: string; + id: string; +} + +export interface MonitorWithChecks extends Monitor { + checks: Check[]; + uptimePercentage?: number; +} + +export interface MonitorsSummary { + totalMonitors: number; + upMonitors: number; + downMonitors: number; + pausedMonitors: number; +} + +export interface MonitorsWithChecksResponse { + count: number; + monitors: MonitorWithChecks[]; + summary: MonitorsSummary; +} + +export interface MonitorStats { + id: string; + monitorId: string; + avgResponseTime: number; + totalChecks: number; + totalUpChecks: number; + totalDownChecks: number; + uptimePercentage: number; + lastCheckTimestamp: number; + lastResponseTime: number; + timeOfLastFailure?: number; + createdAt: string; + updatedAt: string; +} + +export interface MonitorData { + monitor: Monitor; + groupedChecks: GroupedCheck[]; + groupedUpChecks: GroupedCheck[]; + groupedDownChecks: GroupedCheck[]; + groupedAvgResponseTime: number; + groupedUptimePercentage: number; +} + +export interface MonitorDetailsResponse { + monitorData: MonitorData; + monitorStats: MonitorStats | null; +} + +export interface PageSpeedDetailsResponse { + monitor: MonitorWithChecks; + monitorStats: MonitorStats | null; +} diff --git a/client/src/Types/Settings.ts b/client/src/Types/Settings.ts new file mode 100644 index 000000000..28f7bf5ed --- /dev/null +++ b/client/src/Types/Settings.ts @@ -0,0 +1,34 @@ +export interface SettingsThresholds { + cpu?: number; + memory?: number; + disk?: number; + temperature?: number; +} + +export interface Settings { + id: string; + checkTTL: number; + language: string; + systemEmailHost?: string; + systemEmailPort?: number; + systemEmailAddress?: string; + systemEmailUser?: string; + systemEmailConnectionHost?: string; + systemEmailTLSServername?: string; + systemEmailSecure: boolean; + systemEmailPool: boolean; + systemEmailIgnoreTLS: boolean; + systemEmailRequireTLS: boolean; + systemEmailRejectUnauthorized: boolean; + showURL: boolean; + singleton: boolean; + globalThresholds?: SettingsThresholds; + createdAt: string; + updatedAt: string; +} + +export interface AppSettingsResponse { + pagespeedKeySet: boolean; + emailPasswordSet: boolean; + settings: Settings; +} diff --git a/client/src/Types/env.d.ts b/client/src/Types/env.d.ts new file mode 100644 index 000000000..74a2a29e7 --- /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/mui.d.ts b/client/src/Types/mui.d.ts new file mode 100644 index 000000000..3878f9391 --- /dev/null +++ b/client/src/Types/mui.d.ts @@ -0,0 +1,16 @@ +import "@mui/material/Button"; + +declare module "@mui/material/styles" { + interface Palette {} + interface PaletteOptions {} +} + +declare module "@mui/material/Button" { + interface ButtonPropsColorOverrides { + neutral: true; + } +} + +declare module "@mui/material/CircularProgress" { + interface CircularProgressPropsColorOverrides {} +} diff --git a/client/src/Types/state.ts b/client/src/Types/state.ts new file mode 100644 index 000000000..137d67b66 --- /dev/null +++ b/client/src/Types/state.ts @@ -0,0 +1,41 @@ +export interface AuthState { + isLoading: boolean; + authToken: string; + user: string; + success: boolean | null; + msg: string | null; +} + +export interface UIState { + monitors: { + rowsPerPage: number; + }; + team: { + rowsPerPage: number; + }; + maintenance: { + rowsPerPage: number; + }; + infrastructure: { + rowsPerPage: number; + }; + sidebar: { + collapsed: boolean; + }; + mode: "light" | "dark"; + showURL: boolean; + greeting: { + index: number; + lastUpdate: string | null; + }; + timezone: string; + distributedUptimeEnabled: boolean; + language: string; + starPromptOpen: boolean; + chartType: string; +} + +export interface RootState { + auth: AuthState; + ui: UIState; +} diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts new file mode 100644 index 000000000..e90bc9f01 --- /dev/null +++ b/client/src/Utils/ApiClient.ts @@ -0,0 +1,75 @@ +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, +}); + +type StoreType = { + getState: () => { auth?: { authToken?: string } }; +}; + +let storeInstance: StoreType | null = null; +let interceptorsInitialized = false; + +export const initApiClient = (store: StoreType): void => { + storeInstance = store; + + if (interceptorsInitialized) { + return; + } + interceptorsInitialized = true; + + api.interceptors.request.use( + (config) => { + const authToken = storeInstance?.getState()?.auth?.authToken ?? ""; + + config.headers.set("Authorization", `Bearer ${authToken}`); + if (!config.headers.has("Accept-Language")) { + config.headers.set("Accept-Language", "en"); + } + + return config; + }, + (error) => { + return Promise.reject(error); + } + ); + + 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: unknown, + config: AxiosRequestConfig = {} +): Promise> => api.post(url, data, config); + +export const patch = ( + url: string, + data: unknown, + 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..c0bbb2d2a --- /dev/null +++ b/client/src/Utils/DataUtils.ts @@ -0,0 +1,129 @@ +const MIN_OUT = 10; +const MAX_OUT = 100; + +export const normalizeResponseTimes = ( + 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 getVal = (check: T): number => { + const v = check[key] as unknown; + return typeof v === "number" && Number.isFinite(v) ? v : 0; + }; + + const { min, max } = checks.reduce( + (acc, check) => { + const v = getVal(check); + if (v > acc.max) acc.max = v; + if (v < acc.min) acc.min = v; + return acc; + }, + { max: -Infinity, min: Infinity } + ); + + const range = max - min || 1; + + return checks.map((check) => ({ + ...check, + normalResponseTime: MIN_OUT + ((getVal(check) - min) * (MAX_OUT - MIN_OUT)) / range, + })); +}; + +// Interpolate color between three theme colors over 0-100 range. +// 0-50 => start (success.light), 50-75 => mid (warning.light), 75-100 => end (error.light) +export const getResponseColor = ( + ms: number, + colors: { + start: string | undefined; + mid: string | undefined; + end: string | undefined; + } +): string => { + // New ranges with open high end: + // 0–300ms: interpolate start (good) -> mid (warning) + // 300–600ms: interpolate mid (warning) -> end (error) + // >600ms: solid end (error) + const safe = { ...colors }; + if (!safe.start) safe.start = "#4caf50"; // fallback green + if (!safe.mid) safe.mid = "#ff9800"; // fallback orange + if (!safe.end) safe.end = "#f44336"; // fallback red + + const toHex = (c: number) => c.toString(16).padStart(2, "0"); + const clamp = (n: number) => Math.min(255, Math.max(0, Math.round(n))); + const rgbToHex = (r: number, g: number, b: number) => + `#${toHex(clamp(r))}${toHex(clamp(g))}${toHex(clamp(b))}`; + + const normalizeToHex = (value: string) => { + const v = value.trim(); + if (v.startsWith("#")) { + const h = v.slice(1); + const full = + h.length === 3 + ? h + .split("") + .map((c) => c + c) + .join("") + : h.substring(0, 6); + return `#${full.toLowerCase()}`; + } + const m = v + .replace(/\s+/g, "") + .match(/^rgba?\((\d{1,3}),(\d{1,3}),(\d{1,3})(?:,(0|0?\.\d+|1))?\)$/i); + if (m) { + const r = parseInt(m[1], 10); + const g = parseInt(m[2], 10); + const b = parseInt(m[3], 10); + return rgbToHex(r, g, b); + } + // Fallback neutral + return "#7f7f7f"; + }; + + const parseHex = (hex: string) => { + const h = hex.replace("#", ""); + const full = + h.length === 3 + ? h + .split("") + .map((c) => c + c) + .join("") + : h; + const n = parseInt(full, 16); + return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; + }; + const mix = (a: number, b: number, t: number) => Math.round(a + (b - a) * t); + + const v = Math.max(0, ms); + if (v <= 300) { + const t = v / 300; // 0..1 from start->mid + const s = parseHex(normalizeToHex(safe.start)); + const m = parseHex(normalizeToHex(safe.mid)); + const r = mix(s.r, m.r, t); + const g = mix(s.g, m.g, t); + const b = mix(s.b, m.b, t); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + if (v <= 600) { + const t = (v - 300) / 300; // 0..1 from mid->end + const m = parseHex(normalizeToHex(safe.mid)); + const e = parseHex(normalizeToHex(safe.end)); + const r = mix(m.r, e.r, t); + const g = mix(m.g, e.g, t); + const b = mix(m.b, e.b, t); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + return normalizeToHex(safe.end); +}; diff --git a/client/src/Utils/Logger.js b/client/src/Utils/Logger.js index f132ad408..849e10854 100644 --- a/client/src/Utils/Logger.js +++ b/client/src/Utils/Logger.js @@ -1,14 +1,25 @@ -import store from "../store"; const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL || "debug"; const NO_OP = () => {}; class Logger { constructor() { let logLevel = LOG_LEVEL; - this.unsubscribe = store.subscribe(() => { - logLevel = "debug"; - this.updateLogLevel(logLevel); - }); + this.updateLogLevel(logLevel); + // Defer store subscription to avoid circular dependency during HMR + setTimeout(() => { + try { + import("../store").then(({ default: store }) => { + if (store) { + this.unsubscribe = store.subscribe(() => { + logLevel = "debug"; + this.updateLogLevel(logLevel); + }); + } + }); + } catch (e) { + // Store not ready yet, ignore + } + }, 0); } updateLogLevel(logLevel) { diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts new file mode 100644 index 000000000..ed4e2894e --- /dev/null +++ b/client/src/Utils/MonitorUtils.ts @@ -0,0 +1,120 @@ +import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor"; +import type { PaletteKey } from "@/Utils/Theme/v2Theme"; +import type { ValueType } from "@/Components/v2/design-elements/StatusLabel"; + +export const getMonitorPath = (type: MonitorType): string => { + const pathMap: Record = { + http: "uptime", + port: "uptime", + ping: "uptime", + hardware: "hardware", + pagespeed: "pagespeed", + docker: "docker", + game: "game-servers", + unknown: "uptime", + }; + return pathMap[type]; +}; + +export const getStatusPalette = (status: MonitorStatus): PaletteKey => { + if (status === true) { + return "success"; + } + if (status === false) { + return "error"; + } + return "warning"; +}; + +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 => { + if (status === true) { + return theme.palette.success.light; + } + + if (status === false) { + return theme.palette.error.light; + } + + return theme.palette.warning.light; +}; + +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.light; + } else { + return theme.palette.error.light; + } +}; + +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: Monitor[], + t: any +): IStatusPageHeaderConfig => { + if (!monitors || monitors.length === 0) { + return { paletteKey: "error", message: "No monitors available" }; + } + + const allUp = monitors.every((monitor) => monitor.status === true); + const anyDown = monitors.some((monitor) => monitor.status === false); + const allDown = monitors.every((monitor) => monitor.status === false); + + if (allUp) + return { + paletteKey: "success", + message: t("statusPage.details.statusHeader.allUp"), + }; + if (allDown) + return { + paletteKey: "error", + message: t("statusPage.details.statusHeader.allDown"), + }; + if (anyDown) + return { + paletteKey: "warning", + message: t("statusPage.details.statusHeader.anyDown"), + }; + return { + paletteKey: "warning", + message: t("statusPage.details.statusHeader.anyDown"), + }; +}; diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index dce0faaf5..4a096badd 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -144,20 +144,16 @@ class NetworkService { */ async getMonitorsByTeamId(config) { - const { limit, types, page, rowsPerPage, filter, field, order } = config; + const { types, filter } = config ?? {}; const params = new URLSearchParams(); - if (limit) params.append("limit", limit); if (types) { types.forEach((type) => { params.append("type", type); }); } - if (page) params.append("page", page); - if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); - if (filter) params.append("filter", filter); - if (field) params.append("field", field); - if (order) params.append("order", order); + if (filter !== undefined && filter !== null && filter !== "") + params.append("filter", filter); return this.axiosInstance.get(`/monitors/team?${params.toString()}`, { headers: { @@ -202,6 +198,14 @@ class NetworkService { `/monitors/hardware/details/${config.monitorId}?${params.toString()}` ); } + async getPageSpeedDetailsByMonitorId(config) { + const params = new URLSearchParams(); + if (config.dateRange) params.append("dateRange", config.dateRange); + + return this.axiosInstance.get( + `/monitors/pagespeed/details/${config.monitorId}?${params.toString()}` + ); + } async getUptimeDetailsById(config) { const params = new URLSearchParams(); if (config.dateRange) params.append("dateRange", config.dateRange); @@ -343,17 +347,8 @@ class NetworkService { return this.axiosInstance.get(`/monitors/certificate/${config.monitorId}`); } - /** - * ************************************ - * Registers a new user - * ************************************ - * - * @async - * @param {Object} form - The form data for the new user to be registered. - * @returns {Promise} The response from the axios POST request. - */ - async registerUser(form) { - return this.axiosInstance.post(`/auth/register`, form); + async registerUser(user, token) { + return this.axiosInstance.post(`/auth/register`, { user, token }); } /** @@ -914,7 +909,7 @@ class NetworkService { } async createStatusPage(config) { - const { form, isCreate } = config; + const { form, isCreate, id } = config; const fd = new FormData(); fd.append("type", form.type); @@ -960,7 +955,7 @@ class NetworkService { return this.axiosInstance.post(`/status-page`, fd, {}); } - return this.axiosInstance.put(`/status-page`, fd, {}); + return this.axiosInstance.put(`/status-page/${id}`, fd, {}); } async deleteStatusPage(config) { @@ -1136,6 +1131,50 @@ class NetworkService { }, }); } + + async changePasswordByAdmin(config) { + const { userId, passwordForm } = config; + return this.axiosInstance.put(`auth/users/${userId}/password`, passwordForm, { + headers: { + "Content-Type": "application/json", + }, + }); + } + + async fetchJson() { + return this.axiosInstance.get("/monitors/export/json"); + } + + getIncidentsByTeam = async (config) => { + const params = new URLSearchParams(); + if (config.monitorId) params.append("monitorId", config.monitorId); + if (config.status !== undefined) params.append("status", config.status); + if (config.resolutionType) params.append("resolutionType", config.resolutionType); + if (config.sortOrder) params.append("sortOrder", config.sortOrder); + if (config.dateRange) params.append("dateRange", config.dateRange); + if (config.page) params.append("page", config.page); + if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage); + return this.axiosInstance.get(`/incidents/team?${params.toString()}`); + }; + + getIncidentById = async (incidentId) => { + return this.axiosInstance.get(`/incidents/${incidentId}`); + }; + + resolveIncidentManually = async (incidentId, options = {}) => { + const body = {}; + if (options.comment) body.comment = options.comment; + return this.axiosInstance.put(`/incidents/${incidentId}/resolve`, body); + }; + + getIncidentSummary = async (config = {}) => { + const params = new URLSearchParams(); + if (config.limit) params.append("limit", config.limit); + const queryString = params.toString(); + return this.axiosInstance.get( + `/incidents/team/summary${queryString ? `?${queryString}` : ""}` + ); + }; } export default NetworkService; diff --git a/client/src/Utils/Theme/constants.js b/client/src/Utils/Theme/constants.js index 9df6665d0..2664c6126 100644 --- a/client/src/Utils/Theme/constants.js +++ b/client/src/Utils/Theme/constants.js @@ -1,4 +1,4 @@ -import { lighten, darken } from "@mui/material/styles"; // CAIO_REVIEW +import { lighten } from "@mui/material/styles"; // CAIO_REVIEW const typographyBase = 13; @@ -100,7 +100,6 @@ const newColors = { blueGray900: "#515151", blueBlueWave: "#1570EF", lightBlueWave: "#CDE2FF", - /* I changed green 100 and green 700. Need to change red and warning as well, and refactor the object following the structure */ green100: "#67cd78", green200: "#4B9B77", green400: "#079455", @@ -134,12 +133,6 @@ const newSemanticColors = { light: lighten(newColors.blueBlueWave, 0.2), //CAIO_REVIEW dark: lighten(newColors.blueBlueWave, 0.2), //CAIO_REVIEW }, - darker: { - // CAIO_REVIEW - light: darken(newColors.blueBlueWave, 0.2), // CAIO_REVIEW - dark: darken(newColors.blueBlueWave, 0.2), // CAIO_REVIEW - }, - contrastText: { light: newColors.offWhite, dark: newColors.offWhite, @@ -166,14 +159,6 @@ const newSemanticColors = { light: newColors.blueGray500, dark: newColors.gray500, }, - contrastBorder: { - light: newColors.gray500, - dark: newColors.blueGray600, - }, - contrastBorderDisabled: { - light: newColors.gray100, - dark: newColors.blueGray800, - }, lowContrast: { light: newColors.gray250, dark: newColors.blueGray600, @@ -208,18 +193,6 @@ const newSemanticColors = { light: newColors.blueGray800, dark: newColors.gray100, }, - background: { - light: newColors.gray50, - dark: newColors.offBlack, - }, - border: { - light: newColors.gray500, - dark: newColors.blueGray600, - }, - cardShadow: { - light: "0 0 0 1px rgba(0, 0, 0, 0.04), 0 12px 24px rgba(0, 0, 0, 0.08)", - dark: "0 2px 10px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.04)", - }, }, success: { main: { @@ -235,20 +208,6 @@ const newSemanticColors = { dark: newColors.green200, }, }, - successSecondary: { - main: { - light: undefined, - dark: "#073C2D", - }, - contrastText: { - light: undefined, - dark: "#34D399", - }, - lowContrast: { - light: undefined, - dark: "#094434", - }, - }, warning: { main: { light: newColors.orange700, @@ -317,30 +276,6 @@ const newSemanticColors = { }, }, - map: { - main: { - light: newColors.offWhite, - dark: undefined, - }, - lowContrast: { - light: newColors.gray200, - dark: undefined, - }, - highContrast: { - light: newColors.gray500, - dark: undefined, - }, - }, - chatbot: { - background: { - light: newColors.gray200, - dark: "#112B2B", - }, - textAccent: { - light: "#C084FC", - dark: "#C084FC", - }, - }, alert: { main: { light: newColors.gray200, diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index bda415c90..37143d730 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -58,13 +58,13 @@ const baseTheme = (palette) => ({ variants: [ { props: (props) => props.variant === "contained" && props.color === "accent", + backgroundColor: theme.palette.accent.main, style: { - backgroundColor: theme.palette.accent.main, color: theme.palette.primary.contrastTextSecondaryDarkBg, letterSpacing: "0.5px", textShadow: "0 0 1px rgba(0, 0, 0, 0.15)", "&:hover": { - backgroundColor: darken(theme.palette.accent.darker, 0.05), + backgroundColor: darken(theme.palette.accent.main, 0.25), boxShadow: `0 2px 6px rgba(0, 0, 0, 0.1)`, transition: "all 0.2s ease-in-out", }, @@ -74,7 +74,7 @@ const baseTheme = (palette) => ({ props: (props) => props.color === "accent", style: { "&:hover": { - backgroundColor: theme.palette.accent.darker, + backgroundColor: darken(theme.palette.accent.main, 0.2), }, }, }, @@ -687,6 +687,58 @@ const baseTheme = (palette) => ({ }), }, }, + // DatePicker calendar popup styling + MuiPickersPopper: { + styleOverrides: { + paper: ({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + }), + }, + }, + MuiDateCalendar: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + }), + }, + }, + MuiPickersCalendarHeader: { + styleOverrides: { + root: ({ theme }) => ({ + "& .MuiPickersCalendarHeader-label": { + color: theme.palette.primary.contrastText, + }, + }), + }, + }, + MuiDayCalendar: { + styleOverrides: { + weekDayLabel: ({ theme }) => ({ + color: theme.palette.primary.contrastTextTertiary, + }), + }, + }, + MuiPickersDay: { + styleOverrides: { + root: ({ theme }) => ({ + color: theme.palette.primary.contrastText, + "&:hover": { + backgroundColor: theme.palette.tertiary.main, + }, + "&.Mui-selected": { + backgroundColor: theme.palette.accent.main, + color: theme.palette.accent.contrastText, + "&:hover": { + backgroundColor: theme.palette.accent.main, + }, + }, + "&.Mui-disabled": { + color: theme.palette.primary.contrastTextTertiary, + opacity: 0.5, + }, + }), + }, + }, // code ends here. // For labels of input fields diff --git a/client/src/Utils/Theme/v2Palette.ts b/client/src/Utils/Theme/v2Palette.ts new file mode 100644 index 000000000..d314c8e27 --- /dev/null +++ b/client/src/Utils/Theme/v2Palette.ts @@ -0,0 +1,34 @@ +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 = { + gray200: "#EFEFEF", + gray700: "#313131", + blueBlueWave: "#1570EF", +}; + +export const lightPalette = { + primary: { + main: colors.blueBlueWave, + }, + secondary: { + main: colors.gray200, + }, +}; + +export const darkPalette = { + primary: { + main: colors.blueBlueWave, + }, + secondary: { + main: colors.gray700, + }, +}; diff --git a/client/src/Utils/Theme/v2Theme.ts b/client/src/Utils/Theme/v2Theme.ts new file mode 100644 index 000000000..57ca76669 --- /dev/null +++ b/client/src/Utils/Theme/v2Theme.ts @@ -0,0 +1,148 @@ +import { createTheme } from "@mui/material"; +import { lightPalette, darkPalette, typographyLevels } from "@/Utils/Theme/v2Palette"; + +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, + fontWeight: 500, + }, + h2: { + fontSize: typographyLevels.l, + fontWeight: 400, + }, + body1: { + fontSize: typographyLevels.m, + fontWeight: 400, + }, + body2: { + fontSize: typographyLevels.s, + fontWeight: 400, + }, + }, + + components: { + MuiButtonBase: { + defaultProps: { + disableRipple: true, + disableTouchRipple: true, + }, + }, + MuiButton: { + defaultProps: { + disableRipple: true, + disableTouchRipple: true, + disableFocusRipple: true, + }, + }, + MuiIconButton: { + defaultProps: { + disableRipple: true, + disableTouchRipple: true, + disableFocusRipple: true, + }, + }, + MuiListItemButton: { + defaultProps: { + disableRipple: true, + disableTouchRipple: true, + }, + }, + MuiTab: { + defaultProps: { + disableRipple: true, + disableTouchRipple: true, + disableFocusRipple: true, + }, + }, + MuiToggleButton: { + defaultProps: { + disableRipple: true, + disableTouchRipple: true, + disableFocusRipple: 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.primary.main, + }, + }, + }, + }, + + MuiPaper: { + styleOverrides: { + root: ({ theme }) => { + return { + marginTop: 4, + padding: 0, + border: 1, + borderStyle: "solid", + borderColor: theme.palette.divider, + borderRadius: 4, + boxShadow: shadow, + backgroundColor: theme.palette.background.paper, + backgroundImage: "none", + }; + }, + }, + }, + }, + shape: { + borderRadius: 2, + }, + }); + +export const lightTheme = createTheme(theme("light", lightPalette)); +export const darkTheme = createTheme(theme("dark", darkPalette)); diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts new file mode 100644 index 000000000..a19a0bf8b --- /dev/null +++ b/client/src/Utils/TimeUtils.ts @@ -0,0 +1,52 @@ +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 = { + recent: "h:mm A", + day: "h:mm A", + week: "MM/D, h:mm A", + month: "ddd. M/D", + }; + const format = tickFormatLookup[range]; + if (format === undefined) { + return ""; + } + return format; +}; + +export const tooltipDateFormatLookup = (range: string) => { + const dateFormatLookup: Record = { + 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[range]; + if (format === undefined) { + return ""; + } + return format; +}; diff --git a/client/src/Utils/greeting.jsx b/client/src/Utils/greeting.jsx index d2b7fa033..c4afbf88c 100644 --- a/client/src/Utils/greeting.jsx +++ b/client/src/Utils/greeting.jsx @@ -136,8 +136,8 @@ const Greeting = ({ type = "" }) => { const dispatch = useDispatch(); const { t } = useTranslation(); const { firstName } = useSelector((state) => state.auth.user); - const index = useSelector((state) => state.ui.greeting.index); - const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate); + const index = useSelector((state) => state.ui.greeting?.index ?? 0); + const lastUpdate = useSelector((state) => state.ui.greeting?.lastUpdate ?? null); const now = new Date(); const hour = now.getHours(); diff --git a/client/src/Utils/monitorUtils.js b/client/src/Utils/monitorUtilsLegacy.js similarity index 100% rename from client/src/Utils/monitorUtils.js rename to client/src/Utils/monitorUtilsLegacy.js diff --git a/client/src/Utils/timeUtils.js b/client/src/Utils/timeUtilsLegacy.js similarity index 87% rename from client/src/Utils/timeUtils.js rename to client/src/Utils/timeUtilsLegacy.js index f6e2dcec0..365572f6b 100644 --- a/client/src/Utils/timeUtils.js +++ b/client/src/Utils/timeUtilsLegacy.js @@ -135,3 +135,33 @@ export const formatDateWithTz = (timestamp, format, timezone) => { const formattedDate = dayjs(timestamp).tz(timezone).format(format); return formattedDate; }; + +export const tickDateFormatLookup = (range) => { + switch (range) { + case "recent": + return "h:mm A"; + case "day": + return "h A"; + case "week": + return "MMM D"; + case "month": + return "MMM D"; + default: + return "MMM D, h A"; + } +}; + +export const tooltipDateFormatLookup = (range) => { + switch (range) { + case "recent": + return "MMM D, h:mm A"; + case "day": + return "MMM D, h:mm A"; + case "week": + return "ddd, MMM D"; + case "month": + return "ddd, MMM D"; + default: + return "MMM D, h:mm A"; + } +}; diff --git a/client/src/Utils/toastUtils.jsx b/client/src/Utils/toastUtils.jsx index 21cf13e2b..41b2d5482 100644 --- a/client/src/Utils/toastUtils.jsx +++ b/client/src/Utils/toastUtils.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { toast, Slide } from "react-toastify"; -import Toast from "../Components/Toast"; +import Toast from "../Components/v1/Toast"; /** * @param {object} props * @param {'info' | 'error' | 'warning'} - The variant of the alert (e.g., "info", "error"). diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 67c267ffd..27d585074 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -112,7 +112,7 @@ const loginCredentials = joi.object({ }); const monitorValidation = joi.object({ - _id: joi.string(), + id: joi.string(), userId: joi.string(), teamId: joi.string(), statusWindowSize: joi.number().min(1).max(20).default(5).messages({ @@ -328,6 +328,7 @@ const settingsValidation = joi.object({ systemEmailIgnoreTLS: joi.boolean(), systemEmailRequireTLS: joi.boolean(), systemEmailRejectUnauthorized: joi.boolean(), + showURL: joi.boolean().optional(), globalThresholds: joi .object({ cpu: joi.number().min(1).max(100).allow("").optional(), @@ -448,6 +449,7 @@ const infrastructureMonitorValidation = joi.object({ "number.max": "Status window threshold cannot exceed 100%.", }), notifications: joi.array().items(joi.string()), + selectedDisks: joi.array().items(joi.string()).optional(), }); const notificationValidation = joi.object({ @@ -458,7 +460,7 @@ const notificationValidation = joi.object({ type: joi .string() - .valid("email", "webhook", "slack", "discord", "pager_duty") + .valid("email", "webhook", "slack", "discord", "pager_duty", "matrix") .required() .messages({ "string.empty": "Notification type is required", @@ -495,8 +497,40 @@ const notificationValidation = joi.object({ "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(), + }), }); const editUserValidation = joi.object({ diff --git a/client/src/assets/icons/alert-icon.svg b/client/src/assets/icons/alert-icon.svg deleted file mode 100644 index 1dc7fc2b7..000000000 --- a/client/src/assets/icons/alert-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/average-response-icon.svg b/client/src/assets/icons/average-response-icon.svg deleted file mode 100644 index 572e2002f..000000000 --- a/client/src/assets/icons/average-response-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/bwu-icon.svg b/client/src/assets/icons/bwu-icon.svg deleted file mode 100644 index 80ab50ad9..000000000 --- a/client/src/assets/icons/bwu-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/calendar-check.svg b/client/src/assets/icons/calendar-check.svg deleted file mode 100644 index 07d96dad9..000000000 --- a/client/src/assets/icons/calendar-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/calendar.svg b/client/src/assets/icons/calendar.svg deleted file mode 100644 index be023a4de..000000000 --- a/client/src/assets/icons/calendar.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/certificate.svg b/client/src/assets/icons/certificate.svg deleted file mode 100644 index 2bc6c7e74..000000000 --- a/client/src/assets/icons/certificate.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/changeLog.svg b/client/src/assets/icons/changeLog.svg deleted file mode 100644 index d12f6c10e..000000000 --- a/client/src/assets/icons/changeLog.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check-icon.svg b/client/src/assets/icons/check-icon.svg deleted file mode 100644 index 9a2cfac5a..000000000 --- a/client/src/assets/icons/check-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check-outlined.svg b/client/src/assets/icons/check-outlined.svg deleted file mode 100644 index 1e779d547..000000000 --- a/client/src/assets/icons/check-outlined.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check.svg b/client/src/assets/icons/check.svg deleted file mode 100644 index 357378f40..000000000 --- a/client/src/assets/icons/check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/checkbox-filled.svg b/client/src/assets/icons/checkbox-filled.svg deleted file mode 100644 index 8a5be81f0..000000000 --- a/client/src/assets/icons/checkbox-filled.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/checkbox-green.svg b/client/src/assets/icons/checkbox-green.svg deleted file mode 100644 index dd5676298..000000000 --- a/client/src/assets/icons/checkbox-green.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/checkbox-outline.svg b/client/src/assets/icons/checkbox-outline.svg deleted file mode 100644 index bf5a7218c..000000000 --- a/client/src/assets/icons/checkbox-outline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/checkbox-red.svg b/client/src/assets/icons/checkbox-red.svg deleted file mode 100644 index 3c43a2042..000000000 --- a/client/src/assets/icons/checkbox-red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/clock-snooze.svg b/client/src/assets/icons/clock-snooze.svg deleted file mode 100644 index 708e7ba91..000000000 --- a/client/src/assets/icons/clock-snooze.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/close-icon.svg b/client/src/assets/icons/close-icon.svg deleted file mode 100644 index e1b6b690a..000000000 --- a/client/src/assets/icons/close-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/cpu-chip.svg b/client/src/assets/icons/cpu-chip.svg deleted file mode 100644 index 5645840b1..000000000 --- a/client/src/assets/icons/cpu-chip.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/dashboard.svg b/client/src/assets/icons/dashboard.svg deleted file mode 100644 index 8958e90ed..000000000 --- a/client/src/assets/icons/dashboard.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/discussions.svg b/client/src/assets/icons/discussions.svg deleted file mode 100644 index 293cd51f2..000000000 --- a/client/src/assets/icons/discussions.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/client/src/assets/icons/distributed-uptime.svg b/client/src/assets/icons/distributed-uptime.svg deleted file mode 100644 index e612081e4..000000000 --- a/client/src/assets/icons/distributed-uptime.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/docs.svg b/client/src/assets/icons/docs.svg deleted file mode 100644 index d36458de2..000000000 --- a/client/src/assets/icons/docs.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/dots-vertical.svg b/client/src/assets/icons/dots-vertical.svg deleted file mode 100644 index e720066c4..000000000 --- a/client/src/assets/icons/dots-vertical.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/down-arrow.svg b/client/src/assets/icons/down-arrow.svg deleted file mode 100644 index 59b4b6bc5..000000000 --- a/client/src/assets/icons/down-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/edit.svg b/client/src/assets/icons/edit.svg deleted file mode 100644 index be2c78408..000000000 --- a/client/src/assets/icons/edit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/email.svg b/client/src/assets/icons/email.svg deleted file mode 100644 index 01f55e961..000000000 --- a/client/src/assets/icons/email.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/folder.svg b/client/src/assets/icons/folder.svg deleted file mode 100644 index 15941e5b5..000000000 --- a/client/src/assets/icons/folder.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/client/src/assets/icons/groups.svg b/client/src/assets/icons/groups.svg deleted file mode 100644 index fa2cae03c..000000000 --- a/client/src/assets/icons/groups.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/assets/icons/history-icon.svg b/client/src/assets/icons/history-icon.svg deleted file mode 100644 index 7343455ec..000000000 --- a/client/src/assets/icons/history-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/incidents.svg b/client/src/assets/icons/incidents.svg deleted file mode 100644 index ffe04d09f..000000000 --- a/client/src/assets/icons/incidents.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/integrations.svg b/client/src/assets/icons/integrations.svg deleted file mode 100644 index 50e817f00..000000000 --- a/client/src/assets/icons/integrations.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/interval-check.svg b/client/src/assets/icons/interval-check.svg deleted file mode 100644 index 923475c8b..000000000 --- a/client/src/assets/icons/interval-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/jupiter_ag_logo.svg b/client/src/assets/icons/jupiter_ag_logo.svg deleted file mode 100644 index b344961c8..000000000 --- a/client/src/assets/icons/jupiter_ag_logo.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/jupiter_logo_banner_dark.svg b/client/src/assets/icons/jupiter_logo_banner_dark.svg deleted file mode 100644 index 3d3e23b14..000000000 --- a/client/src/assets/icons/jupiter_logo_banner_dark.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/jupiter_logo_banner_light.svg b/client/src/assets/icons/jupiter_logo_banner_light.svg deleted file mode 100644 index 4744e0bcb..000000000 --- a/client/src/assets/icons/jupiter_logo_banner_light.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/key.svg b/client/src/assets/icons/key.svg deleted file mode 100644 index 5db8d5f16..000000000 --- a/client/src/assets/icons/key.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/left-arrow-double.svg b/client/src/assets/icons/left-arrow-double.svg deleted file mode 100644 index 4cb2a5186..000000000 --- a/client/src/assets/icons/left-arrow-double.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/left-arrow-long.svg b/client/src/assets/icons/left-arrow-long.svg deleted file mode 100644 index 56a774730..000000000 --- a/client/src/assets/icons/left-arrow-long.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/left-arrow.svg b/client/src/assets/icons/left-arrow.svg deleted file mode 100644 index 649d1681c..000000000 --- a/client/src/assets/icons/left-arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/lock.svg b/client/src/assets/icons/lock.svg deleted file mode 100644 index a87535d54..000000000 --- a/client/src/assets/icons/lock.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/logout.svg b/client/src/assets/icons/logout.svg deleted file mode 100644 index 3f3b69c49..000000000 --- a/client/src/assets/icons/logout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/logs.svg b/client/src/assets/icons/logs.svg deleted file mode 100644 index 556c3ba33..000000000 --- a/client/src/assets/icons/logs.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/mail.svg b/client/src/assets/icons/mail.svg deleted file mode 100644 index 75c22b111..000000000 --- a/client/src/assets/icons/mail.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/maintenance.svg b/client/src/assets/icons/maintenance.svg deleted file mode 100644 index 9486f6086..000000000 --- a/client/src/assets/icons/maintenance.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/monitor-graph-line.svg b/client/src/assets/icons/monitor-graph-line.svg deleted file mode 100644 index 86049eb9e..000000000 --- a/client/src/assets/icons/monitor-graph-line.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/monitors.svg b/client/src/assets/icons/monitors.svg deleted file mode 100644 index f40f36433..000000000 --- a/client/src/assets/icons/monitors.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/notifications.svg b/client/src/assets/icons/notifications.svg deleted file mode 100644 index c09ac5746..000000000 --- a/client/src/assets/icons/notifications.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/open-in-new-page.svg b/client/src/assets/icons/open-in-new-page.svg deleted file mode 100644 index 70599f390..000000000 --- a/client/src/assets/icons/open-in-new-page.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/page-speed.svg b/client/src/assets/icons/page-speed.svg deleted file mode 100644 index 30a1c8a53..000000000 --- a/client/src/assets/icons/page-speed.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/pause-icon.svg b/client/src/assets/icons/pause-icon.svg deleted file mode 100644 index c73abfc1d..000000000 --- a/client/src/assets/icons/pause-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/performance-report.svg b/client/src/assets/icons/performance-report.svg deleted file mode 100644 index 8b7073ac3..000000000 --- a/client/src/assets/icons/performance-report.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/radio-checked.svg b/client/src/assets/icons/radio-checked.svg deleted file mode 100644 index 1c584b79a..000000000 --- a/client/src/assets/icons/radio-checked.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/response-time-icon.svg b/client/src/assets/icons/response-time-icon.svg deleted file mode 100644 index db5b2af38..000000000 --- a/client/src/assets/icons/response-time-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/resume-icon.svg b/client/src/assets/icons/resume-icon.svg deleted file mode 100644 index 1da787081..000000000 --- a/client/src/assets/icons/resume-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/right-arrow-double.svg b/client/src/assets/icons/right-arrow-double.svg deleted file mode 100644 index 6ad4882ad..000000000 --- a/client/src/assets/icons/right-arrow-double.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/right-arrow.svg b/client/src/assets/icons/right-arrow.svg deleted file mode 100644 index 77d022798..000000000 --- a/client/src/assets/icons/right-arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/ruler-icon.svg b/client/src/assets/icons/ruler-icon.svg deleted file mode 100644 index 7266d2876..000000000 --- a/client/src/assets/icons/ruler-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/search.svg b/client/src/assets/icons/search.svg deleted file mode 100644 index a1bc74224..000000000 --- a/client/src/assets/icons/search.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/selector-vertical.svg b/client/src/assets/icons/selector-vertical.svg deleted file mode 100644 index 29d9a4998..000000000 --- a/client/src/assets/icons/selector-vertical.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/settings-bold.svg b/client/src/assets/icons/settings-bold.svg deleted file mode 100644 index d8057286c..000000000 --- a/client/src/assets/icons/settings-bold.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/settings.svg b/client/src/assets/icons/settings.svg deleted file mode 100644 index eb86f1d19..000000000 --- a/client/src/assets/icons/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/solana_logo.svg b/client/src/assets/icons/solana_logo.svg deleted file mode 100644 index bd349af26..000000000 --- a/client/src/assets/icons/solana_logo.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/solana_logo_banner.svg b/client/src/assets/icons/solana_logo_banner.svg deleted file mode 100644 index 08ec87cb1..000000000 --- a/client/src/assets/icons/solana_logo_banner.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/spedometer-icon.svg b/client/src/assets/icons/spedometer-icon.svg deleted file mode 100644 index 19b13f44e..000000000 --- a/client/src/assets/icons/spedometer-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/speedometer-icon.svg b/client/src/assets/icons/speedometer-icon.svg deleted file mode 100644 index 19b13f44e..000000000 --- a/client/src/assets/icons/speedometer-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/status-pages.svg b/client/src/assets/icons/status-pages.svg deleted file mode 100644 index 97cea88fb..000000000 --- a/client/src/assets/icons/status-pages.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/support.svg b/client/src/assets/icons/support.svg deleted file mode 100644 index 7de16379a..000000000 --- a/client/src/assets/icons/support.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/top-right-arrow.svg b/client/src/assets/icons/top-right-arrow.svg deleted file mode 100644 index d569cdd7e..000000000 --- a/client/src/assets/icons/top-right-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/trash-bin.svg b/client/src/assets/icons/trash-bin.svg deleted file mode 100644 index 83bca0c6f..000000000 --- a/client/src/assets/icons/trash-bin.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/up-arrow.svg b/client/src/assets/icons/up-arrow.svg deleted file mode 100644 index 0f8f3bc34..000000000 --- a/client/src/assets/icons/up-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/uptime-icon.svg b/client/src/assets/icons/uptime-icon.svg deleted file mode 100644 index 55706ae71..000000000 --- a/client/src/assets/icons/uptime-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user-edit.svg b/client/src/assets/icons/user-edit.svg deleted file mode 100644 index cbd1a252f..000000000 --- a/client/src/assets/icons/user-edit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user-two.svg b/client/src/assets/icons/user-two.svg deleted file mode 100644 index 031b22787..000000000 --- a/client/src/assets/icons/user-two.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user.svg b/client/src/assets/icons/user.svg deleted file mode 100644 index 231c89d40..000000000 --- a/client/src/assets/icons/user.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/warning-icon.svg b/client/src/assets/icons/warning-icon.svg deleted file mode 100644 index 468611758..000000000 --- a/client/src/assets/icons/warning-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/index.css b/client/src/index.css index a5d305d46..48737d4bb 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -4,6 +4,11 @@ box-sizing: border-box; } +html, +body { + overscroll-behavior: none; +} + html { scroll-behavior: smooth; } @@ -15,15 +20,11 @@ html { -moz-osx-font-smoothing: grayscale; /* Generalized Stylings */ - --env-var-radius-1: 4px; --env-var-radius-2: 8px; - --env-var-width-1: 100vw; --env-var-width-2: 360px; - --env-var-width-3: 250px; --env-var-width-4: 100px; - --env-var-height-1: 100vh; --env-var-height-2: 34px; --env-var-nav-bar-height: 70px; @@ -35,9 +36,6 @@ html { --env-var-spacing-1-plus: 16px; --env-var-spacing-1-minus: 10px; --env-var-spacing-2: 24px; - --env-var-spacing-3: 32px; - --env-var-spacing-4: 40px; - --env-var-spacing-5: 65px; --env-var-font-size-small: 11px; --env-var-font-size-small-plus: 12px; @@ -46,23 +44,14 @@ html { --env-var-font-size-large: 16px; --env-var-font-size-large-plus: 22px; --env-var-font-size-xlarge: 30px; - - --env-var-img-width-1: 20px; - --env-var-img-width-2: 16px; - --env-var-img-width-3: 12px; - - --env-var-default-1: 24px; - --env-var-default-2: 40px; - - --env-var-shadow-1: - 0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03); } .MuiInputBase-root.Mui-disabled input { cursor: not-allowed; } -.Toastify__toast-container { +/* .Toastify__toast-container { + min-width: 300px; width: auto; } .Toastify__toast-body .alert { @@ -80,7 +69,7 @@ html { } .Toastify [class*="Toastify__toast-theme"] { background-color: transparent; -} +} */ .MuiTouchRipple-root { display: none; diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json index d8019d5de..6cec253d8 100644 --- a/client/src/locales/ar.json +++ b/client/src/locales/ar.json @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json index 71c7e24b9..837b49803 100644 --- a/client/src/locales/cs.json +++ b/client/src/locales/cs.json @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/de.json b/client/src/locales/de.json index ef8df5df4..fd3918378 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -382,50 +382,56 @@ "created": "Angelegt" }, "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" + "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" }, - "register": "", + "register": "Teammitglied registrieren", "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" + "success": "Benutzer erstellt, Zugangsdaten auf sichere Weise weitergeben.", + "dbUserExists": "Der Benutzer existiert bereits.", + "unknownError": "Unbekannter Fehler aufgetreten." }, "registerTeamMember": { - "title": "", + "title": "Teammitglied registrieren", "auth": { "common": { "inputs": { "firstName": { "errors": { - "empty": "", - "pattern": "" + "empty": "Bitte einen Namen eingeben", + "pattern": "Der Name darf nur Buchstaben, Leerzeichen, Apostrophe oder Bindestriche enthalten" } }, "lastName": { "errors": { - "empty": "", - "pattern": "" + "empty": "Bitte einen Vornamen eingeben", + "pattern": "Der Vorname darf nur Buchstaben, Leerzeichen, Apostrophe oder Bindestriche enthalten" } }, "email": { "errors": { - "empty": "", - "invalid": "" + "empty": "Zum Fortsetzen bitte eine E-Mail-Adresse eingeben", + "invalid": "Bitte die Gültigkeit der E-Mail-Adresse erneut prüfen" } }, "role": { "errors": { - "empty": "" + "empty": "Eine Rolle wird benötigt" } } } } } }, - "role": "" + "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." + } }, "monitorState": { "paused": "Pausiert", @@ -487,7 +493,8 @@ "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": "" + "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." }, "common": { "appName": "Checkmate", @@ -607,7 +614,7 @@ "forgotPassword": "Passwort vergessen?", "register": "Noch keinen Account?", "forgotPasswordLink": "Passwort zurücksetzen", - "registerLink": "Hier resistrieren" + "registerLink": "Hier registrieren" }, "toasts": { "success": "Willkommen zurück. Erfolgreich angemeldet.", @@ -618,7 +625,7 @@ "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" } }, - "welcome": "" + "welcome": "Willkommen zurück bei Checkmate!" }, "registration": { "heading": { @@ -645,7 +652,7 @@ "toasts": { "success": "Willkommen! Benutzer erfolgreich angelegt" }, - "welcome": "" + "welcome": "Willkommen bei Checkmate!" }, "forgotPassword": { "heading": "Passwort vergessen?", @@ -746,8 +753,8 @@ "monitorStatus": { "checkingEvery": "Überprüfung alle {{interval}}", "withCaptureAgent": "mit Capture-Agent {{version}}", - "up": "hoch", - "down": "runter", + "up": "Online", + "down": "Offline", "paused": "pausiert" }, "advancedMatching": "Erweiterter Abgleich", @@ -792,7 +799,7 @@ "lastRunHeader": "Letzter Lauf um", "lastFinishedAtHeader": "Zuletzt abgeschlossen um", "lastRunTookHeader": "Letzter Lauf dauerte", - "intervalHeader": "" + "intervalHeader": "Invervall" }, "metricsTable": { "title": "Warteschlangenmetriken", @@ -900,8 +907,8 @@ "title": "Überwachen Sie IP/URL auf der Statusseite" }, "globalThresholds": { - "title": "", - "description": "" + "title": "Globale Schwellenwerte", + "description": "Globale CPU-, Speicher-, Festplatten- und Temperaturschwellenwerte festlegen. Wenn ein Wert angegeben ist, wird er automatisch für die Überwachung genutzt." } }, "statusPageCreate": { @@ -975,11 +982,11 @@ "oneHour": "1 Stunde", "oneDay": "1 Tag", "oneWeek": "1 Woche", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" + "fourMinutes": "4 Minuten", + "oneMinute": "Eine Minute", + "twoMinutes": "Zwei Minuten", + "fifteenSeconds": "15 Sekunden", + "thirtySeconds": "30 Sekunden" }, "general": { "noOptionsFound": "Keine {{unit}} gefunden" @@ -1050,73 +1057,84 @@ "incidentsPageActionResolveMonitor": "Monitor-Vorfälle beheben", "incidentsPageActionResolveAll": "Alle Vorfälle lösen", "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" + "equal": "Gleich", + "equalPlaceholder": "Erfolg", + "include": "Enthält", + "includePlaceholder": "OK", + "regex": "Regex", + "regexPlaceholder": "^(success|ok)$", + "text": "Übereinstummungsmethode" }, "monitorType": { "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Container Name/ID", + "namePlaceholder": "Mein Container", + "placeholder": "Meine-App" }, "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Zu überwachende URL", + "namePlaceholder": "Google", + "placeholder": "google.de" }, "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Zu überwachende IP-Adresse", + "namePlaceholder": "Google", + "placeholder": "1.1.1.1" }, "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Zu überwachende URL", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" }, "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Zu überwachende URL", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" } }, "uptimeAdvancedMatching": { - "jsonPath": "" + "jsonPath": "JSON Pfad" }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", + "bytesPerSecond": "Bytes pro Sekunde", + "bytesReceived": "Empfangene Bytes", + "bytesSent": "Gesendete Bytes", + "chooseGame": "Spiel auswählen", "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "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." }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + "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", + "v1": { + "infrastructure": { + "disk_selection_title": "Festplattenauswahl", + "disk_selection_info": "Momentan wurde keine Festplatte erkannt.", + "disk_selection_description": "Wählen Sie die spezifischen Festplatten aus, die Sie überwachen möchten." + } + } } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index d89ebc44e..7386ced7d 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1,8 +1,61 @@ { + "common": { + "appName": "Checkmate", + "monitoringAgentName": "Capture", + "labels": { + "responseTime": "Response time" + }, + "buttons": { + "toggleTheme": "Toggles light & dark", + "testNotifications": "Test notifications", + "create": "Create", + "incidents": "Incidents", + "pause": "Pause", + "resume": "Resume", + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm" + }, + "alerts": { + "pageSpeedApiKey": { + "content": "Warning: You haven't added a Google PageSpeed API key yet. Visit Settings to add one. Without it, the PageSpeed monitor won't function." + } + }, + "dialogs": { + "delete": { + "description": "This action cannot be undone.", + "title": "Are you sure you want to delete this?" + } + }, + "toasts": { + "networkError": "Network error", + "checkConnection": "Please check your connection", + "unknownError": "Unknown error" + }, + "table": { + "headers": { + "actions": "Actions", + "active": "Active", + "message": "Message", + "monitor": "Monitor", + "name": "Name", + "status": "Status", + "type": "Type" + }, + "empty": "Nothing here" + }, + "charts": { + "labels": { + "uptime": "Uptime", + "downtime": "Downtime", + "averageResponseTime": "Average response time", + "low": "Low", + "high": "High" + } + } + }, "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", @@ -20,6 +73,13 @@ "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", @@ -31,18 +91,11 @@ "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", @@ -115,37 +168,87 @@ "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", + "pages": { + "common": { + "monitors": { + "status": { + "up": "up", + "down": "down", + "paused": "paused", + "initializing": "initializing" + }, + "actions": { + "openSite": "Open site", + "details": "Details", + "incidents": "Incidents", + "configure": "Configure", + "pause": "Pause", + "resume": "Resume", + "delete": "Delete" + }, + "statBoxes": { + "activeFor": "Active for", + "lastCheck": "Last check", + "lastResponseTime": "Last response time", + "certificateExpiry": "Certificate expiry" + } + } + }, + "uptime": { + "table": { + "headers": { + "responseTime": "Response time" + } + }, + "fallback": { + "title": "An uptime monitor is used to:", + "actionButton": "Create a 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" + ] + }, + "filters": { + "search": { + "placeholder": "Search monitors..." + } + } + }, + "checks": { + "table": { + "headers": { + "dateTime": "Date & time", + "statusCode": "Status code" + } + } + }, + "pageSpeed": { + "table": { + "headers": { + "pageSpeedScore": "PageSpeed score" + } + }, + "charts": { + "common": { + "cls": "Cumulative Layout Shift (CLS)", + "fcp": "First Contentful Paint (FCP)", + "lcp": "Largest Contentful Paint (LCP)", + "si": "Speed Index (SI)", + "tbt": "Total Blocking Time (TBT)" + }, + "pie": { "title": "Performance report" }, + "legend": { + "title": "PageSpeed report", + + "weight": "Weight" + } + } + } + }, "incidentsTableNoIncidents": "No incidents recorded", "incidentsTablePaginationLabel": "incidents", "incidentsTableMonitorName": "Monitor Name", @@ -185,6 +288,8 @@ "total": "Total", "cores": "Cores", "frequency": "Frequency", + "device": "Device", + "mountedOn": "Mounted on", "status": "Status", "cpuPhysical": "CPU (Physical)", "cpuLogical": "CPU (Logical)", @@ -239,6 +344,7 @@ "addMonitors": "Add monitors", "window": "window", "cancel": "Cancel", + "close": "Close", "message": "Message", "low": "low", "high": "high", @@ -425,7 +531,13 @@ } } }, - "role": "Role" + "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", @@ -450,7 +562,8 @@ "team": "Team", "logOut": "Log out", "notifications": "Notifications", - "logs": "Logs" + "logs": "Logs", + "checks": "Checks" }, "settingsEmailUser": "Email user - Username for authentication, overrides email address if specified", "state": "State", @@ -459,6 +572,7 @@ "commonSaving": "Saving...", "navControls": "Controls", "incidentsPageTitle": "Incidents", + "checksPageTitle": "Checks", "passwordPanel": { "passwordChangedSuccess": "Your password was changed successfully.", "passwordInputIncorrect": "Your password input was incorrect.", @@ -467,7 +581,7 @@ "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.", + "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", @@ -487,19 +601,8 @@ "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." - }, - "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "Toggles light & dark" - }, - "toasts": { - "networkError": "Network error", - "checkConnection": "Please check your connection", - "unknownError": "Unknown error" - } + "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." }, "auth": { "common": { @@ -525,7 +628,7 @@ }, "special": { "beginning": "Must contain at least", - "highlighted": "one special character" + "highlighted": "one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" }, "number": { "beginning": "Must contain at least", @@ -550,7 +653,7 @@ "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", + "special": "Password must contain at least 1 special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)", "incorrect": "The password you provided does not match our records" } }, @@ -774,7 +877,16 @@ "error": "Error", "debug": "Debug" } - } + }, + "table": { + "timestamp": "Timestamp", + "level": "Level", + "service": "Service", + "method": "Method", + "message": "Message", + "logs": "logs" + }, + "noLogs": "No logs found" }, "queuePage": { "title": "Queue", @@ -890,6 +1002,9 @@ "description": "Switch between light and dark mode, or change user interface language.", "labelLanguage": "Language", "labelTheme": "Theme mode", + "labelChartType": "Chart type", + "chartTypeHistogram": "Histogram", + "chartTypeHeatmap": "Heatmap", "title": "Appearance" }, "urlSettings": { @@ -968,6 +1083,10 @@ }, "pageSpeedLighthouseAPI": "Use the Lighthouse PageSpeed API to monitor your website", "time": { + "fifteenSeconds": "15 seconds", + "thirtySeconds": "30 seconds", + "fifteenMinutes": "15 minutes", + "thirtyMinutes": "30 minutes", "threeMinutes": "3 minutes", "fiveMinutes": "5 minutes", "tenMinutes": "10 minutes", @@ -977,9 +1096,7 @@ "oneWeek": "1 week", "fourMinutes": "4 minutes", "oneMinute": "1 minute", - "twoMinutes": "2 minutes", - "fifteenSeconds": "15 seconds", - "thirtySeconds": "30 seconds" + "twoMinutes": "2 minutes" }, "general": { "noOptionsFound": "No {{unit}} found" @@ -1048,7 +1165,9 @@ } }, "incidentsPageActionResolveMonitor": "Resolve monitor incidents", + "checksPageActionResolveMonitor": "Resolve monitor checks", "incidentsPageActionResolveAll": "Resolve all incidents", + "checksPageActionResolveAllMonitor": "Resolve all checks", "matchMethodOptions": { "equal": "Equal", "equalPlaceholder": "success", @@ -1096,7 +1215,11 @@ "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" + "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", @@ -1118,5 +1241,88 @@ "packetsReceivedRate": "Packets Received Rate", "packetsSent": "Packets Sent", "rate": "Rate", - "selectInterface": "Select Interface" + "selectInterface": "Select Interface", + "v1": { + "infrastructure": { + "disk_selection_title": "Disk Selection", + "disk_selection_info": "No disk detected for the moment.", + "disk_selection_description": "Select the specific disks you want to monitor." + } + }, + "incidentsPage": { + "title": "Incidents", + "description": "Manage incidents for your monitors. You can resolve incidents here.", + "resolved": "Resolved", + "active": "Active", + "startTime": "Start Time", + "endTime": "End Time", + "resolutionType": "Resolution Type", + "unresolved": "Unresolved", + "all": "All", + "filterByMonitor": "Filter by monitor", + "filterByStatus": "Filter by status", + "filterByType": "Filter by type", + "filterByDate": "Filter by date", + "filterByPriority": "Filter by priority", + "incidentsActivePanelError": "Error loading active incidents", + "allSystemsAreOperational": "All Systems Are Operational", + "incidentsActivePanelTitle": "Total Active Incidents", + "incidentsLatestPanelError": "Error loading latest incidents", + "incidentsLatestPanelTitle": "Latest Incidents", + "incidentsLatestPanelEmpty": "No recent incidents", + "incidentItemMonitor": "Monitor", + "incidentItemStatus": "Status", + "unknownMonitor": "Unknown Monitor", + "incidentsStatisticsPanelError": "Error loading statistics", + "incidentsStatisticsPanelTitle": "General Statistics", + "totalIncidents": "Total Incidents", + "mostAffectedMonitor": "Most Affected Monitor", + "avgResolutionTime": "Avg. Resolution Time", + "resolutions": "Resolutions", + "automatic": "Automatic", + "manual": "Manual", + "incidentResolvedSuccessfully": "Incident resolved successfully", + "incidentResolvedFailed": "Error resolving incident", + "errorFetchingIncidents": "Error fetching incidents", + "errorFetchingIncident": "Error fetching incident", + "noIncidentIdProvided": "No incident ID provided", + "errorFetchingIncidentSummary": "Error fetching incident summary", + "incidentsOptionsHeaderFilterManual": "Manual", + "incidentsOptionsHeaderFilterAutomatic": "Automatic", + "incidentsOptionsHeaderFilterAll": "All", + "incidentsOptionsHeaderFilterActive": "Active", + "incidentsOptionsHeaderFilterResolved": "Resolved", + "incidentsTableResolved": "Closed", + "incidentsTableActionResolveManually": "Resolve Manually", + "incidentsTableActionDetails": "Details", + "incidentsTableActionGoToMonitor": "Go to monitor", + "resolveIncidentDialogTitle": "Resolve Incident", + "resolveIncidentDialogDescription": "Are you sure you want to resolve this incident manually?", + "resolveIncidentDialogCommentLabel": "Comment (optional)", + "resolveIncidentDialogCommentPlaceholder": "Add a comment about the resolution...", + "resolveIncidentDialogConfirm": "Resolve", + "hours": "hours", + "loading": "Loading incident details...", + "incidentNotFound": "Incident not found", + "incidentDetails": "INCIDENT DETAILS", + "activeIncident": "ACTIVE INCIDENT", + "resolvedIncident": "RESOLVED INCIDENT", + "overview": "OVERVIEW", + "URL": "URL", + "incidentAnalysis": "INCIDENT ANALYSIS", + "timeline": "Timeline", + "startedAt": "Started at", + "endedAt": "Ended at", + "downtime": "Downtime", + "errorDetails": "Error details", + "resolutionDetails": "Resolution details", + "statusCode": "Status code", + "resolutionMethod": "Resolution method", + "resolvedBy": "Resolved by", + "comment": "Comment", + "noCommentProvided": "No comment provided", + "loadingIncidentDetails": "Loading incident details...", + "none": "None", + "message": "Message" + } } diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 440d40027..ed6a98c5d 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -129,32 +129,32 @@ "distributedUptimeStatusPublishedLabel": "", "distributedUptimeStatusCompanyNameLabel": "", "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "", - "distributedUptimeStatus60Days": "", - "distributedUptimeStatus90Days": "", + "distributedUptimeStatus30Days": "30 dias", + "distributedUptimeStatus60Days": "60 días", + "distributedUptimeStatus90Days": "90 dias", "distributedUptimeStatusPageNotSetUp": "", "distributedUptimeStatusContactAdmin": "", "distributedUptimeStatusPageNotPublic": "", "distributedUptimeStatusPageDeleteDialog": "", "distributedUptimeStatusPageDeleteConfirm": "", "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", + "distributedUptimeStatusDevices": "Dispositivos", "distributedUptimeStatusUpt": "", "distributedUptimeStatusUptBurned": "", "distributedUptimeStatusUptLogo": "", "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", + "incidentsTablePaginationLabel": "Incidentes", "incidentsTableMonitorName": "", - "incidentsTableStatus": "", + "incidentsTableStatus": "Estado", "incidentsTableDateTime": "", "incidentsTableStatusCode": "", - "incidentsTableMessage": "", + "incidentsTableMessage": "Mensaje", "incidentsOptionsHeader": "", "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", + "incidentsOptionsHeaderFilterAll": "Todo", "incidentsOptionsHeaderFilterDown": "", "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", + "incidentsOptionsHeaderShow": "Mostrar:", "incidentsOptionsHeaderLastHour": "", "incidentsOptionsHeaderLastDay": "", "incidentsOptionsHeaderLastWeek": "", @@ -165,22 +165,22 @@ "infrastructureCustomizeAlerts": "", "infrastructureAlertNotificationDescription": "", "infrastructureCreateMonitor": "", - "infrastructureProtocol": "", + "infrastructureProtocol": "Protocolo", "infrastructureServerUrlLabel": "", "infrastructureDisplayNameLabel": "", "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", + "gb": "GB", + "mb": "MB", + "mem": "Mem", "memoryUsage": "", - "cpu": "", + "cpu": "CPU", "cpuUsage": "", "cpuTemperature": "", "diskUsage": "", "used": "", - "total": "", - "cores": "", - "frequency": "", + "total": "Total", + "cores": "Nucleos", + "frequency": "Frecuencia", "status": "", "cpuPhysical": "", "cpuLogical": "", @@ -189,25 +189,25 @@ "memory": "", "disk": "", "uptime": "", - "os": "", + "os": "SO", "host": "", "actions": "", "integrations": "", "integrationsPrism": "", - "integrationsSlack": "", + "integrationsSlack": "Slack", "integrationsSlackInfo": "", - "integrationsDiscord": "", + "integrationsDiscord": "Discord", "integrationsDiscordInfo": "", - "integrationsZapier": "", + "integrationsZapier": "Zapier", "integrationsZapierInfo": "", - "commonSave": "", + "commonSave": "Guardar", "createYour": "", "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", + "pause": "Pausar", + "resume": "Resumir", + "editing": "Editando...", + "url": "URL", + "access": "Acceso", "timezone": "", "features": "", "administrator": "", @@ -231,13 +231,13 @@ "friendlyNamePlaceholder": "", "maintenanceRepeat": "", "maintenance": "", - "duration": "", + "duration": "Duración", "addMonitors": "", - "window": "", - "cancel": "", - "message": "", - "low": "", - "high": "", + "window": "ventana", + "cancel": "Cancelar", + "message": "Mensaje", + "low": "bajo", + "high": "alto", "statusCode": "", "date&Time": "", "type": "", @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json index d98ece35b..4949817c7 100644 --- a/client/src/locales/fi.json +++ b/client/src/locales/fi.json @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "Tauota", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index bfce12843..9e76440f0 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -425,7 +425,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "Pause", @@ -487,7 +493,8 @@ "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": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1096,7 +1103,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", @@ -1118,5 +1129,12 @@ "packetsReceivedRate": "", "packetsSent": "", "rate": "", - "selectInterface": "" + "selectInterface": "", + "v1": { + "infrastructure": { + "disk_selection_title": "Sélection des disques", + "disk_selection_info": "Aucun disque détecté pour le moment.", + "disk_selection_description": "Sélectionnez les disques spécifiques que vous souhaitez surveiller." + } + } } diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json index 6107c19d0..68d94139f 100644 --- a/client/src/locales/ja.json +++ b/client/src/locales/ja.json @@ -425,7 +425,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "一時停止", @@ -487,7 +493,8 @@ "ping": "pingするIPアドレスまたはホスト名を入力(例: 192.168.1.100 または example.com)し、ダッシュボードに表示される明確な表示名を追加。", "docker": "コンテナのDocker IDを入力。Docker IDは64文字のフルDocker IDである必要があります。docker inspect を実行してフルコンテナIDを取得できます。", "port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1075,7 +1082,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", @@ -1097,5 +1108,12 @@ "packetsReceivedRate": "", "packetsSent": "", "rate": "", - "selectInterface": "" + "selectInterface": "", + "v1": { + "infrastructure": { + "disk_selection_title": "ディスク選択", + "disk_selection_info": "現在ディスクが検出されていません。", + "disk_selection_description": "監視したい特定のディスクを選択してください。" + } + } } diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json index 9e1fa6bc0..193c53788 100644 --- a/client/src/locales/pt-BR.json +++ b/client/src/locales/pt-BR.json @@ -425,7 +425,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "Pausado", @@ -487,7 +493,8 @@ "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": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1096,7 +1103,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", @@ -1118,5 +1129,12 @@ "packetsReceivedRate": "", "packetsSent": "", "rate": "", - "selectInterface": "" + "selectInterface": "", + "v1": { + "infrastructure": { + "disk_selection_title": "Seleção de Discos", + "disk_selection_info": "Nenhum disco detectado no momento.", + "disk_selection_description": "Selecione os discos específicos que você deseja monitorar." + } + } } diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index aa54de393..ab40ecb0d 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -10,7 +10,7 @@ "settingsFailedToClearStats": "Не удалось очистить статистику", "settingsMonitorsDeleted": "Успешно удалены все мониторы", "settingsFailedToDeleteMonitors": "Не удалось удалить все мониторы", - "starPromptTitle": "", + "starPromptTitle": "Запустить Checkmate.", "starPromptDescription": "Ознакомьтесь с последними релизами и помогите развить сообщество на GitHub", "https": "HTTPS", "http": "HTTP", @@ -84,31 +84,35 @@ "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", @@ -332,8 +336,8 @@ "validationFailed": "Не удалось выполнить проверку", "noFileSelected": "Файл не выбран", "fallbackPage": "Импортируйте файл для массовой загрузки списка серверов", - "invalidFileType": "", - "uploadFailed": "" + "invalidFileType": "Неверный тип файла", + "uploadFailed": "Загрузка не удалась" }, "DeleteAccountTitle": "Удалить аккаунт", "DeleteAccountButton": "Удалить аккаунт", @@ -378,50 +382,56 @@ "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": "" + "role": "Роль", + "changeTeamPassword": { + "changePasswordMenu": "Сбросить пароль", + "title": "Сбросить пароль члена команды", + "description": "Создайте новый пароль для этого члена команды. Вам необходимо будет сообщить ему пароль по секретному адресу.", + "success": "Пароль успешно сброшен. Убедитесь, что вы предоставили учётные данные участнику безопасным способом." + } }, "monitorState": { "paused": "Остановлено", @@ -445,8 +455,8 @@ "password": "Пароль", "team": "Команда", "logOut": "Выйти", - "notifications": "", - "logs": "" + "notifications": "Уведомления", + "logs": "Логи" }, "settingsEmailUser": "Email пользователь", "state": "Состояние", @@ -474,8 +484,8 @@ "settingsTestEmailUnknownError": "Неизвестная ошибка", "statusMsg": { "paused": "Мониторинг приостановлен", - "up": "", - "down": "", + "up": "Ваш сайт запущен.", + "down": "Сайт оффлайн.", "pending": "Ожидание..." }, "uptimeGeneralInstructions": { @@ -483,13 +493,14 @@ "ping": "Ведите IP-адрес или имя хоста для пинга (например, 192.168.1.100 или example.com) и добавьте понятное имя, которое будет отображаться на панели инструментов.", "docker": "Введите Docker ID вашего контейнера. Docker ID должен быть полным 64-символьным Docker ID. Вы можете запустить docker inspect , чтобы получить полный ID контейнера.", "port": "Введите URL-адрес или IP-адрес сервера, номер порта и понятное имя, которое будет отображаться на панели инструментов.", - "game": "" + "game": "Введите IP-адрес или имя хоста и номер порта для пинга (например, 192.168.1.100 или example.com) и выберите тип игры.", + "https": "Введите URL-адрес или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное отображаемое имя, которое будет отображаться на панели управления." }, "common": { - "appName": "", - "monitoringAgentName": "", + "appName": "Checkmate", + "monitoringAgentName": "Захватить", "buttons": { - "toggleTheme": "" + "toggleTheme": "Переключить тему" }, "toasts": { "networkError": "Ошибка сети", @@ -506,7 +517,7 @@ "inputs": { "email": { "label": "Электронная почта", - "placeholder": "", + "placeholder": "jordan.ellis@domain.com", "errors": { "empty": "Для продолжения введите свой адрес электронной почты", "invalid": "Пожалуйста, проверьте правильность введённого адреса электронной почты" @@ -560,7 +571,7 @@ }, "firstName": { "label": "Имя", - "placeholder": "", + "placeholder": "Jordan", "errors": { "empty": "Пожалуйста, введите свое имя", "length": "Имя должно быть короче 50 символов", @@ -569,7 +580,7 @@ }, "lastName": { "label": "Фамилия", - "placeholder": "", + "placeholder": "Ellis", "errors": { "empty": "Пожалуйста, введите свою фамилию", "length": "Фамилия должна быть короче 50 символов", @@ -583,12 +594,12 @@ "fields": { "password": { "errors": { - "incorrect": "" + "incorrect": "Введённый вами пароль неверный" } }, "role": { "errors": { - "min": "" + "min": "Требуется хотя бы одна роль" } } } @@ -602,8 +613,8 @@ "links": { "forgotPassword": "Забыли пароль?", "register": "У вас нет учетной записи?", - "forgotPasswordLink": "", - "registerLink": "" + "forgotPasswordLink": "Сбросить пароль", + "registerLink": "Зарегистрируйтесь здесь" }, "toasts": { "success": "С возвращением! Вы успешно вошли в систему.", @@ -611,10 +622,10 @@ }, "errors": { "password": { - "incorrect": "" + "incorrect": "Предоставленный вами пароль не соответствует нашим записям." } }, - "welcome": "" + "welcome": "Добро пожаловать обратно в Checkmate!" }, "registration": { "heading": { @@ -641,7 +652,7 @@ "toasts": { "success": "Добро пожаловать! Ваша учетная запись была создана успешно." }, - "welcome": "" + "welcome": "Добро пожаловать в Checkmate!" }, "forgotPassword": { "heading": "Забыли пароль?", @@ -652,446 +663,478 @@ "stepFour": "Ваш пароль был успешно сброшен. Нажмите ниже, чтобы волшебным образом войти в систему." }, "buttons": { - "openEmail": "", + "openEmail": "Открыть Gmail", "resetPassword": "Сбросить пароль" }, "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "passwordKey": "Password key icon", + "email": "Email icon", + "lock": "Lock icon", + "passwordConfirm": "Password confirm icon" }, "links": { - "login": "", - "resend": "" + "login": "Вернуться к Входу", + "resend": "Не получили письмо? Нажмите, чтобы отправить повторно" }, "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" + "sent": "Инструкция отправлена на .", + "emailNotFound": "Email не найден.", + "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": "" + "title": "Имя", + "description": "Описательное имя для вашей интеграции.", + "nameLabel": "Имя", + "namePlaceholder": "например, уведомления Slack" }, "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" + "title": "Тип", + "description": "Выберите тип канала уведомлений, который вы хотите создать.", + "typeLabel": "Тип" }, "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" + "title": "Электронная почта", + "description": "Адреса электронной почты получателей.", + "emailLabel": "Адрес электронной почты", + "emailPlaceholder": "например, john@example.com" }, "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Slack", + "description": "Настройте свой вебхук Slack здесь", + "webhookLabel": "Slack вебхук URL", + "webhookPlaceholder": "https://hooks.slack.com/services/..." }, "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" + "title": "PagerDuty", + "description": "Настройте интеграцию PagerDuty здесь", + "integrationKeyLabel": "Интеграционный ключ", + "integrationKeyPlaceholder": "1234567890" }, "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Discord", + "description": "Настройте вебхук Discord здесь", + "webhookLabel": "Discord вебхук URL", + "webhookPlaceholder": "https://your-server.com/webhook" }, "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Вебхук", + "description": "Настройте свой вебхук здесь", + "webhookLabel": "URL-адрес веб-перехватчика", + "webhookPlaceholder": "https://your-server.com/webhook" }, - "testNotification": "", - "dialogDeleteTitle": "", - "dialogDeleteConfirm": "" + "testNotification": "Тестовое уведомление", + "dialogDeleteTitle": "Вы уверены, что хотите удалить это уведомление?", + "dialogDeleteConfirm": "Удалить" }, "notificationConfig": { - "title": "", - "description": "" + "title": "Уведомления", + "description": "Выберите каналы уведомлений, которые вы хотите использовать" }, "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" + "checkingEvery": "Проверка каждый {{interval}}", + "withCaptureAgent": "с агентом захвата {{version}}", + "up": "вверх", + "down": "вниз", + "paused": "приостановлено" }, - "advancedMatching": "", - "sendTestNotifications": "", - "selectAll": "", - "showAdminLoginLink": "", + "advancedMatching": "Расширенное сопоставление", + "sendTestNotifications": "Отправить тестовое уведомление", + "selectAll": "Выбрать все", + "showAdminLoginLink": "Показывать ссылку «Администратор? Войти здесь» на странице статуса", "logsPage": { - "title": "", - "description": "", + "title": "Логи", + "description": "Системные логи - последние 1000 строк", "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": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "", - "intervalHeader": "" + "title": "Задачи в настоящее время в очереди", + "idHeader": "ID монитора", + "urlHeader": "URL", + "typeHeader": "Тип", + "activeHeader": "Активный", + "lockedAtHeader": "Заблокировано на", + "runCountHeader": "Количество запусков", + "failCountHeader": "Количество неудач", + "lastRunHeader": "Последний запуск в", + "lastFinishedAtHeader": "Последний раз закончил в", + "lastRunTookHeader": "Последний забег занял", + "intervalHeader": "Интервал" }, "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" + "title": "Метрики очереди", + "metricHeader": "Метрическая", + "valueHeader": "Ценить" }, "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" + "title": "Неудачные задачи", + "monitorIdHeader": "ID монитора", + "monitorUrlHeader": "URL монитора", + "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": "", - "labelVersion": "", - "title": "" + "labelDevelopedBy": "Разработано Bluewave Labs", + "labelVersion": "Версия", + "title": "Об" }, "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" + "buttonAddMonitors": "Добавить демо мониторы", + "description": "Добавьте образцы мониторов для демонстрации.", + "title": "Демо мониторы" }, "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "", - "toastEmailRequiredFieldsError": "" + "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": "Требуется адрес электронной почты, хост, порт и пароль" }, "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" + "description": "Введите ключ API Google PageSpeed, чтобы включить мониторинг Google PageSpeed. Нажмите «Сбросить», чтобы обновить ключ.", + "labelApiKeySet": "API-ключ установлен. Нажмите «Сброс», чтобы изменить его.", + "labelApiKey": "API-ключ PageSpeed", + "title": "API-ключ Google PageSpeed" }, - "saveButtonLabel": "", + "saveButtonLabel": "Сохранить", "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" + "clearAllStatsButton": "Очистить всю статистику", + "clearAllStatsDescription": "Очистить всю статистику. Это необратимо.", + "clearAllStatsDialogConfirm": "Да, очистить всю статистику", + "clearAllStatsDialogDescription": "После удаления история и статистика мониторинга не могут быть восстановлены.", + "clearAllStatsDialogTitle": "Вы хотите очистить всю статистику?", + "description": "Укажите, как долго вы хотите хранить данные. Вы также можете удалить все существующие данные.", + "labelTTL": "Дни, за которые вы хотите посмотреть историю.", + "labelTTLOptional": "0 для бесконечности", + "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": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" + "description": "Отображение IP-адреса или URL-адреса монитора на общедоступной странице состояния. Если эта опция отключена, для защиты конфиденциальной информации будет отображаться только имя монитора.", + "label": "Отображать IP/URL на странице статуса", + "selectDisabled": "Выключено", + "selectEnabled": "Включено", + "title": "Мониторинг IP/URL на странице статуса" }, "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": "", - "instantCpuUsageSubtitle": "" + "heapAllocationTitle": "Использовано кучи", + "heapAllocationSubtitle": "% доступной памяти", + "heapUsageTitle": "Выделено памяти в куче", + "heapUsageSubtitle": "% доступно памяти", + "heapUtilizationTitle": "Использовано кучи", + "heapUtilizationSubtitle": "% от доступного", + "instantCpuUsageTitle": "Загруженность CPU", + "instantCpuUsageSubtitle": "% загрузка CPU за 1s" }, "stats": { - "eventLoopDelayTitle": "", - "uptimeTitle": "", - "usedHeapSizeTitle": "", - "totalHeapSizeTitle": "", - "osMemoryLimitTitle": "" + "eventLoopDelayTitle": "Задержка цикла событий", + "uptimeTitle": "Аптайм", + "usedHeapSizeTitle": "Размер используемой кучи", + "totalHeapSizeTitle": "Общий размер кучи", + "osMemoryLimitTitle": "Ограничение памяти ОS" } }, - "pageSpeedLighthouseAPI": "", + "pageSpeedLighthouseAPI": "Используйте API Lighthouse PageSpeed для мониторинга вашего сайта", "time": { - "threeMinutes": "", - "fiveMinutes": "", - "tenMinutes": "", - "twentyMinutes": "", - "oneHour": "", - "oneDay": "", - "oneWeek": "", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" + "threeMinutes": "3 минуты", + "fiveMinutes": "5 минут", + "tenMinutes": "10 минут", + "twentyMinutes": "20 минут", + "oneHour": "1 час", + "oneDay": "1 день", + "oneWeek": "1 неделя", + "fourMinutes": "4 минуты", + "oneMinute": "1 минута", + "twoMinutes": "2 минуты", + "fifteenSeconds": "15 секунд", + "thirtySeconds": "30 секунд" }, "general": { - "noOptionsFound": "" + "noOptionsFound": "{{unit}} не найдено" }, "infrastructureMonitor": { "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Отслеживайте производительность серверов", + "Выявляйте узкие места и оптимизируйте использование", + "Обеспечивайте надежность с помощью мониторинга в режиме реального времени" + ], + "title": "Монитор инфраструктуры используется для:", + "actionButton": "Давайте создадим ваш первый монитор инфраструктуры!" } }, "maintenanceWindow": { "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Отметьте периоды технического обслуживания.", + "Устраните любые недопонимания.", + "Прекратите отправку оповещений в периоды технического обслуживания." + ], + "title": "Период технического обслуживания используется для:", + "actionButton": "Давайте создадим ваше первое окно технического обслуживания!" } }, "pageSpeed": { "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Отчет об удобстве использования страницы", + "Помощь в анализе скорости загрузки веб-страницы", + "Предложение о том, как можно улучшить страницу" + ], + "title": "Монитор PageSpeed используется для:", + "actionButton": "Давайте создадим ваш первый монитор PageSpeed!" } }, "uptimeMonitor": { "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Проверьте, доступны ли веб-сайты или серверы и отвечают ли они", + "Оповещайте команды о сбоях или проблемах с производительностью", + "Мониторинг конечных точек HTTP, пингов, контейнеров и портов", + "Отслеживайте динамику времени безотказной работы и надежности" + ], + "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": "", - "regexPlaceholder": "", - "text": "" + "equal": "Равный", + "equalPlaceholder": "успех", + "include": "Включить", + "includePlaceholder": "хорошо", + "regex": "Regex", + "regexPlaceholder": "^(success|ok)$", + "text": "Метод сопоставления" }, "monitorType": { "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Имя/ID контейнера", + "namePlaceholder": "Мой контейнер", + "placeholder": "my-app или abcd1234" }, "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "URL для мониторинга", + "namePlaceholder": "Google", + "placeholder": "google.com" }, "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "IP-адрес для мониторинга", + "namePlaceholder": "Google", + "placeholder": "1.1.1.1" }, "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "URL для мониторинга", + "namePlaceholder": "Локальный хост:5173", + "placeholder": "локальный хост" }, "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "URL для мониторинга", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" } }, "uptimeAdvancedMatching": { - "jsonPath": "" + "jsonPath": "путь к JSON" }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", + "bytesPerSecond": "Байты в секунду", + "bytesReceived": "Получено байтов", + "bytesSent": "Отправлено байтов", + "chooseGame": "Выбрать игру", "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigDescription": "Мониторинг использует скользящее окно, чтобы понять, когда ресурс недоступен. Статус изменится, если нужный процент проверок покажет сбой.", + "incidentConfigStatusWindowLabel": "Сколько проверок должно быть в скользящем окне?", + "incidentConfigStatusWindowThresholdLabel": "Какой процент проверок в скользящем окне завершается неудачей/успешностью до изменения статуса монитора?", + "incidentConfigTitle": "Инциденты", + "incidentConfigDescriptionV2": "Количество последовательных проверок, необходимых для изменения статуса монитора. Максимум 25.", + "incidentConfigStatusCheckNumber": "Сколько проверок перед сменой статуса?", + "intervalTitle": "Интервал проверки", + "intervalDescription": "Как часто следует проверять монитор." }, - "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": "Выберите интерфейс", + "v1": { + "infrastructure": { + "disk_selection_title": "Выбор дисков", + "disk_selection_info": "На данный момент диск не обнаружен.", + "disk_selection_description": "Выберите конкретные диски, которые вы хотите отслеживать." + } + } } diff --git a/client/src/locales/th.json b/client/src/locales/th.json index c3f571b6b..d5f9ad137 100644 --- a/client/src/locales/th.json +++ b/client/src/locales/th.json @@ -425,7 +425,13 @@ } } }, - "role": "บทบาท" + "role": "บทบาท", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "หยุดชั่วคราว", @@ -487,7 +493,8 @@ "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) และเลือกประเภทเกม" + "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม", + "https": "" }, "common": { "appName": "Checkmate", @@ -1096,7 +1103,11 @@ "incidentConfigDescription": "การใช้หน้าต่างเลื่อนจะช่วยกำหนดเวลาที่ตัวตรวจสอบล้มเหลว สถานะของตัวตรวจสอบจะเปลี่ยนก็ต่อเมื่อเปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนตรงตามค่าที่กำหนด", "incidentConfigStatusWindowLabel": "ต้องมีการตรวจสอบกี่ครั้งในหน้าต่างเลื่อน?", "incidentConfigStatusWindowThresholdLabel": "ต้องการให้กี่เปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนล้มเหลว/สำเร็จ ก่อนที่จะเปลี่ยนสถานะของตัวตรวจสอบ?", - "incidentConfigTitle": "เหตุการณ์" + "incidentConfigTitle": "เหตุการณ์", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "อัตราการส่งข้อมูล", "dataReceived": "ข้อมูลที่รับ", @@ -1118,5 +1129,12 @@ "packetsReceivedRate": "อัตราการรับแพ็กเก็ต", "packetsSent": "แพ็กเก็ตที่ส่ง", "rate": "อัตรา", - "selectInterface": "เลือกอินเทอร์เฟซ" + "selectInterface": "เลือกอินเทอร์เฟซ", + "v1": { + "infrastructure": { + "disk_selection_title": "การเลือกดิสก์", + "disk_selection_info": "ไม่พบดิสก์ในขณะนี้", + "disk_selection_description": "เลือกดิสก์ที่คุณต้องการตรวจสอบ" + } + } } diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 0ab45ebe9..59f294533 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "Durduruldu", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json index c2d1aa6fc..67b419445 100644 --- a/client/src/locales/zh-CN.json +++ b/client/src/locales/zh-CN.json @@ -425,7 +425,13 @@ } } }, - "role": "角色" + "role": "角色", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "暂停", @@ -487,7 +493,8 @@ "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)并选择游戏类型。" + "game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。", + "https": "" }, "common": { "appName": "Checkmate", @@ -1088,7 +1095,11 @@ "incidentConfigDescription": "滑动窗口用于确定监视器何时发生故障。仅当滑动窗口中的检查百分比满足指定值时,监视器的状态才会改变。", "incidentConfigStatusWindowLabel": "在滑动窗口中应包含多少次检测?", "incidentConfigStatusWindowThresholdLabel": "在监视器状态改变之前,滑动窗口中的检查失败/成功的百分比是多少?", - "incidentConfigTitle": "事件" + "incidentConfigTitle": "事件", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "数据速率", "dataReceived": "已接收数据", @@ -1110,5 +1121,12 @@ "packetsReceivedRate": "数据包接收率", "packetsSent": "已发送数据包", "rate": "速度", - "selectInterface": "选择接口" + "selectInterface": "选择接口", + "v1": { + "infrastructure": { + "disk_selection_title": "磁盘选择", + "disk_selection_info": "目前没有检测到磁盘。", + "disk_selection_description": "选择您要监控的特定磁盘。" + } + } } diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json index e57ebd487..e5ee58e58 100644 --- a/client/src/locales/zh-TW.json +++ b/client/src/locales/zh-TW.json @@ -425,7 +425,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "", @@ -487,7 +493,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1075,7 +1082,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/main.jsx b/client/src/main.jsx index 01ecefcc6..5cfbe6f33 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -8,7 +8,10 @@ import { PersistGate } from "redux-persist/integration/react"; import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx"; import { networkService } from "./Utils/NetworkService"; export { networkService }; -import I18nLoader from "./Components/I18nLoader"; +import I18nLoader from "./Components/v1/I18nLoader"; +import { initApiClient } from "./Utils/ApiClient"; + +initApiClient(store); ReactDOM.createRoot(document.getElementById("root")).render( diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json new file mode 100644 index 000000000..f44839e9f --- /dev/null +++ b/client/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["./src/*"] + }, + "allowJs": true, + "checkJs": false + }, + "include": ["src"] +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 000000000..3b77ae840 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,15 @@ +{ + "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 + } +} diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json new file mode 100644 index 000000000..bbd18d2b2 --- /dev/null +++ b/client/tsconfig.node.json @@ -0,0 +1,27 @@ +{ + "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, + + /* 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 new file mode 100644 index 000000000..685a9569a --- /dev/null +++ b/client/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"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.js b/client/vite.config.js deleted file mode 100644 index 07b5de748..000000000 --- a/client/vite.config.js +++ /dev/null @@ -1,20 +0,0 @@ -import { defineConfig, loadEnv } from "vite"; -import react from "@vitejs/plugin-react"; -import svgr from "vite-plugin-svgr"; -import { execSync } from "child_process"; - -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ""); - let version = "3.2.0"; - - return { - base: "/", - plugins: [svgr(), react()], - optimizeDeps: { - include: ["@mui/material/Tooltip", "@emotion/styled"], - }, - define: { - __APP_VERSION__: JSON.stringify(version), - }, - }; -}); diff --git a/client/vite.config.ts b/client/vite.config.ts new file mode 100644 index 000000000..3d8d66335 --- /dev/null +++ b/client/vite.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import svgr from "vite-plugin-svgr"; +import path from "path"; +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()], + server: { + host: true, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, + optimizeDeps: { + include: ["@mui/material/Tooltip", "@emotion/styled"], + }, + define: { + __APP_VERSION__: JSON.stringify(version), + }, + }; +}); diff --git a/docker/dev/build_images.sh b/docker/dev/build_images.sh index e94be9fa5..eb43e31d8 100755 --- a/docker/dev/build_images.sh +++ b/docker/dev/build_images.sh @@ -5,11 +5,10 @@ cd "$(dirname "$0")" cd ../.. # Define service names and their corresponding Dockerfiles in parallel arrays -services=("uptime_client" "uptime_mongo" "uptime_redis" "uptime_server") +services=("uptime_client" "uptime_mongo" "uptime_server") dockerfiles=( "./docker/dev/client.Dockerfile" "./docker/dev/mongoDB.Dockerfile" - "./docker/dev/redis.Dockerfile" "./docker/dev/server.Dockerfile" ) diff --git a/docker/dev/server.Dockerfile b/docker/dev/server.Dockerfile index d24bff63f..2a6ebcb7c 100755 --- a/docker/dev/server.Dockerfile +++ b/docker/dev/server.Dockerfile @@ -13,6 +13,8 @@ RUN npm install COPY ./server/ ./ +RUN npm run build + EXPOSE 52345 -CMD ["node", "src/index.js"] \ No newline at end of file +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/dist-arm/docker-compose.yaml b/docker/dist-arm/docker-compose.yaml index 0a5825b3e..9a87aa59b 100644 --- a/docker/dist-arm/docker-compose.yaml +++ b/docker/dist-arm/docker-compose.yaml @@ -15,7 +15,7 @@ services: depends_on: - mongodb mongodb: - image: mongo:4.4.18 + image: mongo:4.4.30 restart: always command: ["mongod", "--quiet", "--bind_ip_all"] ports: diff --git a/docker/dist-arm/server.Dockerfile b/docker/dist-arm/server.Dockerfile index e47db9a74..91ccd427e 100644 --- a/docker/dist-arm/server.Dockerfile +++ b/docker/dist-arm/server.Dockerfile @@ -35,10 +35,12 @@ RUN npm install COPY server ./ -COPY --from=frontend-build /app/client/dist ./public - RUN chmod +x ./scripts/inject-vars.sh +RUN npm run build + +COPY --from=frontend-build /app/client/dist ./public + EXPOSE 52345 -CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./src/index.js"] +CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./dist/index.js"] diff --git a/docker/dist-mono/docker-compose-local.yaml b/docker/dist-mono/docker-compose-local.yaml new file mode 100755 index 000000000..a5616695b --- /dev/null +++ b/docker/dist-mono/docker-compose-local.yaml @@ -0,0 +1,27 @@ +services: + server: + image: mono_server + restart: always + ports: + - "52345:52345" + environment: + - UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1 + - UPTIME_APP_CLIENT_HOST=http://localhost + - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db + - CLIENT_HOST=http://localhost + - JWT_SECRET=my_secret + depends_on: + - mongodb + mongodb: + image: mono_mongo + restart: always + command: ["mongod", "--quiet", "--bind_ip_all"] + volumes: + - ./mongo/data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile index 405f9e6ac..84b7127ed 100644 --- a/docker/dist-mono/server.Dockerfile +++ b/docker/dist-mono/server.Dockerfile @@ -19,12 +19,15 @@ WORKDIR /app/server COPY server ./ -COPY --from=frontend-build /app/client/dist ./public RUN npm install +RUN npm run build + +COPY --from=frontend-build /app/client/dist ./public + RUN chmod +x ./scripts/inject-vars.sh EXPOSE 52345 -CMD ./scripts/inject-vars.sh && node ./src/index.js +CMD ./scripts/inject-vars.sh && node ./dist/index.js diff --git a/docker/dist/server.Dockerfile b/docker/dist/server.Dockerfile index d24bff63f..2a6ebcb7c 100755 --- a/docker/dist/server.Dockerfile +++ b/docker/dist/server.Dockerfile @@ -13,6 +13,8 @@ RUN npm install COPY ./server/ ./ +RUN npm run build + EXPOSE 52345 -CMD ["node", "src/index.js"] \ No newline at end of file +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile index b78de2e33..9d9800e7b 100755 --- a/docker/prod/server.Dockerfile +++ b/docker/prod/server.Dockerfile @@ -15,6 +15,8 @@ RUN npm install COPY ./server ./ +RUN npm run build + EXPOSE 52345 CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile index b78de2e33..7341b1e9b 100755 --- a/docker/staging/server.Dockerfile +++ b/docker/staging/server.Dockerfile @@ -15,6 +15,8 @@ RUN npm install COPY ./server ./ +RUN npm run build + EXPOSE 52345 -CMD ["node", "src/index.js"] \ No newline at end of file +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docs/infrastructure-disk-selection.md b/docs/infrastructure-disk-selection.md new file mode 100644 index 000000000..500e3be1e --- /dev/null +++ b/docs/infrastructure-disk-selection.md @@ -0,0 +1,74 @@ +# Infrastructure Monitoring - Disk Selection + +This guide explains how to use the selective disk monitoring feature in Checkmate's infrastructure monitoring. + +## Overview + +By default, Checkmate monitors all detected disks on your server. The disk selection feature allows you to choose specific disks/mountpoints to monitor, giving you more control over what gets tracked and displayed. + +## Prerequisites + +- Checkmate server running with [Capture agent](https://github.com/bluewave-labs/capture) installed on target server +- An existing infrastructure monitor already created and configured +- At least one disk detected by the Capture agent + +## Using Disk Selection + +### Accessing the Feature + +1. Navigate to your Infrastructure monitors page +2. Click on an existing infrastructure monitor to view details +3. Click the "Configure" or "Edit" button +4. Scroll down to the **Disk Selection** section + +> **Note:** The disk selection feature is only available when editing existing monitors that have already detected disks. + +### Selecting Disks to Monitor + +1. **View Available Disks**: The system automatically detects all disks/mountpoints from your server +2. **Select Disks**: Check the boxes next to the disks you want to monitor +3. **Apply Changes**: Click "Save" to update your monitor configuration + +### Understanding Disk Identifiers + +Disks are identified by their mountpoint or device name: +- **Mountpoint**: `/` (root), `/home`, `/var`, etc. +- **Device**: `/dev/sda1`, `/dev/nvme0n1p1`, etc. + +### Behavior + +- **All disks selected**: Shows all detected disks in gauges and charts +- **Specific disks selected**: Only shows selected disks in the monitoring interface +- **No disks detected**: Displays "No disk detected for the moment" message + +## What Gets Filtered + +When you select specific disks, the following elements are filtered: + +- **Disk Usage Gauges**: Only selected disks appear in the dashboard +- **Disk Usage Charts**: Time-series charts show only selected disks +- **Monitor Table**: Summary view reflects only selected disk usage + +## Example Use Cases + +- **Server with multiple drives**: Monitor only critical system disks, ignore temporary storage +- **Database servers**: Focus monitoring on database partition while ignoring logs partition +- **Web servers**: Monitor web content disk separately from system disk +- **Development environments**: Track only project-specific mountpoints + +## Troubleshooting + +### No Disks Appearing +- Verify the Capture agent is running on the target server +- Check that the agent has proper permissions to read disk information +- Ensure the monitor has been running long enough to collect at least one check + +### Missing Disk Information +- Some disks may not be accessible to the Capture agent due to permissions +- Virtual or network-mounted disks may not appear in the list +- Check the Capture agent logs for any disk detection errors + +## Related Documentation + +- [Infrastructure Monitoring Setup](https://docs.checkmate.so/checkmate-2.1) +- [Capture Agent Installation](https://github.com/bluewave-labs/capture) \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index 4bc621f83..9ae4e58ed 100755 --- a/server/.gitignore +++ b/server/.gitignore @@ -8,4 +8,5 @@ coverage .clinic node_modules .vscode/* -public \ No newline at end of file +public +dist \ No newline at end of file diff --git a/server/jest.config.ts b/server/jest.config.ts new file mode 100644 index 000000000..fd0c87ad7 --- /dev/null +++ b/server/jest.config.ts @@ -0,0 +1,25 @@ +import type { Config } from "jest"; + +const config: Config = { + rootDir: ".", + testEnvironment: "node", + extensionsToTreatAsEsm: [".ts"], + transform: { + "^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }], + }, + moduleNameMapper: { + "^@/validation/(.*)\\.js$": "/src/validation/$1.js", + "^@/utils/(AppError)\\.js$": "/src/utils/$1.ts", + "^@/utils/(.*)\\.js$": "/src/utils/$1.js", + "^@/(.*)\\.ts$": "/src/$1.ts", + "^@/(.*)\\.js$": "/src/$1.ts", + "^@/(.*)$": "/src/$1", + }, + testMatch: ["/test/**/*.test.ts"], + setupFilesAfterEnv: [], + collectCoverageFrom: ["src/**/*.ts"], + coveragePathIgnorePatterns: ["/node_modules/", "/test/"], + clearMocks: true, +}; + +export default config; diff --git a/server/nodemon.json b/server/nodemon.json index 88d6ac686..3faf346d3 100755 --- a/server/nodemon.json +++ b/server/nodemon.json @@ -1,5 +1,5 @@ { - "ignore": ["src/locales/*", "*.log", "node_modules/*"], - "watch": ["src/**/*.js", "*.json"], - "ext": "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 index 6cc2e7de0..d56c619bf 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -21,13 +21,9 @@ "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" } } @@ -37,8 +33,6 @@ "description": "Checkmate Demo Server", "variables": { "API_PATH": { - "description": "API Base Path", - "enum": ["api/v1"], "default": "api/v1" } } @@ -88,6 +82,10 @@ { "name": "diagnostic", "description": "System health and performance diagnostics" + }, + { + "name": "incidents", + "description": "Incident management and resolution" } ], "paths": { @@ -164,51 +162,6 @@ } } }, - "/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"], @@ -731,28 +684,54 @@ ] } }, - "/monitors/team/summary": { + "/monitors/team/groups": { "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"] - } - } - } - ], + "summary": "Get monitor groups by team", + "description": "Get all monitor groups for the authenticated user's team", "responses": { "200": { - "description": "Monitor summary retrieved successfully" + "description": "Monitor groups retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Monitor groups retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "monitors": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -992,24 +971,78 @@ ] } }, - "/monitors/stats/{monitorId}": { + "/monitors/pagespeed/details/{monitorId}": { "get": { "tags": ["monitors"], - "summary": "Get monitor statistics", - "description": "Get comprehensive statistics for a monitor", + "summary": "Get PageSpeed monitor details", + "description": "Get detailed PageSpeed monitoring data including performance metrics and Core Web Vitals", "parameters": [ { "name": "monitorId", "in": "path", "required": true, + "description": "The PageSpeed monitor ID", "schema": { "type": "string" } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort order for results", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "default": "desc" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of results to return", + "schema": { + "type": "integer", + "default": 50 + } } ], "responses": { "200": { - "description": "Monitor statistics retrieved successfully" + "description": "PageSpeed details retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "PageSpeed details retrieved successfully" + }, + "data": { + "type": "object", + "properties": { + "monitor": { + "$ref": "#/components/schemas/Monitor" + }, + "checks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Check" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" @@ -1078,22 +1111,44 @@ ] } }, - "/monitors/export": { + "/monitors/export/json": { "get": { "tags": ["monitors"], - "summary": "Export monitors to CSV", - "description": "Export all monitors to CSV format", + "summary": "Export monitors to JSON", + "description": "Export all monitors to JSON format (admin/superadmin only)", "responses": { "200": { - "description": "CSV file generated successfully", + "description": "JSON file generated successfully", "content": { - "text/csv": { + "application/json": { "schema": { - "type": "string" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Monitors exported successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Monitor" + } + } + } } } } }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -1262,6 +1317,39 @@ } }, "/notifications": { + "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/team": { "get": { "tags": ["notifications"], "summary": "Get team notifications", @@ -1301,37 +1389,6 @@ "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}": { @@ -1722,6 +1779,64 @@ ] } }, + "/auth/users/{userId}/password": { + "put": { + "tags": ["auth"], + "summary": "Update user password by ID", + "description": "Update a specific user's password by ID (superadmin only)", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["password"], + "properties": { + "password": { + "type": "string", + "format": "password", + "minLength": 8, + "description": "New password for the user" + } + } + } + } + } + }, + "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"], @@ -2030,6 +2145,21 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status" + } + } + } + } + } + }, "responses": { "200": { "$ref": "#/components/responses/Success" @@ -2048,6 +2178,164 @@ ] } }, + "/checks/{action}": { + "put": { + "tags": ["checks"], + "summary": "Acknowledge all checks for all monitors", + "description": "Acknowledge all checks across all monitors for the team", + "parameters": [ + { + "name": "action", + "in": "path", + "required": true, + "description": "Action to perform (e.g., 'ack')", + "schema": { + "type": "string", + "enum": ["ack"] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["ack"], + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status to set" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Checks acknowledged successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "All checks acknowledged successfully" + }, + "data": { + "type": "object", + "description": "Updated checks information" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/checks/{action}/{monitorId}": { + "put": { + "tags": ["checks"], + "summary": "Acknowledge all checks for a specific monitor", + "description": "Acknowledge all checks for a specific monitor", + "parameters": [ + { + "name": "action", + "in": "path", + "required": true, + "description": "Action to perform (e.g., 'ack')", + "schema": { + "type": "string", + "enum": ["ack"] + } + }, + { + "name": "monitorId", + "in": "path", + "required": true, + "description": "Monitor ID to acknowledge checks for", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["ack"], + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status to set" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Checks acknowledged successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "All checks acknowledged successfully" + }, + "data": { + "type": "object", + "description": "Updated checks information" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/maintenance-window": { "post": { "tags": ["maintenance-window"], @@ -2291,6 +2579,142 @@ ] } }, + "/queue/jobs": { + "get": { + "tags": ["queue"], + "summary": "Get queue jobs", + "description": "Retrieve all jobs in the queue (admin/superadmin only)", + "responses": { + "200": { + "description": "Queue jobs retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Queue jobs fetched successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueJob" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": ["queue"], + "summary": "Add job to queue", + "description": "Add a new job to the queue (admin/superadmin only)", + "responses": { + "200": { + "description": "Job added successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Job added to queue successfully" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/queue/metrics": { + "get": { + "tags": ["queue"], + "summary": "Get queue metrics", + "description": "Retrieve queue metrics (admin/superadmin only)", + "responses": { + "200": { + "description": "Queue metrics retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Queue metrics fetched successfully" + }, + "data": { + "$ref": "#/components/schemas/QueueMetrics" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/queue/health": { "get": { "tags": ["queue"], @@ -2618,6 +3042,286 @@ } ] } + }, + "/incidents/team": { + "get": { + "tags": ["incidents"], + "summary": "Get incidents by team", + "description": "Retrieve all incidents for the authenticated user's team with optional filtering", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter by incident status (true = ongoing, false = resolved)", + "schema": { + "type": "boolean" + } + }, + { + "name": "monitorId", + "in": "query", + "description": "Filter by monitor ID", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of items per page", + "schema": { + "type": "integer", + "default": 10 + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort order for results", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "default": "desc" + } + } + ], + "responses": { + "200": { + "description": "Incidents retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incidents retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/team/summary": { + "get": { + "tags": ["incidents"], + "summary": "Get incident summary", + "description": "Retrieve a summary of incidents for the authenticated user's team", + "parameters": [ + { + "name": "dateRange", + "in": "query", + "description": "Date range for summary", + "schema": { + "type": "string", + "enum": ["day", "week", "month"], + "default": "week" + } + } + ], + "responses": { + "200": { + "description": "Incident summary retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident summary retrieved successfully" + }, + "data": { + "$ref": "#/components/schemas/IncidentSummary" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/{incidentId}": { + "get": { + "tags": ["incidents"], + "summary": "Get incident by ID", + "description": "Retrieve a specific incident by its ID", + "parameters": [ + { + "name": "incidentId", + "in": "path", + "required": true, + "description": "The incident ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Incident retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident retrieved successfully" + }, + "data": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/{incidentId}/resolve": { + "put": { + "tags": ["incidents"], + "summary": "Resolve incident manually", + "description": "Manually resolve an ongoing incident. Requires admin or superadmin role.", + "parameters": [ + { + "name": "incidentId", + "in": "path", + "required": true, + "description": "The incident ID to resolve", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "comment": { + "type": "string", + "description": "Optional comment about the resolution", + "example": "Issue resolved by restarting the service" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Incident resolved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident resolved successfully" + }, + "data": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, "components": { @@ -2680,6 +3384,16 @@ } } }, + "BadRequest": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "InternalServerError": { "description": "Internal server error", "content": { @@ -3375,6 +4089,557 @@ } } } + }, + "Incident": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "description": "Unique incident identifier" + }, + "monitorId": { + "type": "string", + "description": "ID of the associated monitor" + }, + "teamId": { + "type": "string", + "description": "ID of the team" + }, + "startTime": { + "type": "string", + "format": "date-time", + "description": "When the incident started" + }, + "endTime": { + "type": ["string", "null"], + "format": "date-time", + "description": "When the incident was resolved (null if ongoing)" + }, + "status": { + "type": "boolean", + "description": "Incident status (true = ongoing, false = resolved)" + }, + "message": { + "type": ["string", "null"], + "description": "Error message or description" + }, + "statusCode": { + "type": ["integer", "null"], + "description": "HTTP status code if applicable" + }, + "resolutionType": { + "type": ["string", "null"], + "enum": ["automatic", "manual", null], + "description": "How the incident was resolved" + }, + "resolvedBy": { + "type": ["string", "null"], + "description": "User ID who resolved the incident (for manual resolution)" + }, + "comment": { + "type": ["string", "null"], + "description": "Resolution comment" + }, + "checks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of check IDs associated with this incident" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "IncidentSummary": { + "type": "object", + "properties": { + "totalIncidents": { + "type": "integer", + "description": "Total number of incidents" + }, + "activeIncidents": { + "type": "integer", + "description": "Number of ongoing incidents" + }, + "resolvedIncidents": { + "type": "integer", + "description": "Number of resolved incidents" + }, + "averageResolutionTime": { + "type": "number", + "description": "Average time to resolve incidents in milliseconds" + }, + "incidentsByMonitor": { + "type": "array", + "items": { + "type": "object", + "properties": { + "monitorId": { + "type": "string" + }, + "monitorName": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + } + } + } + }, + "QueueJob": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Job ID" + }, + "name": { + "type": "string", + "description": "Job name" + }, + "data": { + "type": "object", + "description": "Job payload data" + }, + "status": { + "type": "string", + "enum": ["waiting", "active", "completed", "failed", "delayed"], + "description": "Current job status" + }, + "progress": { + "type": "number", + "description": "Job progress percentage" + }, + "attemptsMade": { + "type": "integer", + "description": "Number of attempts made" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the job was created" + }, + "processedOn": { + "type": ["string", "null"], + "format": "date-time", + "description": "When the job started processing" + }, + "finishedOn": { + "type": ["string", "null"], + "format": "date-time", + "description": "When the job finished" + } + } + }, + "QueueMetrics": { + "type": "object", + "properties": { + "waiting": { + "type": "integer", + "description": "Number of jobs waiting" + }, + "active": { + "type": "integer", + "description": "Number of jobs currently processing" + }, + "completed": { + "type": "integer", + "description": "Number of completed jobs" + }, + "failed": { + "type": "integer", + "description": "Number of failed jobs" + }, + "delayed": { + "type": "integer", + "description": "Number of delayed jobs" + } + } + }, + "Check": { + "type": "object", + "description": "A monitoring check result", + "properties": { + "_id": { + "type": "string", + "description": "Check ID" + }, + "metadata": { + "type": "object", + "properties": { + "monitorId": { + "type": "string", + "description": "Associated monitor ID" + }, + "teamId": { + "type": "string", + "description": "Associated team ID" + }, + "type": { + "type": "string", + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"], + "description": "Monitor type" + } + }, + "required": ["monitorId", "teamId", "type"] + }, + "status": { + "type": "boolean", + "description": "Check status (true = up, false = down)" + }, + "responseTime": { + "type": "number", + "description": "Response time in milliseconds" + }, + "statusCode": { + "type": "integer", + "description": "HTTP status code (for HTTP monitors)" + }, + "message": { + "type": "string", + "description": "Status message or error description" + }, + "timings": { + "$ref": "#/components/schemas/CheckTimings" + }, + "cpu": { + "$ref": "#/components/schemas/CheckCpuInfo" + }, + "memory": { + "$ref": "#/components/schemas/CheckMemoryInfo" + }, + "disk": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckDiskInfo" + } + }, + "host": { + "$ref": "#/components/schemas/CheckHostInfo" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckErrorInfo" + } + }, + "capture": { + "$ref": "#/components/schemas/CheckCaptureInfo" + }, + "net": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckNetworkInfo" + } + }, + "accessibility": { + "type": "number", + "description": "PageSpeed accessibility score (0-100)" + }, + "bestPractices": { + "type": "number", + "description": "PageSpeed best practices score (0-100)" + }, + "seo": { + "type": "number", + "description": "PageSpeed SEO score (0-100)" + }, + "performance": { + "type": "number", + "description": "PageSpeed performance score (0-100)" + }, + "audits": { + "$ref": "#/components/schemas/CheckAudits" + }, + "ack": { + "type": "boolean", + "description": "Whether the check has been acknowledged" + }, + "ackAt": { + "type": ["string", "null"], + "format": "date-time", + "description": "When the check was acknowledged" + }, + "expiry": { + "type": "string", + "format": "date-time", + "description": "When the check expires" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "CheckTimings": { + "type": "object", + "description": "Detailed timing information for HTTP checks", + "properties": { + "start": { + "type": "number" + }, + "socket": { + "type": "number" + }, + "lookup": { + "type": "number" + }, + "connect": { + "type": "number" + }, + "secureConnect": { + "type": "number" + }, + "upload": { + "type": "number" + }, + "response": { + "type": "number" + }, + "end": { + "type": "number" + }, + "phases": { + "type": "object", + "properties": { + "wait": { + "type": "number" + }, + "dns": { + "type": "number" + }, + "tcp": { + "type": "number" + }, + "tls": { + "type": "number" + }, + "request": { + "type": "number" + }, + "firstByte": { + "type": "number" + }, + "download": { + "type": "number" + }, + "total": { + "type": "number" + } + } + } + } + }, + "CheckCpuInfo": { + "type": "object", + "description": "CPU information for infrastructure checks", + "properties": { + "physical_core": { + "type": "integer" + }, + "logical_core": { + "type": "integer" + }, + "frequency": { + "type": "number" + }, + "temperature": { + "type": "array", + "items": { + "type": "number" + } + }, + "free_percent": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckMemoryInfo": { + "type": "object", + "description": "Memory information for infrastructure checks", + "properties": { + "total_bytes": { + "type": "number" + }, + "available_bytes": { + "type": "number" + }, + "used_bytes": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckDiskInfo": { + "type": "object", + "description": "Disk information for infrastructure checks", + "properties": { + "device": { + "type": "string" + }, + "mountpoint": { + "type": "string" + }, + "read_speed_bytes": { + "type": "number" + }, + "write_speed_bytes": { + "type": "number" + }, + "total_bytes": { + "type": "number" + }, + "free_bytes": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckHostInfo": { + "type": "object", + "description": "Host information for infrastructure checks", + "properties": { + "os": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "kernel_version": { + "type": "string" + } + } + }, + "CheckErrorInfo": { + "type": "object", + "description": "Error information from checks", + "properties": { + "metric": { + "type": "array", + "items": { + "type": "string" + } + }, + "err": { + "type": "string" + } + } + }, + "CheckCaptureInfo": { + "type": "object", + "description": "Capture agent information", + "properties": { + "version": { + "type": "string" + }, + "mode": { + "type": "string" + } + } + }, + "CheckNetworkInfo": { + "type": "object", + "description": "Network interface information", + "properties": { + "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" + } + } + }, + "CheckAudits": { + "type": "object", + "description": "PageSpeed audit results", + "properties": { + "cls": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "si": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "fcp": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "lcp": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "tbt": { + "$ref": "#/components/schemas/LighthouseAudit" + } + } + }, + "LighthouseAudit": { + "type": "object", + "description": "Individual Lighthouse audit result", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "score": { + "type": "number" + }, + "displayValue": { + "type": "string" + }, + "numericValue": { + "type": "number" + }, + "numericUnit": { + "type": "string" + } + } } } } diff --git a/server/package-lock.json b/server/package-lock.json index 3e1130dc4..3f5b824b0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,6 +13,7 @@ "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", @@ -35,26 +36,47 @@ "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.1", + "super-simple-scheduler": "1.4.5", "swagger-ui-express": "5.0.1", "winston": "^3.13.0" }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/compression": "1.8.1", + "@types/cookie-parser": "1.4.10", + "@types/cors": "2.8.19", + "@types/dockerode": "^4.0.0", + "@types/express": "5.0.3", + "@types/gamedig": "^5.0.3", + "@types/jest": "^30.0.0", + "@types/jmespath": "^0.15.2", + "@types/jsdom": "^27.0.0", + "@types/jsonwebtoken": "9.0.10", + "@types/mjml": "^4.7.4", + "@types/multer": "^2.0.0", + "@types/nodemailer": "7.0.1", + "@types/papaparse": "^5.5.2", + "@types/ping": "0.4.4", + "@types/swagger-ui-express": "4.1.8", "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", - "nodemon": "3.1.9", + "jest": "^30.2.0", + "nodemon": "^3.1.11", "prettier": "^3.3.3", - "sinon": "19.0.2" + "ts-jest": "^29.4.6", + "ts-node": "^10.9.2", + "tsc-alias": "1.8.16", + "tsx": "4.20.5", + "typescript": "5.9.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@asamuzakjp/css-color": { @@ -70,13 +92,764 @@ "lru-cache": "^10.4.3" } }, - "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==", + "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": { - "@babel/helper-validator-identifier": "^7.27.1", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -84,20 +857,515 @@ "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==", + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.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==", + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@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/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, "engines": { "node": ">=6.9.0" } @@ -127,6 +1395,30 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -211,6 +1503,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -218,6 +1511,28 @@ "@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", @@ -233,35 +1548,501 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } }, "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "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": { - "colorspace": "1.1.x", + "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "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/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "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.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", - "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", + "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": { @@ -291,9 +2072,9 @@ } }, "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==", + "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": { @@ -301,13 +2082,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "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.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -316,19 +2097,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "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.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "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": { @@ -376,9 +2160,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "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": { @@ -389,9 +2173,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "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": { @@ -399,13 +2183,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "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.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -413,18 +2197,36 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", - "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "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.7.13", + "@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", @@ -872,9 +2674,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz", - "integrity": "sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==", + "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": { @@ -894,6 +2696,113 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -904,6 +2813,377 @@ "node": ">=8" } }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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", @@ -922,9 +3202,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "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": { @@ -943,9 +3223,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", - "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "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" @@ -1029,6 +3309,57 @@ "win32" ] }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.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/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1039,6 +3370,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1151,10 +3495,17 @@ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "license": "BSD-3-Clause" }, + "node_modules/@sinclair/typebox": { + "version": "0.34.47", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz", + "integrity": "sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", - "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", + "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" @@ -1183,33 +3534,681 @@ "@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==", + "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": "BSD-3-Clause", + "license": "Apache-2.0", "dependencies": { - "@sinonjs/commons": "^3.0.1", - "type-detect": "^4.1.0" + "@smithy/types": "^4.8.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.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==", + "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": ">=4" + "node": ">=12.20" } }, - "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/@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", @@ -1229,11 +4228,169 @@ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "license": "ISC", "optional": true, - "peer": true, "engines": { "node": ">=10.13.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/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/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@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.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "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/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.0.tgz", + "integrity": "sha512-NBm/UIL1KYWivh9ihJELK4Jv0dzOQCleS0OT5JklErra8mZ/t0UQpHfTkMmPrAqtra7OyFius1EFLVs0DhiHQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1241,12 +4398,52 @@ "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", + "peer": true, + "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/gamedig": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/gamedig/-/gamedig-5.0.3.tgz", + "integrity": "sha512-VLrOVRf72wVE6P0DhFtRVodiONwL51uojn6dNpP/Ks/Vdm3CDH6dZTAw74WmCDLuyvpytNy8auKzlTKfBlO8jg==", + "dev": true, + "license": "MIT" + }, "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", @@ -1254,6 +4451,56 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jmespath": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.2.tgz", + "integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-27.0.0.tgz", + "integrity": "sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1261,21 +4508,201 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "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": { - "undici-types": "~7.10.0" + "@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/mjml": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/@types/mjml/-/mjml-4.7.4.tgz", + "integrity": "sha512-vyi1vzWgMzFMwZY7GSZYX0GU0dmtC8vLHwpgk+NWmwbwRSrlieVyJ9sn5elodwUfklJM7yGl0zQeet1brKTWaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mjml-core": "*" + } + }, + "node_modules/@types/mjml-core": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@types/mjml-core/-/mjml-core-4.15.2.tgz", + "integrity": "sha512-Q7SxFXgoX979HP57DEVsRI50TV8x1V4lfCA4Up9AvfINDM5oD/X9ARgfoyX1qS987JCnDLv85JjkqAjt3hZSiQ==", + "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/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "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", + "peer": true, + "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/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@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/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -1304,6 +4731,299 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "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/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1332,6 +5052,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1349,6 +5070,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1384,10 +5118,39 @@ "node": ">=6" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "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" @@ -1436,6 +5199,13 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1448,6 +5218,16 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "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/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -1457,16 +5237,6 @@ "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", @@ -1480,9 +5250,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "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", @@ -1490,6 +5260,142 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-istanbul/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/babel-plugin-istanbul/node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1543,6 +5449,15 @@ ], "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", @@ -1561,6 +5476,15 @@ "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", @@ -1585,23 +5509,23 @@ } }, "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==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "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", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -1617,18 +5541,54 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "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/body-parser/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/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", @@ -1652,17 +5612,10 @@ "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.25.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", - "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "funding": [ { "type": "opencollective", @@ -1678,11 +5631,13 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001737", - "electron-to-chromium": "^1.5.211", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "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" @@ -1691,6 +5646,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/bson": { "version": "6.10.4", "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", @@ -1880,16 +5858,13 @@ } }, "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/caniuse-api": { @@ -1905,9 +5880,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001739", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", - "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "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", @@ -1924,23 +5899,6 @@ ], "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", @@ -1958,14 +5916,14 @@ "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==", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 16" + "node": ">=10" } }, "node_modules/cheerio": { @@ -2048,6 +6006,29 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2129,6 +6110,24 @@ "node": ">=0.10.0" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2176,41 +6175,6 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "license": "MIT" }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/colorspace/node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/colorspace/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/colorspace/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2450,6 +6414,13 @@ "node": ">=10.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -2477,9 +6448,9 @@ } }, "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "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" @@ -2505,14 +6476,12 @@ } }, "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "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", - "optional": true, - "peer": true, "dependencies": { - "mdn-data": "2.0.30", + "mdn-data": "2.12.2", "source-map-js": "^1.0.1" }, "engines": { @@ -2548,6 +6517,7 @@ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", "license": "MIT", + "peer": true, "dependencies": { "cssnano-preset-default": "^7.0.9", "lilconfig": "^3.1.3" @@ -2728,19 +6698,6 @@ } } }, - "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", @@ -2774,14 +6731,19 @@ "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==", + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } } }, "node_modules/deep-is": { @@ -2791,6 +6753,16 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -2838,14 +6810,24 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "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-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -2853,15 +6835,28 @@ "license": "MIT" }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, + "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/docker-modem": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.6.tgz", @@ -2950,9 +6945,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", - "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "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" @@ -3020,9 +7015,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.213", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz", - "integrity": "sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==", + "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": { @@ -3033,6 +7028,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3110,9 +7118,9 @@ } }, "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==", + "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" @@ -3163,6 +7171,48 @@ "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", @@ -3204,25 +7254,25 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@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.34.0", - "@eslint/plugin-kit": "^0.3.5", + "@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", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -3398,6 +7448,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -3460,40 +7524,126 @@ "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==", + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "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", + "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", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -3524,15 +7674,6 @@ "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", @@ -3561,6 +7702,36 @@ "dev": true, "license": "MIT" }, + "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", @@ -3576,9 +7747,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.3.tgz", - "integrity": "sha512-OdCYfRqfpuLUFonTNjvd30rCBZUneHpSQkCqfaeWQ9qrKcl6XlWeDBNVwGb+INAIxRshuN2jF+BE0L6gbBO2mw==", + "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", @@ -3593,6 +7764,26 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -3697,16 +7888,6 @@ "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", @@ -3831,6 +8012,13 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "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", @@ -3855,19 +8043,19 @@ } }, "node_modules/gamedig": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.1.tgz", - "integrity": "sha512-RM/eCR8bAKEX+5dA9sxg4D6oQjS5t0MPsIxGquerBLSIu0f1hAVaDGC58Lp1srYwF6A1C2wYY2p9PzOrPTKf4Q==", + "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.3", + "fast-xml-parser": "5.2.5", "gbxremote": "0.2.1", "got": "13.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "long": "5.3.2", "minimist": "1.2.8", "seek-bzip": "2.0.0", - "telnet-client": "2.2.5", + "telnet-client": "2.2.6", "varint": "6.0.0" }, "bin": { @@ -3954,15 +8142,19 @@ } }, "node_modules/gamedig/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==", + "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": { @@ -4017,6 +8209,16 @@ "node": ">=0.10" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4050,6 +8252,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4079,10 +8291,23 @@ "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==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -4149,6 +8374,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -4186,6 +8432,13 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -4256,16 +8509,6 @@ "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", @@ -4295,9 +8538,9 @@ "license": "MIT" }, "node_modules/htmlnano": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.3.tgz", - "integrity": "sha512-mzTUHhxdfDw80X36rv5qFvjvor7r0uCwXI5mzo8CW61tImbe5Jpvl3JzPAetVh54wUYVuoa8x3qw8LFn8B3gHQ==", + "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", @@ -4430,6 +8673,16 @@ "numbered": "^1.1.0" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -4495,6 +8748,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "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", @@ -4505,6 +8778,18 @@ "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", @@ -4512,12 +8797,12 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", - "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", + "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.3.0", + "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -4589,6 +8874,16 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4616,16 +8911,6 @@ "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", @@ -4644,19 +8929,6 @@ "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", @@ -4670,16 +8942,192 @@ "license": "ISC" }, "node_modules/isomorphic-dompurify": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.26.0.tgz", - "integrity": "sha512-nZmoK4wKdzPs5USq4JHBiimjdKSVAOm2T1KyDoadtMPNXYHxiENd19ou4iU/V4juFM6LVgYQnpxCYmxqNP4Obw==", + "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.2.6", - "jsdom": "^26.1.0" + "dompurify": "^3.3.0", + "jsdom": "^27.0.1" }, "engines": { - "node": ">=18" + "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": { @@ -4720,6 +9168,23 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -4735,6 +9200,21 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.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", @@ -4764,6 +9244,600 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/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/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/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/jmespath": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", @@ -4793,9 +9867,9 @@ "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4843,6 +9917,19 @@ } } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "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", @@ -4869,6 +9956,19 @@ "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==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -4892,14 +9992,14 @@ } }, "node_modules/juice": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/juice/-/juice-11.0.1.tgz", - "integrity": "sha512-R3KLud4l/sN9AMmFZs0QY7cugGSiKvPhGyIsufCV5nJ0MjSlngUE7k80TmFeK9I62wOXrjWBtYA1knVs2OkF8w==", + "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", + "cheerio": "1.0.0", "commander": "^12.1.0", - "entities": "^4.5.0", + "entities": "^7.0.0", "mensch": "^0.3.4", "slick": "^1.12.2", "web-resource-inliner": "^7.0.0" @@ -4912,34 +10012,46 @@ } }, "node_modules/juice/node_modules/cheerio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", - "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "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.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.0.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", + "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": "^7.12.0", + "undici": "^6.19.5", "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">=20.18.1" + "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": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "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", { @@ -4951,14 +10063,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, "node_modules/juice/node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "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" @@ -4967,13 +10079,6 @@ "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", @@ -4986,12 +10091,12 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -5019,6 +10124,16 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5152,23 +10267,6 @@ "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", @@ -5192,13 +10290,6 @@ "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", @@ -5218,9 +10309,9 @@ "license": "ISC" }, "node_modules/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "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" @@ -5253,6 +10344,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5263,12 +10371,10 @@ } }, "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0", - "optional": true, - "peer": true + "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", @@ -5300,6 +10406,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "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/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -5309,6 +10432,20 @@ "node": ">= 0.6" } }, + "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": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -5351,6 +10488,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -5841,81 +10988,6 @@ "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", @@ -5938,14 +11010,14 @@ } }, "node_modules/mongodb": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.19.0.tgz", - "integrity": "sha512-H3GtYujOJdeKIMLKBT9PwlDhGrQfplABNF1G904w6r5ZXKWyv77aB0X9B+rhmaAwjtllHzaEkvi9mkGVZxs2Bw==", + "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.1.9", + "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" @@ -5994,14 +11066,14 @@ } }, "node_modules/mongoose": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.0.tgz", - "integrity": "sha512-3TixPihQKBdyaYDeJqRjzgb86KbilEH07JmzV8SoSjgoskNTpa6oTBmDxeoF9p8YnWQoz7shnCyPkSV/48y3yw==", + "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.18.0", + "mongodb": "~6.20.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -6015,52 +11087,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/mongodb": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", - "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" - }, - "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.2.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/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -6138,6 +11164,20 @@ "node": ">= 6.0.0" } }, + "node_modules/mylas": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/mylas/-/mylas-2.1.14.tgz", + "integrity": "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/raouldeheer" + } + }, "node_modules/nan": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", @@ -6163,6 +11203,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6191,31 +11247,6 @@ "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", @@ -6299,10 +11330,17 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "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==", + "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": { @@ -6315,9 +11353,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", "dev": true, "license": "MIT", "dependencies": { @@ -6376,9 +11414,9 @@ } }, "node_modules/normalize-url": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", - "integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==", + "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" @@ -6387,6 +11425,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -6406,9 +11457,9 @@ "license": "MIT" }, "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "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": { @@ -6471,6 +11522,22 @@ "fn.name": "1.x.x" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6530,18 +11597,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "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", @@ -6640,6 +11711,16 @@ "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", @@ -6671,14 +11752,14 @@ "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==", + "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": ">= 14.16" + "node": ">=8" } }, "node_modules/picocolors": { @@ -6708,6 +11789,98 @@ "node": ">=4.0.0" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -6727,6 +11900,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7154,25 +12328,6 @@ "node": ">=16" } }, - "node_modules/postcss-svgo/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/postcss-svgo/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/postcss-svgo/node_modules/svgo": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", @@ -7220,9 +12375,9 @@ "license": "MIT" }, "node_modules/posthtml": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", - "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==", + "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", @@ -7364,6 +12519,34 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?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", @@ -7439,13 +12622,30 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -7454,6 +12654,37 @@ "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", @@ -7473,16 +12704,6 @@ "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", @@ -7493,20 +12714,56 @@ } }, "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==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/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/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==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7563,12 +12820,44 @@ "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-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -7578,6 +12867,16 @@ "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", @@ -7593,12 +12892,47 @@ "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/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/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", @@ -7635,10 +12969,13 @@ "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" + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } }, "node_modules/saxes": { "version": "6.0.0", @@ -7675,9 +13012,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "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" @@ -7734,16 +13071,6 @@ "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", @@ -7916,18 +13243,18 @@ } }, "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "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.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "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/simple-update-notifier": { @@ -7943,33 +13270,14 @@ "node": ">=10" } }, - "node_modules/sinon": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", - "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", + "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": "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", + "license": "MIT", "engines": { - "node": ">=0.3.1" + "node": ">=8" } }, "node_modules/slick": { @@ -7999,6 +13307,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -8014,6 +13333,13 @@ "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", "license": "ISC" }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/ssh2": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", @@ -8046,6 +13372,29 @@ "node": "*" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -8087,6 +13436,43 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/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/string-length/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/string-to-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz", @@ -8193,9 +13579,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "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" @@ -8229,6 +13615,26 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8271,15 +13677,27 @@ } }, "node_modules/super-simple-scheduler": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.1.tgz", - "integrity": "sha512-jKVwxdRBXOHrUstA2pvO4jBZIA3CUinKiQ6k5C2Wn5tscoV3srppPxW0zG3VNKTCaGxo3yPn6Zaf3IDVBcAwRA==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.5.tgz", + "integrity": "sha512-xHR3a+pLywY7HtwjWj/+OaANdEwAieQhckC/i0s6OoHtVJQ4+VLO1VfkKUbxp+oSROGXcaZet8PTGONVdKNUUw==", "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", @@ -8293,6 +13711,28 @@ "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", @@ -8312,7 +13752,6 @@ "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "license": "MIT", "optional": true, - "peer": true, "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", @@ -8339,15 +13778,35 @@ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "license": "MIT", "optional": true, - "peer": true, "engines": { "node": ">= 10" } }, + "node_modules/svgo/node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/svgo/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "license": "CC0-1.0", + "optional": true + }, "node_modules/swagger-ui-dist": { - "version": "5.28.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz", - "integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==", + "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" @@ -8374,10 +13833,26 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "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", @@ -8403,9 +13878,9 @@ } }, "node_modules/telnet-client": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.5.tgz", - "integrity": "sha512-X5xEkmKHgpCpngnH7QnkFX87UyBErauHsjzlCGVp87MbhnmHoaAeacuALGfqovHh3MXAfrpPs+g30PgyBpy8Jw==", + "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", @@ -8481,6 +13956,13 @@ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "license": "MIT" }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8545,12 +14027,162 @@ "node": ">= 14.0.0" } }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "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/tsc-alias/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/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", @@ -8611,6 +14243,21 @@ "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", + "peer": true, + "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", @@ -8632,18 +14279,18 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", - "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", + "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=18.17" } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "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": { @@ -8664,10 +14311,45 @@ "node": ">= 0.8" } }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, "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==", + "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", @@ -8732,6 +14414,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8783,6 +14472,16 @@ "node": ">=18" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/web-resource-inliner": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz", @@ -8988,13 +14687,13 @@ } }, "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==", + "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.2", + "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", @@ -9051,13 +14750,6 @@ "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", @@ -9135,9 +14827,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "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" @@ -9152,6 +14844,20 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -9235,6 +14941,13 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -9262,22 +14975,6 @@ "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", @@ -9319,6 +15016,16 @@ "node": ">=8" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/server/package.json b/server/package.json index d879776a8..2421aeb1c 100755 --- a/server/package.json +++ b/server/package.json @@ -5,8 +5,10 @@ "main": "index.js", "type": "module", "scripts": { - "test": "c8 mocha", - "dev": "nodemon src/index.js", + "test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand", + "dev": "nodemon --exec tsx src/index.js", + "start": "node --watch ./dist/index.js", + "build": "tsc && tsc-alias", "lint": "eslint .", "lint-fix": "eslint --fix .", "format": "prettier --write .", @@ -15,11 +17,15 @@ "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", @@ -42,25 +48,43 @@ "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.1", + "super-simple-scheduler": "1.4.5", "swagger-ui-express": "5.0.1", "winston": "^3.13.0" }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/compression": "1.8.1", + "@types/cookie-parser": "1.4.10", + "@types/cors": "2.8.19", + "@types/dockerode": "^4.0.0", + "@types/express": "5.0.3", + "@types/gamedig": "^5.0.3", + "@types/jest": "^30.0.0", + "@types/jmespath": "^0.15.2", + "@types/jsdom": "^27.0.0", + "@types/jsonwebtoken": "9.0.10", + "@types/mjml": "^4.7.4", + "@types/multer": "^2.0.0", + "@types/nodemailer": "7.0.1", + "@types/papaparse": "^5.5.2", + "@types/ping": "0.4.4", + "@types/swagger-ui-express": "4.1.8", "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", - "nodemon": "3.1.9", + "jest": "^30.2.0", + "nodemon": "^3.1.11", "prettier": "^3.3.3", - "sinon": "19.0.2" + "ts-jest": "^29.4.6", + "ts-node": "^10.9.2", + "tsc-alias": "1.8.16", + "tsx": "4.20.5", + "typescript": "5.9.2" } } diff --git a/server/scripts/generate-checks.js b/server/scripts/generate-checks.js new file mode 100644 index 000000000..d3a33b033 --- /dev/null +++ b/server/scripts/generate-checks.js @@ -0,0 +1,176 @@ +import mongoose from "mongoose"; +import { MonitorModel } from "../dist/db/models/Monitor.js"; +import { CheckModel } from "../dist/db/models/Check.js"; + +const DEFAULT_MONITOR_ID = "000000000000000000000001"; +const DEFAULT_TEAM_ID = "0000000000000000000000aa"; +const DEFAULT_USER_ID = "0000000000000000000000bb"; +const DEFAULT_MONITOR_TYPE = "http"; +const DEFAULT_TOTAL = 1_000_000; +const DEFAULT_BATCH_SIZE = 5_000; + +const parseObjectId = (value, fallback) => { + try { + return new mongoose.Types.ObjectId(value || fallback); + } catch (error) { + console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`); + return new mongoose.Types.ObjectId(fallback); + } +}; + +async function ensureMonitor({ monitorId, teamId, userId, type }) { + const existing = await MonitorModel.findById(monitorId); + if (existing) { + return existing; + } + + console.log(`Monitor ${monitorId.toString()} not found, creating it.`); + const monitor = new MonitorModel({ + _id: monitorId, + userId, + teamId, + name: `Seed Monitor ${monitorId.toString()}`, + description: "Synthetic monitor for performance testing", + statusWindow: [], + statusWindowSize: 5, + statusWindowThreshold: 60, + type, + ignoreTlsErrors: false, + url: "https://example.com", + isActive: true, + interval: 60000, + alertThreshold: 5, + cpuAlertThreshold: 5, + memoryAlertThreshold: 5, + diskAlertThreshold: 5, + tempAlertThreshold: 5, + selectedDisks: [], + }); + + await monitor.save(); + return monitor; +} + +async function run() { + const mongoUri = process.env.MONGO_URI ?? "mongodb://localhost:27017/uptime_db"; + const monitorId = parseObjectId(process.env.MONITOR_ID ?? DEFAULT_MONITOR_ID, DEFAULT_MONITOR_ID); + const teamId = parseObjectId("69648b0578209af45f9ffe30"); + const userId = parseObjectId("69648b0678209af45f9ffe32"); + const monitorType = process.env.MONITOR_TYPE ?? DEFAULT_MONITOR_TYPE; + const total = Number(process.env.CHECK_TOTAL ?? DEFAULT_TOTAL); + const batchSize = Number(process.env.CHECK_BATCH_SIZE ?? DEFAULT_BATCH_SIZE); + + console.log(`Connecting to MongoDB at ${mongoUri}`); + await mongoose.connect(mongoUri); + + await ensureMonitor({ monitorId, teamId, userId, type: monitorType }); + + console.log(`Seeding ${total} checks for monitor ${monitorId.toString()} (team ${teamId.toString()}) in batches of ${batchSize}.`); + + const docs = []; + const startTime = Date.now(); + + for (let i = 0; i < total; i += 1) { + const baseTime = Date.now() - (total - i) * 1000; + const createdAt = new Date(baseTime); + docs.push({ + metadata: { + monitorId, + teamId, + type: monitorType, + }, + status: i % 50 !== 0, + statusCode: i % 50 !== 0 ? 200 : 500, + responseTime: Math.floor(Math.random() * 1000), + message: i % 50 !== 0 ? "OK" : "Error", + expiry: createdAt, + createdAt, + updatedAt: createdAt, + timings: { + start: baseTime, + socket: baseTime, + lookup: baseTime, + connect: baseTime, + secureConnect: baseTime, + upload: baseTime, + response: baseTime + 40, + end: baseTime + 45, + phases: { + wait: 0, + dns: 1, + tcp: 2, + tls: 4, + request: 0, + firstByte: 30, + download: 5, + total: 45, + }, + }, + cpu: { + physical_core: 8, + logical_core: 16, + frequency: 3600, + temperature: [50 + Math.random() * 10], + free_percent: 40, + usage_percent: Math.random() * 100, + }, + memory: { + total_bytes: 32 * 1024 ** 3, + available_bytes: 16 * 1024 ** 3, + used_bytes: 16 * 1024 ** 3, + usage_percent: Math.random() * 100, + }, + disk: [ + { + device: "/dev/sda1", + mountpoint: "/", + read_speed_bytes: Math.random() * 10_000_000, + write_speed_bytes: Math.random() * 10_000_000, + total_bytes: 512 * 1024 ** 3, + free_bytes: 128 * 1024 ** 3, + usage_percent: Math.random() * 100, + }, + ], + host: { + os: "linux", + platform: "ubuntu", + kernel_version: "5.15.0", + }, + net: [ + { + name: "eth0", + bytes_sent: Math.random() * 10_000_000, + bytes_recv: Math.random() * 10_000_000, + packets_sent: Math.random() * 1_000_000, + packets_recv: Math.random() * 1_000_000, + err_in: 0, + err_out: 0, + drop_in: 0, + drop_out: 0, + fifo_in: 0, + fifo_out: 0, + }, + ], + errors: i % 50 === 0 ? [{ metric: ["uptime"], err: "500" }] : [], + }); + + if (docs.length === batchSize) { + await CheckModel.insertMany(docs, { ordered: false }); + console.log(`Inserted ${i + 1} / ${total}`); + docs.length = 0; + } + } + + if (docs.length > 0) { + await CheckModel.insertMany(docs, { ordered: false }); + } + + await mongoose.disconnect(); + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`Finished inserting ${total} checks in ${duration}s`); +} + +run().catch((error) => { + console.error("Failed to seed checks", error); + process.exit(1); +}); diff --git a/server/src/app.js b/server/src/app.ts similarity index 65% rename from server/src/app.js rename to server/src/app.ts index 9cabda6b6..04699964d 100644 --- a/server/src/app.js +++ b/server/src/app.ts @@ -1,33 +1,42 @@ import express from "express"; import path from "path"; -import { responseHandler } from "./middleware/responseHandler.js"; import cors from "cors"; import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; -import languageMiddleware from "./middleware/languageMiddleware.js"; import swaggerUi from "swagger-ui-express"; -import { handleErrors } from "./middleware/handleErrors.js"; -import { setupRoutes } from "./config/routes.js"; -import { generalApiLimiter } from "./middleware/rateLimiter.js"; -import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js"; +import { handleErrors } from "@/middleware/handleErrors.js"; +import { generalApiLimiter } from "@/middleware/rateLimiter.js"; +import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js"; +import { setupRoutes } from "@/config/routes.js"; +import { InitializedServices } from "@/config/services.js"; +import { InitializedControllers } from "@/config/controllers.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; -export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { +export const createApp = ({ + services, + controllers, + envSettings, + frontendPath, + openApiSpec, +}: { + services: InitializedServices; + controllers: InitializedControllers; + envSettings: EnvConfig; + frontendPath: string; + openApiSpec: any; +}) => { 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: "*", + allowedHeaders: ["Content-Type", "Authorization"], credentials: true, }) ); @@ -63,7 +72,6 @@ export const createApp = ({ services, controllers, envSettings, frontendPath, op }, }) ); - app.use(languageMiddleware(services.stringService, services.translationService, services.settingsService)); // Swagger UI app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); @@ -74,7 +82,7 @@ export const createApp = ({ services, controllers, envSettings, frontendPath, op }); // Main app routes - setupRoutes(app, controllers); + setupRoutes(app, controllers, services); // FE routes app.get("*", (req, res) => { diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js deleted file mode 100644 index 693e35ab8..000000000 --- a/server/src/config/controllers.js +++ /dev/null @@ -1,66 +0,0 @@ -import { createCommonDependencies } from "../controllers/baseController.js"; - -// Services - -// Controllers -import MonitorController from "../controllers/monitorController.js"; -import AuthController from "../controllers/authController.js"; -import SettingsController from "../controllers/settingsController.js"; -import CheckController from "../controllers/checkController.js"; -import InviteController from "../controllers/inviteController.js"; -import MaintenanceWindowController from "../controllers/maintenanceWindowController.js"; -import QueueController from "../controllers/queueController.js"; -import LogController from "../controllers/logController.js"; -import StatusPageController from "../controllers/statusPageController.js"; -import NotificationController from "../controllers/notificationController.js"; -import DiagnosticController from "../controllers/diagnosticController.js"; - -export const initializeControllers = (services) => { - const controllers = {}; - const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService); - - 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, - }); - - return controllers; -}; diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts new file mode 100644 index 000000000..7a20ed93c --- /dev/null +++ b/server/src/config/controllers.ts @@ -0,0 +1,44 @@ +import MonitorController from "../controllers/monitorController.js"; +import AuthController from "../controllers/authController.js"; +import SettingsController from "../controllers/settingsController.js"; +import CheckController from "../controllers/checkController.js"; +import InviteController from "../controllers/inviteController.js"; +import MaintenanceWindowController from "../controllers/maintenanceWindowController.js"; +import QueueController from "../controllers/queueController.js"; +import LogController from "../controllers/logController.js"; +import StatusPageController from "../controllers/statusPageController.js"; +import NotificationController from "../controllers/notificationController.js"; +import DiagnosticController from "../controllers/diagnosticController.js"; +import IncidentController from "../controllers/incidentController.js"; +import type { InitializedServices } from "@/config/services.js"; + +export interface InitializedControllers { + authController: AuthController; + monitorController: MonitorController; + settingsController: SettingsController; + checkController: CheckController; + inviteController: InviteController; + maintenanceWindowController: MaintenanceWindowController; + queueController: QueueController; + logController: LogController; + statusPageController: StatusPageController; + notificationController: NotificationController; + diagnosticController: DiagnosticController; + incidentController: IncidentController; +} +export const initializeControllers = (services: InitializedServices): InitializedControllers => { + return { + authController: new AuthController(services.userService), + monitorController: new MonitorController(services.monitorService), + settingsController: new SettingsController(services.settingsService, services.emailService, services.db), + checkController: new CheckController(services.checkService), + inviteController: new InviteController(services.inviteService), + maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService), + queueController: new QueueController(services.jobQueue), + logController: new LogController(services.logger), + statusPageController: new StatusPageController(services.statusPageService, services.monitorsRepository, services.settingsService), + notificationController: new NotificationController(services.notificationsService, services.monitorsRepository), + diagnosticController: new DiagnosticController(services.diagnosticService), + incidentController: new IncidentController(services.incidentService), + }; +}; 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/routes.js b/server/src/config/routes.ts similarity index 78% rename from server/src/config/routes.js rename to server/src/config/routes.ts index 7056393f9..d38ac69ae 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.ts @@ -1,4 +1,4 @@ -import { verifyJWT } from "../middleware/verifyJWT.js"; +import { createVerifyJWT } from "../middleware/verifyJWT.js"; import { authApiLimiter } from "../middleware/rateLimiter.js"; import AuthRoutes from "../routes/authRoute.js"; @@ -13,18 +13,22 @@ import LogRoutes from "../routes/logRoutes.js"; import DiagnosticRoutes from "../routes/diagnosticRoute.js"; import NotificationRoutes from "../routes/notificationRoute.js"; -export const setupRoutes = (app, controllers) => { - const authRoutes = new AuthRoutes(controllers.authController); +import IncidentRoutes from "../routes/incidentRoute.js"; + +export const setupRoutes = (app: any, controllers: Record, services: Record) => { + const verifyJWT = createVerifyJWT(services.settingsService); + const authRoutes = new AuthRoutes(controllers.authController, verifyJWT); 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 inviteRoutes = new InviteRoutes(controllers.inviteController, verifyJWT); 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 statusPageRoutes = new StatusPageRoutes(controllers.statusPageController, verifyJWT); const notificationRoutes = new NotificationRoutes(controllers.notificationController); - const diagnosticRoutes = new DiagnosticRoutes(controllers.diagnosticController); + const diagnosticRoutes = new DiagnosticRoutes(controllers.diagnosticController, verifyJWT); + const incidentRoutes = new IncidentRoutes(controllers.incidentController); app.use("/api/v1/auth", authApiLimiter, authRoutes.getRouter()); app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter()); @@ -37,4 +41,5 @@ export const setupRoutes = (app, controllers) => { app.use("/api/v1/status-page", statusPageRoutes.getRouter()); app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); + app.use("/api/v1/incidents", verifyJWT, incidentRoutes.getRouter()); }; diff --git a/server/src/config/services.js b/server/src/config/services.js deleted file mode 100644 index 08c2525f3..000000000 --- a/server/src/config/services.js +++ /dev/null @@ -1,237 +0,0 @@ -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import TranslationService from "../service/system/translationService.js"; -import StringService from "../service/system/stringService.js"; -import MongoDB from "../db/mongo/MongoDB.js"; -import NetworkService from "../service/infrastructure/networkService.js"; -import EmailService from "../service/infrastructure/emailService.js"; -import BufferService from "../service/infrastructure/bufferService.js"; -import StatusService from "../service/infrastructure/statusService.js"; -import NotificationUtils from "../service/infrastructure/notificationUtils.js"; -import NotificationService from "../service/infrastructure/notificationService.js"; -import ErrorService from "../service/infrastructure/errorService.js"; -import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; -import UserService from "../service/business/userService.js"; -import CheckService from "../service/business/checkService.js"; -import DiagnosticService from "../service/business/diagnosticService.js"; -import InviteService from "../service/business/inviteService.js"; -import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; -import MonitorService from "../service/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/models/Check.js"; -import Monitor from "../db/models/Monitor.js"; -import User from "../db/models/User.js"; -import InviteToken from "../db/models/InviteToken.js"; -import StatusPage from "../db/models/StatusPage.js"; -import Team from "../db/models/Team.js"; -import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; -import MonitorStats from "../db/models/MonitorStats.js"; -import Notification from "../db/models/Notification.js"; -import RecoveryToken from "../db/models/RecoveryToken.js"; -import AppSettings from "../db/models/AppSettings.js"; - -import InviteModule from "../db/mongo/modules/inviteModule.js"; -import CheckModule from "../db/mongo/modules/checkModule.js"; -import StatusPageModule from "../db/mongo/modules/statusPageModule.js"; -import UserModule from "../db/mongo/modules/userModule.js"; -import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js"; -import MonitorModule from "../db/mongo/modules/monitorModule.js"; -import NotificationModule from "../db/mongo/modules/notificationModule.js"; -import RecoveryModule from "../db/mongo/modules/recoveryModule.js"; -import SettingsModule from "../db/mongo/modules/settingsModule.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, - }); - - const services = { - settingsService, - translationService, - stringService, - db, - networkService, - emailService, - bufferService, - statusService, - notificationService, - jobQueue: superSimpleQueue, - userService, - checkService, - diagnosticService, - inviteService, - maintenanceWindowService, - monitorService, - errorService, - logger, - }; - - Object.values(services).forEach((service) => { - ServiceRegistry.register(service.serviceName, service); - }); - - return services; -}; diff --git a/server/src/config/services.ts b/server/src/config/services.ts new file mode 100644 index 000000000..06701491d --- /dev/null +++ b/server/src/config/services.ts @@ -0,0 +1,292 @@ +import MongoDB from "../db/MongoDB.js"; +import NetworkService from "../service/infrastructure/networkService.js"; +import EmailService from "../service/infrastructure/emailService.js"; +import BufferService from "../service/infrastructure/bufferService.js"; +import { + NotificationsService, + StatusService, + WebhookProvider, + SlackProvider, + EmailProvider, + DiscordProvider, + PagerDutyProvider, + MatrixProvider, + INotificationsService, +} from "@/service/index.js"; +import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import UserService from "../service/business/userService.js"; +import CheckService from "../service/business/checkService.js"; +import DiagnosticService from "../service/business/diagnosticService.js"; +import InviteService from "../service/business/inviteService.js"; +import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; +import { MonitorService } from "@/service/index.js"; +import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js"; +import IncidentService from "../service/business/incidentService.js"; +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, GameDig } from "gamedig"; +import jmespath from "jmespath"; + +// DB Modules +import { GenerateAvatarImage } from "../utils/imageProcessing.js"; +import { ParseBoolean } from "../utils/utils.js"; + +// Models +import InviteToken from "../db/models/Invite.js"; +import Team from "../db/models/Team.js"; +import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; +import MonitorStats from "../db/models/MonitorStats.js"; +import NotificationModel from "../db/models/Notification.js"; +import RecoveryToken from "../db/models/RecoveryToken.js"; + +// repositories +import { + MongoMonitorsRepository, + MongoChecksRepository, + MongoMonitorStatsRepository, + MongoStatusPagesRepository, + MongoUsersRepository, + MongoInvitesRepository, + MongoRecoveryTokensRepository, + MongoNotificationsRepository, + MongoIncidentRepository, + MongoTeamsRepository, + MongoMaintenanceWindowsRepository, + IMonitorsRepository, + IChecksRepository, + IMonitorStatsRepository, + IStatusPagesRepository, + IUsersRepository, + IInvitesRepository, + IRecoveryTokensRepository, + ISettingsRepository, + INotificationsRepository, + IIncidentsRepository, + ITeamsRepository, + IMaintenanceWindowsRepository, +} from "@/repositories/index.js"; +import { ILogger } from "@/utils/logger.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; + +export type InitializedServices = { + settingsService: any; + db: any; + networkService: any; + emailService: any; + bufferService: any; + statusService: any; + jobQueue: any; + userService: any; + checkService: any; + diagnosticService: any; + inviteService: any; + maintenanceWindowService: any; + monitorService: any; + incidentService: any; + logger: any; + notificationsService: INotificationsService; + statusPageService: IStatusPageService; + + // Repositories + monitorsRepository: IMonitorsRepository; + checksRepository: IChecksRepository; + monitorStatsRepository: IMonitorStatsRepository; + statusPagesRepository: IStatusPagesRepository; + usersRepository: IUsersRepository; + invitesRepository: IInvitesRepository; + recoveryTokensRepository: IRecoveryTokensRepository; + settingsRepository: ISettingsRepository; + notificationsRepository: INotificationsRepository; + incidentsRepository: IIncidentsRepository; + teamsRepository: ITeamsRepository; + maintenanceWindowsRepository: IMaintenanceWindowsRepository; +}; + +export const initializeServices = async ({ + logger, + envSettings, + settingsService, + settingsRepository, +}: { + logger: ILogger; + envSettings: EnvConfig; + settingsService: any; + settingsRepository: ISettingsRepository; +}): Promise => { + // Create DB + + const db = new MongoDB(logger, envSettings); + + await db.connect(); + + // Repositories + const monitorsRepository = new MongoMonitorsRepository(); + const checksRepository = new MongoChecksRepository(logger); + const monitorStatsRepository = new MongoMonitorStatsRepository(); + const statusPagesRepository = new MongoStatusPagesRepository(); + const usersRepository = new MongoUsersRepository(); + const invitesRepository = new MongoInvitesRepository(); + const recoveryTokensRepository = new MongoRecoveryTokensRepository(); + const notificationsRepository = new MongoNotificationsRepository(); + const incidentsRepository = new MongoIncidentRepository(); + const teamsRepository = new MongoTeamsRepository(); + const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); + + const networkService = new NetworkService({ + axios, + got, + https, + jmespath, + GameDig, + ping, + logger, + http, + Docker, + net, + settingsService, + }); + const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); + + const incidentService = new IncidentService({ + logger, + incidentsRepository, + monitorsRepository, + usersRepository, + }); + + const checkService = new CheckService({ + monitorsRepository, + logger, + checksRepository, + }); + + const bufferService = new BufferService({ logger, checkService, settingsService }); + + const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); + + const webhookProvider = new WebhookProvider(logger); + const slackProvider = new SlackProvider(logger); + const emailProvider = new EmailProvider(emailService, logger); + const discordProvider = new DiscordProvider(logger); + const pagerDutyProvider = new PagerDutyProvider(logger); + const matrixProvider = new MatrixProvider(logger); + + const notificationsService = new NotificationsService( + notificationsRepository, + monitorsRepository, + webhookProvider, + emailProvider, + slackProvider, + discordProvider, + pagerDutyProvider, + matrixProvider, + logger + ); + + const superSimpleQueueHelper = new SuperSimpleQueueHelper({ + logger, + networkService, + statusService, + notificationsService, + checkService, + buffer: bufferService, + incidentService, + maintenanceWindowsRepository, + }); + + const superSimpleQueue = await SuperSimpleQueue.create({ + logger, + helper: superSimpleQueueHelper, + monitorsRepository, + }); + + // Business services + const userService = new UserService({ + crypto, + emailService, + settingsService, + logger, + jwt, + jobQueue: superSimpleQueue, + monitorsRepository, + usersRepository, + invitesRepository, + recoveryTokensRepository, + settingsRepository, + teamsRepository, + }); + + const diagnosticService = new DiagnosticService(); + const inviteService = new InviteService({ + invitesRepository, + settingsService, + emailService, + }); + const maintenanceWindowService = new MaintenanceWindowService({ + monitorsRepository, + maintenanceWindowsRepository, + }); + const monitorService = new MonitorService({ + jobQueue: superSimpleQueue, + emailService, + logger, + games, + monitorsRepository, + checksRepository, + monitorStatsRepository, + statusPagesRepository, + }); + + const statusPageService = new StatusPageService(statusPagesRepository); + + const services = { + //v1 + settingsService, + db, + networkService, + emailService, + bufferService, + statusService, + jobQueue: superSimpleQueue, + userService, + checkService, + diagnosticService, + inviteService, + maintenanceWindowService, + monitorService, + incidentService, + logger, + notificationsService, + statusPageService, + + // Repositories + monitorsRepository, + checksRepository, + monitorStatsRepository, + statusPagesRepository, + usersRepository, + invitesRepository, + recoveryTokensRepository, + settingsRepository, + notificationsRepository, + incidentsRepository, + teamsRepository, + maintenanceWindowsRepository, + }; + + return services; +}; diff --git a/server/src/controllers/announcementsController.js b/server/src/controllers/announcementsController.js deleted file mode 100755 index 430859791..000000000 --- a/server/src/controllers/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/authController.js b/server/src/controllers/authController.js deleted file mode 100755 index c186e8423..000000000 --- a/server/src/controllers/authController.js +++ /dev/null @@ -1,461 +0,0 @@ -import BaseController from "./baseController.js"; -import { - registrationBodyValidation, - loginValidation, - editUserBodyValidation, - recoveryValidation, - recoveryTokenBodyValidation, - newPasswordValidation, - getUserByIdParamValidation, - editUserByIdParamValidation, - editUserByIdBodyValidation, - editSuperadminUserByIdBodyValidation, -} 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 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" - ); -} - -export default AuthController; diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts new file mode 100755 index 000000000..23ea6e42e --- /dev/null +++ b/server/src/controllers/authController.ts @@ -0,0 +1,237 @@ +import { Request, Response, NextFunction } from "express"; +import { AppError } from "@/utils/AppError.js"; + +import { + registrationBodyValidation, + loginValidation, + editUserBodyValidation, + recoveryValidation, + recoveryTokenBodyValidation, + newPasswordValidation, + getUserByIdParamValidation, + editUserByIdParamValidation, + editUserByIdBodyValidation, + editSuperadminUserByIdBodyValidation, + editUserPasswordByIdBodyValidation, +} from "@/validation/joi.js"; + +const SERVICE_NAME = "authController"; + +class AuthController { + static SERVICE_NAME = SERVICE_NAME; + + private userService: any; + + constructor(userService: any) { + this.userService = userService; + } + + get serviceName() { + return AuthController.SERVICE_NAME; + } + + registerUser = async (req: Request, res: Response, next: NextFunction) => { + try { + const newUser = req.body.user; + const newUserToken = req.body.token; + if (newUser?.email) { + newUser.email = newUser.email.toLowerCase(); + } + await registrationBodyValidation.validateAsync(newUser); + + const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); + res.status(200).json({ + success: true, + msg: "User registered successfully", + data: { user, token }, + }); + } catch (error) { + next(error); + } + }; + + loginUser = async (req: Request, res: Response, next: NextFunction) => { + try { + 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.status(200).json({ + success: true, + msg: "User logged in successfully", + data: { + user, + token, + }, + }); + } catch (error) { + next(error); + } + }; + + editUser = async (req: Request, res: Response, next: NextFunction) => { + try { + await editUserBodyValidation.validateAsync(req.body); + + const updatedUser = await this.userService.editUser(req.body, req.file, req.user); + + res.status(200).json({ + success: true, + msg: "User updated successfully", + data: updatedUser, + }); + } catch (error) { + next(error); + } + }; + + checkSuperadminExists = async (req: Request, res: Response, next: NextFunction) => { + try { + const superAdminExists = await this.userService.checkSuperadminExists(); + return res.status(200).json({ + success: true, + msg: "Superadmin existence checked successfully", + data: superAdminExists, + }); + } catch (error) { + next(error); + } + }; + + requestRecovery = async (req: Request, res: Response, next: NextFunction) => { + try { + await recoveryValidation.validateAsync(req.body); + const email = req?.body?.email; + const msgId = await this.userService.requestRecovery(email); + return res.status(200).json({ + success: true, + msg: "Password recovery email sent successfully", + data: msgId, + }); + } catch (error) { + next(error); + } + }; + + validateRecovery = async (req: Request, res: Response, next: NextFunction) => { + try { + await recoveryTokenBodyValidation.validateAsync(req.body); + await this.userService.validateRecovery(req.body.recoveryToken); + return res.status(200).json({ + success: true, + msg: "Recovery token is valid", + }); + } catch (error) { + next(error); + } + }; + + resetPassword = async (req: Request, res: Response, next: NextFunction) => { + try { + await newPasswordValidation.validateAsync(req.body); + const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); + return res.status(200).json({ + success: true, + msg: "Password has been reset successfully", + data: { user, token }, + }); + } catch (error) { + next(error); + } + }; + + deleteUser = async (req: Request, res: Response, next: NextFunction) => { + try { + await this.userService.deleteUser(req.user); + return res.status(200).json({ + success: true, + msg: "User deleted successfully", + }); + } catch (error) { + next(error); + } + }; + + getAllUsers = async (req: Request, res: Response, next: NextFunction) => { + try { + const allUsers = await this.userService.getAllUsers(); + return res.status(200).json({ + success: true, + msg: "Users retrieved successfully", + data: allUsers, + }); + } catch (error) { + next(error); + } + }; + + getUserById = async (req: Request, res: Response, next: NextFunction) => { + try { + 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.status(200).json({ success: true, msg: "ok", data: user }); + } catch (error) { + next(error); + } + }; + + editUserById = async (req: Request, res: Response, next: NextFunction) => { + try { + const roles = req?.user?.role; + + if (!roles || !roles.includes("superadmin")) { + throw new AppError({ message: "Unauthorized", status: 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.status(200).json({ success: true, msg: "ok" }); + } catch (error) { + next(error); + } + }; + + editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { + try { + const roles = req?.user?.role; + if (!roles || !roles.includes("superadmin")) { + throw new AppError({ message: "Unauthorized", status: 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.status(200).json({ success: true, msg: "Password reset successfully" }); + } catch (error) { + next(error); + } + }; +} + +export default AuthController; diff --git a/server/src/controllers/baseController.js b/server/src/controllers/baseController.js deleted file mode 100644 index 910cf57b4..000000000 --- a/server/src/controllers/baseController.js +++ /dev/null @@ -1,83 +0,0 @@ -import { AppError } from "../service/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/checkController.js b/server/src/controllers/checkController.js deleted file mode 100755 index 4069df3ff..000000000 --- a/server/src/controllers/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/checkController.ts b/server/src/controllers/checkController.ts new file mode 100755 index 000000000..8277df3e7 --- /dev/null +++ b/server/src/controllers/checkController.ts @@ -0,0 +1,134 @@ +import { Request, Response, NextFunction } from "express"; + +import { + getChecksParamValidation, + getChecksQueryValidation, + getTeamChecksQueryValidation, + deleteChecksParamValidation, + deleteChecksByTeamIdParamValidation, + updateChecksTTLBodyValidation, + ackCheckBodyValidation, + ackAllChecksParamValidation, + ackAllChecksBodyValidation, +} from "@/validation/joi.js"; + +const SERVICE_NAME = "checkController"; + +class CheckController { + static SERVICE_NAME = SERVICE_NAME; + + private checkService: any; + constructor(checkService: any) { + this.checkService = checkService; + } + + get serviceName() { + return CheckController.SERVICE_NAME; + } + + getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { + try { + 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.status(200).json({ + success: true, + msg: "Checks retrieved successfully", + data: result, + }); + } catch (error) { + next(error); + } + }; + + getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { + try { + await getTeamChecksQueryValidation.validateAsync(req.query); + const checkData = await this.checkService.getChecksByTeam({ + teamId: req?.user?.teamId, + query: req?.query, + }); + return res.status(200).json({ + success: true, + msg: "Team checks retrieved successfully", + data: checkData, + }); + } catch (error) { + next(error); + } + }; + + getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId }); + return res.status(200).json({ + success: true, + msg: "Checks summary retrieved successfully", + data: summary, + }); + } catch (error) { + next(error); + } + }; + + deleteChecks = async (req: Request, res: Response, next: NextFunction) => { + try { + await deleteChecksParamValidation.validateAsync(req.params); + + const deletedCount = await this.checkService.deleteChecks({ + monitorId: req.params.monitorId, + teamId: req?.user?.teamId, + }); + + return res.status(200).json({ + success: true, + msg: "Checks deleted successfully", + data: { deletedCount }, + }); + } catch (error) { + next(error); + } + }; + + deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + await deleteChecksByTeamIdParamValidation.validateAsync(req.params); + + const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); + + return res.status(200).json({ + success: true, + msg: "Checks deleted successfully", + data: { deletedCount }, + }); + } catch (error) { + next(error); + } + }; + + updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => { + try { + await updateChecksTTLBodyValidation.validateAsync(req.body); + + await this.checkService.updateChecksTTL({ + teamId: req?.user?.teamId, + ttl: req?.body?.ttl, + }); + + return res.status(200).json({ + success: true, + msg: "Checks TTL updated successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default CheckController; diff --git a/server/src/controllers/controllerUtils.js b/server/src/controllers/controllerUtils.js deleted file mode 100755 index f21ba154b..000000000 --- a/server/src/controllers/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/controllerUtils.ts b/server/src/controllers/controllerUtils.ts new file mode 100755 index 000000000..e6c8767fa --- /dev/null +++ b/server/src/controllers/controllerUtils.ts @@ -0,0 +1,119 @@ +import { AppError } from "@/utils/AppError.js"; +import { type MonitorType, MonitorTypes } from "@/types/index.js"; + +const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => { + 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; +}; +const requireString = (value: unknown, fieldName: string): string => { + if (typeof value === "string" && value.trim().length > 0) { + return value; + } + throw new AppError({ message: `${fieldName} is required`, status: 400 }); +}; + +const optionalString = (value: unknown, fieldName: string): string | undefined => { + if (value === undefined) { + return undefined; + } + if (typeof value === "string") { + return value; + } + throw new AppError({ message: `${fieldName} must be a string`, status: 400 }); +}; + +const optionalNumber = (value: unknown, fieldName: string): number | undefined => { + if (value === undefined) { + return undefined; + } + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + if (!Number.isNaN(parsed)) { + return parsed; + } + } + throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); +}; + +const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { + if (value === undefined) { + return undefined; + } + if (typeof value === "boolean") { + return value; + } + if (typeof value === "string") { + if (value === "true") { + return true; + } + if (value === "false") { + return false; + } + } + throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); +}; + +const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { + const parseSingle = (input: unknown): MonitorType => { + if (typeof input !== "string") { + throw new AppError({ message: "Monitor type must be a string", status: 400 }); + } + if (!MonitorTypes.includes(input as MonitorType)) { + throw new AppError({ message: `Invalid monitor type: ${input}`, status: 400 }); + } + return input as MonitorType; + }; + + if (value === undefined) { + return undefined; + } + if (Array.isArray(value)) { + return value.map((entry) => parseSingle(entry)); + } + return parseSingle(value); +}; + +const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { + if (value === undefined) { + return undefined; + } + if (value === "asc" || value === "desc") { + return value; + } + throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 }); +}; + +const requireTeamId = (teamId?: string): string => { + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + return teamId; +}; + +const requireUserId = (userId?: string): string => { + if (!userId) { + throw new AppError({ message: "User ID is required", status: 400 }); + } + return userId; +}; + +export { + fetchMonitorCertificate, + requireString, + optionalString, + optionalNumber, + optionalBoolean, + parseMonitorTypeFilter, + parseSortOrder, + requireTeamId, + requireUserId, +}; diff --git a/server/src/controllers/diagnosticController.js b/server/src/controllers/diagnosticController.js deleted file mode 100755 index 0723a9f6c..000000000 --- a/server/src/controllers/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/diagnosticController.ts b/server/src/controllers/diagnosticController.ts new file mode 100755 index 000000000..b4d59ceaf --- /dev/null +++ b/server/src/controllers/diagnosticController.ts @@ -0,0 +1,31 @@ +import { Request, Response, NextFunction } from "express"; + +const SERVICE_NAME = "diagnosticController"; +class DiagnosticController { + static SERVICE_NAME = SERVICE_NAME; + + private diagnosticService: any; + + constructor(diagnosticService: any) { + this.diagnosticService = diagnosticService; + } + + get serviceName() { + return DiagnosticController.SERVICE_NAME; + } + + getSystemStats = async (req: Request, res: Response, next: NextFunction) => { + try { + const diagnostics = await this.diagnosticService.getSystemStats(); + return res.status(200).json({ + success: true, + msg: "OK", + data: diagnostics, + }); + } 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..72e6498f9 --- /dev/null +++ b/server/src/controllers/incidentController.ts @@ -0,0 +1,107 @@ +import { AppError } from "@/utils/AppError.js"; +import { Request, Response, NextFunction } from "express"; +import { requireTeamId } from "./controllerUtils.js"; + +const SERVICE_NAME = "incidentController"; + +class IncidentController { + static SERVICE_NAME = SERVICE_NAME; + + private incidentService: any; + constructor(incidentService: any) { + this.incidentService = incidentService; + } + + get serviceName() { + return IncidentController.SERVICE_NAME; + } + + getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { + try { + const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {}; + + const teamId = requireTeamId(req.user?.teamId); + const result = await this.incidentService.getIncidentsByTeam( + teamId, + sortOrder, + dateRange, + page, + rowsPerPage, + status, + monitorId, + resolutionType + ); + + return res.status(200).json({ + success: true, + msg: "Incidents retrieved successfully", + data: result, + }); + } catch (error) { + next(error); + } + }; + + getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req.user?.teamId; + + if (!teamId) { + throw new AppError({ message: "Team ID is required", service: SERVICE_NAME, status: 400 }); + } + + const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10; + + const summary = await this.incidentService.getIncidentSummary(req?.user?.teamId, limit); + + return res.status(200).json({ + success: true, + msg: "Incident summary retrieved successfully", + data: summary, + }); + } catch (error) { + next(error); + } + }; + + getIncidentById = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = requireTeamId(req.user?.teamId); + const incidentId = req.params.incidentId; + if (!incidentId) { + throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 }); + } + + const incident = await this.incidentService.getIncidentById(incidentId, teamId); + + return res.status(200).json({ + success: true, + msg: "Incident retrieved successfully", + data: incident, + }); + } catch (error) { + next(error); + } + }; + + resolveIncidentManually = async (req: Request, res: Response, next: NextFunction) => { + try { + const resolvedIncident = await this.incidentService.resolveIncident( + req?.params?.incidentId, + req?.user?.id, + req?.user?.teamId, + req?.body?.comment + ); + + return res.status(200).json({ + success: true, + msg: "Incident resolved successfully", + data: resolvedIncident, + }); + } catch (error) { + next(error); + } + }; +} + +export default IncidentController; diff --git a/server/src/controllers/inviteController.js b/server/src/controllers/inviteController.js deleted file mode 100755 index a377234c0..000000000 --- a/server/src/controllers/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/inviteController.ts b/server/src/controllers/inviteController.ts new file mode 100755 index 000000000..c3d63aa80 --- /dev/null +++ b/server/src/controllers/inviteController.ts @@ -0,0 +1,68 @@ +import { Request, Response, NextFunction } from "express"; +import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/joi.js"; +const SERVICE_NAME = "inviteController"; + +class InviteController { + static SERVICE_NAME = SERVICE_NAME; + private inviteService: any; + constructor(inviteService: any) { + this.inviteService = inviteService; + } + + get serviceName() { + return InviteController.SERVICE_NAME; + } + + getInviteToken = async (req: Request, res: Response, next: NextFunction) => { + try { + 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.status(200).json({ + success: true, + msg: "Invite token generated successfully", + data: inviteToken, + }); + } catch (error) { + next(error); + } + }; + + sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { + try { + 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.status(200).json({ + success: true, + msg: "Invite issued successfully", + data: inviteToken, + }); + } catch (error) { + next(error); + } + }; + + verifyInviteToken = async (req: Request, res: Response, next: NextFunction) => { + try { + await inviteVerificationBodyValidation.validateAsync(req.body); + const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); + return res.status(200).json({ + success: true, + msg: "Invite verified successfully", + data: invite, + }); + } catch (error) { + next(error); + } + }; +} + +export default InviteController; diff --git a/server/src/controllers/logController.js b/server/src/controllers/logController.js deleted file mode 100644 index 3537246bb..000000000 --- a/server/src/controllers/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/logController.ts b/server/src/controllers/logController.ts new file mode 100644 index 000000000..d99de1e9c --- /dev/null +++ b/server/src/controllers/logController.ts @@ -0,0 +1,28 @@ +import { Request, Response, NextFunction } from "express"; + +const SERVICE_NAME = "LogController"; + +class LogController { + static SERVICE_NAME = SERVICE_NAME; + private logger: any; + constructor(logger: any) { + this.logger = logger; + } + get serviceName() { + return LogController.SERVICE_NAME; + } + + getLogs = async (req: Request, res: Response, next: NextFunction) => { + try { + const logs = await this.logger.getLogs(); + res.status(200).json({ + success: true, + msg: "Logs fetched successfully", + data: logs, + }); + } catch (error) { + next(error); + } + }; +} +export default LogController; diff --git a/server/src/controllers/maintenanceWindowController.js b/server/src/controllers/maintenanceWindowController.js deleted file mode 100755 index 2fdb1ee15..000000000 --- a/server/src/controllers/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/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts new file mode 100644 index 000000000..0cc9610ea --- /dev/null +++ b/server/src/controllers/maintenanceWindowController.ts @@ -0,0 +1,130 @@ +import { Request, Response, NextFunction } from "express"; +import { + createMaintenanceWindowBodyValidation, + editMaintenanceWindowByIdParamValidation, + editMaintenanceByIdWindowBodyValidation, + getMaintenanceWindowByIdParamValidation, + getMaintenanceWindowsByMonitorIdParamValidation, + getMaintenanceWindowsByTeamIdQueryValidation, + deleteMaintenanceWindowByIdParamValidation, +} from "@/validation/joi.js"; +import { requireTeamId } from "@/controllers/controllerUtils.js"; + +const SERVICE_NAME = "maintenanceWindowController"; + +class MaintenanceWindowController { + static SERVICE_NAME = SERVICE_NAME; + private maintenanceWindowService: any; + constructor(maintenanceWindowService: any) { + this.maintenanceWindowService = maintenanceWindowService; + } + + get serviceName() { + return MaintenanceWindowController.SERVICE_NAME; + } + + createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { + try { + await createMaintenanceWindowBodyValidation.validateAsync(req.body); + const teamId = requireTeamId(req?.user?.teamId); + + await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); + + return res.status(200).json({ + success: true, + msg: "Maintenance window created successfully", + }); + } catch (error) { + next(error); + } + }; + getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { + try { + await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); + + const teamId = requireTeamId(req.user?.teamId); + + const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId }); + + return res.status(200).json({ + success: true, + msg: "Maintenance window fetched successfully", + data: maintenanceWindow, + }); + } catch (error) { + next(error); + } + }; + + getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); + + const teamId = requireTeamId(req?.user?.teamId); + + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query }); + + return res.status(200).json({ + success: true, + msg: "Maintenance windows fetched successfully", + data: maintenanceWindows, + }); + } catch (error) { + next(error); + } + }; + + getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { + try { + await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); + + const teamId = requireTeamId(req?.user?.teamId); + + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId }); + + return res.status(200).json({ + success: true, + msg: "Maintenance windows fetched successfully", + data: maintenanceWindows, + }); + } catch (error) { + next(error); + } + }; + deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { + try { + await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); + + const teamId = requireTeamId(req?.user?.teamId); + + await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId }); + + return res.status(200).json({ + success: true, + msg: "Maintenance window deleted successfully", + }); + } catch (error) { + next(error); + } + }; + + editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { + try { + await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); + await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); + + const teamId = requireTeamId(req.user?.teamId); + + const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId }); + return res.status(200).json({ + success: true, + msg: "Maintenance window edited successfully", + data: editedMaintenanceWindow, + }); + } catch (error) { + next(error); + } + }; +} + +export default MaintenanceWindowController; diff --git a/server/src/controllers/monitorController.js b/server/src/controllers/monitorController.js deleted file mode 100755 index 0f40a4a01..000000000 --- a/server/src/controllers/monitorController.js +++ /dev/null @@ -1,457 +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" - ); -} - -export default MonitorController; diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts new file mode 100644 index 000000000..30ceabbc5 --- /dev/null +++ b/server/src/controllers/monitorController.ts @@ -0,0 +1,410 @@ +import { Request, Response, NextFunction } from "express"; + +import { + getMonitorByIdParamValidation, + getMonitorByIdQueryValidation, + getMonitorsByTeamIdParamValidation, + getMonitorsByTeamIdQueryValidation, + getMonitorsWithChecksQueryValidation, + createMonitorBodyValidation, + editMonitorBodyValidation, + pauseMonitorParamValidation, + getCertificateParamValidation, + getHardwareDetailsByIdParamValidation, + getHardwareDetailsByIdQueryValidation, +} from "@/validation/joi.js"; +import sslChecker from "ssl-checker"; +import { + fetchMonitorCertificate, + requireString, + optionalString, + optionalNumber, + optionalBoolean, + parseMonitorTypeFilter, + parseSortOrder, + requireTeamId, +} from "./controllerUtils.js"; +import { AppError } from "@/utils/AppError.js"; +import { IMonitorService } from "@/service/index.js"; + +const SERVICE_NAME = "monitorController"; +class MonitorController { + static SERVICE_NAME = SERVICE_NAME; + + private monitorService: IMonitorService; + + constructor(monitorService: IMonitorService) { + this.monitorService = monitorService; + } + + get serviceName() { + return MonitorController.SERVICE_NAME; + } + + getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { + try { + await getCertificateParamValidation.validateAsync(req.params); + const teamId = requireTeamId(req?.user?.teamId); + const monitorId = requireString(req.params?.monitorId, "Monitor ID"); + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); + const certificate = await fetchMonitorCertificate(sslChecker, monitor); + + return res.status(200).json({ + success: true, + msg: "SSL certificate retrieved successfully", + data: { + certificateDate: new Date(certificate.validTo), + }, + }); + } catch (error) { + next(error); + } + }; + + getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => { + try { + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const normalize = optionalBoolean(req?.query?.normalize, "normalize"); + const teamId = requireTeamId(req?.user?.teamId); + + const data = await this.monitorService.getUptimeDetailsById({ + teamId, + monitorId, + dateRange, + normalize, + }); + return res.status(200).json({ + success: true, + msg: "Uptime details retrieved successfully", + data: data, + }); + } catch (error) { + next(error); + } + }; + + getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { + try { + await getHardwareDetailsByIdParamValidation.validateAsync(req.params); + await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); + + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent"; + const teamId = requireTeamId(req?.user?.teamId); + + const monitor = await this.monitorService.getHardwareDetailsById({ + teamId, + monitorId, + dateRange, + }); + + return res.status(200).json({ + success: true, + msg: "Hardware details retrieved successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { + try { + await getHardwareDetailsByIdParamValidation.validateAsync(req.params); + await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); + + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const teamId = requireTeamId(req?.user?.teamId); + + const data = await this.monitorService.getPageSpeedDetailsById({ + teamId, + monitorId, + dateRange, + }); + + return res.status(200).json({ + success: true, + msg: "Page speed details retrieved successfully", + data, + }); + } catch (error) { + next(error); + } + }; + + getMonitorById = async (req: Request, res: Response, next: NextFunction) => { + try { + await getMonitorByIdParamValidation.validateAsync(req.params); + await getMonitorByIdQueryValidation.validateAsync(req.query); + + const teamId = requireTeamId(req?.user?.teamId); + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); + + return res.status(200).json({ + success: true, + msg: "Monitor retrieved successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + createMonitor = async (req: Request, res: Response, next: NextFunction) => { + try { + await createMonitorBodyValidation.validateAsync(req.body); + + const userId = requireString(req?.user?.id, "User ID"); + const teamId = requireTeamId(req?.user?.teamId); + + const monitor = await this.monitorService.createMonitor(teamId, userId, req.body); + + return res.status(200).json({ + success: true, + msg: "Monitor created successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = req.user?.id; + if (!userId) { + throw new AppError({ message: "User ID is required", status: 400 }); + } + + const teamId = requireTeamId(req?.user?.teamId); + + const data = req.body; + const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId); + + return res.status(200).json({ + success: true, + msg: "Bulk monitors created successfully", + data: monitors, + }); + } catch (error) { + next(error); + } + }; + + deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { + try { + await getMonitorByIdParamValidation.validateAsync(req.params); + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const teamId = requireTeamId(req?.user?.teamId); + + const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); + + return res.status(200).json({ + success: true, + msg: "Monitor deleted successfully", + data: deletedMonitor, + }); + } catch (error) { + next(error); + } + }; + + deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = requireTeamId(req?.user?.teamId); + + const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); + + return res.status(200).json({ + success: true, + msg: `Deleted ${deletedCount} monitors`, + }); + } catch (error) { + next(error); + } + }; + + editMonitor = async (req: Request, res: Response, next: NextFunction) => { + try { + await getMonitorByIdParamValidation.validateAsync(req.params); + await editMonitorBodyValidation.validateAsync(req.body); + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const teamId = requireTeamId(req?.user?.teamId); + + const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); + + return res.status(200).json({ + success: true, + msg: "Monitor edited successfully", + data: editedMonitor, + }); + } catch (error) { + next(error); + } + }; + + pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { + try { + await pauseMonitorParamValidation.validateAsync(req.params); + + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const teamId = requireTeamId(req?.user?.teamId); + + const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); + + return res.status(200).json({ + success: true, + msg: monitor.isActive ? "Monitor resumed successfully" : "Monitor paused successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { + try { + const id = requireString(req?.user?.id, "User ID"); + const teamId = requireTeamId(req?.user?.teamId); + const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); + + return res.status(200).json({ + success: true, + msg: "Demo monitors added successfully", + data: demoMonitors?.length ?? 0, + }); + } catch (error) { + next(error); + } + }; + + sendTestEmail = async (req: Request, res: Response, next: NextFunction) => { + try { + const { to } = req.body; + if (!to || typeof to !== "string") { + throw new AppError({ message: "Invalid 'to' email address", status: 400 }); + } + + const messageId = await this.monitorService.sendTestEmail({ to }); + return res.status(200).json({ + success: true, + msg: "Test email sent successfully", + data: { messageId }, + }); + } catch (error) { + next(error); + } + }; + + getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + await getMonitorsByTeamIdParamValidation.validateAsync(req.params); + await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); + + const teamId = requireTeamId(req?.user?.teamId); + const type = parseMonitorTypeFilter(req.query?.type); + const filter = optionalString(req.query?.filter, "filter"); + + const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); + + return res.status(200).json({ + success: true, + msg: "Monitors retrieved successfully", + data: monitors, + }); + } catch (error) { + next(error); + } + }; + + getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + await getMonitorsByTeamIdParamValidation.validateAsync(req.params); + await getMonitorsWithChecksQueryValidation.validateAsync(req.query); + + const explain = optionalBoolean(req?.query?.explain, "explain"); + const limit = optionalNumber(req?.query?.limit, "limit"); + const page = optionalNumber(req?.query?.page, "page"); + const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage"); + const filter = optionalString(req?.query?.filter, "filter"); + const field = optionalString(req?.query?.field, "field"); + const order = parseSortOrder(req?.query?.order); + const type = parseMonitorTypeFilter(req?.query?.type); + const teamId = requireTeamId(req?.user?.teamId); + + const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ + teamId, + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + explain, + }); + + return res.status(200).json({ + msg: "Monitors retrieved successfully", + data: monitors, + }); + } catch (error) { + next(error); + } + }; + + exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const json = await this.monitorService.exportMonitorsToJSON({ teamId }); + + return res.status(200).json({ + success: true, + msg: "Monitors exported successfully", + data: json, + }); + } catch (error) { + next(error); + } + }; + + getAllGames = async (req: Request, res: Response, next: NextFunction) => { + try { + const games = this.monitorService.getAllGames(); + return res.status(200).json({ + success: true, + msg: "Supported games retrieved successfully", + data: games, + }); + } catch (error) { + next(error); + } + }; + + getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const groups = await this.monitorService.getGroupsByTeamId({ teamId }); + + return res.status(200).json({ + msg: "Groups retrieved successfully", + data: groups, + }); + } catch (error) { + next(error); + } + }; +} + +export default MonitorController; diff --git a/server/src/controllers/notificationController.js b/server/src/controllers/notificationController.js deleted file mode 100755 index e00dd5b6f..000000000 --- a/server/src/controllers/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/notificationController.ts b/server/src/controllers/notificationController.ts new file mode 100644 index 000000000..1aa10a82c --- /dev/null +++ b/server/src/controllers/notificationController.ts @@ -0,0 +1,188 @@ +import { Request, Response, NextFunction } from "express"; + +import { Notification } from "@/types/index.js"; +import { createNotificationBodyValidation } from "@/validation/joi.js"; +import { AppError } from "@/utils/AppError.js"; +import { IMonitorsRepository } from "@/repositories/index.js"; +import { INotificationsService } from "@/service/index.js"; +import { requireTeamId } from "./controllerUtils.js"; + +const SERVICE_NAME = "NotificationController"; + +class NotificationController { + static SERVICE_NAME = SERVICE_NAME; + private notificationsService: INotificationsService; + private monitorsRepository: IMonitorsRepository; + constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { + this.notificationsService = notificationsService; + this.monitorsRepository = monitorsRepository; + } + + get serviceName() { + return NotificationController.SERVICE_NAME; + } + + testNotification = async (req: Request, res: Response, next: NextFunction) => { + try { + const notification: Notification = req.body; + const success = await this.notificationsService.sendTestNotification(notification); + + if (!success) { + throw new AppError({ message: "Sending notification failed", status: 500 }); + } + + return res.status(200).json({ + success: true, + msg: "Notification sent successfully", + }); + } catch (error) { + next(error); + } + }; + + createNotification = async (req: Request, res: Response, next: NextFunction) => { + try { + await createNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + }); + + const body = req.body; + + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const userId = req?.user?.id; + if (!userId) { + throw new AppError({ message: "User ID is required", status: 400 }); + } + body.userId = userId; + body.teamId = teamId; + + const notification = await this.notificationsService.createNotification(body); + return res.status(200).json({ + success: true, + msg: "Notification created successfully", + data: notification, + }); + } catch (error) { + next(error); + } + }; + + getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); + + return res.status(200).json({ + success: true, + msg: "Notifications fetched successfully", + data: notifications, + }); + } catch (error) { + next(error); + } + }; + + deleteNotification = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); + } + + await this.notificationsService.deleteById(notificationId, teamId); + return res.status(200).json({ + success: true, + msg: "Notification deleted successfully", + }); + } catch (error) { + next(error); + } + }; + + getNotificationById = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = requireTeamId(req.user?.teamId); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); + } + + const notification = await this.notificationsService.findById(notificationId, teamId); + + return res.status(200).json({ + success: true, + msg: "Notification fetched successfully", + data: notification, + }); + } catch (error) { + next(error); + } + }; + + editNotification = async (req: Request, res: Response, next: NextFunction) => { + try { + await createNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + }); + + const teamId = requireTeamId(req.user?.teamId); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); + } + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body); + return res.status(200).json({ + success: true, + msg: "Notification updated successfully", + data: editedNotification, + }); + } catch (error) { + next(error); + } + }; + + testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { + try { + const monitorId = req.body.monitorId; + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + + const notifications = monitor.notifications; + if (notifications.length === 0) { + throw new AppError({ message: "No notifications", status: 400 }); + } + + const result = await this.notificationsService.testAllNotifications(notifications); + + if (!result) { + throw new AppError({ message: "Failed to send all notifications", status: 500 }); + } + + return res.status(200).json({ + success: true, + msg: "All notifications sent successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default NotificationController; diff --git a/server/src/controllers/queueController.js b/server/src/controllers/queueController.js deleted file mode 100755 index d2c3fd06f..000000000 --- a/server/src/controllers/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/queueController.ts b/server/src/controllers/queueController.ts new file mode 100755 index 000000000..9885bab87 --- /dev/null +++ b/server/src/controllers/queueController.ts @@ -0,0 +1,94 @@ +import { Request, Response, NextFunction } from "express"; + +const SERVICE_NAME = "JobQueueController"; + +class JobQueueController { + static SERVICE_NAME = SERVICE_NAME; + private jobQueue: any; + constructor(jobQueue: any) { + this.jobQueue = jobQueue; + } + + get serviceName() { + return JobQueueController.SERVICE_NAME; + } + + getMetrics = async (req: Request, res: Response, next: NextFunction) => { + try { + const metrics = await this.jobQueue.getMetrics(); + res.status(200).json({ + success: true, + msg: "Queue metrics fetched successfully", + data: metrics, + }); + } catch (error) { + next(error); + } + }; + + getJobs = async (req: Request, res: Response, next: NextFunction) => { + try { + const jobs = await this.jobQueue.getJobs(); + return res.status(200).json({ + success: true, + msg: "Queue jobs fetched successfully", + data: jobs, + }); + } catch (error) { + next(error); + } + }; + + getAllMetrics = async (req: Request, res: Response, next: NextFunction) => { + try { + const jobs = await this.jobQueue.getJobs(); + const metrics = await this.jobQueue.getMetrics(); + return res.status(200).json({ + success: true, + msg: "Queue metrics fetched successfully", + data: { jobs, metrics }, + }); + } catch (error) { + next(error); + } + }; + + addJob = async (req: Request, res: Response, next: NextFunction) => { + try { + await this.jobQueue.addJob(Math.random().toString(36).substring(7)); + return res.status(200).json({ + success: true, + msg: "Job added to queue successfully", + }); + } catch (error) { + next(error); + } + }; + + flushQueue = async (req: Request, res: Response, next: NextFunction) => { + try { + const result = await this.jobQueue.flushQueues(); + return res.status(200).json({ + success: true, + msg: "Queue flushed successfully", + data: result, + }); + } catch (error) { + next(error); + } + }; + + checkQueueHealth = async (req: Request, res: Response, next: NextFunction) => { + try { + const stuckQueues = await this.jobQueue.checkQueueHealth(); + return res.status(200).json({ + success: true, + msg: "Queue health checked successfully", + data: stuckQueues, + }); + } catch (error) { + next(error); + } + }; +} +export default JobQueueController; diff --git a/server/src/controllers/settingsController.js b/server/src/controllers/settingsController.ts old mode 100755 new mode 100644 similarity index 57% rename from server/src/controllers/settingsController.js rename to server/src/controllers/settingsController.ts index 7da724d23..0a01daa0c --- a/server/src/controllers/settingsController.js +++ b/server/src/controllers/settingsController.ts @@ -1,28 +1,32 @@ -import { updateAppSettingsBodyValidation } from "../validation/joi.js"; -import { sendTestEmailBodyValidation } from "../validation/joi.js"; -import BaseController from "./baseController.js"; +import { Request, Response, NextFunction } from "express"; +import { sendTestEmailBodyValidation, updateAppSettingsBodyValidation } from "@/validation/joi.js"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "SettingsController"; -class SettingsController extends BaseController { +class SettingsController { static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { settingsService, emailService }) { - super(commonDependencies); + private settingsService: any; + private emailService: any; + private db: any; + constructor(settingsService: any, emailService: any, db: any) { this.settingsService = settingsService; this.emailService = emailService; + this.db = db; } get serviceName() { return SettingsController.SERVICE_NAME; } - buildAppSettings = (dbSettings) => { + buildAppSettings = (dbSettings: any) => { const sanitizedSettings = { ...dbSettings }; delete sanitizedSettings.version; - + delete sanitizedSettings.jwtSecret; const returnSettings = { pagespeedKeySet: false, emailPasswordSet: false, + settings: null, }; if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") { @@ -37,37 +41,35 @@ class SettingsController extends BaseController { return returnSettings; }; - getAppSettings = this.asyncHandler( - async (req, res) => { + getAppSettings = async (req: Request, res: Response, next: NextFunction) => { + try { const dbSettings = await this.settingsService.getDBSettings(); const returnSettings = this.buildAppSettings(dbSettings); - return res.success({ - msg: this.stringService.getAppSettings, + return res.status(200).json({ + success: true, + msg: "App settings fetched successfully", data: returnSettings, }); - }, - SERVICE_NAME, - "getAppSettings" - ); + } catch (error) { + next(error); + } + }; - updateAppSettings = this.asyncHandler( - async (req, res) => { - await updateAppSettingsBodyValidation.validateAsync(req.body); + updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { + 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" - ); + const updatedSettings = await this.settingsService.updateDbSettings(req.body); + const returnSettings = this.buildAppSettings(updatedSettings); + return res.status(200).json({ + success: true, + msg: "App settings updated successfully", + data: returnSettings, + }); + }; - sendTestEmail = this.asyncHandler( - async (req, res) => { + sendTestEmail = async (req: Request, res: Response, next: NextFunction) => { + try { await sendTestEmailBodyValidation.validateAsync(req.body); const { @@ -86,7 +88,7 @@ class SettingsController extends BaseController { systemEmailTLSServername, } = req.body; - const subject = this.stringService.testEmailSubject; + const subject = "This is a test email from Checkmate"; const context = { testName: "Monitoring System" }; const html = await this.emailService.buildEmail("testEmailTemplate", context); @@ -106,17 +108,18 @@ class SettingsController extends BaseController { }); if (!messageId) { - throw this.errorService.createServerError("Failed to send test email."); + throw new AppError({ message: "Failed to send test email.", status: 500 }); } - return res.success({ - msg: this.stringService.sendTestEmail, + return res.status(200).json({ + success: true, + msg: "Test email sent successfully", data: { messageId }, }); - }, - SERVICE_NAME, - "sendTestEmail" - ); + } catch (error) { + next(error); + } + }; } export default SettingsController; diff --git a/server/src/controllers/statusPageController.js b/server/src/controllers/statusPageController.js deleted file mode 100755 index a46688590..000000000 --- a/server/src/controllers/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/statusPageController.ts b/server/src/controllers/statusPageController.ts new file mode 100644 index 000000000..3501a6592 --- /dev/null +++ b/server/src/controllers/statusPageController.ts @@ -0,0 +1,136 @@ +import { Request, Response, NextFunction } from "express"; + +import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "@/validation/joi.js"; +import { AppError } from "@/utils/AppError.js"; +import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js"; +import { IStatusPageService } from "@/service/business/statusPageService.js"; +import { IMonitorsRepository } from "@/repositories/index.js"; +import { ISettingsService } from "@/service/system/settingsService.js"; +import { ParseBoolean } from "@/utils/utils.js"; +import { MonitorWithChecks } from "@/types/index.js"; +import { NormalizeData } from "@/utils/dataUtils.js"; + +const SERVICE_NAME = "statusPageController"; + +class StatusPageController { + static SERVICE_NAME = SERVICE_NAME; + private statusPageService: IStatusPageService; + private monitorsRepository: IMonitorsRepository; + private settingsService: ISettingsService; + constructor(statusPageService: IStatusPageService, monitorsRepository: IMonitorsRepository, settingsService: ISettingsService) { + this.statusPageService = statusPageService; + this.monitorsRepository = monitorsRepository; + this.settingsService = settingsService; + } + + get serviceName() { + return StatusPageController.SERVICE_NAME; + } + + createStatusPage = async (req: Request, res: Response, next: NextFunction) => { + try { + await createStatusPageBodyValidation.validateAsync(req.body); + await imageValidation.validateAsync(req.file); + + const teamId = requireTeamId(req?.user?.teamId); + const userId = requireUserId(req?.user?.id); + const statusPage = await this.statusPageService.createStatusPage(userId, teamId, req.file, req.body); + + return res.status(200).json({ + success: true, + msg: "Status page created successfully", + data: statusPage, + }); + } catch (error) { + next(error); + } + }; + + updateStatusPage = async (req: Request, res: Response, next: NextFunction) => { + try { + await createStatusPageBodyValidation.validateAsync(req.body); + await imageValidation.validateAsync(req.file); + const teamId = requireTeamId(req?.user?.teamId); + const statusPageId = req.params.id; + if (!statusPageId) { + throw new AppError({ message: "Status page ID is required", status: 400 }); + } + const statusPage = await this.statusPageService.updateStatusPage(statusPageId, teamId, req.file, req.body); + if (statusPage === null) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + return res.status(200).json({ + success: true, + msg: "Status page updated successfully", + data: statusPage, + }); + } catch (error) { + next(error); + } + }; + + getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => { + try { + await getStatusPageParamValidation.validateAsync(req.params); + await getStatusPageQueryValidation.validateAsync(req.query); + + if (!req.params.url) { + throw new AppError({ message: "Status page URL is required", status: 400 }); + } + + const statusPage = await this.statusPageService.getStatusPageByUrl(req.params.url); + const settings = await this.settingsService.getDBSettings(); + const showURL = settings.showURL; + + const monitors = await this.monitorsRepository.findByIdsWithChecks(statusPage.monitors); + const normalizedMonitors = monitors.map((monitor) => { + const normalizedChecks = NormalizeData(monitor.checks, 10, 100); + if (!showURL) { + const { url, port, secret, notifications, ...rest } = monitor; + return { ...rest, checks: normalizedChecks }; + } + return { ...monitor, checks: normalizedChecks }; + }); + return res.status(200).json({ + success: true, + msg: "Status page retrieved successfully", + data: { statusPage, monitors: normalizedMonitors }, + }); + } catch (error) { + next(error); + } + }; + getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = requireTeamId(req.user?.teamId); + const statusPages = await this.statusPageService.getStatusPagesByTeamId(teamId); + + return res.status(200).json({ + success: true, + msg: "Status pages retrieved successfully", + data: statusPages, + }); + } catch (error) { + next(error); + } + }; + + deleteStatusPage = async (req: Request, res: Response, next: NextFunction) => { + try { + const statusPageId = req.params.id; + if (!statusPageId) { + throw new AppError({ message: "Status page ID is required", status: 400 }); + } + const teamId = requireTeamId(req.user?.teamId); + await this.statusPageService.deleteStatusPage(statusPageId, teamId); + return res.status(200).json({ + success: true, + msg: "Status page deleted successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default StatusPageController; diff --git a/server/src/db/mongo/MongoDB.js b/server/src/db/MongoDB.ts similarity index 53% rename from server/src/db/mongo/MongoDB.js rename to server/src/db/MongoDB.ts index 74fcbd89f..d62f4bbad 100755 --- a/server/src/db/mongo/MongoDB.js +++ b/server/src/db/MongoDB.ts @@ -1,39 +1,18 @@ import mongoose from "mongoose"; -import AppSettings from "../models/AppSettings.js"; +import AppSettings from "./models/AppSettings.js"; import { runMigrations } from "./migration/index.js"; +import { ILogger } from "@/utils/logger.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; +const SERVICE_NAME = "MongoDB"; class MongoDB { - static SERVICE_NAME = "MongoDB"; + static SERVICE_NAME = SERVICE_NAME; - constructor({ - logger, - envSettings, - checkModule, - inviteModule, - statusPageModule, - userModule, - hardwareCheckModule, - maintenanceWindowModule, - monitorModule, - networkCheckModule, - notificationModule, - pageSpeedCheckModule, - recoveryModule, - settingsModule, - }) { + private logger: ILogger; + private envSettings: EnvConfig; + + constructor(logger: ILogger, envSettings: EnvConfig) { 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() { @@ -62,15 +41,15 @@ class MongoDB { this.logger.info({ message: "Connected to MongoDB", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "connect", }); await runMigrations(); - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "connect", stack: error.stack, }); @@ -80,14 +59,14 @@ class MongoDB { disconnect = async () => { try { - this.logger.info({ message: "Disconnecting from MongoDB" }); + this.logger.info({ message: "Disconnecting from MongoDB", service: SERVICE_NAME, method: "disconnect" }); await mongoose.disconnect(); - this.logger.info({ message: "Disconnected from MongoDB" }); + this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" }); return; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "disconnect", stack: error.stack, }); diff --git a/server/src/db/migration/0001_migrateStatusWindowThreshold.ts b/server/src/db/migration/0001_migrateStatusWindowThreshold.ts new file mode 100644 index 000000000..d9dd12aa1 --- /dev/null +++ b/server/src/db/migration/0001_migrateStatusWindowThreshold.ts @@ -0,0 +1,10 @@ +import Monitor from "@/db/models/Monitor.js"; +async function migrateStatusWindowThreshold() { + const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } }); + for (const monitor of monitors) { + monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100; + await monitor.save(); + } +} + +export { migrateStatusWindowThreshold }; diff --git a/server/src/db/migration/0002_convertChecksToTimeSeries.ts b/server/src/db/migration/0002_convertChecksToTimeSeries.ts new file mode 100644 index 000000000..222d210b0 --- /dev/null +++ b/server/src/db/migration/0002_convertChecksToTimeSeries.ts @@ -0,0 +1,184 @@ +import mongoose from "mongoose"; +import { BulkWriteResult } from "mongodb"; + +const CHECKS_COLLECTION = "checks"; +const BACKUP_COLLECTION = "checks_backup"; +const FAILED_DOCS_COLLECTION = "checks_migration_failed"; +const BATCH_SIZE = 1000; + +interface MigrationStats { + totalSource: number; + totalMigrated: number; + totalFailed: number; +} + +const getDb = () => { + const db = mongoose.connection.db; + if (!db) { + throw new Error("Database connection is not initialized"); + } + return db; +}; + +const restoreFromBackup = async () => { + const db = getDb(); + const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); + if (backupExists.length === 0) { + throw new Error("Cannot restore: backup collection does not exist"); + } + + const checksExists = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (checksExists.length > 0) { + await db.collection(CHECKS_COLLECTION).drop(); + } + + await db.collection(BACKUP_COLLECTION).rename(CHECKS_COLLECTION); +}; + +const backupAndDropExistingCollection = async () => { + const db = getDb(); + const collections = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (collections.length === 0) { + return false; + } + const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); + if (backupExists.length > 0) { + throw new Error(`Backup collection "${BACKUP_COLLECTION}" already exists. ` + `Please remove it manually before running migration.`); + } + + await db + .collection(CHECKS_COLLECTION) + .aggregate([{ $match: {} }, { $out: BACKUP_COLLECTION }]) + .toArray(); + await db.collection(CHECKS_COLLECTION).drop(); + return true; +}; + +const createTimeSeriesCollection = async (backedUp: boolean) => { + const db = getDb(); + try { + const existing = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (existing.length === 0) { + await db.createCollection(CHECKS_COLLECTION, { + timeseries: { + timeField: "createdAt", + metaField: "metadata", + granularity: "seconds", + }, + }); + } + + await db + .collection(CHECKS_COLLECTION) + .createIndexes([ + { key: { createdAt: 1 } }, + { key: { "metadata.monitorId": 1, createdAt: 1 } }, + { key: { "metadata.monitorId": 1, createdAt: -1 } }, + { key: { "metadata.teamId": 1, createdAt: -1 } }, + { key: { "metadata.monitorId": 1, "metadata.type": 1, createdAt: -1 } }, + { key: { "metadata.teamId": 1, status: 1, createdAt: -1 } }, + ]); + } catch (error) { + if (backedUp) { + await restoreFromBackup(); + } + throw error; + } +}; + +const validateDocument = (doc: any): boolean => { + if (!doc.createdAt) { + return false; + } + if (!doc.monitorId || !doc.teamId || !doc.type) { + return false; + } + return true; +}; + +const storeFailedDocument = async (doc: any, reason: string) => { + const db = getDb(); + await db.collection(FAILED_DOCS_COLLECTION).insertOne({ + originalDoc: doc, + reason, + failedAt: new Date(), + }); +}; + +const migrateBackupData = async (backedUp: boolean): Promise => { + const db = getDb(); + const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); + if (backupExists.length === 0) { + return { totalSource: 0, totalMigrated: 0, totalFailed: 0 }; + } + + const stats: MigrationStats = { + totalSource: 0, + totalMigrated: 0, + totalFailed: 0, + }; + + try { + const source = db.collection(BACKUP_COLLECTION); + const target = db.collection(CHECKS_COLLECTION); + + stats.totalSource = await source.countDocuments(); + + const cursor = source.find().addCursorFlag("noCursorTimeout", true); + const operations: any[] = []; + const invalidDocs: any[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) { + continue; + } + + if (!validateDocument(doc)) { + invalidDocs.push(doc); + continue; + } + + const { _id, monitorId, teamId, type, ...rest } = doc; + const metadata = { monitorId, teamId, type }; + operations.push({ insertOne: { document: { ...rest, metadata } } }); + + if (operations.length >= BATCH_SIZE) { + const result: BulkWriteResult = await target.bulkWrite(operations, { ordered: false }); + stats.totalMigrated += result.insertedCount; + operations.length = 0; + } + } + + if (operations.length) { + const result: BulkWriteResult = await target.bulkWrite(operations, { ordered: false }); + stats.totalMigrated += result.insertedCount; + } + + for (const doc of invalidDocs) { + await storeFailedDocument(doc, "Missing required fields (createdAt, monitorId, teamId, or type)"); + stats.totalFailed++; + } + + await cursor.close(); + + return stats; + } catch (error) { + if (backedUp) { + await restoreFromBackup(); + } + throw error; + } +}; + +export const convertChecksToTimeSeries = async () => { + const backedUp = await backupAndDropExistingCollection(); + await createTimeSeriesCollection(backedUp); + if (backedUp) { + const stats = await migrateBackupData(backedUp); + console.log( + `Migration completed: ${stats.totalMigrated} documents migrated, ` + + `${stats.totalFailed} documents failed (stored in ${FAILED_DOCS_COLLECTION})` + ); + } +}; diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts new file mode 100644 index 000000000..c8e9e7c75 --- /dev/null +++ b/server/src/db/migration/index.ts @@ -0,0 +1,47 @@ +import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; +import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; +import MigrationModel from "../models/Migration.js"; + +type MigrationEntry = { + name: string; + execute: () => Promise; +}; + +const migrations: MigrationEntry[] = [ + { name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold }, + { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, +]; + +const runMigrations = async (logger?: { info: Function; error: Function }) => { + try { + logger?.info({ message: "Running migrations", service: "Migrations" }); + for (const migration of migrations) { + const exists = await MigrationModel.findOne({ name: migration.name, status: "completed" }); + if (exists) { + logger?.info({ message: `Skipping ${migration.name}`, service: "Migrations" }); + continue; + } + + try { + await migration.execute(); + await MigrationModel.findOneAndUpdate( + { name: migration.name }, + { status: "completed", completedAt: new Date(), error: undefined }, + { upsert: true } + ); + logger?.info({ message: `Completed ${migration.name}`, service: "Migrations" }); + } catch (error) { + const err = error as Error; + await MigrationModel.findOneAndUpdate({ name: migration.name }, { status: "failed", error: err?.message }, { upsert: true }); + throw error; + } + } + logger?.info({ message: "Migrations completed", service: "Migrations" }); + } catch (error) { + const err = error as Error; + logger?.error({ message: "Migration failed", service: "Migrations", details: err?.message, stack: err?.stack }); + throw error; + } +}; + +export { runMigrations }; diff --git a/server/src/db/models/AppSettings.js b/server/src/db/models/AppSettings.js deleted file mode 100755 index 2313d638c..000000000 --- a/server/src/db/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/models/AppSettings.ts b/server/src/db/models/AppSettings.ts new file mode 100644 index 000000000..9eeaa640a --- /dev/null +++ b/server/src/db/models/AppSettings.ts @@ -0,0 +1,50 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Settings, SettingsThresholds } from "@/types/settings.js"; + +interface AppSettingsDocument extends Omit { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const thresholdsSchema = new Schema( + { + cpu: { type: Number }, + memory: { type: Number }, + disk: { type: Number }, + temperature: { type: Number }, + }, + { _id: false } +); + +const AppSettingsSchema = new Schema( + { + checkTTL: { type: Number, default: 30 }, + language: { type: String, default: "gb" }, + jwtSecret: { type: String }, + 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 }, + showURL: { type: Boolean, default: false }, + singleton: { type: Boolean, required: true, unique: true, default: true }, + version: { type: Number, default: 1 }, + globalThresholds: { type: thresholdsSchema }, + }, + { timestamps: true } +); + +const AppSettingsModel = model("AppSettings", AppSettingsSchema); + +export type { AppSettingsDocument }; +export { AppSettingsModel }; +export default AppSettingsModel; diff --git a/server/src/db/models/Check.js b/server/src/db/models/Check.js deleted file mode 100755 index 789cebaa5..000000000 --- a/server/src/db/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/models/Check.ts b/server/src/db/models/Check.ts new file mode 100644 index 000000000..4d9c499de --- /dev/null +++ b/server/src/db/models/Check.ts @@ -0,0 +1,298 @@ +import { Schema, model, Types } from "mongoose"; +import { MonitorTypes, type MonitorType } from "@/types/monitor.js"; +import type { + Check, + CheckAudits, + CheckCaptureInfo, + CheckCpuInfo, + CheckDiskInfo, + CheckErrorInfo, + CheckHostInfo, + CheckMemoryInfo, + CheckMetadata, + CheckNetworkInterfaceInfo, + GotTimings, + ILighthouseAudit, +} from "@/types/check.js"; + +type CheckMetadataDocument = Omit & { + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + type: MonitorType; +}; + +type CheckDocumentBase = Omit & { + metadata: CheckMetadataDocument; + expiry: Date; + ackAt?: Date | null; + createdAt: Date; + updatedAt: Date; + __v: number; +}; + +interface CheckDocument extends CheckDocumentBase { + _id: Types.ObjectId; +} + +const timingPhasesSchema = new Schema( + { + wait: { type: Number, default: 0 }, + dns: { type: Number, default: 0 }, + tcp: { type: Number, default: 0 }, + tls: { type: Number, default: 0 }, + request: { type: Number, default: 0 }, + firstByte: { type: Number, default: 0 }, + download: { type: Number, default: 0 }, + total: { type: Number, default: 0 }, + }, + { _id: false } +); + +const timingsSchema = new Schema( + { + start: { type: Number, default: 0 }, + socket: { type: Number, default: 0 }, + lookup: { type: Number, default: 0 }, + connect: { type: Number, default: 0 }, + secureConnect: { type: Number, default: 0 }, + upload: { type: Number, default: 0 }, + response: { type: Number, default: 0 }, + end: { type: Number, default: 0 }, + phases: { + type: timingPhasesSchema, + default: () => ({}), + }, + }, + { _id: false } +); + +const cpuSchema = new 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 }, + }, + { _id: false } +); + +const memorySchema = new 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 }, + }, + { _id: false } +); + +const diskSchema = new Schema( + { + device: { type: String, default: "" }, + mountpoint: { type: String, default: "" }, + 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 }, + }, + { _id: false } +); + +const hostSchema = new Schema( + { + os: { type: String, default: "" }, + platform: { type: String, default: "" }, + kernel_version: { type: String, default: "" }, + }, + { _id: false } +); + +const errorSchema = new Schema( + { + metric: { type: [String], default: [] }, + err: { type: String, default: "" }, + }, + { _id: false } +); + +const captureSchema = new Schema( + { + version: { type: String, default: "" }, + mode: { type: String, default: "" }, + }, + { _id: false } +); + +const networkInterfaceSchema = new Schema( + { + name: { type: String, default: "" }, + 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 }, + }, + { _id: false } +); + +const lighthouseAuditSchema = new Schema( + { + id: { type: String }, + title: { type: String }, + score: { type: Number }, + displayValue: { type: String }, + numericValue: { type: Number }, + numericUnit: { type: String }, + }, + { _id: false } +); + +const auditsSchema = new Schema( + { + cls: { type: lighthouseAuditSchema, default: undefined }, + si: { type: lighthouseAuditSchema, default: undefined }, + fcp: { type: lighthouseAuditSchema, default: undefined }, + lcp: { type: lighthouseAuditSchema, default: undefined }, + tbt: { type: lighthouseAuditSchema, default: undefined }, + }, + { _id: false } +); + +const metadataSchema = new Schema( + { + monitorId: { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + immutable: true, + index: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + required: true, + immutable: true, + index: true, + }, + type: { + type: String, + enum: MonitorTypes, + required: true, + index: true, + }, + }, + { _id: false } +); + +const CheckSchema = new Schema( + { + metadata: { + type: metadataSchema, + required: true, + }, + status: { + type: Boolean, + index: true, + }, + responseTime: { + type: Number, + }, + timings: { + type: timingsSchema, + default: undefined, + }, + statusCode: { + type: Number, + index: true, + }, + message: { + type: String, + }, + expiry: { + type: Date, + default: Date.now, + }, + ack: { + type: Boolean, + default: false, + }, + ackAt: { + type: Date, + default: undefined, + }, + 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: () => [], + }, + accessibility: { + type: Number, + }, + bestPractices: { + type: Number, + }, + seo: { + type: Number, + }, + performance: { + type: Number, + }, + audits: { + type: auditsSchema, + default: undefined, + }, + }, + { + timestamps: true, + strict: false, + timeseries: { + timeField: "createdAt", + metaField: "metadata", + granularity: "seconds", + }, + } +); + +CheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 }); +CheckSchema.index({ "metadata.monitorId": 1, createdAt: 1 }); +CheckSchema.index({ createdAt: 1 }); +CheckSchema.index({ "metadata.teamId": 1, createdAt: -1 }); +CheckSchema.index({ "metadata.monitorId": 1, "metadata.type": 1, createdAt: -1 }); +CheckSchema.index({ "metadata.teamId": 1, status: 1, createdAt: -1 }); + +const CheckModel = model("Check", CheckSchema); + +export type { CheckDocument, CheckMetadataDocument }; +export { CheckModel }; +export default CheckModel; diff --git a/server/src/db/models/Incident.ts b/server/src/db/models/Incident.ts new file mode 100644 index 000000000..8de1c7c2c --- /dev/null +++ b/server/src/db/models/Incident.ts @@ -0,0 +1,86 @@ +import { Schema, model, type Types } from "mongoose"; +import { IncidentResolutionTypes, type Incident } from "@/types/incident.js"; + +type IncidentDocumentBase = Omit & { + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + resolvedBy?: Types.ObjectId | null; + startTime: Date; + endTime: Date | null; + createdAt: Date; + updatedAt: Date; +}; + +export interface IncidentDocument extends IncidentDocumentBase { + _id: Types.ObjectId; +} + +const IncidentSchema = new Schema( + { + monitorId: { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + immutable: true, + index: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + required: true, + immutable: true, + index: true, + }, + startTime: { + type: Date, + immutable: true, + required: true, + }, + endTime: { + type: Date, + default: null, + }, + status: { + type: Boolean, + default: true, + index: true, + }, + message: { + type: String, + default: null, + }, + statusCode: { + type: Number, + default: null, + index: true, + }, + resolutionType: { + type: String, + enum: IncidentResolutionTypes, + default: null, + }, + resolvedBy: { + type: Schema.Types.ObjectId, + ref: "User", + default: null, + }, + comment: { + type: String, + default: null, + }, + }, + { timestamps: true } +); + +IncidentSchema.index({ monitorId: 1, status: 1 }); +IncidentSchema.index({ teamId: 1, status: 1 }); +IncidentSchema.index({ teamId: 1, startTime: -1 }); +IncidentSchema.index({ status: 1, startTime: -1 }); +IncidentSchema.index({ resolutionType: 1, status: 1 }); +IncidentSchema.index({ resolvedBy: 1, status: 1 }); +IncidentSchema.index({ createdAt: -1 }); + +const IncidentModel = model("Incident", IncidentSchema); + +export { IncidentModel }; +export default IncidentModel; diff --git a/server/src/db/models/Invite.ts b/server/src/db/models/Invite.ts new file mode 100755 index 000000000..25ddd9a72 --- /dev/null +++ b/server/src/db/models/Invite.ts @@ -0,0 +1,30 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Invite } from "@/types/invite.js"; + +type InviteDocumentBase = Omit & { + teamId: Types.ObjectId; + expiry: Date; +}; + +interface InviteDocument extends InviteDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const InviteSchema = new Schema( + { + email: { type: String, required: true, unique: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", immutable: true, required: true }, + role: { type: [String], required: true, default: ["user"] }, + token: { type: String, required: true }, + expiry: { type: Date, default: Date.now, expires: 3600 }, + }, + { timestamps: true } +); + +const InviteModel = model("Invite", InviteSchema); + +export type { InviteDocument }; +export { InviteModel }; +export default InviteModel; diff --git a/server/src/db/models/InviteToken.js b/server/src/db/models/InviteToken.js deleted file mode 100755 index 079600647..000000000 --- a/server/src/db/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/models/MaintenanceWindow.js b/server/src/db/models/MaintenanceWindow.js deleted file mode 100755 index b68abb413..000000000 --- a/server/src/db/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/models/MaintenanceWindow.ts b/server/src/db/models/MaintenanceWindow.ts new file mode 100644 index 000000000..b68de206e --- /dev/null +++ b/server/src/db/models/MaintenanceWindow.ts @@ -0,0 +1,60 @@ +import { Schema, model, type Types } from "mongoose"; +import type { MaintenanceWindow } from "@/types/maintenanceWindow.js"; + +type MaintenanceWindowDocumentBase = Omit & { + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + start: Date; + end: Date; + expiry?: Date; + createdAt: Date; + updatedAt: Date; +}; + +interface MaintenanceWindowDocument extends MaintenanceWindowDocumentBase { + _id: Types.ObjectId; +} + +const MaintenanceWindowSchema = new Schema( + { + monitorId: { + type: Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + }, + teamId: { + type: 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, + } +); + +const MaintenanceWindowModel = model("MaintenanceWindow", MaintenanceWindowSchema); + +export type { MaintenanceWindowDocument }; +export { MaintenanceWindowModel }; +export default MaintenanceWindowModel; diff --git a/server/src/db/models/Migration.ts b/server/src/db/models/Migration.ts new file mode 100644 index 000000000..ba928d910 --- /dev/null +++ b/server/src/db/models/Migration.ts @@ -0,0 +1,13 @@ +import mongoose from "mongoose"; + +const MigrationSchema = new mongoose.Schema( + { + name: { type: String, required: true, unique: true }, + status: { type: String, enum: ["completed", "failed"], default: "completed" }, + completedAt: { type: Date }, + error: { type: String }, + }, + { timestamps: true } +); + +export default mongoose.model("Migration", MigrationSchema); diff --git a/server/src/db/models/Monitor.js b/server/src/db/models/Monitor.ts old mode 100755 new mode 100644 similarity index 62% rename from server/src/db/models/Monitor.js rename to server/src/db/models/Monitor.ts index 0e12d3c26..17565a2e4 --- a/server/src/db/models/Monitor.js +++ b/server/src/db/models/Monitor.ts @@ -1,18 +1,49 @@ -import mongoose from "mongoose"; +import { Schema, model, Types, type UpdateQuery } from "mongoose"; +import type { Monitor, MonitorMatchMethod, MonitorThresholds } from "@/types/monitor.js"; +import { MonitorTypes } from "@/types/monitor.js"; import Check from "./Check.js"; import MonitorStats from "./MonitorStats.js"; import StatusPage from "./StatusPage.js"; -const MonitorSchema = mongoose.Schema( +type MonitorDocumentBase = Omit< + Monitor, + "id" | "userId" | "teamId" | "notifications" | "selectedDisks" | "statusWindow" | "createdAt" | "updatedAt" +> & { + statusWindow: boolean[]; + notifications: Types.ObjectId[]; + selectedDisks: string[]; + matchMethod?: MonitorMatchMethod; + thresholds?: MonitorThresholds; +}; + +interface MonitorDocument extends MonitorDocumentBase { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const thresholdsSchema = new Schema( + { + usage_cpu: { type: Number }, + usage_memory: { type: Number }, + usage_disk: { type: Number }, + usage_temperature: { type: Number }, + }, + { _id: false } +); + +const MonitorSchema = new Schema( { userId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "User", immutable: true, required: true, }, teamId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Team", immutable: true, required: true, @@ -43,7 +74,7 @@ const MonitorSchema = mongoose.Schema( type: { type: String, required: true, - enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"], + enum: MonitorTypes, }, ignoreTlsErrors: { type: Boolean, @@ -71,7 +102,6 @@ const MonitorSchema = mongoose.Schema( default: true, }, interval: { - // in milliseconds type: Number, default: 60000, }, @@ -81,7 +111,7 @@ const MonitorSchema = mongoose.Schema( }, notifications: [ { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Notification", }, ], @@ -89,13 +119,7 @@ const MonitorSchema = mongoose.Schema( type: String, }, thresholds: { - type: { - usage_cpu: { type: Number }, - usage_memory: { type: Number }, - usage_disk: { type: Number }, - usage_temperature: { type: Number }, - }, - _id: false, + type: thresholdsSchema, }, alertThreshold: { type: Number, @@ -125,48 +149,28 @@ const MonitorSchema = mongoose.Schema( return this.alertThreshold; }, }, + selectedDisks: { + type: [String], + default: [], + }, gameId: { type: String, }, + group: { + type: String, + trim: true, + maxLength: 50, + default: null, + set(value: string | null) { + 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; @@ -184,8 +188,8 @@ MonitorSchema.pre("save", function (next) { }); MonitorSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if (update.alertThreshold) { + const update = this.getUpdate() as UpdateQuery | null; + if (update && !Array.isArray(update) && update.alertThreshold !== undefined) { update.cpuAlertThreshold = update.alertThreshold; update.memoryAlertThreshold = update.alertThreshold; update.diskAlertThreshold = update.alertThreshold; @@ -196,4 +200,8 @@ MonitorSchema.pre("findOneAndUpdate", function (next) { MonitorSchema.index({ teamId: 1, type: 1 }); -export default mongoose.model("Monitor", MonitorSchema); +const MonitorModel = model("Monitor", MonitorSchema); + +export type { MonitorDocument }; +export { MonitorModel }; +export default MonitorModel; diff --git a/server/src/db/models/MonitorStats.js b/server/src/db/models/MonitorStats.js deleted file mode 100755 index c081c8a4f..000000000 --- a/server/src/db/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/models/MonitorStats.ts b/server/src/db/models/MonitorStats.ts new file mode 100755 index 000000000..d1d49ca9f --- /dev/null +++ b/server/src/db/models/MonitorStats.ts @@ -0,0 +1,63 @@ +import { Schema, model, type Types } from "mongoose"; +import type { MonitorStats as MonitorStatsEntity } from "@/types/monitorStats.js"; + +type MonitorStatsDocumentBase = Omit & { + monitorId: Types.ObjectId; +}; + +interface MonitorStatsDocument extends MonitorStatsDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const MonitorStatsSchema = new Schema( + { + monitorId: { + type: Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + required: 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: undefined, + }, + }, + { timestamps: true } +); + +const MonitorStatsModel = model("MonitorStats", MonitorStatsSchema); + +export type { MonitorStatsDocument }; +export { MonitorStatsModel }; +export default MonitorStatsModel; diff --git a/server/src/db/models/Notification.js b/server/src/db/models/Notification.js deleted file mode 100755 index a6a3c7c09..000000000 --- a/server/src/db/models/Notification.js +++ /dev/null @@ -1,64 +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"], - }, - notificationName: { - type: String, - required: true, - }, - address: { - type: String, - }, - phone: { - 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/models/Notification.ts b/server/src/db/models/Notification.ts new file mode 100755 index 000000000..97e2d43fb --- /dev/null +++ b/server/src/db/models/Notification.ts @@ -0,0 +1,50 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Notification, NotificationChannel } from "@/types/notification.js"; + +interface NotificationDocument extends Omit { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const NotificationSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + immutable: true, + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + type: { + type: String, + enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[], + required: true, + }, + notificationName: { + type: String, + required: true, + }, + address: { type: String }, + phone: { type: String }, + homeserverUrl: { type: String }, + roomId: { type: String }, + accessToken: { type: String }, + }, + { + timestamps: true, + } +); + +const NotificationModel = model("Notification", NotificationSchema); + +export type { NotificationDocument }; +export { NotificationModel }; +export default NotificationModel; diff --git a/server/src/db/models/RecoveryToken.js b/server/src/db/models/RecoveryToken.js deleted file mode 100755 index 2219a4bca..000000000 --- a/server/src/db/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/models/RecoveryToken.ts b/server/src/db/models/RecoveryToken.ts new file mode 100644 index 000000000..07b697d9e --- /dev/null +++ b/server/src/db/models/RecoveryToken.ts @@ -0,0 +1,27 @@ +import { Schema, model, type Types } from "mongoose"; +import type { RecoveryToken as RecoveryTokenEntity } from "@/types/recoveryToken.js"; + +type RecoveryTokenDocumentBase = Omit & { + expiry: Date; +}; + +interface RecoveryTokenDocument extends RecoveryTokenDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const RecoveryTokenSchema = new Schema( + { + email: { type: String, required: true, unique: true }, + token: { type: String, required: true }, + expiry: { type: Date, default: Date.now, expires: 600 }, + }, + { timestamps: true } +); + +const RecoveryTokenModel = model("RecoveryToken", RecoveryTokenSchema); + +export type { RecoveryTokenDocument }; +export { RecoveryTokenModel }; +export default RecoveryTokenModel; diff --git a/server/src/db/models/StatusPage.js b/server/src/db/models/StatusPage.js deleted file mode 100755 index 621749fb8..000000000 --- a/server/src/db/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/models/StatusPage.ts b/server/src/db/models/StatusPage.ts new file mode 100644 index 000000000..42d11bc46 --- /dev/null +++ b/server/src/db/models/StatusPage.ts @@ -0,0 +1,115 @@ +import { Schema, model, type Types } from "mongoose"; +import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; +import { StatusPageTypes } from "@/types/statusPage.js"; + +type StatusPageDocumentBase = Omit< + StatusPage, + "id" | "userId" | "teamId" | "monitors" | "subMonitors" | "originalMonitors" | "createdAt" | "updatedAt" +> & { + monitors: Types.ObjectId[]; + subMonitors: Types.ObjectId[]; + originalMonitors?: Types.ObjectId[]; + logo?: StatusPageLogo | null; +}; + +interface StatusPageDocument extends StatusPageDocumentBase { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const logoSchema = new Schema( + { + data: { type: Buffer }, + contentType: { type: String }, + }, + { _id: false } +); + +const StatusPageSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + immutable: true, + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + type: { + type: String, + required: true, + default: "uptime", + enum: StatusPageTypes, + }, + companyName: { + type: String, + required: true, + default: "", + }, + url: { + type: String, + unique: true, + required: true, + default: "", + }, + timezone: { + type: String, + }, + color: { + type: String, + default: "#4169E1", + }, + monitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + subMonitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + logo: { + type: logoSchema, + default: null, + }, + 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 } +); + +const StatusPageModel = model("StatusPage", StatusPageSchema); + +export type { StatusPageDocument }; +export { StatusPageModel }; +export default StatusPageModel; diff --git a/server/src/db/models/Team.js b/server/src/db/models/Team.js deleted file mode 100755 index 95337c201..000000000 --- a/server/src/db/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/models/Team.ts b/server/src/db/models/Team.ts new file mode 100755 index 000000000..608f90ad8 --- /dev/null +++ b/server/src/db/models/Team.ts @@ -0,0 +1,28 @@ +import { Schema, model, type Types } from "mongoose"; + +import type { Team } from "@/types/index.js"; + +type TeamDocumentBase = Omit & {}; +interface TeamDocument extends TeamDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const TeamSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + }, + }, + { + timestamps: true, + } +); +const TeamModel = model("Team", TeamSchema); + +export type { TeamDocument }; +export { TeamModel }; +export default TeamModel; diff --git a/server/src/db/models/User.js b/server/src/db/models/User.js deleted file mode 100755 index 9368d98b1..000000000 --- a/server/src/db/models/User.js +++ /dev/null @@ -1,106 +0,0 @@ -import mongoose from "mongoose"; -import bcrypt from "bcryptjs"; -import logger from "../../utils/logger.js"; -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/models/User.ts b/server/src/db/models/User.ts new file mode 100644 index 000000000..6aa3835a6 --- /dev/null +++ b/server/src/db/models/User.ts @@ -0,0 +1,95 @@ +import { Schema, model, type Types } from "mongoose"; +import bcrypt from "bcryptjs"; +import type { User, UserProfileImage, UserRole } from "@/types/index.js"; +import { MonitorModel } from "@/db/models/index.js"; +import Team from "./Team.js"; +import NotificationModel from "./Notification.js"; + +type UserDocumentBase = Omit & { + teamId?: Types.ObjectId; + profileImage?: Required; +}; + +interface UserDocument extends UserDocumentBase { + _id: Types.ObjectId; + teamId?: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const profileImageSchema = new Schema>( + { + data: { type: Buffer }, + contentType: { type: String }, + }, + { _id: false } +); + +const UserSchema = new 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: { type: profileImageSchema }, + isActive: { type: Boolean, default: true }, + isVerified: { type: Boolean, default: false }, + role: { + type: [String], + enum: ["user", "admin", "superadmin", "demo" satisfies UserRole], + default: ["user"], + }, + teamId: { + type: 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 (update && "password" in update) { + const salt = bcrypt.genSaltSync(10); + (update as any).password = bcrypt.hashSync((update as any).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 MonitorModel.deleteMany({ userId: userToDelete._id }); + await this.model.deleteMany({ teamId: userToDelete.teamId, _id: { $ne: userToDelete._id } }); + await NotificationModel.deleteMany({ teamId: userToDelete.teamId }); + } + next(); + } catch (error) { + next(error as Error); + } +}); + +UserSchema.methods.comparePassword = async function (submittedPassword: string) { + return bcrypt.compare(submittedPassword, this.password); +}; + +const UserModel = model("User", UserSchema); + +export type { UserDocument }; +export { UserModel }; +export default UserModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts new file mode 100644 index 000000000..fee369e2b --- /dev/null +++ b/server/src/db/models/index.ts @@ -0,0 +1,35 @@ +export * from "@/db/models/Monitor.js"; +export { default as MonitorModel } from "@/db/models/Monitor.js"; + +export * from "@/db/models/Check.js"; +export { default as CheckModel } from "@/db/models/Check.js"; + +export * from "@/db/models/MonitorStats.js"; +export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; + +export * from "@/db/models/StatusPage.js"; +export { default as StatusPageModel } from "@/db/models/StatusPage.js"; + +export * from "@/db/models/User.js"; +export { default as UserModel } from "@/db/models/User.js"; + +export * from "@/db/models/Invite.js"; +export { default as InviteModel } from "@/db/models/Invite.js"; + +export * from "@/db/models/AppSettings.js"; +export { default as AppSettingsModel } from "@/db/models/AppSettings.js"; + +export * from "@/db/models/RecoveryToken.js"; +export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; + +export * from "@/db/models/Notification.js"; +export { default as NotificationModel } from "@/db/models/Notification.js"; + +export * from "@/db/models/Incident.js"; +export { default as IncidentModel } from "@/db/models/Incident.js"; + +export * from "@/db/models/Team.js"; +export { default as TeamModel } from "@/db/models/Team.js"; + +export * from "@/db/models/MaintenanceWindow.js"; +export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow.js"; diff --git a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js b/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js deleted file mode 100644 index 8f6988de4..000000000 --- a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js +++ /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/mongo/migration/index.js b/server/src/db/mongo/migration/index.js deleted file mode 100644 index 4bf56895a..000000000 --- a/server/src/db/mongo/migration/index.js +++ /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/mongo/modules/checkModule.js b/server/src/db/mongo/modules/checkModule.js deleted file mode 100755 index dfca02bed..000000000 --- a/server/src/db/mongo/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/mongo/modules/checkModuleQueries.js b/server/src/db/mongo/modules/checkModuleQueries.js deleted file mode 100644 index f2ad2a922..000000000 --- a/server/src/db/mongo/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/mongo/modules/inviteModule.js b/server/src/db/mongo/modules/inviteModule.js deleted file mode 100755 index 444e51efd..000000000 --- a/server/src/db/mongo/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/mongo/modules/maintenanceWindowModule.js b/server/src/db/mongo/modules/maintenanceWindowModule.js deleted file mode 100755 index 61523c9f0..000000000 --- a/server/src/db/mongo/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/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js deleted file mode 100755 index 8beb9316c..000000000 --- a/server/src/db/mongo/modules/monitorModule.js +++ /dev/null @@ -1,578 +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-%dT%H: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-%dT%H: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; - } - }; -} - -export default MonitorModule; diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js deleted file mode 100755 index 32aed1529..000000000 --- a/server/src/db/mongo/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/mongo/modules/notificationModule.js b/server/src/db/mongo/modules/notificationModule.js deleted file mode 100755 index 3c6edb54a..000000000 --- a/server/src/db/mongo/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/mongo/modules/recoveryModule.js b/server/src/db/mongo/modules/recoveryModule.js deleted file mode 100755 index cc22306a3..000000000 --- a/server/src/db/mongo/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/mongo/modules/settingsModule.js b/server/src/db/mongo/modules/settingsModule.js deleted file mode 100755 index 37b851bf2..000000000 --- a/server/src/db/mongo/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/mongo/modules/statusPageModule.js b/server/src/db/mongo/modules/statusPageModule.js deleted file mode 100755 index aadde0c81..000000000 --- a/server/src/db/mongo/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/mongo/modules/userModule.js b/server/src/db/mongo/modules/userModule.js deleted file mode 100755 index 83b3b15f9..000000000 --- a/server/src/db/mongo/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/mongo/utils/seedDb.js b/server/src/db/mongo/utils/seedDb.js deleted file mode 100755 index 224744a51..000000000 --- a/server/src/db/mongo/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/index.js b/server/src/index.ts similarity index 75% rename from server/src/index.js rename to server/src/index.ts index d1ff649eb..ca6cea2ba 100755 --- a/server/src/index.js +++ b/server/src/index.ts @@ -5,13 +5,14 @@ import { initShutdownListener } from "./shutdown.js"; import { fileURLToPath } from "url"; import path from "path"; import fs from "fs"; +import { runMigrations } from "./db/migration/index.js"; -import Logger from "./utils/logger.js"; -import SettingsService from "./service/system/settingsService.js"; -import AppSettings from "./db/models/AppSettings.js"; +import Logger, { ILogger } from "@/utils/logger.js"; +import SettingsService from "@/service/system/settingsService.js"; +import { MongoSettingsRepository } from "./repositories/index.js"; const SERVICE_NAME = "Server"; -let logger; +let logger: ILogger; const startApp = async () => { // FE path @@ -21,14 +22,18 @@ const startApp = async () => { const frontendPath = path.join(__dirname, "..", "public"); // Create services - const settingsService = new SettingsService(AppSettings); + const settingsRepository = new MongoSettingsRepository(); + const settingsService = new SettingsService(settingsRepository); + const envSettings = settingsService.loadSettings(); // Create logger logger = new Logger({ envSettings }); // Initialize services - const services = await initializeServices({ logger, envSettings, settingsService }); + const services = await initializeServices({ logger, envSettings, settingsService, settingsRepository }); + + await runMigrations(logger); // Initialize controllers const controllers = initializeControllers(services); diff --git a/server/src/locales/en.json b/server/src/locales/en.json deleted file mode 100755 index 1345f84a8..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: One of your monitors is back online", - "downtimeAlert": "Downtime Alert: One of your monitors 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/middleware/handleErrors.js b/server/src/middleware/handleErrors.js deleted file mode 100755 index b0ded5ab7..000000000 --- a/server/src/middleware/handleErrors.js +++ /dev/null @@ -1,22 +0,0 @@ -import { logger } from "../utils/logger.js"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/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/handleErrors.ts b/server/src/middleware/handleErrors.ts new file mode 100755 index 000000000..1fffc9a8c --- /dev/null +++ b/server/src/middleware/handleErrors.ts @@ -0,0 +1,20 @@ +import type { NextFunction, Request, Response } from "express"; +import { logger } from "@/utils/logger.js"; + +const handleErrors = (error: any, req: Request, res: Response, next: NextFunction) => { + const status = error.status || 500; + const message = error.message || "Server error"; + const service = error.service || "unknownService"; + logger.error({ + message: message, + service: service, + method: error.method, + stack: error.stack, + }); + res.status(status).json({ + status, + msg: message, + }); +}; + +export { handleErrors }; diff --git a/server/src/middleware/isAllowed.js b/server/src/middleware/isAllowed.js deleted file mode 100755 index 5d61974be..000000000 --- a/server/src/middleware/isAllowed.js +++ /dev/null @@ -1,57 +0,0 @@ -import jwt from "jsonwebtoken"; -const TOKEN_PREFIX = "Bearer "; -const SERVICE_NAME = "allowedRoles"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/system/stringService.js"; -import SettingsService from "../service/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/isAllowed.ts b/server/src/middleware/isAllowed.ts new file mode 100755 index 000000000..21690ee38 --- /dev/null +++ b/server/src/middleware/isAllowed.ts @@ -0,0 +1,29 @@ +import type { Request, Response, NextFunction } from "express"; +const SERVICE_NAME = "allowedRoles"; +import { AppError } from "@/utils/AppError.js"; +import type { UserRole } from "@/types/index.js"; + +const isAllowed = (allowedRoles: UserRole[]) => { + return (req: Request, res: Response, next: NextFunction) => { + try { + const user = req.user; + if (!user) { + throw new AppError({ message: "Unauthorized", status: 403, service: SERVICE_NAME }); + } + const userRoles = req.user?.role || []; + + // Check if the user has the required role + if (userRoles.some((role) => allowedRoles.includes(role))) { + next(); + return; + } else { + throw new AppError({ message: "Unauthorized", status: 403, service: SERVICE_NAME }); + } + } catch (error) { + next(error); + return; + } + }; +}; + +export { isAllowed }; diff --git a/server/src/middleware/languageMiddleware.js b/server/src/middleware/languageMiddleware.js deleted file mode 100755 index 6293dc837..000000000 --- a/server/src/middleware/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/rateLimiter.js b/server/src/middleware/rateLimiter.ts similarity index 86% rename from server/src/middleware/rateLimiter.js rename to server/src/middleware/rateLimiter.ts index 70b080d23..99655149d 100644 --- a/server/src/middleware/rateLimiter.js +++ b/server/src/middleware/rateLimiter.ts @@ -1,7 +1,7 @@ import rateLimit from "express-rate-limit"; export const generalApiLimiter = rateLimit({ - window: 60 * 1000, + windowMs: 60 * 1000, limit: 600, standardHeaders: true, legacyHeaders: false, @@ -9,7 +9,7 @@ export const generalApiLimiter = rateLimit({ }); export const authApiLimiter = rateLimit({ - window: 60 * 1000, + windowMs: 60 * 1000, limit: 15, standardHeaders: true, legacyHeaders: false, diff --git a/server/src/middleware/responseHandler.js b/server/src/middleware/responseHandler.js deleted file mode 100755 index 5169e7160..000000000 --- a/server/src/middleware/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/sanitization.js b/server/src/middleware/sanitization.js deleted file mode 100644 index a89325de1..000000000 --- a/server/src/middleware/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/sanitization.ts b/server/src/middleware/sanitization.ts new file mode 100644 index 000000000..be971a298 --- /dev/null +++ b/server/src/middleware/sanitization.ts @@ -0,0 +1,59 @@ +import type { Request, Response, NextFunction } from "express"; +import DOMPurify from "isomorphic-dompurify"; + +export const sanitizeInput = (input: any, options = {}) => { + if (typeof input !== "string") { + return input; + } + + // Default configuration - remove all HTML tags and attributes + const defaultConfig = { + ALLOWED_TAGS: [] as string[], + ALLOWED_ATTR: [] as string[], + KEEP_CONTENT: true, + ...options, + }; + + return DOMPurify.sanitize(input, defaultConfig); +}; + +export const sanitizeObject = (obj: Record, options = {}): Record => { + if (typeof obj !== "object" || obj === null) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map((item) => sanitizeObject(item, options)); + } + + const sanitized: Record = {}; + 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; +}; + +export const sanitizeBody = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => { + return (req: Request, res: Response, next: NextFunction) => { + if (req.body && typeof req.body === "object") { + req.body = sanitizeObject(req.body, options); + } + next(); + }; +}; + +export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => { + return (req: Request, res: Response, next: NextFunction) => { + if (req.query && typeof req.query === "object") { + req.query = sanitizeObject(req.query, options); + } + next(); + }; +}; diff --git a/server/src/middleware/verifyJWT.js b/server/src/middleware/verifyJWT.js deleted file mode 100755 index d01db0a97..000000000 --- a/server/src/middleware/verifyJWT.js +++ /dev/null @@ -1,57 +0,0 @@ -import jwt from "jsonwebtoken"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import SettingsService from "../service/system/settingsService.js"; -import StringService from "../service/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/verifyJWT.ts b/server/src/middleware/verifyJWT.ts new file mode 100644 index 000000000..6c2b0a229 --- /dev/null +++ b/server/src/middleware/verifyJWT.ts @@ -0,0 +1,52 @@ +import { NextFunction, Request, Response } from "express"; + +import jwt from "jsonwebtoken"; +import { AppError } from "@/utils/AppError.js"; +import { Settings } from "@/types/settings.js"; + +const SERVICE_NAME = "verifyJWT"; +const TOKEN_PREFIX = "Bearer "; + +export const createVerifyJWT = (settingsService: { getSettings: () => Settings }) => { + return (req: Request, res: Response, next: NextFunction) => { + const token = req.headers["authorization"]; + // Make sure a token is provided + if (!token) { + const error = new AppError({ message: "No token provided", status: 401, service: SERVICE_NAME }); + next(error); + return; + } + // Make sure it is properly formatted + if (!token.startsWith(TOKEN_PREFIX)) { + const error = new AppError({ message: "Invalid token format", status: 401, service: SERVICE_NAME, method: "verifyJWT" }); + next(error); + return; + } + + const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); + // Verify the token's authenticity + const { jwtSecret } = settingsService.getSettings(); + if (!jwtSecret) { + const error = new AppError({ message: "JWT secret not configured", status: 500, service: SERVICE_NAME }); + next(error); + return; + } + jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => { + if (err) { + const error = new AppError({ + message: "Failed to authenticate token", + details: err, + status: 401, + service: SERVICE_NAME, + method: "verifyJWT", + }); + next(error); + return; + } else { + // Token is valid, carry on + req.user = decoded; + next(); + } + }); + }; +}; diff --git a/server/src/middleware/verifyOwnership.js b/server/src/middleware/verifyOwnership.js deleted file mode 100755 index 4431b0c26..000000000 --- a/server/src/middleware/verifyOwnership.js +++ /dev/null @@ -1,59 +0,0 @@ -import { logger } from "../utils/logger.js"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/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/verifySuperAdmin.js b/server/src/middleware/verifySuperAdmin.js deleted file mode 100755 index a5a93eede..000000000 --- a/server/src/middleware/verifySuperAdmin.js +++ /dev/null @@ -1,66 +0,0 @@ -const jwt = require("jsonwebtoken"); -const logger = require("../utils/logger"); -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/verifyTeamAccess.js b/server/src/middleware/verifyTeamAccess.js deleted file mode 100644 index 60647bdc2..000000000 --- a/server/src/middleware/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/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts new file mode 100644 index 000000000..ab5dc6ce5 --- /dev/null +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -0,0 +1,41 @@ +import type { + Check, + ChecksQueryResult, + ChecksSummary, + MonitorType, + PageSpeedChecksResult, + HardwareChecksResult, + UptimeChecksResult, +} from "@/types/index.js"; +import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; + +export interface IChecksRepository { + // create + createChecks(checks: Check[]): Promise; + + // single fetch + // collection fetch + findByMonitorId( + monitorId: string, + sortOrder: string, + dateRange: string, + filter: string, + page: number, + rowsPerPage: number, + status: boolean | undefined + ): Promise; + findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise; + findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; + findByDateRangeAndMonitorId( + monitorId: string, + startDate: Date, + endDate: Date, + dateString: string, + options?: { type?: MonitorType } + ): Promise; + findSummaryByTeamId(teamId: string): Promise; + // update + //delete + deleteByMonitorId(monitorId: string): Promise; + deleteByTeamId(teamId: string): Promise; +} diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts new file mode 100644 index 000000000..802f6f0d0 --- /dev/null +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -0,0 +1,775 @@ +import { IChecksRepository } from "@/repositories/index.js"; +import type { + Check, + CheckAudits, + CheckCaptureInfo, + CheckCpuInfo, + CheckDiskInfo, + CheckErrorInfo, + CheckHostInfo, + CheckMemoryInfo, + CheckMetadata, + CheckNetworkInterfaceInfo, + GotTimings, + MonitorType, +} from "@/types/index.js"; +import { CheckModel, type CheckDocument } from "@/db/models/index.js"; +import mongoose from "mongoose"; + +const SERVICE_NAME = "StatusService"; + +const dateRangeLookup: Record = { + 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, +}; + +export type LatestChecksMap = Record; +type DateRange = { start: Date; end: Date }; +type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number }; +type HardwareUpChecks = { totalChecks: number }; + +class MongoChecksRepository implements IChecksRepository { + static SERVICE_NAME = SERVICE_NAME; + + private logger: any; + constructor(logger: any) { + this.logger = logger; + } + + private toEntity = (doc: CheckDocument): Check => { + const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + const toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + const toOptionalDateString = (value?: Date | string | null): string | undefined => { + if (!value) { + return undefined; + } + return toDateString(value); + }; + + const mapTimings = (timings?: GotTimings): GotTimings => { + const phases = timings?.phases ?? { + wait: 0, + dns: 0, + tcp: 0, + tls: 0, + request: 0, + firstByte: 0, + download: 0, + total: 0, + }; + + return { + start: timings?.start ?? 0, + socket: timings?.socket ?? 0, + lookup: timings?.lookup ?? 0, + connect: timings?.connect ?? 0, + secureConnect: timings?.secureConnect ?? 0, + upload: timings?.upload ?? 0, + response: timings?.response ?? 0, + end: timings?.end ?? 0, + phases, + }; + }; + + const mapCpu = (cpu?: CheckCpuInfo): CheckCpuInfo => ({ + physical_core: cpu?.physical_core ?? 0, + logical_core: cpu?.logical_core ?? 0, + frequency: cpu?.frequency ?? 0, + temperature: cpu?.temperature ?? [], + free_percent: cpu?.free_percent ?? 0, + usage_percent: cpu?.usage_percent ?? 0, + }); + + const mapMemory = (memory?: CheckMemoryInfo): CheckMemoryInfo => ({ + total_bytes: memory?.total_bytes ?? 0, + available_bytes: memory?.available_bytes ?? 0, + used_bytes: memory?.used_bytes ?? 0, + usage_percent: memory?.usage_percent ?? 0, + }); + + const mapHost = (host?: CheckHostInfo): CheckHostInfo => ({ + os: host?.os ?? "", + platform: host?.platform ?? "", + kernel_version: host?.kernel_version ?? "", + }); + + const mapCapture = (capture?: CheckCaptureInfo): CheckCaptureInfo => ({ + version: capture?.version ?? "", + mode: capture?.mode ?? "", + }); + + const mapDisks = (disks?: CheckDiskInfo[]): CheckDiskInfo[] => + (disks ?? []).map((disk) => ({ + device: disk?.device ?? "", + mountpoint: disk?.mountpoint ?? "", + read_speed_bytes: disk?.read_speed_bytes ?? 0, + write_speed_bytes: disk?.write_speed_bytes ?? 0, + total_bytes: disk?.total_bytes ?? 0, + free_bytes: disk?.free_bytes ?? 0, + usage_percent: disk?.usage_percent ?? 0, + })); + + const mapErrors = (errors?: CheckErrorInfo[]): CheckErrorInfo[] => + (errors ?? []).map((error) => ({ + metric: error?.metric ?? [], + err: error?.err ?? "", + })); + + const mapNet = (net?: CheckNetworkInterfaceInfo[]): CheckNetworkInterfaceInfo[] => + (net ?? []).map((iface) => ({ + name: iface?.name ?? "", + bytes_sent: iface?.bytes_sent ?? 0, + bytes_recv: iface?.bytes_recv ?? 0, + packets_sent: iface?.packets_sent ?? 0, + packets_recv: iface?.packets_recv ?? 0, + err_in: iface?.err_in ?? 0, + err_out: iface?.err_out ?? 0, + drop_in: iface?.drop_in ?? 0, + drop_out: iface?.drop_out ?? 0, + fifo_in: iface?.fifo_in ?? 0, + fifo_out: iface?.fifo_out ?? 0, + })); + + const mapAudits = (audits?: CheckAudits): CheckAudits | undefined => { + if (!audits) { + return undefined; + } + return { + cls: audits.cls, + si: audits.si, + fcp: audits.fcp, + lcp: audits.lcp, + tbt: audits.tbt, + }; + }; + + const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({ + monitorId: toStringId(metadata.monitorId), + teamId: toStringId(metadata.teamId), + type: metadata.type, + }); + + return { + id: toStringId(doc._id), + metadata: mapMetadata(doc.metadata), + status: doc.status ?? false, + responseTime: doc.responseTime ?? 0, + timings: mapTimings(doc.timings), + statusCode: doc.statusCode ?? 0, + message: doc.message ?? "", + ack: doc.ack ?? false, + ackAt: toOptionalDateString(doc.ackAt), + expiry: toDateString(doc.expiry), + cpu: mapCpu(doc.cpu), + memory: mapMemory(doc.memory), + disk: mapDisks(doc.disk), + host: mapHost(doc.host), + errors: mapErrors(doc.errors), + capture: mapCapture(doc.capture), + net: mapNet(doc.net), + accessibility: doc.accessibility, + bestPractices: doc.bestPractices, + seo: doc.seo, + performance: doc.performance, + audits: mapAudits(doc.audits), + __v: doc.__v ?? 0, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; + + createChecks = async (checks: Check[]) => { + return await CheckModel.insertMany(checks); + }; + + findByMonitorId = async ( + monitorId: string, + sortOrder: string, + dateRange: string, + filter: string, + page: number, + rowsPerPage: number, + status: boolean | undefined + ) => { + // Match + const matchStage: Record = { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + ...(typeof status !== "undefined" && { status }), + ...(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 + const convertedSortOrder = sortOrder === "asc" ? 1 : -1; + + // Pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const checks = await CheckModel.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: convertedSortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { + $ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0], + }, + checks: { + $ifNull: ["$checks", []], + }, + }, + }, + ]); + return checks[0]; + }; + + findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => { + const matchStage: Record = { + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + status: false, + ...(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; + } + } + + const parsedSortOrder = sortOrder === "asc" ? 1 : -1; + + // pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const aggregatePipeline: any = [ + { $match: matchStage }, + + { $sort: { createdAt: parsedSortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, + checks: "$checks", + }, + }, + ]; + + const checks = await CheckModel.aggregate(aggregatePipeline); + return checks[0]; + }; + + findLatestByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { + if (monitorIds.length === 0) { + return {}; + } + const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const limitPerMonitor = options?.limitPerMonitor ?? 25; + const checkGroups = await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": { $in: mongoIds }, + }, + }, + { $sort: { "metadata.monitorId": 1, createdAt: -1 } }, + { + $group: { + _id: "$metadata.monitorId", + latestChecks: { + $topN: { + n: limitPerMonitor, + sortBy: { createdAt: -1 }, + output: "$$ROOT", + }, + }, + }, + }, + ]); + + return checkGroups.reduce((acc, group) => { + const monitorId = group._id.toString(); + acc[monitorId] = (group.latestChecks ?? []).map((doc: CheckDocument) => this.toEntity(doc)); + return acc; + }, {}); + }; + + findByDateRangeAndMonitorId = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => { + const monitorObjectId = new mongoose.Types.ObjectId(monitorId); + if (options?.type === "hardware") { + return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString); + } + if (options?.type === "pagespeed") { + return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate); + } + return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString); + }; + + findSummaryByTeamId = async (teamId: string) => { + const matchStage = { + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + }; + const checks = await CheckModel.aggregate([ + { $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] }, + }, + }, + ]); + return checks[0].summary; + }; + + deleteByMonitorId = async (monitorId: string): Promise => { + const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) }); + return result.deletedCount; + }; + + deleteByTeamId = async (teamId: string) => { + const deleteResult = await CheckModel.deleteMany({ "metadata.teamId": teamId }); + return deleteResult.deletedCount; + }; + + private findUptimeDateRangeChecks = async ( + monitorType: Exclude, + monitorObjectId: mongoose.Types.ObjectId, + startDate: Date, + endDate: Date, + dateString: string + ) => { + const matchStage = { + "metadata.monitorId": monitorObjectId, + createdAt: { $gte: startDate, $lte: endDate }, + }; + const [result] = await CheckModel.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: 1 } }, + { + $facet: { + 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 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, + ], + groupedUpChecks: [ + { $match: { status: true } }, + { + $group: { + _id: { + $dateToString: { format: dateString, date: "$createdAt" }, + }, + totalChecks: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + { $sort: { _id: 1 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, + ], + groupedDownChecks: [ + { $match: { status: false } }, + { + $group: { + _id: { + $dateToString: { format: dateString, date: "$createdAt" }, + }, + totalChecks: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + { $sort: { _id: 1 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, + ], + }, + }, + ]); + + const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0; + const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0; + + return { + monitorType, + groupedChecks: result?.groupedChecks ?? [], + groupedUpChecks: result?.groupedUpChecks ?? [], + groupedDownChecks: result?.groupedDownChecks ?? [], + uptimePercentage, + avgResponseTime, + }; + }; + + private findHardwareDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => { + const monitorId = monitorObjectId.toHexString(); + const dates = { start: startDate, end: endDate }; + const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([ + this.getHardwareAggregateData(monitorId, dates), + this.getHardwareUpChecks(monitorId, dates), + this.getHardwareStats(monitorId, dates, dateString), + ]); + + const aggregateData = { + latestCheck: aggregateDataDoc?.latestCheck ? this.toEntity(aggregateDataDoc.latestCheck as CheckDocument) : null, + totalChecks: aggregateDataDoc?.totalChecks ?? 0, + }; + + const upChecks = { + totalChecks: upChecksDoc?.totalChecks ?? 0, + }; + + const checks = (hardwareMetrics ?? []).map((metric) => ({ + _id: metric._id, + avgCpuUsage: metric.avgCpuUsage ?? 0, + avgMemoryUsage: metric.avgMemoryUsage ?? 0, + avgTemperature: metric.avgTemperature ?? [], + disks: (metric.disks ?? []).map((disk: { [key: string]: number | string | undefined }) => ({ + name: disk?.name ?? "", + readSpeed: disk?.readSpeed ?? 0, + writeSpeed: disk?.writeSpeed ?? 0, + totalBytes: disk?.totalBytes ?? 0, + freeBytes: disk?.freeBytes ?? 0, + usagePercent: disk?.usagePercent ?? 0, + })), + net: (metric.net ?? []).map((iface: { [key: string]: number | string | undefined }) => ({ + name: iface?.name ?? "", + bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0, + deltaBytesRecv: iface?.deltaBytesRecv ?? 0, + deltaPacketsSent: iface?.deltaPacketsSent ?? 0, + deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0, + deltaErrIn: iface?.deltaErrIn ?? 0, + deltaErrOut: iface?.deltaErrOut ?? 0, + deltaDropIn: iface?.deltaDropIn ?? 0, + deltaDropOut: iface?.deltaDropOut ?? 0, + deltaFifoIn: iface?.deltaFifoIn ?? 0, + deltaFifoOut: iface?.deltaFifoOut ?? 0, + })), + })); + + return { + monitorType: "hardware" as const, + aggregateData, + upChecks, + checks, + }; + }; + + private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => { + const matchStage = { + "metadata.monitorId": monitorObjectId, + createdAt: { $gte: startDate, $lte: endDate }, + }; + + const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean(); + return { + monitorType: "pagespeed" as const, + checks: checks.map((doc) => this.toEntity(doc)), + }; + }; + + private getHardwareAggregateData = async (monitorId: string, dates: DateRange): Promise => { + const result = await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: -1 } }, + { + $group: { + _id: null, + latestCheck: { $first: "$$ROOT" }, + totalChecks: { $sum: 1 }, + }, + }, + ]); + return result[0] || { totalChecks: 0, latestCheck: null }; + }; + + private getHardwareUpChecks = async (monitorId: string, dates: DateRange): Promise => { + const count = await CheckModel.countDocuments({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + status: true, + }); + return { totalChecks: count }; + }; + + private getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string) => { + return await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: 1 } }, + { + $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" }, + createdAts: { $push: "$createdAt" }, + sampleDoc: { $first: "$$ROOT" }, + }, + }, + { + $project: { + _id: 1, + avgCpuUsage: 1, + avgMemoryUsage: 1, + avgTemperature: { + $map: { + input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] }, + as: "idx", + in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } }, + }, + }, + disks: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] }, + as: "dIdx", + in: { + name: { $concat: ["disk", { $toString: "$$dIdx" }] }, + readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } }, + writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } }, + totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, + freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, + usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, + }, + }, + }, + net: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] }, + as: "nIdx", + in: { + name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, + bytesSentPerSecond: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaBytesRecv: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaPacketsSent: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaPacketsRecv: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaErrIn: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaErrOut: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaDropIn: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaDropOut: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + }, + }, + }, + }, + }, + { $sort: { _id: 1 } }, + ]); + }; +} + +export default MongoChecksRepository; diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts new file mode 100644 index 000000000..3c136bf43 --- /dev/null +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -0,0 +1,27 @@ +import type { Incident } from "@/types/index.js"; +import type { IncidentSummary } from "@/types/index.js"; +export interface IIncidentsRepository { + // create + create(incident: Partial): Promise; + // fetch + findById(incidentId: string, teamId: string): Promise; + findActiveByIncidentId(incidentId: string, teamId: string): Promise; + findActiveByMonitorId(monitorId: string, teamId: string): Promise; + findByTeamId( + teamId: string, + startDate: Date | undefined, + page: number, + rowsPerPage: number, + sortOrder?: string, + status?: boolean, + monitorId?: string, + resolutionType?: string + ): Promise; + findSummaryByTeamId(teamId: string, limit?: number): Promise; + countByTeamId(teamId: string, startDate: Date | undefined, status?: boolean, monitorId?: string, resolutionType?: string): Promise; + + // update + updateById(incidentId: string, teamId: string, updateData: Partial): Promise; + // delete + // other +} diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts new file mode 100644 index 000000000..b029953ca --- /dev/null +++ b/server/src/repositories/incidents/MongoIncidentRepository.ts @@ -0,0 +1,276 @@ +import { IncidentModel } from "@/db/models/index.js"; +import type { IncidentDocument } from "@/db/models/Incident.js"; +import type { Incident, IncidentSummary } from "@/types/index.js"; +import type { IIncidentsRepository } from "@/repositories/index.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; + +class MongoIncidentRepository implements IIncidentsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private buildMatchStage({ + teamId, + startDate, + status, + monitorId, + resolutionType, + }: { + teamId: string; + startDate: Date | undefined; + status?: boolean; + monitorId?: string; + resolutionType?: string; + }): Record { + const matchStage: Record = { + teamId: new mongoose.Types.ObjectId(teamId), + ...(status !== undefined && { status }), + ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), + ...(resolutionType && { resolutionType }), + }; + + if (startDate) { + matchStage.createdAt = { $gte: startDate }; + } + return matchStage; + } + + protected toEntity = (doc: IncidentDocument): Incident => { + return { + id: this.toStringId(doc._id), + monitorId: this.toStringId(doc.monitorId), + teamId: this.toStringId(doc.teamId), + startTime: this.toDateString(doc.startTime), + endTime: doc.endTime ? this.toDateString(doc.endTime) : null, + status: doc.status, + message: doc.message ?? null, + statusCode: doc.statusCode ?? null, + resolutionType: doc.resolutionType ?? null, + resolvedBy: doc.resolvedBy ? this.toStringId(doc.resolvedBy) : null, + comment: doc.comment ?? null, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + protected mapDocuments = (documents: IncidentDocument[] | IncidentDocument | null): Incident[] => { + if (!documents) { + return []; + } + if (Array.isArray(documents)) { + return documents.map((doc) => this.toEntity(doc)); + } + return [this.toEntity(documents)]; + }; + + async create(incident: Partial): Promise { + const newIncident = await IncidentModel.create(incident); + return this.toEntity(newIncident); + } + + findById = async (incidentId: string, teamId: string): Promise => { + const incident = await IncidentModel.findOne({ + _id: new mongoose.Types.ObjectId(incidentId), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!incident) { + throw new AppError({ message: `Incident with id ${incidentId} not found`, status: 404 }); + } + return this.toEntity(incident); + }; + + findActiveByIncidentId = async (incidentId: string, teamId: string): Promise => { + const incident = await IncidentModel.findOne({ + _id: new mongoose.Types.ObjectId(incidentId), + teamId: new mongoose.Types.ObjectId(teamId), + status: true, + }); + if (!incident) { + return null; + } + return this.toEntity(incident); + }; + + findActiveByMonitorId = async (monitorId: string, teamId: string): Promise => { + const incident = await IncidentModel.findOne({ + monitorId: new mongoose.Types.ObjectId(monitorId), + teamId: new mongoose.Types.ObjectId(teamId), + status: true, + }); + if (!incident) { + return null; + } + return this.toEntity(incident); + }; + + findByTeamId = async ( + teamId: string, + startDate: Date | undefined, + page: number, + rowsPerPage: number, + sortOrder?: string, + status?: boolean, + monitorId?: string, + resolutionType?: string + ): Promise => { + const matchStage = this.buildMatchStage({ teamId, startDate, status, monitorId, resolutionType }); + const incidents = await IncidentModel.find(matchStage) + .sort({ createdAt: sortOrder === "asc" ? 1 : -1 }) + .skip(page * rowsPerPage) + .limit(rowsPerPage); + return this.mapDocuments(incidents); + }; + + updateById = async (incidentId: string, teamId: string, patch: Partial) => { + const updatedIncident = await IncidentModel.findOneAndUpdate( + { _id: new mongoose.Types.ObjectId(incidentId), teamId: new mongoose.Types.ObjectId(teamId) }, + { + $set: { + ...patch, + }, + }, + { new: true, runValidators: true } + ); + if (!updatedIncident) { + throw new AppError({ message: `Failed to update incident with id ${incidentId}`, status: 500 }); + } + return this.toEntity(updatedIncident); + }; + + countByTeamId = async ( + teamId: string, + startDate: Date | undefined, + status?: boolean, + monitorId?: string, + resolutionType?: string + ): Promise => { + const matchStage = this.buildMatchStage({ teamId, startDate, status, monitorId, resolutionType }); + return IncidentModel.countDocuments(matchStage); + }; + + findSummaryByTeamId = async (teamId: string, limit: number): Promise => { + const matchStage = { teamId: new mongoose.Types.ObjectId(teamId) }; + + const counts = await IncidentModel.aggregate([ + { $match: matchStage }, + { + $group: { + _id: "$status", + count: { $sum: 1 }, + manualResolutions: { + $sum: { $cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0] }, + }, + automaticResolutions: { + $sum: { $cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0] }, + }, + }, + }, + ]); + + let total = 0; + let active = 0; + let manual = 0; + let automatic = 0; + + counts.forEach((item) => { + total += item.count; + if (item._id === true) { + active = item.count; + } + manual += item.manualResolutions; + automatic += item.automaticResolutions; + }); + + const resolutionTimeResult = await IncidentModel.aggregate([ + { $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } }, + { $project: { resolutionTime: { $subtract: ["$endTime", "$startTime"] } } }, + { $group: { _id: null, avgResolutionTime: { $avg: "$resolutionTime" } } }, + ]); + const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0; + const avgResolutionTimeHours = Math.round((avgResolutionTimeMs / (1000 * 60 * 60) || 0) * 100) / 100; + + const monitorResult = await IncidentModel.aggregate([ + { $match: matchStage }, + { $group: { _id: "$monitorId", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 1 }, + { + $lookup: { + from: "monitors", + localField: "_id", + foreignField: "_id", + as: "monitor", + }, + }, + { $project: { monitorId: "$_id", count: 1, monitorName: { $arrayElemAt: ["$monitor.name", 0] } } }, + ]); + + const latestLimit = Math.max(1, Number.isFinite(Number(limit)) ? Number(limit) : 10); + const latestIncidents = await IncidentModel.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: -1 } }, + { $limit: latestLimit }, + { + $lookup: { + from: "monitors", + localField: "monitorId", + foreignField: "_id", + as: "monitor", + }, + }, + { + $project: { + _id: 1, + monitorId: 1, + monitorName: { $arrayElemAt: ["$monitor.name", 0] }, + status: 1, + startTime: 1, + endTime: 1, + resolutionType: 1, + message: 1, + statusCode: 1, + createdAt: 1, + }, + }, + ]); + + return { + total, + totalActive: active, + totalManualResolutions: manual, + totalAutomaticResolutions: automatic, + avgResolutionTimeHours, + topMonitor: monitorResult[0] + ? { + monitorId: this.toStringId(monitorResult[0].monitorId), + monitorName: monitorResult[0].monitorName ?? null, + incidentCount: monitorResult[0].count, + } + : null, + latestIncidents: latestIncidents.map((incident) => ({ + id: this.toStringId(incident._id), + monitorId: this.toStringId(incident.monitorId), + monitorName: incident.monitorName ?? null, + status: incident.status, + startTime: this.toDateString(incident.startTime), + endTime: incident.endTime ? this.toDateString(incident.endTime) : null, + resolutionType: incident.resolutionType ?? null, + message: incident.message ?? null, + statusCode: incident.statusCode ?? null, + createdAt: this.toDateString(incident.createdAt), + })), + }; + }; +} +export default MongoIncidentRepository; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts new file mode 100644 index 000000000..da31a8c75 --- /dev/null +++ b/server/src/repositories/index.ts @@ -0,0 +1,35 @@ +export * from "@/repositories/monitors/IMonitorsRepository.js"; +export { default as MongoMonitorsRepository } from "@/repositories/monitors/MongoMonitorsRepository.js"; + +export * from "@/repositories/checks/IChecksRepository.js"; +export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js"; + +export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js"; +export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js"; + +export * from "@/repositories/status-pages/IStatusPagesRepository.js"; +export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; + +export * from "@/repositories/users/IUsersRepository.js"; +export { default as MongoUsersRepository } from "@/repositories/users/MongoUsersRepository.js"; + +export * from "@/repositories/invites/IInvitesRepository.js"; +export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js"; + +export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js"; +export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js"; + +export * from "@/repositories/settings/ISettingsRepository.js"; +export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js"; + +export * from "@/repositories/notifications/INotificationsRepository.js"; +export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js"; + +export * from "@/repositories/incidents/IIncidentsRepository.js"; +export { default as MongoIncidentRepository } from "@/repositories/incidents/MongoIncidentRepository.js"; + +export * from "@/repositories/teams/ITeamsRepository.js"; +export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js"; + +export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js"; +export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js"; diff --git a/server/src/repositories/invites/IInvitesRepository.ts b/server/src/repositories/invites/IInvitesRepository.ts new file mode 100644 index 000000000..74a7ad68c --- /dev/null +++ b/server/src/repositories/invites/IInvitesRepository.ts @@ -0,0 +1,13 @@ +import type { Invite } from "@/types/index.js"; + +export interface IInvitesRepository { + // create + create(invite: Partial): Promise; + // fetch + findByToken(token: string): Promise; + findByTokenAndDelete(token: string): Promise; + // update + + // delete + // other +} diff --git a/server/src/repositories/invites/MongoInviteRepository.ts b/server/src/repositories/invites/MongoInviteRepository.ts new file mode 100644 index 000000000..e9243e96e --- /dev/null +++ b/server/src/repositories/invites/MongoInviteRepository.ts @@ -0,0 +1,63 @@ +import { IInvitesRepository } from "@/repositories/index.js"; +import type { Invite } from "@/types/index.js"; +import { type InviteDocument, InviteModel } from "@/db/models/index.js"; +import { AppError } from "@/utils/AppError.js"; +import mongoose from "mongoose"; +import crypto from "crypto"; + +class MongoInvitesRepository implements IInvitesRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: InviteDocument): Invite => { + return { + id: this.toStringId(doc._id), + email: doc.email, + teamId: this.toStringId(doc.teamId), + role: doc.role ?? [], + token: doc.token, + expiry: this.toDateString(doc.expiry), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (invite: Partial) => { + await InviteModel.deleteMany({ email: invite.email }); + invite.token = crypto.randomBytes(32).toString("hex"); + let inviteToken = await InviteModel.create(invite); + return this.toEntity(inviteToken); + }; + + findByToken = async (token: string) => { + const invite = await InviteModel.findOne({ + token, + }); + if (invite === null) { + throw new AppError({ message: "Invite not found", status: 404 }); + } + return this.toEntity(invite); + }; + + findByTokenAndDelete = async (token: string) => { + const invite = await InviteModel.findOneAndDelete({ + token, + }); + if (invite === null) { + throw new AppError({ message: "Invite not found", status: 404 }); + } + return this.toEntity(invite); + }; +} +export default MongoInvitesRepository; diff --git a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts new file mode 100644 index 000000000..1bcb02347 --- /dev/null +++ b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts @@ -0,0 +1,16 @@ +import type { MaintenanceWindow } from "@/types/index.js"; +export interface IMaintenanceWindowsRepository { + // create + create(data: Partial): Promise; + // fetch + findById(id: string, teamId: string): Promise; + findByMonitorId(monitorId: string, teamId: string): Promise; + findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise; + + // update + updateById(id: string, teamId: string, data: Partial): Promise; + // delete + deleteById(id: string, teamId: string): Promise; + // other + countByTeamId(teamId: string, active: boolean): Promise; +} diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts new file mode 100644 index 000000000..e85af5cb9 --- /dev/null +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -0,0 +1,136 @@ +import type { MaintenanceWindow } from "@/types/index.js"; +import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js"; +import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; + +class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private mapDocuments = (documents: MaintenanceWindowDocument[]): MaintenanceWindow[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + private toEntity = (doc: MaintenanceWindowDocument): MaintenanceWindow => { + return { + id: this.toStringId(doc._id), + monitorId: this.toStringId(doc.monitorId), + teamId: this.toStringId(doc.teamId), + active: doc.active, + name: doc.name, + repeat: doc.repeat, + start: this.toDateString(doc.start), + end: this.toDateString(doc.end), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (data: Partial): Promise => { + const maintenanceWindow = new MaintenanceWindowModel(data); + + // If the maintenance window is a one time window, set the expiry to the end date + if (maintenanceWindow.repeat === 0) { + maintenanceWindow.expiry = maintenanceWindow.end; + } + const result = await maintenanceWindow.save(); + return this.toEntity(result); + }; + + findById = async (id: string, teamId: string): Promise => { + const maintenanceWindow = await MaintenanceWindowModel.findOne({ + _id: id, + teamId: teamId, + }); + if (!maintenanceWindow) { + throw new AppError({ message: "Maintenance Window not found", status: 404 }); + } + return this.toEntity(maintenanceWindow); + }; + + findByMonitorId = async (monitorId: string, teamId: string): Promise => { + const maintenanceWindows = await MaintenanceWindowModel.find({ + monitorId: monitorId, + teamId: teamId, + }); + return this.mapDocuments(maintenanceWindows); + }; + + findByTeamId = async ( + teamId: string, + active: boolean, + page: number, + rowsPerPage: number, + field: string, + order: string + ): Promise => { + const maintenanceQuery: Record = { teamId }; + + if (active !== undefined) maintenanceQuery.active = active; + + // Pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + // Sorting + let sort: Record = {}; + if (field !== undefined && order !== undefined) { + sort[field] = order === "asc" ? 1 : -1; + } + + const maintenanceWindows = await MaintenanceWindowModel.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort); + return this.mapDocuments(maintenanceWindows); + }; + + updateById = async (id: string, teamId: string, data: Partial): Promise => { + const updated = await MaintenanceWindowModel.findOneAndUpdate( + { + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }, + { $set: data }, + { new: true, runValidators: true } + ); + if (!updated) { + throw new AppError({ message: "Maintenance window not found or could not be updated", status: 404 }); + } + return this.toEntity(updated); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await MaintenanceWindowModel.findOneAndDelete({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!deleted) { + throw new AppError({ message: "Maintenance window not found or could not be deleted", status: 404 }); + } + return this.toEntity(deleted); + }; + countByTeamId = async (teamId: string, active: boolean) => { + const maintenanceQuery: Record = { teamId }; + + if (active !== undefined) maintenanceQuery.active = active; + + return await MaintenanceWindowModel.countDocuments(maintenanceQuery); + }; +} + +export default MongoMaintenanceWindowsRepository; diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts new file mode 100644 index 000000000..08309b4ee --- /dev/null +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -0,0 +1,10 @@ +import type { MonitorStats } from "@/types/index.js"; +export interface IMonitorStatsRepository { + // create + // single fetch + findByMonitorId(monitorId: string): Promise; + // update + // delete + deleteByMonitorId(monitorId: string): Promise; + // other +} diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts new file mode 100644 index 000000000..52dbe8aa5 --- /dev/null +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -0,0 +1,52 @@ +import { type MonitorStatsDocument, MonitorStatsModel } from "@/db/models/index.js"; +import type { MonitorStats } from "@/types/index.js"; +import { IMonitorStatsRepository } from "@/repositories/index.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; +class MongoMonitorStatsRepository implements IMonitorStatsRepository { + private toEntity = (doc: MonitorStatsDocument): MonitorStats => { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + return { + id: toStringId(doc._id), + monitorId: toStringId(doc.monitorId), + avgResponseTime: doc.avgResponseTime, + totalChecks: doc.totalChecks, + totalUpChecks: doc.totalUpChecks, + totalDownChecks: doc.totalDownChecks, + uptimePercentage: doc.uptimePercentage, + lastCheckTimestamp: doc.lastCheckTimestamp, + lastResponseTime: doc.lastResponseTime, + timeOfLastFailure: doc.timeOfLastFailure, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; + + findByMonitorId = async (monitorId: string): Promise => { + const monitorStats = await MonitorStatsModel.findOne({ monitorId: new mongoose.Types.ObjectId(monitorId) }); + if (!monitorStats) { + throw new AppError({ message: "Monitor stats not found", status: 404 }); + } + return this.toEntity(monitorStats); + }; + + deleteByMonitorId = async (monitorId: string) => { + const deleted = await MonitorStatsModel.findOneAndDelete({ monitorId: new mongoose.Types.ObjectId(monitorId) }); + if (!deleted) { + throw new AppError({ message: "Monitor stats not found", status: 404 }); + } + return this.toEntity(deleted); + }; +} + +export default MongoMonitorStatsRepository; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts new file mode 100644 index 000000000..072772e9e --- /dev/null +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -0,0 +1,44 @@ +import { type MonitorType, type Monitor, type MonitorsSummary, type MonitorWithChecks } from "@/types/index.js"; + +export interface TeamQueryConfig { + limit?: number; + type?: MonitorType | MonitorType[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; +} + +export interface SummaryConfig { + type?: MonitorType | MonitorType[]; +} + +export interface IMonitorsRepository { + // create + create(monitor: Monitor, teamId: string, userId: string): Promise; + createBulkMonitors(monitors: Monitor[]): Promise; + // single fetch + findById(monitorId: string, teamId: string): Promise; + + // collection fetch + findAll(): Promise; + findByTeamId(teamId: string, config: TeamQueryConfig): Promise; + findByIds(monitorIds: string[]): Promise; + findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise; + + // update + updateById(monitorId: string, teamId: string, updates: Partial): Promise; + togglePauseById(monitorId: string, teamId: string): Promise; + // delete + deleteById(monitorId: string, teamId: string): Promise; + deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>; + + // counts + findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; + + // other + findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; + findGroupsByTeamId(teamId: string): Promise; + removeNotificationFromMonitors(notificationId: string): Promise; +} diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts new file mode 100644 index 000000000..5ce029634 --- /dev/null +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -0,0 +1,402 @@ +import { MonitorModel } from "@/db/models/index.js"; +import type { MonitorDocument } from "@/db/models/index.js"; +import type { Monitor, MonitorsSummary, MonitorWithChecks, Check } from "@/types/index.js"; +import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; +import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; +import { AppError } from "@/utils/AppError.js"; + +class MongoMonitorsRepository implements IMonitorsRepository { + create = async (monitor: Monitor, teamId: string, userId: string) => { + const monitorModel = new MonitorModel({ ...monitor, teamId, userId }); + const saved = await monitorModel.save(); + return this.toEntity(saved); + }; + + createBulkMonitors = async (monitors: Monitor[]): Promise => { + if (!monitors.length) { + return []; + } + const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined })); + const inserted = await MonitorModel.insertMany(payload, { ordered: false }); + return this.mapDocuments(inserted); + }; + + findById = async (monitorId: string, teamId: string): Promise => { + const match: { _id: string; teamId: string } = { _id: monitorId, teamId }; + const monitor = await MonitorModel.findOne(match); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found`, status: 404 }); + } + return this.toEntity(monitor); + }; + + findAll = async (): Promise => { + const monitors = await MonitorModel.find(); + return this.mapDocuments(monitors); + }; + + findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { + const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + + const query: Record = { + teamId: new mongoose.Types.ObjectId(teamId), + }; + + if (type !== undefined) { + query.type = Array.isArray(type) ? { $in: type } : type; + } + + if (filter !== undefined) { + switch (field) { + case "name": + query.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; + break; + case "isActive": + query.isActive = filter === "true"; + break; + case "status": + query.status = filter === "true"; + break; + case "type": + query.type = filter; + break; + default: + break; + } + } + + const sort = { [field]: order === "asc" ? 1 : -1 } as const; + const skip = Math.max(page, 0) * rowsPerPage; + + const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage); + + return this.mapDocuments(documents); + }; + + findByIds = async (monitorIds: string[]): Promise => { + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const monitors = await MonitorModel.find({ _id: { $in: objectIds } }); + return this.mapDocuments(monitors); + }; + + findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise => { + if (!monitorIds.length) { + return []; + } + + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + + const pipeline: PipelineStage[] = [ + { $match: { _id: { $in: objectIds } } }, + { + $lookup: { + from: "checks", + let: { monitorId: "$_id" }, + pipeline: [{ $match: { $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] } } }, { $sort: { createdAt: -1 } }, { $limit: checksCount }], + as: "checks", + }, + }, + { + $lookup: { + from: "maintenancewindows", + let: { monitorId: "$_id" }, + pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }], + as: "maintenanceWindows", + }, + }, + { + $lookup: { + from: "monitorstats", + localField: "_id", + foreignField: "monitorId", + as: "stats", + }, + }, + { + $addFields: { + isMaintenance: { + $reduce: { + input: "$maintenanceWindows", + initialValue: false, + in: { + $or: [ + "$$value", + { + $and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }], + }, + ], + }, + }, + }, + uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] }, + }, + }, + { + $project: { + maintenanceWindows: 0, + stats: 0, + }, + }, + ]; + + const documents = await MonitorModel.aggregate(pipeline); + return documents.map((doc) => this.toEntityWithChecks(doc)); + }; + + findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise => { + const { type } = config ?? {}; + + const query: FilterQuery = { + teamId: new mongoose.Types.ObjectId(teamId), + }; + + if (type !== undefined) { + query.type = Array.isArray(type) ? { $in: type } : type; + } + + const count = await MonitorModel.countDocuments(query); + return count; + }; + + updateById = async (monitorId: string, teamId: string, patch: Partial) => { + const updatedMonitor = await MonitorModel.findOneAndUpdate( + { _id: monitorId, teamId }, + { + $set: { + ...patch, + }, + }, + { new: true, runValidators: true } + ); + if (!updatedMonitor) { + throw new AppError({ message: `Failed to update monitor with id ${monitorId}`, status: 500 }); + } + return this.toEntity(updatedMonitor); + }; + + togglePauseById = async (monitorId: string, teamId: string) => { + const monitor = await MonitorModel.findOneAndUpdate( + { _id: monitorId, teamId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: "$$REMOVE", + }, + }, + ], + { new: true } + ); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 }); + } + return this.toEntity(monitor); + }; + + deleteById = async (monitorId: string, teamId: string) => { + const deletedMonitor = await MonitorModel.findOneAndDelete({ _id: monitorId, teamId }); + + if (!deletedMonitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 }); + } + + return this.toEntity(deletedMonitor); + }; + + deleteByTeamId = async (teamId: string) => { + const monitors = await MonitorModel.find({ teamId }); + const { deletedCount } = await MonitorModel.deleteMany({ teamId }); + + return { monitors: this.mapDocuments(monitors), deletedCount }; + }; + + findMonitorsSummaryByTeamId = async (teamId: string, config?: SummaryConfig): Promise => { + const match: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; + if (config?.type !== undefined) { + match.type = Array.isArray(config.type) ? { $in: config.type } : config.type; + } + const pipeline = [ + { $match: match }, + { + $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 [summary] = await MonitorModel.aggregate(pipeline); + return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; + }; + + findGroupsByTeamId = async (teamId: string): Promise => { + const groups = await MonitorModel.distinct("group", { + teamId: new mongoose.Types.ObjectId(teamId), + group: { $nin: [null, ""] }, + }); + return groups.sort(); + }; + + removeNotificationFromMonitors = async (notificationId: string): Promise => { + await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); + }; + + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + private toEntity = (doc: MonitorDocument): Monitor => { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + const notificationIds = (doc.notifications ?? []).map((notification) => toStringId(notification)); + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + name: doc.name, + description: doc.description ?? undefined, + status: doc.status ?? undefined, + statusWindow: doc.statusWindow ?? [], + statusWindowSize: doc.statusWindowSize, + statusWindowThreshold: doc.statusWindowThreshold, + type: doc.type, + ignoreTlsErrors: doc.ignoreTlsErrors, + jsonPath: doc.jsonPath ?? undefined, + expectedValue: doc.expectedValue ?? undefined, + matchMethod: doc.matchMethod ?? undefined, + url: doc.url, + port: doc.port ?? undefined, + isActive: doc.isActive, + interval: doc.interval, + uptimePercentage: doc.uptimePercentage ?? undefined, + notifications: notificationIds, + secret: doc.secret ?? undefined, + thresholds: doc.thresholds ?? undefined, + alertThreshold: doc.alertThreshold, + cpuAlertThreshold: doc.cpuAlertThreshold, + memoryAlertThreshold: doc.memoryAlertThreshold, + diskAlertThreshold: doc.diskAlertThreshold, + tempAlertThreshold: doc.tempAlertThreshold, + selectedDisks: doc.selectedDisks ?? [], + gameId: doc.gameId ?? undefined, + group: doc.group ?? null, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; + + private toEntityWithChecks = (doc: any): MonitorWithChecks => { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + if (!value) return ""; + return value instanceof Date ? value.toISOString() : value; + }; + + const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification)); + + const checks: Check[] = (doc.checks ?? []).map((check: any) => ({ + id: toStringId(check._id), + metadata: { + monitorId: toStringId(check.metadata?.monitorId), + teamId: toStringId(check.metadata?.teamId), + type: check.metadata?.type, + }, + status: check.status, + responseTime: check.responseTime, + timings: check.timings, + statusCode: check.statusCode, + message: check.message, + ack: check.ack, + ackAt: check.ackAt ?? null, + expiry: toDateString(check.expiry), + cpu: check.cpu, + memory: check.memory, + disk: check.disk, + host: check.host, + errors: check.errors, + capture: check.capture, + net: check.net, + accessibility: check.accessibility, + bestPractices: check.bestPractices, + seo: check.seo, + performance: check.performance, + audits: check.audits, + __v: check.__v, + createdAt: toDateString(check.createdAt), + updatedAt: toDateString(check.updatedAt), + })); + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + name: doc.name, + description: doc.description ?? undefined, + status: doc.status ?? undefined, + statusWindow: doc.statusWindow ?? [], + statusWindowSize: doc.statusWindowSize, + statusWindowThreshold: doc.statusWindowThreshold, + type: doc.type, + ignoreTlsErrors: doc.ignoreTlsErrors, + jsonPath: doc.jsonPath ?? undefined, + expectedValue: doc.expectedValue ?? undefined, + matchMethod: doc.matchMethod ?? undefined, + url: doc.url, + port: doc.port ?? undefined, + isActive: doc.isActive, + interval: doc.interval, + uptimePercentage: doc.uptimePercentage ?? undefined, + notifications: notificationIds, + secret: doc.secret ?? undefined, + thresholds: doc.thresholds ?? undefined, + alertThreshold: doc.alertThreshold, + cpuAlertThreshold: doc.cpuAlertThreshold, + memoryAlertThreshold: doc.memoryAlertThreshold, + diskAlertThreshold: doc.diskAlertThreshold, + tempAlertThreshold: doc.tempAlertThreshold, + selectedDisks: doc.selectedDisks ?? [], + gameId: doc.gameId ?? undefined, + group: doc.group ?? null, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + checks, + }; + }; +} + +export default MongoMonitorsRepository; diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts new file mode 100644 index 000000000..0236c3a89 --- /dev/null +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -0,0 +1,13 @@ +import type { Notification } from "@/types/index.js"; +export interface INotificationsRepository { + // create + create(notificationData: Partial): Promise; + // fetch + findById(id: string, teamId: string): Promise; + findNotificationsByIds(ids: string[]): Promise; + findByTeamId(teamId: string): Promise; + // update + updateById(id: string, teamId: string, updateData: Partial): Promise; + // delete + deleteById(id: string, teamId: string): Promise; +} diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts new file mode 100644 index 000000000..730072d8c --- /dev/null +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -0,0 +1,98 @@ +import mongoose from "mongoose"; +import { NotificationModel, type NotificationDocument } from "@/db/models/index.js"; +import { INotificationsRepository } from "@/repositories/index.js"; +import type { Notification } from "@/types/index.js"; +import { AppError } from "@/utils/AppError.js"; +import { not } from "joi"; + +class MongoNotificationsRepository implements INotificationsRepository { + private mapDocuments = (documents: NotificationDocument[]): Notification[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + private toEntity = (doc: NotificationDocument): Notification => { + const toStringId = (value: mongoose.Types.ObjectId | string): string => { + return value instanceof mongoose.Types.ObjectId ? value.toString() : value; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + type: doc.type, + notificationName: doc.notificationName, + address: doc.address ?? undefined, + phone: doc.phone ?? undefined, + homeserverUrl: doc.homeserverUrl ?? undefined, + roomId: doc.roomId ?? undefined, + accessToken: doc.accessToken ?? undefined, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; + + create = async (notificationData: Partial) => { + const notification = await NotificationModel.create({ ...notificationData }); + if (!notification) { + throw new AppError({ message: "Failed to create notification", status: 500 }); + } + return this.toEntity(notification); + }; + + findById = async (id: string, teamId: string): Promise => { + const notification = await NotificationModel.findOne({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!notification) { + throw new AppError({ message: "Notification not found", status: 404 }); + } + return this.toEntity(notification); + }; + + findNotificationsByIds = async (ids: string[]) => { + const mongoIds = ids.map((id) => new mongoose.Types.ObjectId(id)); + const documents = await NotificationModel.find({ _id: { $in: mongoIds } }); + return this.mapDocuments(documents); + }; + + findByTeamId = async (teamId: string): Promise => { + const documents = await NotificationModel.find({ teamId }); + return this.mapDocuments(documents); + }; + + updateById = async (id: string, teamId: string, patch: Partial): Promise => { + const notification = await NotificationModel.findOneAndUpdate( + { + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }, + { $set: patch }, + { new: true, runValidators: true } + ); + if (!notification) { + throw new AppError({ message: "Notification not found or could not be updated", status: 404 }); + } + return this.toEntity(notification); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await NotificationModel.findOneAndDelete({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!deleted) { + throw new AppError({ message: "Notification not found or could not be deleted", status: 404 }); + } + return this.toEntity(deleted); + }; +} + +export default MongoNotificationsRepository; diff --git a/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts b/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts new file mode 100644 index 000000000..9df1fa793 --- /dev/null +++ b/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts @@ -0,0 +1,12 @@ +import type { RecoveryToken } from "@/types/recoveryToken.js"; + +export interface IRecoveryTokensRepository { + // create + create(email: string): Promise; + // fetch + findByToken(token: string): Promise; + // update + // delete + deleteManyByEmail(email: string): Promise; + // other +} diff --git a/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts b/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts new file mode 100644 index 000000000..4c9f9ebb6 --- /dev/null +++ b/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts @@ -0,0 +1,61 @@ +import type { RecoveryToken } from "@/types/index.js"; +import type { IRecoveryTokensRepository } from "./IRecoveryTokensRepository.js"; +import type { RecoveryTokenDocument } from "@/db/models/RecoveryToken.js"; +import { RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; +import mongoose from "mongoose"; +import crypto from "crypto"; +import { AppError } from "@/utils/AppError.js"; +const SERVICE_NAME = "MongoRecoveryTokensRepository"; + +class MongoRecoveryTokensRepository implements IRecoveryTokensRepository { + static SERVICE_NAME = SERVICE_NAME; + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: RecoveryTokenDocument): RecoveryToken => { + return { + id: this.toStringId(doc._id), + email: doc.email, + token: doc.token, + expiry: this.toDateString(doc.expiry), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (email: string): Promise => { + const token = await RecoveryTokenModel.create({ + email, + token: crypto.randomBytes(32).toString("hex"), + }); + return this.toEntity(token); + }; + + findByToken = async (token: string): Promise => { + const recoveryToken = await RecoveryTokenModel.findOne({ token }); + if (!recoveryToken) { + throw new AppError({ message: "Recovery token not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(recoveryToken); + }; + + deleteManyByEmail = async (email: string) => { + const result = await RecoveryTokenModel.deleteMany({ + email, + }); + return Promise.resolve(result.deletedCount || 0); + }; +} + +export default MongoRecoveryTokensRepository; diff --git a/server/src/repositories/settings/ISettingsRepository.ts b/server/src/repositories/settings/ISettingsRepository.ts new file mode 100644 index 000000000..578d7f40d --- /dev/null +++ b/server/src/repositories/settings/ISettingsRepository.ts @@ -0,0 +1,11 @@ +import type { Settings } from "@/types/index.js"; +export interface ISettingsRepository { + // create + create(settings: Partial): Promise; + // fetch + findSingleton(): Promise; + // update + update(settings: Partial): Promise; + // delete + deleteLegacy: () => Promise; +} diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts new file mode 100644 index 000000000..064993fa3 --- /dev/null +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -0,0 +1,95 @@ +import mongoose from "mongoose"; +import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; +import type { Settings } from "@/types/index.js"; +import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js"; +import { SERVFAIL } from "dns"; + +class MongoSettingsRepository implements ISettingsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: AppSettingsDocument): Settings => { + return { + id: this.toStringId(doc._id), + checkTTL: doc.checkTTL, + language: doc.language, + jwtSecret: doc.jwtSecret ?? undefined, + pagespeedApiKey: doc.pagespeedApiKey ?? undefined, + systemEmailHost: doc.systemEmailHost ?? undefined, + systemEmailPort: doc.systemEmailPort ?? undefined, + systemEmailAddress: doc.systemEmailAddress ?? undefined, + systemEmailPassword: doc.systemEmailPassword ?? undefined, + systemEmailUser: doc.systemEmailUser ?? undefined, + systemEmailConnectionHost: doc.systemEmailConnectionHost ?? undefined, + systemEmailTLSServername: doc.systemEmailTLSServername ?? undefined, + systemEmailSecure: doc.systemEmailSecure ?? false, + systemEmailPool: doc.systemEmailPool ?? false, + systemEmailIgnoreTLS: doc.systemEmailIgnoreTLS ?? false, + systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false, + systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true, + showURL: doc.showURL ?? false, + singleton: doc.singleton, + version: doc.version ?? 1, + globalThresholds: doc.globalThresholds ?? undefined, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (settings: Partial) => { + const newSettings = await AppSettingsModel.create(settings); + return this.toEntity(newSettings); + }; + + findSingleton = async () => { + let settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + if (!settings) { + return null; + } + return this.toEntity(settings); + }; + + update = async (settings: Partial) => { + const update: Record = { $set: { ...settings } }; + + if (settings.pagespeedApiKey === "") { + update.$unset = { pagespeedApiKey: "" }; + delete update.$set.pagespeedApiKey; + } + + if (settings.systemEmailPassword === "") { + update.$unset = { systemEmailPassword: "" }; + delete update.$set.systemEmailPassword; + } + + await AppSettingsModel.findOneAndUpdate({}, update, { + upsert: true, + }); + + const updatedSettings = await AppSettingsModel.findOneAndUpdate({}, update, { + upsert: true, + new: true, + projection: "-__v -_id -createdAt -updatedAt -singleton", + }); + + return this.toEntity(updatedSettings); + }; + + deleteLegacy = async () => { + const res = await AppSettingsModel.deleteMany({ version: { $exists: false } }); + return res.deletedCount > 0; + }; +} + +export default MongoSettingsRepository; diff --git a/server/src/repositories/status-pages/IStatusPagesRepository.ts b/server/src/repositories/status-pages/IStatusPagesRepository.ts new file mode 100644 index 000000000..c901b3c6d --- /dev/null +++ b/server/src/repositories/status-pages/IStatusPagesRepository.ts @@ -0,0 +1,16 @@ +import type { StatusPage } from "@/types/statusPage.js"; + +export interface IStatusPagesRepository { + // create + create(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; + // single fetch + findByUrl(url: string): Promise; + findByTeamId(teamId: string): Promise; + // collection fetch + // update + updateById(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; + // delete + deleteById(id: string, teamId: string): Promise; + // other + removeMonitorFromStatusPages(monitorId: string): Promise; +} diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts new file mode 100644 index 000000000..d5461bef3 --- /dev/null +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -0,0 +1,134 @@ +import { IStatusPagesRepository } from "@/repositories/index.js"; +import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js"; +import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; + +class MongoStatusPagesRepository implements IStatusPagesRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private mapIdArray = (values?: Array): string[] => { + return values?.map((value) => this.toStringId(value)) ?? []; + }; + + private mapLogo = (logo?: StatusPageLogo | null): StatusPageLogo | undefined => { + if (!logo) { + return undefined; + } + return { + data: logo.data, + contentType: logo.contentType, + }; + }; + + private toEntity = (doc: StatusPageDocument): StatusPage => { + return { + id: this.toStringId(doc._id), + userId: this.toStringId(doc.userId), + teamId: this.toStringId(doc.teamId), + type: doc.type, + companyName: doc.companyName, + url: doc.url, + timezone: doc.timezone ?? undefined, + color: doc.color, + monitors: this.mapIdArray(doc.monitors), + subMonitors: this.mapIdArray(doc.subMonitors), + originalMonitors: this.mapIdArray(doc.originalMonitors), + logo: this.mapLogo(doc.logo), + isPublished: doc.isPublished, + showCharts: doc.showCharts, + showUptimePercentage: doc.showUptimePercentage, + showAdminLoginLink: doc.showAdminLoginLink, + customCSS: doc.customCSS, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + private mapDocuments = (documents: StatusPageDocument[]): StatusPage[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise => { + const statusPage = new StatusPageModel({ + ...data, + userId, + teamId, + }); + if (image) { + statusPage.logo = { + data: image.buffer, + contentType: image.mimetype, + }; + } + await statusPage.save(); + return this.toEntity(statusPage); + }; + + findByUrl = async (url: string): Promise => { + const statusPage = await StatusPageModel.findOne({ + url, + }); + if (!statusPage) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + return this.toEntity(statusPage); + // Get status page + }; + + findByTeamId = async (teamId: string): Promise => { + const statusPages = await StatusPageModel.find({ teamId }); + return this.mapDocuments(statusPages); + }; + + updateById = async (id: string, teamId: string, image: Express.Multer.File | undefined, patch: Partial): Promise => { + if (image) { + patch.logo = { + data: image.buffer, + contentType: image.mimetype, + }; + } else { + patch.logo = null; + } + + const statusPage = await StatusPageModel.findOneAndUpdate({ teamId, _id: id }, patch, { + new: true, + }); + + if (!statusPage) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + + return this.toEntity(statusPage); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const statusPage = await StatusPageModel.findOneAndDelete({ _id: id, teamId }); + if (!statusPage) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + return this.toEntity(statusPage); + }; + + removeMonitorFromStatusPages = async (monitorId: string): Promise => { + const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } }); + return res.modifiedCount; + }; +} + +export default MongoStatusPagesRepository; diff --git a/server/src/repositories/teams/ITeamsRepository.ts b/server/src/repositories/teams/ITeamsRepository.ts new file mode 100644 index 000000000..9ccdd97fb --- /dev/null +++ b/server/src/repositories/teams/ITeamsRepository.ts @@ -0,0 +1,9 @@ +import type { Team } from "@/types/index.js"; +export interface ITeamsRepository { + // create + create(email: string): Promise; + // fetch + // update + // delete + // other +} diff --git a/server/src/repositories/teams/MongoTeamsRepository.ts b/server/src/repositories/teams/MongoTeamsRepository.ts new file mode 100644 index 000000000..93a306787 --- /dev/null +++ b/server/src/repositories/teams/MongoTeamsRepository.ts @@ -0,0 +1,36 @@ +import { Team } from "@/types/index.js"; +import { TeamDocument, TeamModel } from "@/db/models/index.js"; +import { ITeamsRepository } from "@/repositories/index.js"; +import mongoose from "mongoose"; + +class MongoTeamsRepository implements ITeamsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private toEntity = (doc: TeamDocument): Team => { + return { + id: this.toStringId(doc._id), + email: doc.email, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (email: string) => { + const team = await TeamModel.create({ email }); + return this.toEntity(team); + }; +} + +export default MongoTeamsRepository; diff --git a/server/src/repositories/users/IUsersRepository.ts b/server/src/repositories/users/IUsersRepository.ts new file mode 100644 index 000000000..3fa094c54 --- /dev/null +++ b/server/src/repositories/users/IUsersRepository.ts @@ -0,0 +1,15 @@ +import type { User } from "@/types/index.js"; +export interface IUsersRepository { + // create + create(user: Partial, imageFile?: Express.Multer.File | null): Promise; + // fetch + findByEmail(email: string): Promise; + findById(id: string): Promise; + findAll(): Promise; + // update + updateById(id: string, patch: Partial, file?: Express.Multer.File | null): Promise; + // delete + deleteById(id: string): Promise; + // other + findSuperAdmin(): Promise; +} diff --git a/server/src/repositories/users/MongoUsersRepository.ts b/server/src/repositories/users/MongoUsersRepository.ts new file mode 100644 index 000000000..023e8aefb --- /dev/null +++ b/server/src/repositories/users/MongoUsersRepository.ts @@ -0,0 +1,154 @@ +import mongoose from "mongoose"; +import { IUsersRepository } from "@/repositories/index.js"; +import { UserModel, type UserDocument } from "@/db/models/index.js"; +import type { User, UserProfileImage } from "@/types/index.js"; +import { GenerateAvatarImage } from "@/utils/imageProcessing.js"; +import { ParseBoolean } from "@/utils/utils.js"; +import { AppError } from "@/utils/AppError.js"; +const SERVICE_NAME = "MongoUsersRepository"; + +class MongoUsersRepository implements IUsersRepository { + static SERVICE_NAME = SERVICE_NAME; + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private mapProfileImage = (image?: (UserProfileImage & { data?: Buffer }) | null) => { + if (!image) { + return undefined; + } + return { + data: image.data, + contentType: image.contentType, + }; + }; + + protected toEntity = (doc: UserDocument): User => { + return { + id: this.toStringId(doc._id), + firstName: doc.firstName, + lastName: doc.lastName, + email: doc.email, + password: doc.password, + avatarImage: doc.avatarImage ?? undefined, + profileImage: this.mapProfileImage(doc.profileImage), + isActive: doc.isActive ?? false, + isVerified: doc.isVerified ?? false, + role: doc.role ?? [], + teamId: this.toStringId(doc.teamId), + checkTTL: doc.checkTTL ?? undefined, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (user: Partial, imageFile: Express.Multer.File | null) => { + if (imageFile) { + // 1. Save the full size image + user.profileImage = { + data: imageFile.buffer, + contentType: imageFile.mimetype, + }; + + // 2. Get the avatar sized image + const avatar = await GenerateAvatarImage(imageFile); + user.avatarImage = avatar; + } + + const newUser = new UserModel(user); + await newUser.save(); + const sanitizedUser = await UserModel.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); + if (!sanitizedUser) { + throw new AppError({ message: "Failed to create user", service: SERVICE_NAME, status: 500 }); + } + return this.toEntity(sanitizedUser); + }; + + findByEmail = async (email: string) => { + const user = await UserModel.findOne({ email: email }).select("-profileImage"); + if (!user) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(user); + }; + + findById = async (id: string) => { + const user = await UserModel.findById(id).select("-password").select("-profileImage"); + if (!user) { + throw new Error("User not found"); + } + + return this.toEntity(user); + }; + + findAll = async () => { + const users = await UserModel.find().select("-password").select("-profileImage"); + return this.mapDocuments(users); + }; + + updateById = async (id: string, patch: Partial, file?: Express.Multer.File | null): Promise => { + const candidateUser = { ...patch }; + + if (ParseBoolean(candidateUser.deleteProfileImage) === true) { + candidateUser.profileImage = undefined; + candidateUser.avatarImage = undefined; + } 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 GenerateAvatarImage(file); + candidateUser.avatarImage = avatar; + } + + const updatedUser = await UserModel.findOneAndUpdate( + { _id: id }, + candidateUser, + { new: true } // Returns updated user instead of pre-update user + ) + .select("-password") + .select("-profileImage"); + if (!updatedUser) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(updatedUser); + }; + + deleteById = async (id: string) => { + const deletedUser = await UserModel.findByIdAndDelete(id); + if (!deletedUser) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(deletedUser); + }; + + findSuperAdmin = async () => { + const superAdmin = await UserModel.findOne({ role: "superadmin" }); + if (superAdmin !== null) { + return true; + } + return false; + }; + + private mapDocuments = (documents: UserDocument[]): User[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; +} + +export default MongoUsersRepository; diff --git a/server/src/routes/announcementsRoute.js b/server/src/routes/announcementsRoute.js deleted file mode 100755 index 011dfac4a..000000000 --- a/server/src/routes/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/authRoute.js b/server/src/routes/authRoute.ts similarity index 77% rename from server/src/routes/authRoute.js rename to server/src/routes/authRoute.ts index e790c60b0..eec6ea11b 100755 --- a/server/src/routes/authRoute.js +++ b/server/src/routes/authRoute.ts @@ -1,20 +1,20 @@ -import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { verifyOwnership } from "../middleware/verifyOwnership.js"; +import { Router, RequestHandler } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; import multer from "multer"; -import User from "../db/models/User.js"; const upload = multer(); class AuthRoutes { - constructor(authController) { + private router: Router; + private authController: any; + + constructor(authController: any, verifyJWT: RequestHandler) { this.router = Router(); this.authController = authController; - this.initRoutes(); + this.initRoutes(verifyJWT); } - initRoutes() { + initRoutes(verifyJWT: RequestHandler) { this.router.post("/register", upload.single("profileImage"), this.authController.registerUser); this.router.post("/login", this.authController.loginUser); @@ -27,6 +27,7 @@ class AuthRoutes { 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); diff --git a/server/src/routes/checkRoute.js b/server/src/routes/checkRoute.ts similarity index 66% rename from server/src/routes/checkRoute.js rename to server/src/routes/checkRoute.ts index 8bdb2d4fb..4df286846 100755 --- a/server/src/routes/checkRoute.js +++ b/server/src/routes/checkRoute.ts @@ -1,12 +1,12 @@ import { Router } from "express"; -import { verifyOwnership } from "../middleware/verifyOwnership.js"; -import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js"; + import { isAllowed } from "../middleware/isAllowed.js"; -import Monitor from "../db/models/Monitor.js"; -import Check from "../db/models/Check.js"; class CheckRoutes { - constructor(checkController) { + private router: Router; + private checkController: any; + + constructor(checkController: any) { this.router = Router(); this.checkController = checkController; this.initRoutes(); @@ -17,13 +17,8 @@ class CheckRoutes { 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() { diff --git a/server/src/routes/diagnosticRoute.js b/server/src/routes/diagnosticRoute.ts similarity index 58% rename from server/src/routes/diagnosticRoute.js rename to server/src/routes/diagnosticRoute.ts index a15f5a038..b2cefe4aa 100755 --- a/server/src/routes/diagnosticRoute.js +++ b/server/src/routes/diagnosticRoute.ts @@ -1,14 +1,17 @@ -import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; +import { RequestHandler, Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; class DiagnosticRoutes { - constructor(diagnosticController) { + private router: Router; + private diagnosticController: any; + + constructor(diagnosticController: any, verifyJWT: RequestHandler) { this.router = Router(); this.diagnosticController = diagnosticController; - this.initRoutes(); + this.initRoutes(verifyJWT); } - initRoutes() { + + initRoutes(verifyJWT: RequestHandler) { this.router.get("/system", verifyJWT, isAllowed(["admin", "superadmin"]), this.diagnosticController.getSystemStats); } diff --git a/server/src/routes/incidentRoute.ts b/server/src/routes/incidentRoute.ts new file mode 100644 index 000000000..83f1ff8f9 --- /dev/null +++ b/server/src/routes/incidentRoute.ts @@ -0,0 +1,29 @@ +import { Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; + +class IncidentRoutes { + private router: Router; + private incidentController: any; + + constructor(incidentController: any) { + this.router = Router(); + this.incidentController = incidentController; + this.initRoutes(); + } + + initRoutes() { + // Team routes + this.router.get("/team", this.incidentController.getIncidentsByTeam); + this.router.get("/team/summary", this.incidentController.getIncidentSummary); + + // Individual incident routes + this.router.get("/:incidentId", this.incidentController.getIncidentById); + this.router.put("/:incidentId/resolve", isAllowed(["admin", "superadmin"]), this.incidentController.resolveIncidentManually); + } + + getRouter() { + return this.router; + } +} + +export default IncidentRoutes; diff --git a/server/src/routes/inviteRoute.js b/server/src/routes/inviteRoute.ts similarity index 68% rename from server/src/routes/inviteRoute.js rename to server/src/routes/inviteRoute.ts index d495c5476..b91f922df 100755 --- a/server/src/routes/inviteRoute.js +++ b/server/src/routes/inviteRoute.ts @@ -1,15 +1,17 @@ -import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; +import { RequestHandler, Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; class InviteRoutes { - constructor(inviteController) { + private router: Router; + private inviteController: any; + + constructor(inviteController: any, verifyJWT: RequestHandler) { this.router = Router(); this.inviteController = inviteController; - this.initRoutes(); + this.initRoutes(verifyJWT); } - initRoutes() { + initRoutes(verifyJWT: RequestHandler) { 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); diff --git a/server/src/routes/logRoutes.js b/server/src/routes/logRoutes.ts similarity index 80% rename from server/src/routes/logRoutes.js rename to server/src/routes/logRoutes.ts index 867826dde..c35361fd0 100755 --- a/server/src/routes/logRoutes.js +++ b/server/src/routes/logRoutes.ts @@ -1,7 +1,10 @@ import { Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; class LogRoutes { - constructor(logController) { + private router: Router; + private logController: any; + + constructor(logController: any) { this.router = Router(); this.logController = logController; this.initRoutes(); diff --git a/server/src/routes/maintenanceWindowRoute.js b/server/src/routes/maintenanceWindowRoute.ts similarity index 86% rename from server/src/routes/maintenanceWindowRoute.js rename to server/src/routes/maintenanceWindowRoute.ts index 774434ed6..0230ba4bd 100755 --- a/server/src/routes/maintenanceWindowRoute.js +++ b/server/src/routes/maintenanceWindowRoute.ts @@ -1,7 +1,10 @@ import { Router } from "express"; -import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; + class MaintenanceWindowRoutes { - constructor(maintenanceWindowController) { + private router: Router; + private mwController: any; + + constructor(maintenanceWindowController: any) { this.router = Router(); this.mwController = maintenanceWindowController; this.initRoutes(); diff --git a/server/src/routes/monitorRoute.js b/server/src/routes/monitorRoute.ts similarity index 79% rename from server/src/routes/monitorRoute.js rename to server/src/routes/monitorRoute.ts index 043d361d1..eb5604c24 100755 --- a/server/src/routes/monitorRoute.js +++ b/server/src/routes/monitorRoute.ts @@ -1,13 +1,15 @@ import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { isAllowed } from "@/middleware/isAllowed.js"; import multer from "multer"; -import { fetchMonitorCertificate } from "../controllers/controllerUtils.js"; +import { fetchMonitorCertificate } from "@/controllers/controllerUtils.js"; const upload = multer({ storage: multer.memoryStorage(), // Store file in memory as Buffer }); class MonitorRoutes { - constructor(monitorController) { + private router: Router; + private monitorController: any; + constructor(monitorController: any) { this.router = Router(); this.monitorController = monitorController; this.initRoutes(); @@ -17,17 +19,18 @@ class MonitorRoutes { // 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); + // PageSpeed routes + this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById); // 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) => { @@ -35,13 +38,12 @@ class MonitorRoutes { }); // 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.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); 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); diff --git a/server/src/routes/notificationRoute.js b/server/src/routes/notificationRoute.ts similarity index 88% rename from server/src/routes/notificationRoute.js rename to server/src/routes/notificationRoute.ts index f88b0b96c..7fe4bf5cb 100755 --- a/server/src/routes/notificationRoute.js +++ b/server/src/routes/notificationRoute.ts @@ -1,6 +1,9 @@ import { Router } from "express"; class NotificationRoutes { - constructor(notificationController) { + private router: Router; + private notificationController: any; + + constructor(notificationController: any) { this.router = Router(); this.notificationController = notificationController; this.initializeRoutes(); diff --git a/server/src/routes/queueRoute.js b/server/src/routes/queueRoute.ts similarity index 90% rename from server/src/routes/queueRoute.js rename to server/src/routes/queueRoute.ts index 55e829b91..d5b036b15 100755 --- a/server/src/routes/queueRoute.js +++ b/server/src/routes/queueRoute.ts @@ -1,7 +1,10 @@ import { Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; class QueueRoutes { - constructor(queueController) { + private router: Router; + private queueController: any; + + constructor(queueController: any) { this.router = Router(); this.queueController = queueController; this.initRoutes(); diff --git a/server/src/routes/settingsRoute.js b/server/src/routes/settingsRoute.ts similarity index 85% rename from server/src/routes/settingsRoute.js rename to server/src/routes/settingsRoute.ts index 88eb67e65..c7db1ed3b 100755 --- a/server/src/routes/settingsRoute.js +++ b/server/src/routes/settingsRoute.ts @@ -2,7 +2,10 @@ import { Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; class SettingsRoutes { - constructor(settingsController) { + private router: Router; + private settingsController: any; + + constructor(settingsController: any) { this.router = Router(); this.settingsController = settingsController; this.initRoutes(); diff --git a/server/src/routes/statusPageRoute.js b/server/src/routes/statusPageRoute.ts similarity index 62% rename from server/src/routes/statusPageRoute.js rename to server/src/routes/statusPageRoute.ts index 53eb21fba..f70802f73 100755 --- a/server/src/routes/statusPageRoute.js +++ b/server/src/routes/statusPageRoute.ts @@ -1,21 +1,22 @@ -import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; +import { RequestHandler, Router } from "express"; import multer from "multer"; const upload = multer(); class StatusPageRoutes { - constructor(statusPageController) { + private router: Router; + private statusPageController: any; + + constructor(statusPageController: any, verifyJWT: RequestHandler) { this.router = Router(); this.statusPageController = statusPageController; - this.initRoutes(); + this.initRoutes(verifyJWT); } - initRoutes() { - this.router.get("/", this.statusPageController.getStatusPage); + initRoutes(verifyJWT: RequestHandler) { 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.put("/:id", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage); this.router.get("/:url", this.statusPageController.getStatusPageByUrl); this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage); diff --git a/server/src/service/business/checkService.js b/server/src/service/business/checkService.js deleted file mode 100644 index 9beb43326..000000000 --- a/server/src/service/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/business/checkService.ts b/server/src/service/business/checkService.ts new file mode 100644 index 000000000..42788eba8 --- /dev/null +++ b/server/src/service/business/checkService.ts @@ -0,0 +1,182 @@ +import { requireTeamId } from "@/controllers/controllerUtils.js"; +import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; +import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; +import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; +import { AppError } from "@/utils/AppError.js"; +import { ParseBoolean } from "@/utils/utils.js"; + +const SERVICE_NAME = "checkService"; + +class CheckService { + static SERVICE_NAME = SERVICE_NAME; + + private monitorsRepository: IMonitorsRepository; + private checksRepository: IChecksRepository; + private logger: any; + constructor({ + monitorsRepository, + logger, + checksRepository, + }: { + monitorsRepository: IMonitorsRepository; + logger: any; + checksRepository: IChecksRepository; + }) { + this.monitorsRepository = monitorsRepository; + this.logger = logger; + this.checksRepository = checksRepository; + } + + get serviceName() { + return CheckService.SERVICE_NAME; + } + + createChecks = async (checks: Check[]) => { + return this.checksRepository.createChecks(checks); + }; + + buildCheck = (statusResponse: MonitorStatusResponse) => { + const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse; + + const check: Partial = { + metadata: { + monitorId, + teamId, + type, + }, + status, + statusCode: code, + responseTime: responseTime || 0, + timings: timings, + message, + }; + + if (type === "pagespeed") { + const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; + if (!pageSpeedPayload) { + this.logger.warn({ + message: "Failed to build check", + service: SERVICE_NAME, + method: "buildCheck", + details: "empty payload", + }); + return undefined; + } + const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; + const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; + const mapAudit = (audit: any) => { + if (!audit || typeof audit !== "object") { + return undefined; + } + return { + id: audit.id, + title: audit.title, + score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), + displayValue: audit.displayValue, + numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, + numericUnit: audit.numericUnit, + }; + }; + 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: mapAudit(audits?.["cumulative-layout-shift"]), + si: mapAudit(audits?.["speed-index"]), + fcp: mapAudit(audits?.["first-contentful-paint"]), + lcp: mapAudit(audits?.["largest-contentful-paint"]), + tbt: mapAudit(audits?.["total-blocking-time"]), + }; + } + + if (type === "hardware") { + const hardwarePayload = payload as HardwareStatusPayload | undefined; + const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; + const errorsSource = Array.isArray(hardwarePayload?.errors) + ? hardwarePayload?.errors + : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; + check.cpu = cpu; + check.memory = memory; + check.disk = disk; + check.host = host; + check.errors = errorsSource; + check.capture = hardwarePayload?.capture; + check.net = net; + } + return check; + }; + + getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { + if (!monitorId) { + throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); + } + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); + } + + // For verificaiton, throws an error if monitor doesn't belong to team + await this.monitorsRepository.findById(monitorId, teamId); + + let { sortOrder, dateRange, filter, page, rowsPerPage, status } = query; + const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status); + const parsedPage = page ? parseInt(page) : page; + const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; + + const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus); + + return result; + }; + + getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => { + let { sortOrder, dateRange, filter, page, rowsPerPage } = query; + + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 }); + } + + const parsedPage = page ? parseInt(page) : page; + const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; + + const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId); + return checkData; + }; + + getChecksSummaryByTeamId = async ({ teamId }: { teamId: string }) => { + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksSummaryByTeamId", status: 400 }); + } + const summary = await this.checksRepository.findSummaryByTeamId(teamId); + return summary; + }; + + deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { + if (!monitorId) { + throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 }); + } + + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 }); + } + + // For verificaiton, throws an error if monitor doesn't belong to team + await this.monitorsRepository.findById(monitorId, teamId); + + const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId); + return deletedCount; + }; + deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => { + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecksByTeamId", status: 400 }); + } + + const deletedCount = await this.checksRepository.deleteByTeamId(teamId); + return deletedCount; + }; + + updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => { + throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500 }); + }; +} + +export default CheckService; diff --git a/server/src/service/business/diagnosticService.js b/server/src/service/business/diagnosticService.ts similarity index 88% rename from server/src/service/business/diagnosticService.js rename to server/src/service/business/diagnosticService.ts index 353604932..6c49933a4 100644 --- a/server/src/service/business/diagnosticService.js +++ b/server/src/service/business/diagnosticService.ts @@ -3,6 +3,14 @@ import os from "os"; const SERVICE_NAME = "diagnosticService"; +interface MemoryUsageMetrics { + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + arrayBuffers: number; +} + class DiagnosticService { static SERVICE_NAME = SERVICE_NAME; @@ -47,8 +55,8 @@ class DiagnosticService { }; const used = process.memoryUsage(); - const memoryUsage = {}; - for (let key in used) { + const memoryUsage: Record = {}; + for (const key of Object.keys(used) as Array) { memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB } @@ -70,7 +78,7 @@ class DiagnosticService { performance.mark("end"); performance.measure("eventLoopDelay", "start", "end"); const entries = performance.getEntriesByName("eventLoopDelay"); - if (entries.length > 0) { + if (entries.length > 0 && entries[0] !== undefined) { eventLoopDelay = entries[0].duration; } diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts new file mode 100644 index 000000000..d30492bcb --- /dev/null +++ b/server/src/service/business/incidentService.ts @@ -0,0 +1,217 @@ +const SERVICE_NAME = "incidentService"; +import type { Monitor } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import { ParseBoolean } from "@/utils/utils.js"; +import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; +import type { Incident } from "@/types/index.js"; + +const dateRangeLookup: Record = { + 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 IncidentService { + static SERVICE_NAME = SERVICE_NAME; + + private logger: any; + private incidentsRepository: IIncidentsRepository; + private monitorsRepository: IMonitorsRepository; + private usersRepository: IUsersRepository; + + constructor({ + logger, + incidentsRepository, + monitorsRepository, + usersRepository, + }: { + logger: any; + incidentsRepository: IIncidentsRepository; + monitorsRepository: IMonitorsRepository; + usersRepository: IUsersRepository; + }) { + this.logger = logger; + this.incidentsRepository = incidentsRepository; + this.monitorsRepository = monitorsRepository; + this.usersRepository = usersRepository; + } + + get serviceName() { + return IncidentService.SERVICE_NAME; + } + + handleIncident = async (monitor: Monitor, code: number): Promise => { + const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); + // Monitor is down, create an incident + if (monitor.status === false) { + if (activeIncident) { + return activeIncident; + } else { + const incident = { + monitorId: monitor.id, + teamId: monitor.teamId, + startTime: Date.now().toString(), + status: true, + statusCode: code, + }; + return await this.incidentsRepository.create(incident); + } + } + + // Monitor is up, resolve active incidents + if (!activeIncident) { + return null; + } + activeIncident.status = false; + activeIncident.endTime = Date.now().toString(); + activeIncident.resolutionType = "automatic"; + return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident); + }; + + resolveIncident = async (incidentId: string, userId: string, teamId: string, comment?: string) => { + try { + if (!incidentId) { + throw new AppError({ message: "No incident ID in request", service: SERVICE_NAME, method: "resolveIncident" }); + } + + if (!userId) { + throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "resolveIncident" }); + } + + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "resolveIncident" }); + } + + const incident = await this.incidentsRepository.findActiveByIncidentId(incidentId, teamId); + + if (!incident) { + throw new AppError({ message: "Incident not found", service: SERVICE_NAME, method: "resolveIncident" }); + } + + if (incident.status === false) { + throw new AppError({ message: "Incident is already resolved", service: SERVICE_NAME, method: "resolveIncident" }); + } + + incident.resolutionType = "manual"; + incident.status = false; + incident.resolvedBy = userId; + incident.comment = comment || null; + incident.endTime = Date.now().toString(); + + const resolvedIncident = await this.incidentsRepository.updateById(incident.id, teamId, incident); + + this.logger.debug({ + service: SERVICE_NAME, + method: "resolveIncidentManually", + message: `Incident manually resolved by user`, + details: resolvedIncident.id, + }); + + return resolvedIncident; + } catch (error: any) { + this.logger.error({ + service: SERVICE_NAME, + method: "resolveIncident", + message: error.message, + details: incidentId, + stack: error.stack, + }); + throw error; + } + }; + + getIncidentsByTeam = async ( + teamId: string, + sortOrder: string, + dateRange: string, + page: string, + rowsPerPage: string, + status: string, + monitorId: string, + resolutionType: string + ) => { + try { + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentsByTeam", status: 400 }); + } + + const startDate = dateRangeLookup[dateRange]; + + const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; + const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20; + const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status); + + const incidents = await this.incidentsRepository.findByTeamId( + teamId, + startDate, + parsedPage, + parsedRowsPerPage, + sortOrder, + parsedStatus, + monitorId, + resolutionType + ); + + const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType); + + return { incidents, count }; + } catch (error: any) { + this.logger.error({ + service: SERVICE_NAME, + method: "getIncidentsByTeam", + message: error.message, + details: teamId, + stack: error.stack, + }); + throw error; + } + }; + + getIncidentSummary = async (teamId: string, limit?: string) => { + try { + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 }); + } + + const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10; + const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit); + + return summary; + } catch (error: any) { + this.logger.error({ + service: SERVICE_NAME, + method: "getIncidentSummary", + message: error.message, + details: teamId, + stack: error.stack, + }); + throw error; + } + }; + + getIncidentById = async (incidentId: string, teamId: string) => { + try { + const incident = await this.incidentsRepository.findById(incidentId, teamId); + const monitor = await this.monitorsRepository.findById(incident.monitorId, teamId); + let user = null; + if (incident.resolvedBy) { + user = await this.usersRepository.findById(incident.resolvedBy); + } + return { incident, monitor, user }; + } catch (error: any) { + this.logger.error({ + service: SERVICE_NAME, + method: "getIncidentById", + message: error.message, + details: incidentId, + stack: error.stack, + }); + throw error; + } + }; +} + +export default IncidentService; diff --git a/server/src/service/business/inviteService.js b/server/src/service/business/inviteService.js deleted file mode 100644 index 369ed195a..000000000 --- a/server/src/service/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/business/inviteService.ts b/server/src/service/business/inviteService.ts new file mode 100644 index 000000000..eee0f993c --- /dev/null +++ b/server/src/service/business/inviteService.ts @@ -0,0 +1,62 @@ +import type { Invite } from "@/types/index.js"; +import type { IInvitesRepository } from "@/repositories/index.js"; +import { AppError } from "@/utils/AppError.js"; + +const SERVICE_NAME = "inviteService"; + +class InviteService { + static SERVICE_NAME = SERVICE_NAME; + + private settingsService: any; + private emailService: any; + private invitesRepository: IInvitesRepository; + + constructor({ + invitesRepository, + settingsService, + emailService, + }: { + invitesRepository: IInvitesRepository; + settingsService: any; + emailService: any; + }) { + this.invitesRepository = invitesRepository; + this.settingsService = settingsService; + this.emailService = emailService; + } + + get serviceName() { + return InviteService.SERVICE_NAME; + } + + getInviteToken = async ({ invite, teamId }: { invite: Partial; teamId: string }) => { + invite.teamId = teamId; + const inviteToken = await this.invitesRepository.create(invite); + return inviteToken; + }; + + sendInviteEmail = async ({ invite, firstName }: { invite: Partial; firstName: any }) => { + const inviteToken = await this.invitesRepository.create(invite); + 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(invite.email, "Welcome to Uptime Monitor", html); + if (!result) { + throw new AppError({ + message: "Failed to send invite e-mail... Please verify your settings.", + service: SERVICE_NAME, + method: "sendInviteEmail", + status: 500, + }); + } + }; + + verifyInviteToken = async ({ inviteToken }: { inviteToken: string }) => { + return await this.invitesRepository.findByToken(inviteToken); + }; +} + +export default InviteService; diff --git a/server/src/service/business/maintenanceWindowService.js b/server/src/service/business/maintenanceWindowService.js deleted file mode 100644 index 9d52702e9..000000000 --- a/server/src/service/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/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts new file mode 100644 index 000000000..faf0ee943 --- /dev/null +++ b/server/src/service/business/maintenanceWindowService.ts @@ -0,0 +1,85 @@ +import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; +import { ParseBoolean } from "@/utils/utils.js"; +import { AppError } from "@/utils/AppError.js"; + +const SERVICE_NAME = "maintenanceWindowService"; + +class MaintenanceWindowService { + static SERVICE_NAME = SERVICE_NAME; + private monitorsRepository: IMonitorsRepository; + private maintenanceWindowsRepository: IMaintenanceWindowsRepository; + + constructor({ + monitorsRepository, + maintenanceWindowsRepository, + }: { + monitorsRepository: IMonitorsRepository; + maintenanceWindowsRepository: IMaintenanceWindowsRepository; + }) { + this.monitorsRepository = monitorsRepository; + this.maintenanceWindowsRepository = maintenanceWindowsRepository; + } + + get serviceName() { + return MaintenanceWindowService.SERVICE_NAME; + } + + createMaintenanceWindow = async ({ teamId, body }: { teamId: string; body: any }) => { + const monitorIds = body.monitors; + const monitors = await this.monitorsRepository.findByIds(monitorIds); + + const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId); + + if (unauthorizedMonitors.length > 0) { + throw new AppError({ + message: "Unauthorized to create maintenance window for one or more monitors", + service: SERVICE_NAME, + method: "createMaintenanceWindow", + status: 403, + }); + } + + const dbTransactions = monitorIds.map((monitorId: string) => { + return this.maintenanceWindowsRepository.create({ + 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 }: { id: string; teamId: string }) => { + return await this.maintenanceWindowsRepository.findById(id, teamId); + }; + + getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => { + let { active, page, rowsPerPage, field, order } = query || {}; + + active = typeof active === "undefined" ? undefined : ParseBoolean(active); + page = parseInt(page) || 0; + rowsPerPage = parseInt(rowsPerPage) || 10; + + const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, active, page, rowsPerPage, field, order); + const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active); + return { maintenanceWindows, maintenanceWindowCount }; + }; + + getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { + return await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId); + }; + + deleteMaintenanceWindow = async ({ id, teamId }: { id: string; teamId: string }) => { + return await this.maintenanceWindowsRepository.deleteById(id, teamId); + }; + + editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => { + return await this.maintenanceWindowsRepository.updateById(id, teamId, body); + }; +} + +export default MaintenanceWindowService; diff --git a/server/src/service/business/monitorService.js b/server/src/service/business/monitorService.js deleted file mode 100644 index 637870b71..000000000 --- a/server/src/service/business/monitorService.js +++ /dev/null @@ -1,272 +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; - }; -} - -export default MonitorService; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts new file mode 100644 index 000000000..38fcaef78 --- /dev/null +++ b/server/src/service/business/monitorService.ts @@ -0,0 +1,475 @@ +import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; +import { type Monitor } from "@/types/index.js"; +import type { + MonitorType, + MonitorsWithChecksByTeamIdResult, + UptimeDetailsResult, + HardwareDetailsResult, + PageSpeedDetailsResult, +} from "@/types/monitor.js"; +import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js"; +import fs from "fs"; +import { fileURLToPath } from "url"; +import path from "path"; +import { AppError } from "@/utils/AppError.js"; +import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; + +const SERVICE_NAME = "MonitorService"; +type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; + +export interface IMonitorService { + readonly serviceName: string; + + // create + createMonitor(teamId: string, userId: string, body: Monitor): Promise; + createBulkMonitors(monitors: Array, userId: string, teamId: string): Promise; + addDemoMonitors(args: { userId: string; teamId: string }): Promise; + + // read + getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; + getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getMonitorById(args: { teamId: string; monitorId: string }): Promise; + getMonitorsByTeamId(args: { + teamId: string; + limit?: number; + type?: MonitorType | MonitorType[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; + }): Promise; + getMonitorsWithChecksByTeamId(args: { + teamId: string; + limit?: number; + type?: MonitorType | MonitorType[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; + explain?: boolean; + }): Promise; + getAllGames(): any; + getGroupsByTeamId(args: { teamId: string }): Promise; + + // update + editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; + pauseMonitor(args: { teamId: string; monitorId: string }): Promise; + + // delete + deleteMonitor(args: { teamId: string; monitorId: string }): Promise; + deleteAllMonitors(args: { teamId: string }): Promise; + + // other + sendTestEmail(args: { to: string }): Promise; + exportMonitorsToJSON(args: { teamId: string }): Promise; +} + +export class MonitorService implements IMonitorService { + static SERVICE_NAME = SERVICE_NAME; + + private jobQueue: ISuperSimpleQueue; + private emailService: any; + private logger: any; + private games: any; + private monitorsRepository: IMonitorsRepository; + private checksRepository: IChecksRepository; + private monitorStatsRepository: IMonitorStatsRepository; + private statusPagesRepository: IStatusPagesRepository; + + constructor({ + jobQueue, + emailService, + logger, + games, + monitorsRepository, + checksRepository, + monitorStatsRepository, + statusPagesRepository, + }: { + jobQueue: ISuperSimpleQueue; + emailService: any; + logger: any; + games: any; + monitorsRepository: IMonitorsRepository; + checksRepository: IChecksRepository; + monitorStatsRepository: IMonitorStatsRepository; + statusPagesRepository: IStatusPagesRepository; + }) { + this.jobQueue = jobQueue; + this.emailService = emailService; + this.logger = logger; + this.games = games; + this.monitorsRepository = monitorsRepository; + this.checksRepository = checksRepository; + this.monitorStatsRepository = monitorStatsRepository; + this.statusPagesRepository = statusPagesRepository; + } + + get serviceName(): string { + return MonitorService.SERVICE_NAME; + } + + private getDateRange = (dateRange: DateRangeKey) => { + 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(), + }; + }; + + private getDateFormat = (dateRange: DateRangeKey): string => { + 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", + all: "%Y-%m-%dT00:00:00Z", + }; + return formatLookup[dateRange]; + }; + + createMonitor = async (teamId: string, userId: string, body: Monitor): Promise => { + const monitor = await this.monitorsRepository.create(body, teamId, userId); + if (!monitor) { + throw new AppError({ message: "Failed to create monitor", status: 500, service: SERVICE_NAME, method: "createMonitor" }); + } + + this.jobQueue.addJob(monitor.id, monitor); + }; + + createBulkMonitors = async (monitors: Array, userId: string, teamId: string): Promise => { + const createdMonitors = await this.monitorsRepository.createBulkMonitors(monitors); + if (!monitors || monitors.length === 0) { + throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createBulkMonitors" }); + } + + await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor))); + return createdMonitors; + }; + + addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise => { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const demoMonitorsPath = path.resolve(__dirname, "../../utils/demoMonitors.json"); + + const demoData = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8")); + const monitors: Monitor[] = demoData.map((monitor: Monitor) => { + return { + userId, + teamId, + name: monitor.name, + description: monitor.name, + type: "http", + url: monitor.url, + interval: 60000, + }; + }); + const demoMonitors = await this.monitorsRepository.createBulkMonitors(monitors); + + await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor))); + return demoMonitors; + }; + + getUptimeDetailsById = async ({ + teamId, + monitorId, + dateRange, + normalize, + }: { + teamId: string; + monitorId: string; + dateRange: string; + normalize?: boolean; + }): Promise => { + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); + } + const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const { start, end } = this.getDateRange(rangeKey); + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + type: monitor.type, + }); + const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); + + if ( + checksData.monitorType !== "http" && + checksData.monitorType !== "ping" && + checksData.monitorType !== "docker" && + checksData.monitorType !== "port" && + checksData.monitorType !== "game" + ) { + throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 }); + } + + return { + monitorData: { + monitor, + groupedChecks: NormalizeDataUptimeDetails(checksData.groupedChecks, 10, 100), + groupedUpChecks: NormalizeDataUptimeDetails(checksData.groupedUpChecks, 10, 100), + groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100), + groupedAvgResponseTime: checksData.avgResponseTime, + groupedUptimePercentage: checksData.uptimePercentage, + }, + monitorStats, + }; + }; + + getHardwareDetailsById = async ({ + teamId, + monitorId, + dateRange, + }: { + teamId: string; + monitorId: string; + dateRange: string; + }): Promise => { + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); + } + if (monitor.type !== "hardware") { + throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 }); + } + + const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const { start, end } = this.getDateRange(rangeKey); + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + type: monitor.type, + }); + + if (checksData.monitorType !== "hardware") { + throw new AppError({ message: "Unable to load hardware stats for this monitor", status: 500 }); + } + + const stats = { + aggregateData: checksData.aggregateData, + upChecks: checksData.upChecks, + checks: checksData.checks, + }; + + return { + ...monitor, + stats, + }; + }; + + getPageSpeedDetailsById = async ({ + teamId, + monitorId, + dateRange, + }: { + teamId: string; + monitorId: string; + dateRange: string; + }): Promise => { + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); + } + if (monitor.type !== "pagespeed") { + throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 }); + } + + const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const { start, end } = this.getDateRange(rangeKey); + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + type: monitor.type, + }); + + if (checksData.monitorType !== "pagespeed") { + throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 }); + } + + const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); + + return { + monitor: { + ...monitor, + checks: checksData.checks, + }, + monitorStats, + }; + }; + getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + return monitor; + }; + + getMonitorsByTeamId = async ({ + teamId, + type, + filter, + }: { + teamId: string; + type?: MonitorType | MonitorType[]; + filter?: string; + }): Promise => { + const monitors = await this.monitorsRepository.findByTeamId(teamId, { + type, + filter, + }); + return monitors; + }; + + getMonitorsWithChecksByTeamId = async ({ + teamId, + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + explain, + }: { + teamId: string; + limit?: number; + type?: MonitorType | MonitorType[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; + explain?: boolean; + }): Promise => { + const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId, { type }); + const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); + const monitors = await this.monitorsRepository.findByTeamId(teamId, { + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + }); + + const monitorsList = (monitors ?? []) as Monitor[]; + const snapshotTypes: MonitorType[] = ["hardware"]; + const requestedTypes = Array.isArray(type) ? type : type ? [type] : []; + const snapshotOnlyRequest = + requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType)); + + const limitPerMonitor = snapshotOnlyRequest ? 1 : 25; + const checksMap = await this.checksRepository.findLatestByMonitorIds( + monitorsList.map((monitor) => monitor.id), + { limitPerMonitor } + ); + + const monitorsWithChecks = monitorsList.map((monitor: Monitor) => { + const rawChecks = checksMap[monitor.id] ?? []; + const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type); + const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100); + return { + ...monitor, + checks, + }; + }); + + return { summary: summary ?? null, count, monitors: monitorsWithChecks }; + }; + + getAllGames = (): any => { + return this.games; + }; + + getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { + const groups = await this.monitorsRepository.findGroupsByTeamId(teamId); + return groups; + }; + + editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { + const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); + await this.jobQueue.updateJob(editedMonitor); + return editedMonitor; + }; + + pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); + monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); + return monitor; + }; + + deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); + await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error removing monitor ${monitor.id} with name ${monitor.name} from status pages`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); + await this.jobQueue.deleteJob(monitor); + return monitor; + }; + + deleteAllMonitors = async ({ teamId }: { teamId: string }): Promise => { + const { monitors, deletedCount } = await this.monitorsRepository.deleteByTeamId(teamId); + await Promise.all( + monitors.map(async (monitor) => { + try { + await this.jobQueue.deleteJob(monitor); + await this.checksRepository.deleteByMonitorId(monitor.id); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id); + } catch (error: any) { + 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; + }; + + sendTestEmail = async ({ to }: { to: string }): Promise => { + const subject = "Test email from Checkmate"; + 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 new AppError({ message: "Failed to send test email.", service: SERVICE_NAME, method: "sendTestEmail", status: 500 }); + } + + return messageId; + }; + + exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { + const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); + + if (!monitors || monitors.length === 0) { + throw new AppError({ message: "No monitors found to export.", service: SERVICE_NAME, method: "exportMonitorsToJSON", status: 400 }); + } + + return monitors; + }; +} diff --git a/server/src/service/business/statusPageService.ts b/server/src/service/business/statusPageService.ts new file mode 100644 index 000000000..971f6897f --- /dev/null +++ b/server/src/service/business/statusPageService.ts @@ -0,0 +1,42 @@ +import { type IStatusPagesRepository } from "@/repositories/index.js"; +import { StatusPage } from "@/types/index.js"; +export interface IStatusPageService { + createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; + getStatusPageByUrl(url: string): Promise; + getStatusPagesByTeamId(teamId: string): Promise; + updateStatusPage(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; + + deleteStatusPage(statusPageId: string, teamId: string): Promise; +} + +export class StatusPageService implements IStatusPageService { + private statusPagesRepository: IStatusPagesRepository; + constructor(statusPagesRepository: IStatusPagesRepository) { + this.statusPagesRepository = statusPagesRepository; + } + + createStatusPage = async ( + userId: string, + teamId: string, + image: Express.Multer.File | undefined, + data: Partial + ): Promise => { + return await this.statusPagesRepository.create(userId, teamId, image, data); + }; + + getStatusPageByUrl = async (url: string): Promise => { + return await this.statusPagesRepository.findByUrl(url); + }; + + getStatusPagesByTeamId = async (teamId: string): Promise => { + return await this.statusPagesRepository.findByTeamId(teamId); + }; + + updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise => { + return await this.statusPagesRepository.updateById(id, teamId, image, data); + }; + + deleteStatusPage = async (statusPageId: string, teamId: string): Promise => { + return await this.statusPagesRepository.deleteById(statusPageId, teamId); + }; +} diff --git a/server/src/service/business/userService.js b/server/src/service/business/userService.js deleted file mode 100644 index 0a965bced..000000000 --- a/server/src/service/business/userService.js +++ /dev/null @@ -1,215 +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); - }; -} -export default UserService; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts new file mode 100644 index 000000000..a0273af99 --- /dev/null +++ b/server/src/service/business/userService.ts @@ -0,0 +1,299 @@ +import { + IInvitesRepository, + IMonitorsRepository, + IRecoveryTokensRepository, + IUsersRepository, + ISettingsRepository, + ITeamsRepository, +} from "@/repositories/index.js"; +import type { User } from "@/types/index.js"; +import bcrypt from "bcryptjs"; +import { AppError } from "@/utils/AppError.js"; +import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; + +const SERVICE_NAME = "userService"; + +class UserService { + static SERVICE_NAME = SERVICE_NAME; + + private emailService: any; + private settingsService: any; + private logger: any; + private jwt: any; + private jobQueue: ISuperSimpleQueue; + private crypto: any; + private monitorsRepository: IMonitorsRepository; + private usersRepository: IUsersRepository; + private invitesRepository: IInvitesRepository; + private recoveryTokensRepository: IRecoveryTokensRepository; + private settingsRepository: ISettingsRepository; + private teamsRepository: ITeamsRepository; + + constructor({ + crypto, + emailService, + settingsService, + logger, + jwt, + jobQueue, + monitorsRepository, + usersRepository, + invitesRepository, + recoveryTokensRepository, + settingsRepository, + teamsRepository, + }: { + crypto: any; + emailService: any; + settingsService: any; + logger: any; + jwt: any; + jobQueue: ISuperSimpleQueue; + monitorsRepository: IMonitorsRepository; + usersRepository: IUsersRepository; + invitesRepository: IInvitesRepository; + recoveryTokensRepository: IRecoveryTokensRepository; + settingsRepository: ISettingsRepository; + teamsRepository: ITeamsRepository; + }) { + this.emailService = emailService; + this.settingsService = settingsService; + this.logger = logger; + this.jwt = jwt; + this.jobQueue = jobQueue; + this.crypto = crypto; + this.monitorsRepository = monitorsRepository; + this.usersRepository = usersRepository; + this.invitesRepository = invitesRepository; + this.recoveryTokensRepository = recoveryTokensRepository; + this.settingsRepository = settingsRepository; + this.teamsRepository = teamsRepository; + } + + get serviceName() { + return UserService.SERVICE_NAME; + } + + issueToken = (payload: any, appSettings: any) => { + const tokenTTL = appSettings?.jwtTTL ?? "2h"; + const tokenSecret = appSettings?.jwtSecret; + const payloadData = payload; + return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); + }; + + registerUser = async (user: Partial, inviteToken: string, file: any) => { + // Create a new user + // If superAdmin exists, a token should be attached to all further register requests + const superAdminExists = await this.usersRepository.findSuperAdmin(); + if (superAdminExists) { + const invite = await this.invitesRepository.findByTokenAndDelete(inviteToken); + user.role = invite.role; + user.teamId = invite.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.settingsRepository.update({ jwtSecret }); + // Create a new team + if (!user.email) { + throw new AppError({ message: "Email is required for first user", service: SERVICE_NAME, method: "registerUser", status: 400 }); + } + const team = await this.teamsRepository.create(user.email); + user.teamId = team.id; + } + + const newUser = await this.usersRepository.create({ ...user }, file); + + this.logger.debug({ + message: "New user created", + service: SERVICE_NAME, + method: "registerUser", + details: newUser.id, + }); + + delete newUser.profileImage; + delete newUser.avatarImage; + + const appSettings = await this.settingsService.getSettings(); + + const token = this.issueToken(newUser, appSettings); + + try { + const html = await this.emailService.buildEmail("welcomeEmailTemplate", { + name: newUser.firstName, + }); + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + }); + } catch (error: any) { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "registerUser", + stack: error.stack, + }); + } + + return { user: newUser, token }; + }; + + loginUser = async (email: string, password: string) => { + // Check if user exists + const user = await this.usersRepository.findByEmail(email); + // Compare password + const match = await bcrypt.compare(password, user.password); + + if (match !== true) { + throw new AppError({ message: "Incorrect password", service: SERVICE_NAME, status: 401 }); + } + + // Remove password from user object. Should this be abstracted to DB layer? + const userWithoutPassword = { ...user }; + userWithoutPassword.password = ""; + 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: Partial, file: any, currentUser: any) => { + // 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.usersRepository.findByEmail(currentUser.email); + // Compare passwords + const match = await bcrypt.compare(updates?.password, user.password); + // If not a match, throw a 403 + // 403 instead of 401 to avoid triggering axios interceptor + if (!match) { + throw new AppError({ message: "Incorrect current password", service: SERVICE_NAME, status: 403 }); + } + // If a match, update the password + updates.password = updates.newPassword; + } + + return await this.usersRepository.updateById(currentUser.id, updates, file); + }; + + checkSuperadminExists = async () => { + return await this.usersRepository.findSuperAdmin(); + }; + + requestRecovery = async (email: string) => { + const user = await this.usersRepository.findByEmail(email); + + // Delete existing tokens + await this.recoveryTokensRepository.deleteManyByEmail(email); + const recoveryToken = await this.recoveryTokensRepository.create(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: string) => { + // Throws if token not found, validating + await this.recoveryTokensRepository.findByToken(recoveryToken); + }; + + resetPassword = async (password: string, recoveryToken: string) => { + const existingToken = await this.recoveryTokensRepository.findByToken(recoveryToken); + const existingUser = await this.usersRepository.findByEmail(existingToken.email); + + const match = await bcrypt.compare(password, existingUser.password); + if (match === true) { + throw new AppError({ message: "New password cannot be same as old password", service: SERVICE_NAME, status: 400 }); + } + + existingUser.password = password; + await this.usersRepository.updateById(existingUser.id, existingUser, null); + await this.recoveryTokensRepository.deleteManyByEmail(existingUser.email); + + existingUser.password = ""; + existingUser.profileImage = undefined; + + const token = this.issueToken(existingUser, await this.settingsService.getSettings()); + + return { user: existingUser, token }; + }; + + deleteUser = async (user: User) => { + const email = user?.email; + if (!email) { + throw new AppError({ message: "No email in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); + } + + const teamId = user?.teamId; + const userId = user?.id; + + if (!teamId) { + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); + } + + if (!userId) { + throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); + } + + const roles = user?.role; + if (roles.includes("demo")) { + throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUser", status: 400 }); + } + + // 1. Find all the monitors associated with the team ID if superadmin + const res = await this.monitorsRepository.findByTeamId(teamId, {}); + + if (roles.includes("superadmin")) { + // 2. Remove all jobs, delete checks and alerts + res && + res?.length > 0 && + (await Promise.all( + res.map(async (monitor) => { + await this.jobQueue.deleteJob(monitor.id); + }) + )); + } + // 6. Delete the user by id + await this.usersRepository.deleteById(userId); + }; + + getAllUsers = async () => { + return await this.usersRepository.findAll(); + }; + + getUserById = async (roles: any, userId: any) => { + if (!roles.includes("superadmin")) { + throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 }); + } + const user = await this.usersRepository.findById(userId); + + return user; + }; + + editUserById = async (userId: any, patch: Partial) => { + await this.usersRepository.updateById(userId, patch, null); + }; + + setPasswordByUserId = async (userId: any, password: string) => { + const updatedUser = await this.usersRepository.updateById(userId, { password }, null); + return updatedUser; + }; +} +export default UserService; diff --git a/server/src/service/index.ts b/server/src/service/index.ts new file mode 100644 index 000000000..7ec49288f --- /dev/null +++ b/server/src/service/index.ts @@ -0,0 +1,14 @@ +export * from "@/service/business/monitorService.js"; +export * from "@/service/infrastructure/networkService.js"; +export * from "@/service/infrastructure/notificationProviders/discord.js"; +export * from "@/service/infrastructure/notificationProviders/email.js"; +export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationProviders/pagerduty.js"; +export * from "@/service/infrastructure/notificationProviders/matrix.js"; +export * from "@/service/infrastructure/notificationProviders/slack.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationsService.js"; +export * from "@/service/infrastructure/statusService.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/matrix.js"; +export * from "@/service/business/statusPageService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js deleted file mode 100644 index c1e82af65..000000000 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ /dev/null @@ -1,172 +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) { - await this.addJob(monitor._id, monitor); - } - 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/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts new file mode 100644 index 000000000..8fd55ff70 --- /dev/null +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -0,0 +1,254 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; +import Scheduler from "super-simple-scheduler"; +const SERVICE_NAME = "JobQueue"; + +type QueueJobFailure = { + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + failedAt: number | null; + failCount: number; + failReason: string | null; +}; + +type QueueMetrics = { + jobs: number; + activeJobs: number; + failingJobs: number; + jobsWithFailures: QueueJobFailure[]; + totalRuns: number; + totalFailures: number; +}; + +type QueueJobSummary = { + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + monitorInterval: number | null; + active: boolean; + lockedAt: number | null; + runCount: number; + failCount: number; + failReason: string | null; + lastRunAt: number | null; + lastFinishedAt: number | null; + lastRunTook: number | null; + lastFailedAt: number | null; +}; + +export interface ISuperSimpleQueue { + readonly serviceName: string; + init(): Promise; + addJob(monitorId: string, monitor: any): Promise; + deleteJob(monitor: any): Promise; + pauseJob(monitor: any): Promise; + resumeJob(monitor: any): Promise; + updateJob(monitor: any): Promise; + shutdown(): Promise; + getMetrics(): Promise; + getJobs(): Promise; + flushQueues(): Promise<{ success: boolean }>; + obliterate(): Promise; +} + +class SuperSimpleQueue implements ISuperSimpleQueue { + static SERVICE_NAME = SERVICE_NAME; + + private logger: any; + private helper: any; + private monitorsRepository: IMonitorsRepository; + private readonly scheduler: Scheduler; + + constructor({ + logger, + helper, + monitorsRepository, + scheduler, + }: { + logger: any; + helper: any; + monitorsRepository: IMonitorsRepository; + scheduler: Scheduler; + }) { + this.logger = logger; + this.helper = helper; + this.monitorsRepository = monitorsRepository; + this.scheduler = scheduler; + } + + get serviceName() { + return SuperSimpleQueue.SERVICE_NAME; + } + + static async create({ logger, helper, monitorsRepository }: { logger: any; helper: any; monitorsRepository: IMonitorsRepository }) { + const scheduler = new Scheduler({ + // storeType: "mongo", + // storeType: "redis", + logLevel: "debug", + // dbUri: envSettings.dbConnectionString, + }); + const instance = new SuperSimpleQueue({ logger, helper, monitorsRepository, scheduler }); + await instance.init(); + return instance; + } + + init = async () => { + try { + this.scheduler.start(); + + this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); + const monitors = await this.monitorsRepository.findAll(); + if (!monitors) { + return true; + } + 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: string, monitor: any) => { + this.scheduler.addJob({ + id: monitorId, + template: "monitor-job", + repeat: monitor.interval, + active: monitor.isActive, + data: monitor, + }); + }; + + deleteJob = async (monitor: any) => { + this.scheduler.removeJob(monitor.id); + }; + + pauseJob = async (monitor: any) => { + const result = await this.scheduler.pauseJob(monitor.id); + 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: any) => { + const result = await this.scheduler.resumeJob(monitor.id); + 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: any) => { + this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); + }; + + shutdown = async () => { + this.scheduler.stop(); + }; + + getMetrics = async () => { + const jobs = await this.scheduler.getJobs(); + const metrics = jobs.reduce( + (acc, job) => { + const runCount = job.runCount ?? 0; + const failCount = job.failCount ?? 0; + const lastFailedAt = job.lastFailedAt ?? 0; + const lastRunAt = job.lastRunAt ?? 0; + acc.totalRuns += runCount; + acc.totalFailures += failCount; + acc.jobs++; + if (failCount > 0 && lastFailedAt >= lastRunAt) { + acc.failingJobs++; + } + + if (job.lockedAt) { + acc.activeJobs++; + } + + if (failCount > 0) { + acc.jobsWithFailures.push({ + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + failedAt: job.lastFailedAt ?? null, + failCount, + failReason: job.lastFailReason ?? null, + }); + } + return acc; + }, + { + jobs: 0, + activeJobs: 0, + failingJobs: 0, + jobsWithFailures: [] as Array<{ + monitorId: string | number; + monitorUrl: any; + monitorType: any; + failedAt: number | null; + failCount: number; + failReason: string | null; + }>, + 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 ?? null, + runCount: job.runCount ?? 0, + failCount: job.failCount ?? 0, + failReason: job.lastFailReason ?? null, + lastRunAt: job.lastRunAt ?? null, + lastFinishedAt: job.lastFinishedAt ?? null, + lastRunTook: job.lockedAt ? null : (job.lastFinishedAt ?? 0) - (job.lastRunAt ?? 0), + lastFailedAt: job.lastFailedAt ?? null, + }; + }); + }; + + flushQueues = async () => { + const stopRes = await this.scheduler.stop(); + const flushRes = await this.scheduler.flushJobs(); + const initRes = await this.init(); + return { + success: Boolean(stopRes && flushRes && initRes), + }; + }; + + obliterate = async () => { + console.log("obliterate not implemented"); + }; +} + +export default SuperSimpleQueue; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js deleted file mode 100644 index b348e2534..000000000 --- a/server/src/service/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").Database, - * logger: import("../logger").Logger, - * networkService: import("../networkService").NetworkService, - * statusService: import("../statusService").StatusService, - * notificationService: import("../notificationService").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/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts new file mode 100644 index 000000000..cf3824d60 --- /dev/null +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -0,0 +1,156 @@ +const SERVICE_NAME = "JobQueueHelper"; +import type { Monitor } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; +import IncidentService from "@/service/business/incidentService.js"; +import { IMaintenanceWindowsRepository } from "@/repositories/index.js"; + +class SuperSimpleQueueHelper { + static SERVICE_NAME = SERVICE_NAME; + + private logger: any; + private networkService: INetworkService; + private statusService: IStatusService; + private notificationsService: INotificationsService; + private checkService: any; + private buffer: any; + private incidentService: IncidentService; + private maintenanceWindowsRepository: IMaintenanceWindowsRepository; + + constructor({ + logger, + networkService, + statusService, + notificationsService, + checkService, + buffer, + incidentService, + maintenanceWindowsRepository, + }: { + logger: any; + networkService: INetworkService; + statusService: IStatusService; + notificationsService: INotificationsService; + checkService: any; + buffer: any; + incidentService: IncidentService; + maintenanceWindowsRepository: IMaintenanceWindowsRepository; + }) { + this.logger = logger; + this.networkService = networkService; + this.statusService = statusService; + this.checkService = checkService; + this.buffer = buffer; + this.notificationsService = notificationsService; + this.incidentService = incidentService; + this.maintenanceWindowsRepository = maintenanceWindowsRepository; + } + + get serviceName() { + return SuperSimpleQueueHelper.SERVICE_NAME; + } + + getMonitorJob = () => { + return async (monitor: Monitor) => { + try { + const monitorId = monitor.id; + const teamId = monitor.teamId; + if (!monitorId) { + throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" }); + } + + // Step 1. Check for maintenacne window, if found, skip the check + const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); + if (maintenanceWindowActive) { + this.logger.debug({ + message: `Monitor ${monitorId} is in maintenance window`, + service: SERVICE_NAME, + method: "getMonitorJob", + }); + return; + } + + // Step 2. Request monitor status + const status = await this.networkService.requestStatus(monitor); + if (!status) { + throw new Error("No network response"); + } + + // Step 3. Build check + const check = await this.checkService.buildCheck(status); + + // Step 4 Add check to buffer + this.buffer.addToBuffer({ check }); + + // Step 4. Update monitor status + const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); + + // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) + this.notificationsService + .handleNotifications(statusChangeResult.monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) + .catch((error: any) => { + this.logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`, + stack: error.stack, + }); + }); + + // Step 6. Handle incidents (best effort, don't wait) + if (statusChangeResult.statusChanged) { + this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code).catch((error: any) => { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error handling incident for job ${monitor.id}: ${error.message}`, + stack: error.stack, + }); + }); + } + } catch (error: any) { + this.logger.warn({ + message: error.message, + service: error.service || SERVICE_NAME, + method: error.method || "getMonitorJob", + stack: error.stack, + }); + throw error; + } + }; + }; + + async isInMaintenanceWindow(monitorId: string, teamId: string) { + const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId); + // Check for active maintenance window: + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => { + 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/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.js deleted file mode 100755 index ed48cc654..000000000 --- a/server/src/service/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/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts new file mode 100755 index 000000000..461001800 --- /dev/null +++ b/server/src/service/infrastructure/bufferService.ts @@ -0,0 +1,122 @@ +import type { Check } from "@/types/index.js"; + +const SERVICE_NAME = "BufferService"; + +class BufferService { + static SERVICE_NAME = SERVICE_NAME; + private BUFFER_TIMEOUT: number; + private logger: any; + private SERVICE_NAME: string; + private buffer: any[]; + private bufferTimer: NodeJS.Timeout | null = null; + private checksService: any; + + constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) { + this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute + console.log(this.BUFFER_TIMEOUT); + this.logger = logger; + this.checksService = checkService; + 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 }: { check: Check }) { + try { + this.buffer.push(check); + } catch (error: any) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "addToBuffer", + stack: error.stack, + }); + } + } + + removeCheckFromBuffer(checkToRemove: Check) { + try { + if (!checkToRemove) { + return false; + } + + const index = this.buffer.findIndex((check) => { + if (checkToRemove.id && check.id) { + return check.id.toString() === checkToRemove.id.toString(); + } + return ( + check.monitorId?.toString() === checkToRemove.metadata.monitorId && + check.teamId?.toString() === checkToRemove.metadata.teamId && + check.type === checkToRemove.metadata.type && + check.status === checkToRemove.status && + check.statusCode === checkToRemove.statusCode && + check.responseTime === checkToRemove.responseTime && + check.message === checkToRemove.message + ); + }); + + if (index !== -1) { + this.buffer.splice(index, 1); + return true; + } + + return false; + } catch (error: any) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "removeCheckFromBuffer", + stack: error.stack, + }); + return false; + } + } + + scheduleNextFlush() { + if (this.bufferTimer) { + clearTimeout(this.bufferTimer); + } + this.bufferTimer = setTimeout(async () => { + try { + await this.flushBuffer(); + } catch (error: any) { + 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() { + try { + if (this.buffer.length > 0) { + await this.checksService.createChecks(this.buffer); + } + } catch (error: any) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "flushBuffer", + stack: error.stack, + }); + } + + this.buffer = []; + } +} + +export default BufferService; diff --git a/server/src/service/infrastructure/emailService.js b/server/src/service/infrastructure/emailService.ts similarity index 69% rename from server/src/service/infrastructure/emailService.js rename to server/src/service/infrastructure/emailService.ts index bc040d51a..8598f112f 100755 --- a/server/src/service/infrastructure/emailService.js +++ b/server/src/service/infrastructure/emailService.ts @@ -1,28 +1,27 @@ import { fileURLToPath } from "url"; import path from "path"; +import { EmailTransportConfig } from "@/types/index.js"; 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) { + private settingsService: any; + private fs: any; + private path: any; + private compile: any; + private mjml2html: any; + private nodemailer: any; + private logger: any; + private transporter: any; + private templateLookup: Record; + private loadTemplate: (templateName: string) => Function; + + constructor(settingsService: any, fs: any, path: any, compile: any, mjml2html: any, nodemailer: any, logger: any) { this.settingsService = settingsService; this.fs = fs; this.path = path; @@ -31,6 +30,10 @@ class EmailService { this.nodemailer = nodemailer; this.logger = logger; this.init(); + this.templateLookup = {}; + this.loadTemplate = () => { + return () => {}; + }; } get serviceName() { @@ -38,18 +41,12 @@ class EmailService { } 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) { + } catch (error: any) { this.logger.error({ message: error.message, service: SERVICE_NAME, @@ -59,11 +56,6 @@ class EmailService { } }; - /** - * 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"), @@ -74,19 +66,14 @@ class EmailService { hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"), testEmailTemplate: this.loadTemplate("testEmailTemplate"), }; - - /** - * The email transporter used to send emails. - * @type {Object} - */ }; - buildEmail = async (template, context) => { + buildEmail = async (template: string, context: Record) => { try { - const mjml = this.templateLookup[template](context); + const mjml = this.templateLookup[template]?.(context); const html = await this.mjml2html(mjml); return html.html; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: SERVICE_NAME, @@ -96,8 +83,8 @@ class EmailService { } }; - sendEmail = async (to, subject, html, transportConfig) => { - let config; + sendEmail = async (to: string, subject: string, html: string, transportConfig?: EmailTransportConfig) => { + let config: EmailTransportConfig; if (typeof transportConfig !== "undefined") { config = transportConfig; } else { @@ -157,7 +144,7 @@ class EmailService { html: html, }); return info?.messageId; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: SERVICE_NAME, diff --git a/server/src/service/infrastructure/errorService.js b/server/src/service/infrastructure/errorService.js deleted file mode 100644 index 680bb3765..000000000 --- a/server/src/service/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/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.ts similarity index 56% rename from server/src/service/infrastructure/networkService.js rename to server/src/service/infrastructure/networkService.ts index 47ef6878f..7db8fc6e2 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.ts @@ -1,9 +1,141 @@ +import { HTTPError, RequestError } from "got"; +import type { Got, Response } from "got"; +import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; + +import CacheableLookup from "cacheable-lookup"; const SERVICE_NAME = "NetworkService"; -class NetworkService { +type MonitorStatusResponseOverrides = Partial, "monitorId" | "teamId" | "type">>; + +interface BuildStatusResponseArgs { + monitor: Monitor; + response?: Response | null; + error?: Error | RequestError | HTTPError | null; + payload?: T | null; + jsonPath?: string; + matchMethod?: MonitorStatusResponse["matchMethod"]; + expectedValue?: string; + extracted?: unknown; + overrides?: MonitorStatusResponseOverrides; +} + +export interface INetworkService { + readonly serviceName: string; + requestStatus(monitor: Monitor): Promise; + requestWebhook(type: string, url: string, body: any): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>; + requestPagerDuty(args: { message: string; routingKey: string; monitorUrl: string }): Promise; + requestMatrix(args: { homeserverUrl: string; accessToken: string; roomId: string; message: string }): Promise<{ + status: boolean; + code: number; + message: string; + payload?: unknown; + }>; +} + +class NetworkService implements INetworkService { static SERVICE_NAME = SERVICE_NAME; - constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) { + private TYPE_PING: string; + private TYPE_HTTP: string; + private TYPE_PAGESPEED: string; + private TYPE_HARDWARE: string; + private TYPE_DOCKER: string; + private TYPE_PORT: string; + private TYPE_GAME: string; + private SERVICE_NAME: string; + private NETWORK_ERROR: number; + private PING_ERROR: number; + + private axios: any; + private got: Got; + private https: any; + private jmespath: any; + private GameDig: any; + private ping: any; + private logger: any; + private Docker: any; + private net: any; + private settingsService: any; + + private buildStatusResponse = ({ + monitor, + response, + error, + payload, + jsonPath, + matchMethod, + expectedValue, + extracted, + overrides, + }: BuildStatusResponseArgs): MonitorStatusResponse => { + if (error) { + const statusResponse: MonitorStatusResponse = { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: this.NETWORK_ERROR, + message: error.message ?? "Network error", + responseTime: 0, + timings: undefined, + jsonPath, + matchMethod, + expectedValue, + extracted, + payload, + }; + if (error instanceof HTTPError || error instanceof RequestError) { + statusResponse.code = error?.response?.statusCode ?? this.NETWORK_ERROR; + statusResponse.message = error.message; + statusResponse.responseTime = error.timings?.phases?.total ?? 0; + statusResponse.timings = error.timings; + } + return { ...statusResponse, ...(overrides ?? {}) }; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: response?.ok ?? false, + code: response?.statusCode ?? this.NETWORK_ERROR, + message: response?.statusMessage ?? "", + responseTime: response?.timings?.phases?.total ?? 0, + timings: response?.timings, + payload: payload ?? response?.body, + jsonPath, + matchMethod, + expectedValue, + extracted, + ...(overrides ?? {}), + }; + }; + + constructor({ + axios, + got, + https, + jmespath, + GameDig, + ping, + logger, + http, + Docker, + net, + settingsService, + }: { + axios: any; + got: Got; + https: any; + jmespath: any; + GameDig: any; + ping: any; + logger: any; + http: any; + Docker: any; + net: any; + settingsService: any; + }) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; @@ -15,26 +147,37 @@ class NetworkService { this.NETWORK_ERROR = 5000; this.PING_ERROR = 5001; this.axios = axios; - this.got = got; 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 }, + }); + } + + get serviceName(): string { + return NetworkService.SERVICE_NAME; } // Helper functions - async timeRequest(operation) { + private async timeRequest(operation: () => Promise): Promise<{ response: T | null; responseTime: number; error: unknown }> { 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 }; + return { response, responseTime: elapsedMs, error: null }; } catch (error) { const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); return { response: null, responseTime: elapsedMs, error }; @@ -42,7 +185,7 @@ class NetworkService { } // Main entry point - async requestStatus(monitor) { + async requestStatus(monitor: Monitor): Promise { const type = monitor?.type || "unknown"; switch (type) { case this.TYPE_PING: @@ -60,17 +203,22 @@ class NetworkService { case this.TYPE_GAME: return await this.requestGame(monitor); default: - return await this.handleUnsupportedType(type); + return this.handleUnsupportedType(type); } } - async requestPing(monitor) { + private async requestPing(monitor: Monitor): Promise { try { if (!monitor?.url) { throw new Error("Monitor URL is required"); } - const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url)); + 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) { @@ -79,44 +227,49 @@ class NetworkService { throw new Error("Ping failed - no result returned"); } - const pingResponse = { - monitorId: monitor._id, - type: "ping", - status: response.alive, - code: 200, - responseTime, - message: "Success", + const pingResponse = this.buildStatusResponse({ + monitor, payload: response, - }; + overrides: { + status: (response as { alive?: boolean })?.alive ?? false, + code: 200, + message: "Success", + responseTime: (response as { time?: number })?.time ?? 0, + payload: response, + }, + }); if (error) { pingResponse.status = false; - pingResponse.code = 200; + pingResponse.code = this.PING_ERROR; pingResponse.message = "Ping failed"; return pingResponse; } return pingResponse; - } catch (err) { + } catch (err: any) { 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, - }; + private async requestHttp(monitor: Monitor): Promise { + const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; + const httpResponse = this.buildStatusResponse({ + monitor, + overrides: { + status: false, + code: this.NETWORK_ERROR, + message: "Request not executed", + }, + }); try { if (!url) { throw new Error("Monitor URL is required"); } - const config = { + const config: Record = { headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, }; @@ -130,7 +283,7 @@ class NetworkService { const response = await this.got(url, config); - let payload; + let payload: any; const contentType = response.headers["content-type"]; if (contentType && contentType.includes("application/json")) { @@ -143,12 +296,14 @@ class NetworkService { 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 || {}; + Object.assign(httpResponse, { + code: response.statusCode, + status: response.ok, + message: response.statusMessage ?? "", + responseTime: response.timings.phases.total || 0, + payload, + timings: response.timings, + }); if (!expectedValue && !jsonPath) { return httpResponse; @@ -165,7 +320,7 @@ class NetworkService { } else { httpResponse.code = 500; httpResponse.status = false; - httpResponse.message = this.stringService.httpExpectedValueFail; + httpResponse.message = "Expected value did not match"; return httpResponse; } } @@ -175,7 +330,7 @@ class NetworkService { const isJson = contentType?.includes("application/json"); if (!isJson) { httpResponse.status = false; - httpResponse.message = this.stringService.httpNotJson; + httpResponse.message = "Response is not JSON"; return httpResponse; } try { @@ -192,7 +347,7 @@ class NetworkService { } else { httpResponse.status = false; httpResponse.code = 500; - httpResponse.message = this.stringService.httpJsonPathFail; + httpResponse.message = "Expected value did not match"; httpResponse.extracted = extracted; return httpResponse; } @@ -204,19 +359,19 @@ class NetworkService { } else { httpResponse.status = false; httpResponse.code = 500; - httpResponse.message = this.stringService.httpJsonPathFail; + httpResponse.message = "Expected value did not match"; httpResponse.extracted = extracted; return httpResponse; } } } catch { httpResponse.status = false; - httpResponse.message = this.stringService.httpJsonPathError; + httpResponse.message = "Error evaluating JSON path"; return httpResponse; } } return httpResponse; - } catch (err) { + } catch (err: any) { if (err.name === "HTTPError" || err.name === "RequestError") { httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR; httpResponse.status = false; @@ -232,7 +387,7 @@ class NetworkService { } } - async requestPageSpeed(monitor) { + private async requestPageSpeed(monitor: Monitor): Promise { try { const url = monitor.url; if (!url) { @@ -254,24 +409,24 @@ class NetworkService { ...monitor, url: pageSpeedUrl, }); - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestPageSpeed"; throw err; } } - async requestHardware(monitor) { + private async requestHardware(monitor: Monitor): Promise { try { return await this.requestHttp(monitor); - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestHardware"; throw err; } } - async requestDocker(monitor) { + private async requestDocker(monitor: Monitor): Promise { try { if (!monitor.url) { throw new Error("Monitor URL is required"); @@ -282,10 +437,14 @@ class NetworkService { handleError: true, // Enable error handling }); - const dockerResponse = { - monitorId: monitor._id, - type: monitor.type, - }; + const dockerResponse = this.buildStatusResponse({ + monitor, + overrides: { + status: false, + code: this.NETWORK_ERROR, + message: "No response", + }, + }); const containers = await docker.listContainers({ all: true }); @@ -294,18 +453,18 @@ class NetworkService { // Priority-based matching to avoid ambiguity: // 1. Exact full ID match (64-char) - let exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); + let exactIdMatch = containers.find((c: any) => c.Id.toLowerCase() === normalizedInput); // 2. Exact container name match (case-insensitive) - let exactNameMatch = containers.find((c) => - c.Names.some((name) => { + let exactNameMatch = containers.find((c: any) => + c.Names.some((name: string) => { 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)); + let partialIdMatch = containers.find((c: any) => c.Id.toLowerCase().startsWith(normalizedInput)); // Select container based on priority let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; @@ -321,7 +480,7 @@ class NetworkService { dockerResponse.code = 404; dockerResponse.status = false; - dockerResponse.message = this.stringService.dockerNotFound; + dockerResponse.message = "Docker container not found"; return dockerResponse; } @@ -338,14 +497,15 @@ class NetworkService { 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()); + const { response, responseTime, error }: { response?: any; responseTime?: number; error?: any } = await this.timeRequest(() => + container.inspect() + ); dockerResponse.responseTime = responseTime; dockerResponse.status = response?.State?.Status === "running" ? true : false; @@ -360,14 +520,14 @@ class NetworkService { } return dockerResponse; - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestDocker"; throw err; } } - async requestPort(monitor) { + private async requestPort(monitor: Monitor): Promise { try { const { url, port } = monitor; const { response, responseTime, error } = await this.timeRequest(async () => { @@ -390,54 +550,56 @@ class NetworkService { reject(new Error("Connection timeout")); }); - socket.on("error", (err) => { + socket.on("error", (err: any) => { socket.destroy(); reject(err); }); }); }); - const portResponse = { - code: 200, - status: response.success, - message: this.stringService.portSuccess, - monitorId: monitor._id, - type: monitor.type, - responseTime, - }; + const portResponse = this.buildStatusResponse({ + monitor, + overrides: { + code: 200, + status: (response as { success?: boolean })?.success ?? false, + message: "Port check successful", + responseTime, + }, + }); if (error) { portResponse.code = this.NETWORK_ERROR; portResponse.status = false; - portResponse.message = this.stringService.portFail; + portResponse.message = "Port check failed"; return portResponse; } return portResponse; - } catch (error) { + } catch (error: any) { error.service = this.SERVICE_NAME; error.method = "requestTCP"; throw error; } } - async requestGame(monitor) { + private async requestGame(monitor: Monitor): Promise { try { const { url, port, gameId } = monitor; - const gameResponse = { - code: 200, - status: true, - message: "Success", - monitorId: monitor._id, - type: "game", - }; + const gameResponse = this.buildStatusResponse({ + monitor, + overrides: { + code: 200, + status: true, + message: "Success", + }, + }); const state = await this.GameDig.query({ type: gameId, host: url, port: port, - }).catch((error) => { + }).catch((error: any) => { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, @@ -456,21 +618,25 @@ class NetworkService { gameResponse.responseTime = state.ping; gameResponse.payload = state; return gameResponse; - } catch (error) { + } catch (error: any) { 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; + private async handleUnsupportedType(type: string): Promise { + return { + monitorId: "unknown", + teamId: "unknown", + type: "unknown", + status: false, + code: this.NETWORK_ERROR, + message: `Unsupported type: ${type}`, + }; } // Other network requests unrelated to monitoring: - async requestWebhook(type, url, body) { + async requestWebhook(type: string, url: string, body: any) { try { const response = await this.axios.post(url, body, { headers: { @@ -485,7 +651,7 @@ class NetworkService { message: `Successfully sent ${type} notification`, payload: response.data, }; - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, @@ -502,7 +668,7 @@ class NetworkService { } } - async requestPagerDuty({ message, routingKey, monitorUrl }) { + async requestPagerDuty({ message, routingKey, monitorUrl }: { message: string; routingKey: string; monitorUrl: string }) { try { const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, { routing_key: routingKey, @@ -517,13 +683,59 @@ class NetworkService { if (response?.data?.status !== "success") return false; return true; - } catch (error) { + } catch (error: any) { error.details = error.response?.data; error.service = this.SERVICE_NAME; error.method = "requestPagerDuty"; throw error; } } + + async requestMatrix({ + homeserverUrl, + accessToken, + roomId, + message, + }: { + homeserverUrl: string; + accessToken: string; + roomId: string; + message: string; + }) { + 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: any) { + 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/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts new file mode 100644 index 000000000..2d363d6b3 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -0,0 +1,6 @@ +import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; + +export interface INotificationProvider { + sendAlert: (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; + sendTestAlert(notification: Notification): Promise; +} diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts new file mode 100644 index 000000000..ad1f2f28a --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -0,0 +1,72 @@ +const SERVICE_NAME = "DiscordProvider"; +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; + +export class DiscordProvider implements INotificationProvider { + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + return discordPayload; + }; + + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = buildDiscordBody(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: body, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Discord alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); + return false; + } + }; + sendTestAlert = async (notification: Notification) => { + if (!notification.address) { + return false; + } + try { + await got.post(notification.address, { + json: { content: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Discord test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); + return false; + } + }; +} diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts new file mode 100644 index 000000000..16e48f5c1 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -0,0 +1,61 @@ +const SERVICE_NAME = "EmailProvider"; +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; + +export class EmailProvider implements INotificationProvider { + private emailService: any; + private logger: any; + + constructor(emailService: any, logger: any) { + this.emailService = emailService; + this.logger = logger; + } + + private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const html = buildHardwareEmail(this.emailService, monitor, alertsToSend); + return html; + }; + + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + const subject = `Monitor ${monitor.name} infrastructure alerts`; + let html; + if (monitor.type === "hardware") { + html = this.buildHardwareEmail(monitor, monitorStatusResponse); + } else { + html = await buildEmail(this.emailService, monitor); + } + + if (!notification.address) { + return false; + } + + const messageId = await this.emailService.sendEmail(notification.address, subject, html); + if (!messageId) { + this.logger.warn({ + message: "Email alert failed", + service: SERVICE_NAME, + method: "sendAlert", + }); + return false; + } + return true; + } + + async sendTestAlert(notification: Notification): Promise { + const subject = "Test notification"; + const html = await buildTestEmail(this.emailService); + + const messageId = await this.emailService.sendEmail(notification.address, subject, html); + if (!messageId) { + this.logger.warn({ + message: "Email test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + }); + return false; + } + return true; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts new file mode 100644 index 000000000..8914dbf80 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -0,0 +1,99 @@ +const SERVICE_NAME = "MatrixProvider"; +import got from "got"; +import type { INotificationProvider } from "@/service/index.js"; +import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; + +export class MatrixProvider implements INotificationProvider { + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { homeserverUrl, accessToken, roomId } = notification; + + let content; + if (monitor.type === "hardware") { + content = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + content = this.getContent(monitor, monitorStatusResponse); + } + + const title = `Checkmate status for ${monitor.name}`; + + const formattedMessage = `## ${title}\n${content}`; + + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; + const body = { + msgtype: "m.text", + body: formattedMessage, + format: "org.matrix.custom.html", + formatted_body: formattedMessage, + }; + try { + await got.post(url, { + json: body, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Matrix alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); + return false; + } + }; + + sendTestAlert = async (notification: Notification) => { + const { homeserverUrl, accessToken, roomId } = notification; + if (!homeserverUrl || !accessToken || !roomId) { + return false; + } + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; + const body = { + msgtype: "m.text", + body: getTestMessage(), + }; + try { + await got.post(url, { + json: body, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Matrix test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); + return false; + } + }; +} diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts new file mode 100644 index 000000000..f65e50c68 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -0,0 +1,91 @@ +const SERVICE_NAME = "PagerDutyProvider"; +import got from "got"; +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; + +export class PagerDutyProvider implements INotificationProvider { + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + try { + await got.post("https://events.pagerduty.com/v2/enqueue", { + json: { + routing_key: notification.address, + event_action: "trigger", + payload: { + summary: body, + severity: "critical", + source: monitor.url, + timestamp: new Date().toISOString(), + }, + }, + }); + + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "PagerDuty alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); + return false; + } + } + + async sendTestAlert(notification: Notification): Promise { + try { + await got.post("https://events.pagerduty.com/v2/enqueue", { + json: { + routing_key: notification.address, + event_action: "trigger", + payload: { + summary: getTestMessage(), + severity: "info", + source: "checkmate", + timestamp: new Date().toISOString(), + }, + }, + responseType: "json", + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "PagerDuty test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); + return false; + } + } +} diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts new file mode 100644 index 000000000..2aeb442eb --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -0,0 +1,85 @@ +const SERVICE_NAME = "SlackProvider"; +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; +import got, { HTTPError } from "got"; + +export class SlackProvider implements INotificationProvider { + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: body }, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Slack alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); + return false; + } + } + + async sendTestAlert(notification: Notification): Promise { + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as HTTPError; + this.logger.warn({ + message: "Slack test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); + return false; + } + } +} diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts new file mode 100644 index 000000000..f71933425 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -0,0 +1,202 @@ +import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; + +const getTime = (): { localTimeZone: string; localTime: string; utcTime: string } => { + const now = new Date(); + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const localTime = new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + timeZone: localTimeZone, + timeZoneName: "short", + }).format(now); + const utcTime = now.toUTCString(); + return { localTimeZone, localTime, utcTime }; +}; + +export const buildHardwareAlerts = ( + clientHost: string, + monitor: Monitor, + networkResponse: MonitorStatusResponse +): { alertsToSend: string[]; discordPayload: any } => { + const thresholds = monitor.thresholds || {}; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + + const payload = networkResponse?.payload as HardwareStatusPayload; + const metrics = payload.data || {}; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + + const alerts: Record = { + 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 goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor.id}` }; + 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, goToIncidentField], + 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, goToIncidentField], + 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: any, idx: number) => ({ + name: `Disk ${idx}`, + value: `${(d?.usage_percent * 100).toFixed(0)}%`, + inline: true, + })), + goToIncidentField, + ], + }), + }; + const alertTypes = ["cpu", "memory", "disk"] as const; + const alertThresholdKeyMap: Record<(typeof alertTypes)[number], "cpuAlertThreshold" | "memoryAlertThreshold" | "diskAlertThreshold"> = { + cpu: "cpuAlertThreshold", + memory: "memoryAlertThreshold", + disk: "diskAlertThreshold", + }; + for (const type of alertTypes) { + const thresholdKey = alertThresholdKeyMap[type]; + // Iterate over each alert type to see if any need to be decremented + if (alerts[type] === true) { + const nextValue = ((monitor[thresholdKey] ?? monitor.alertThreshold) as number) - 1; + monitor[thresholdKey] = nextValue; // Decrement threshold if an alert is triggered + + if (monitor[thresholdKey] <= 0) { + // If threshold drops below 0, reset and send notification + monitor[thresholdKey] = 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 ?? 0 * 100).toFixed(0)}%)`) + .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + }; + alertsToSend.push(formatAlert[type]()); + discordEmbeds.push(formatDiscordAlert[type]()); + } + } + } + const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; + return { alertsToSend, discordPayload }; +}; + +export const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { + const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; + const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`]; + const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; + return alertText.map((alert) => alert).join("\n"); +}; + +export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): string => { + const content = alerts.map((alert) => alert).join("\n"); + return content; +}; + +export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { + const thresholds = monitor.thresholds || {}; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + + const payload = networkResponse?.payload as HardwareStatusPayload; + const metrics = payload.data || {}; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + + const cpuBreach = cpuThreshold !== -1 && cpuUsage > cpuThreshold; + if (cpuBreach && ((monitor.cpuAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + const memoryBreach = memoryThreshold !== -1 && memoryUsage > memoryThreshold; + if (memoryBreach && ((monitor.memoryAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + const diskBreach = disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold); + if (diskBreach && ((monitor.diskAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + return false; +}; + +export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { status, code } = monitorStatusResponse; + const { localTimeZone, localTime, utcTime } = getTime(); + return `Monitor: ${monitor.name}\nLocal Time (${localTimeZone}): ${localTime}\nUTC Time: ${utcTime}\nStatus: ${status ? "UP" : "DOWN"}\nStatus Code: ${code}\n\u200B\n`; +}; + +export const buildHardwareEmail = async (emailService: any, monitor: Monitor, alerts: string[]) => { + const template = "hardwareIncidentTemplate"; + const context = { monitor: monitor.name, url: monitor.url, alerts }; + const html = await emailService.buildEmail(template, context); + return html; +}; + +export const buildEmail = async (emailService: any, monitor: Monitor): Promise => { + const template = monitor.status === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const context = { monitor: monitor.name, url: monitor.url }; + const html = await emailService.buildEmail(template, context); + return html; +}; + +export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { localTime, utcTime } = getTime(); + let body = { + embeds: [ + { + title: `Monitor name: ${monitor.name}`, + color: monitorStatusResponse.status ? 5763719 : 15548997, + + fields: [ + { name: "Monitor", value: monitor.name, inline: false }, + { name: "Status", value: monitorStatusResponse.status ? "Up" : "Down", inline: false }, + { name: "Status Code", value: String(monitorStatusResponse.code), inline: false }, + { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: false }, + { name: "URL", value: monitor.url, inline: false }, + ], + footer: { text: "Checkmate" }, + }, + ], + }; + return body; +}; + +export const buildTestEmail = async (emailService: any) => { + const context = { testName: "Monitoring System" }; + const html = await emailService.buildEmail("testEmailTemplate", context); + return html; +}; + +export const getTestMessage = () => { + return "This is a test notification from Checkmate"; +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts new file mode 100644 index 000000000..9e743ae53 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -0,0 +1,84 @@ +const SERVICE_NAME = "WebhookProvider"; +import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; + +export class WebhookProvider implements INotificationProvider { + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): string => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: body }, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Webhook alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); + return false; + } + }; + + sendTestAlert = async (notification: Notification) => { + if (!notification.address) { + return false; + } + try { + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Webhook test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); + return false; + } + }; +} diff --git a/server/src/service/infrastructure/notificationService.js b/server/src/service/infrastructure/notificationService.js deleted file mode 100644 index 6c5b8b25e..000000000 --- a/server/src/service/infrastructure/notificationService.js +++ /dev/null @@ -1,119 +0,0 @@ -const SERVICE_NAME = "NotificationService"; - -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; - } - }; - - 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/infrastructure/notificationUtils.js b/server/src/service/infrastructure/notificationUtils.js deleted file mode 100644 index d6b92cc0e..000000000 --- a/server/src/service/infrastructure/notificationUtils.js +++ /dev/null @@ -1,196 +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 : dn.downtimeAlert, - 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/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts new file mode 100644 index 000000000..4e0c9d842 --- /dev/null +++ b/server/src/service/infrastructure/notificationsService.ts @@ -0,0 +1,181 @@ +import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; +import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; +import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; +import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; +export interface INotificationsService { + createNotification: (notificationData: Partial) => Promise; + findById: (id: string, teamId: string) => Promise; + findNotificationsByTeamId: (teamId: string) => Promise; + updateById(id: string, teamId: string, updateData: Partial): Promise; + deleteById: (id: string, teamId: string) => Promise; + handleNotifications: ( + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + prevStatus: boolean | undefined, + statusChanged: boolean + ) => Promise; + + sendTestNotification: (notification: Notification) => Promise; + testAllNotifications: (notificationIds: string[]) => Promise; +} + +const SERVICE_NAME = "NotificationsService"; + +export class NotificationsService implements INotificationsService { + static SERVICE_NAME = SERVICE_NAME; + + private notificationsRepository: INotificationsRepository; + private monitorsRepository: IMonitorsRepository; + private webhookProvider: INotificationProvider; + private emailProvider: INotificationProvider; + private slackProvider: INotificationProvider; + private discordProvider: INotificationProvider; + private pagerDutyProvider: INotificationProvider; + private matrixProvider: INotificationProvider; + private logger: any; + + constructor( + notificationsRepository: INotificationsRepository, + monitorsRepository: IMonitorsRepository, + webhookProvider: INotificationProvider, + emailProvider: INotificationProvider, + slackProvider: INotificationProvider, + discordProvider: INotificationProvider, + pagerDutyProvider: INotificationProvider, + matrixProvider: INotificationProvider, + logger: any + ) { + this.notificationsRepository = notificationsRepository; + this.monitorsRepository = monitorsRepository; + this.webhookProvider = webhookProvider; + this.emailProvider = emailProvider; + this.slackProvider = slackProvider; + this.discordProvider = discordProvider; + this.pagerDutyProvider = pagerDutyProvider; + this.matrixProvider = matrixProvider; + this.logger = logger; + } + + private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { + switch (notification.type) { + case "email": + return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "slack": + return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "discord": + return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "pager_duty": + return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "matrix": + return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "webhook": + return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); + default: + return false; + } + }; + + private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const notificationIds = monitor.notifications ?? []; + const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); + const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse)); + const outcomes = await Promise.all(tasks); + const succeeded = outcomes.filter(Boolean).length; + const failed = outcomes.length - succeeded; + if (failed > 0) { + this.logger.warn({ + message: `Notification send completed with ${succeeded} success, ${failed} failure(s)`, + service: SERVICE_NAME, + method: "getMonitorJob", + }); + } + // Return true if all notificaitons succeeded + return succeeded === notifications.length; + }; + + handleNotifications = async ( + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + prevStatus: boolean | undefined, + statusChanged: boolean + ) => { + const { type } = monitor; + const payload = monitorStatusResponse.payload as HardwareStatusPayload; + // If this is a non-hardeware type monitor and status did not change, we're done + if (type !== "hardware" && statusChanged === false) return false; + // if prevStatus is undefined, monitor is resuming, we're done + if (type !== "hardware" && prevStatus === undefined) return false; + + // Deal with hardware thresholds + if (type === "hardware") { + const thresholds = monitor.thresholds; + + if (thresholds === undefined) return false; // No thresholds set, we're done + const metrics = payload?.data ?? null; + if (metrics === null) return false; // No metrics, we're done + + // We should send a notificaiton + + const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); + if (shouldSend === false) return false; + + return await this.sendNotifications(monitor, monitorStatusResponse); + } + + // We should send a notification for non-hardware monitor status change + return await this.sendNotifications(monitor, monitorStatusResponse); + }; + + sendTestNotification = async (notification: Notification) => { + switch (notification.type) { + case "email": + return await this.emailProvider.sendTestAlert(notification); + case "slack": + return await this.slackProvider.sendTestAlert(notification); + case "discord": + return await this.discordProvider.sendTestAlert(notification); + case "pager_duty": + return await this.pagerDutyProvider.sendTestAlert(notification); + case "matrix": + return await this.matrixProvider.sendTestAlert(notification); + case "webhook": + return await this.webhookProvider.sendTestAlert(notification); + default: + return false; + } + }; + + testAllNotifications = async (notificationIds: string[]) => { + const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); + const tasks = notifications.map((notification) => this.sendTestNotification(notification)); + const outcomes = await Promise.all(tasks); + const succeeded = outcomes.filter(Boolean).length; + const failed = outcomes.length - succeeded; + if (failed > 0) { + return false; + } + return true; + }; + + createNotification = async (notificationData: Partial): Promise => { + return await this.notificationsRepository.create(notificationData); + }; + + findById = async (id: string, teamId: string): Promise => { + return await this.notificationsRepository.findById(id, teamId); + }; + + findNotificationsByTeamId = async (teamId: string): Promise => { + return await this.notificationsRepository.findByTeamId(teamId); + }; + + updateById = async (id: string, teamId: string, updateData: Partial): Promise => { + return await this.notificationsRepository.updateById(id, teamId, updateData); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await this.notificationsRepository.deleteById(id, teamId); + await this.monitorsRepository.removeNotificationFromMonitors(id); + return deleted; + }; +} diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js deleted file mode 100755 index 72fa359d7..000000000 --- a/server/src/service/infrastructure/statusService.js +++ /dev/null @@ -1,317 +0,0 @@ -import MonitorStats from "../../db/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/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts new file mode 100755 index 000000000..3a83dbd28 --- /dev/null +++ b/server/src/service/infrastructure/statusService.ts @@ -0,0 +1,260 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; +import MonitorStats from "../../db/models/MonitorStats.js"; +import { CheckModel } from "@/db/models/index.js"; +import type { + CheckErrorInfo, + Monitor, + MonitorStatusResponse, + StatusChangeResult, + Check, + HardwareStatusPayload, + PageSpeedStatusPayload, +} from "@/types/index.js"; +const SERVICE_NAME = "StatusService"; + +export interface IStatusService { + updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise; + getStatusString(status: boolean | undefined): string; + handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise; + updateMonitorStatus( + statusResponse: MonitorStatusResponse, + check: Check + ): Promise; +} + +export class StatusService implements IStatusService { + static SERVICE_NAME = SERVICE_NAME; + private db: any; + private logger: any; + private buffer: any; + private monitorsRepository: IMonitorsRepository; + + constructor({ db, logger, buffer, monitorsRepository }: { db: any; logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) { + this.db = db; + this.logger = logger; + this.buffer = buffer; + this.monitorsRepository = monitorsRepository; + } + + get serviceName() { + return StatusService.SERVICE_NAME; + } + + async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) { + 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: any) { + this.logger.error({ + service: SERVICE_NAME, + message: error.message, + method: "updateRunningStats", + stack: error.stack, + }); + return false; + } + } + + getStatusString = (status: boolean | undefined) => { + if (status === true) return "up"; + if (status === false) return "down"; + return "unknown"; + }; + + handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => { + try { + let savedCheck; + if (!check.id) { + try { + const checkModel = new CheckModel(check); + savedCheck = await checkModel.save(); + + this.buffer.removeCheckFromBuffer(check); + } catch (checkError: any) { + this.logger.error({ + service: SERVICE_NAME, + method: "handleIncidentForCheck", + message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`, + monitorId: monitor.id, + stack: checkError.stack, + }); + savedCheck = null; + } + } + + if (savedCheck && savedCheck.id) { + try { + this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action }); + } catch (incidentError: any) { + this.logger.error({ + service: SERVICE_NAME, + method: "handleIncidentForCheck", + message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`, + monitorId: monitor.id, + action, + stack: incidentError.stack, + }); + } + } + } catch (error: any) { + this.logger.error({ + service: SERVICE_NAME, + method: "handleIncidentForCheck", + message: `Error in ${errorContext}: ${error.message}`, + monitorId: monitor?.id, + stack: error.stack, + }); + } + }; + + updateMonitorStatus = async ( + statusResponse: MonitorStatusResponse, + check: Check + ): Promise => { + try { + const { monitorId, teamId, status, code } = statusResponse; + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + + // Update running stats + this.updateRunningStats({ monitor, networkResponse: statusResponse }); + + // If the status window size has changed, empty + + monitor.statusWindow = monitor.statusWindow || []; + monitor.statusWindow.push(status); + while (monitor.statusWindow.length > monitor.statusWindowSize) { + monitor.statusWindow.shift(); + } + + if (monitor.status === undefined || monitor.status === null) { + monitor.status = status; + } + + const prevStatus = monitor.status; + let newStatus = monitor.status; + let statusChanged = false; + + // Return early if not enough data points + if (monitor.statusWindow.length < monitor.statusWindowSize) { + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); + return { + monitor: updated, + 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; + } + + monitor.status = newStatus; + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); + + return { + monitor: updated, + statusChanged, + prevStatus, + code, + timestamp: new Date().getTime(), + }; + } catch (error: any) { + error.service = SERVICE_NAME; + error.method = "updateStatus"; + throw error; + } + }; + + insertCheck = async (check: Check) => { + try { + if (typeof check === "undefined") { + this.logger.warn({ + message: "Failed to build check", + service: SERVICE_NAME, + method: "insertCheck", + }); + return false; + } + this.buffer.addToBuffer({ check }); + return true; + } catch (error: any) { + this.logger.error({ + message: error.message, + service: error.service || SERVICE_NAME, + method: error.method || "insertCheck", + details: error.details || `Error inserting check for monitor: ${check?.metadata.monitorId}`, + stack: error.stack, + }); + } + }; +} diff --git a/server/src/service/system/serviceRegistry.js b/server/src/service/system/serviceRegistry.js deleted file mode 100755 index 4e3e28464..000000000 --- a/server/src/service/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/system/settingsService.js b/server/src/service/system/settingsService.js deleted file mode 100755 index 70d8d192c..000000000 --- a/server/src/service/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/system/settingsService.ts b/server/src/service/system/settingsService.ts new file mode 100755 index 000000000..a0db6a140 --- /dev/null +++ b/server/src/service/system/settingsService.ts @@ -0,0 +1,83 @@ +import { ISettingsRepository } from "@/repositories/index.js"; +import { Settings } from "@/types/index.js"; +import { AppError } from "@/utils/AppError.js"; + +const SERVICE_NAME = "SettingsService"; + +export type EnvConfig = { + jwtSecret: string | undefined; + jwtTTL: string | undefined; + systemEmailHost: string | undefined; + nodeEnv: string | undefined; + logLevel: string | undefined; + clientHost: string | undefined; + dbConnectionString: string | undefined; + port: string | undefined; +}; + +const envConfig: 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, +}; + +export interface ISettingsService { + readonly serviceName: string; + loadSettings(): EnvConfig; + getSettings(): EnvConfig; + getDBSettings(): Promise; +} + +class SettingsService implements ISettingsService { + static SERVICE_NAME = "SettingsService"; + private settings: EnvConfig; + private settingsRepository: ISettingsRepository; + + constructor(settingsRepository: ISettingsRepository) { + this.settingsRepository = settingsRepository; + this.settings = { ...envConfig }; + } + + get serviceName() { + return SettingsService.SERVICE_NAME; + } + + loadSettings() { + return this.settings; + } + + getSettings() { + if (!this.settings) { + throw new Error("Settings have not been loaded"); + } + return this.settings; + } + + updateDbSettings = async (newSettings: Partial) => { + return await this.settingsRepository.update(newSettings); + }; + + getDBSettings = async () => { + // Remove any old settings + await this.settingsRepository.deleteLegacy(); + + let settings = await this.settingsRepository.findSingleton(); + if (settings === null) { + await this.settingsRepository.create({}); + settings = await this.settingsRepository.findSingleton(); + } + + if (!settings) { + throw new AppError({ message: "Settings not found", status: 500 }); + } + + return settings; + }; +} + +export default SettingsService; diff --git a/server/src/service/system/stringService.js b/server/src/service/system/stringService.js deleted file mode 100755 index 231550be7..000000000 --- a/server/src/service/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/system/translationService.js b/server/src/service/system/translationService.js deleted file mode 100755 index 0d3893852..000000000 --- a/server/src/service/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/shutdown.js b/server/src/shutdown.ts similarity index 71% rename from server/src/shutdown.js rename to server/src/shutdown.ts index 27eb0f21d..877aef325 100644 --- a/server/src/shutdown.js +++ b/server/src/shutdown.ts @@ -1,6 +1,8 @@ +import { InitializedServices } from "./config/services.js"; import { logger } from "./utils/logger.js"; +import type { Server } from "http"; -export const initShutdownListener = (server, services) => { +export const initShutdownListener = (server: Server, services: InitializedServices) => { const SERVICE_NAME = "Server"; let isShuttingDown = false; @@ -19,11 +21,12 @@ export const initShutdownListener = (server, services) => { logger.info({ message: "Graceful shutdown complete" }); process.exit(0); } catch (error) { + const err = error as Error; logger.error({ - message: error.message, + message: err.message, service: SERVICE_NAME, method: "shutdown", - stack: error.stack, + stack: err.stack, }); process.exit(1); } diff --git a/server/src/types/alert.ts b/server/src/types/alert.ts new file mode 100644 index 000000000..4725ebee8 --- /dev/null +++ b/server/src/types/alert.ts @@ -0,0 +1,29 @@ +import { NotificationChannel } from "@/types/index.js"; +export interface AlertWebhookPayload { + body: Record; +} + +export interface AlertPagerDutyPayload { + routingKey: string; + monitorUrl?: string; +} + +export interface AlertMatrixPayload { + friendlyName?: string; + homeserverUrl: string; + accessToken: string; + roomId: string; + monitorName: string; +} + +export interface Alert { + channel: NotificationChannel; + address?: string; + subject: string; + message: string; + html?: string; + discordContent?: Record | null; + webhook?: AlertWebhookPayload | null; + pagerDuty?: AlertPagerDutyPayload | null; + matrix?: AlertMatrixPayload | null; +} diff --git a/server/src/types/check.ts b/server/src/types/check.ts new file mode 100644 index 000000000..ee40ff6dc --- /dev/null +++ b/server/src/types/check.ts @@ -0,0 +1,187 @@ +import type { MonitorType } from "@/types/index.js"; +import type { Response } from "got"; +export type GotTimings = Response["timings"]; + +export interface CheckMetadata { + monitorId: string; + teamId: string; + type: MonitorType; +} + +export interface CheckCpuInfo { + physical_core?: number; + logical_core?: number; + frequency?: number; + temperature?: number[]; + free_percent?: number; + usage_percent?: number; +} + +export interface CheckMemoryInfo { + total_bytes?: number; + available_bytes?: number; + used_bytes?: number; + usage_percent?: number; +} + +export interface CheckHostInfo { + os?: string; + platform?: string; + kernel_version?: string; +} + +export interface CheckCaptureInfo { + version?: string; + mode?: string; +} + +export interface CheckDiskInfo { + device?: string; + mountpoint?: string; + read_speed_bytes?: number; + write_speed_bytes?: number; + total_bytes?: number; + free_bytes?: number; + usage_percent?: number; +} + +export interface CheckErrorInfo { + metric: string[]; + err: string; +} + +export interface CheckNetworkInterfaceInfo { + 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 CheckAudits { + cls?: ILighthouseAudit; + si?: ILighthouseAudit; + fcp?: ILighthouseAudit; + lcp?: ILighthouseAudit; + tbt?: ILighthouseAudit; +} + +export interface ILighthouseAudit { + id?: string; + title?: string; + score?: number | null; + displayValue?: string; + numericValue?: number; + numericUnit?: string; +} + +export interface Check { + id: string; + metadata: CheckMetadata; + status: boolean; + responseTime: number; + timings?: GotTimings; + statusCode: number; + message: string; + ack: boolean; + ackAt?: string | null; + expiry: string; + cpu?: CheckCpuInfo; + memory?: CheckMemoryInfo; + disk?: CheckDiskInfo[]; + host?: CheckHostInfo; + errors?: CheckErrorInfo[]; + capture?: CheckCaptureInfo; + net?: CheckNetworkInterfaceInfo[]; + accessibility?: number; + bestPractices?: number; + seo?: number; + performance?: number; + audits?: CheckAudits; + __v: number; + createdAt: string; + updatedAt: string; +} +export interface ChecksQueryResult { + checksCount: number; + checks: Check[]; +} + +export interface PageSpeedChecksResult { + monitorType: "pagespeed"; + checks: Check[]; +} + +export interface HardwareChecksResult { + monitorType: "hardware"; + aggregateData: { + latestCheck: Check | null; + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: Array<{ + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: Array<{ + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; + }>; + net: Array<{ + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; + }>; + }>; +} + +export interface GroupedCheck { + bucketDate: string; + avgResponseTime: number; + totalChecks: number; +} + +export interface UptimeChecksResult { + monitorType: Exclude; + groupedChecks: GroupedCheck[]; + groupedUpChecks: GroupedCheck[]; + groupedDownChecks: GroupedCheck[]; + uptimePercentage: number; + avgResponseTime: number; +} + +export interface ChecksSummary { + totalChecks: number; + resolvedChecks: number; + downChecks: number; + cannotResolveChecks: number; +} + +export type NormalizedCheck = T & { + originalResponseTime: number; +}; + +export type NormalizedUptimeCheck = T & { + originalAvgResponseTime: number; +}; diff --git a/server/src/types/email.ts b/server/src/types/email.ts new file mode 100644 index 000000000..2ffae187b --- /dev/null +++ b/server/src/types/email.ts @@ -0,0 +1,17 @@ +import { Settings } from "./settings.js"; + +export type EmailTransportConfig = Pick< + Settings, + | "systemEmailHost" + | "systemEmailPort" + | "systemEmailAddress" + | "systemEmailPassword" + | "systemEmailUser" + | "systemEmailConnectionHost" + | "systemEmailTLSServername" + | "systemEmailSecure" + | "systemEmailPool" + | "systemEmailIgnoreTLS" + | "systemEmailRequireTLS" + | "systemEmailRejectUnauthorized" +>; diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts new file mode 100644 index 000000000..66755659c --- /dev/null +++ b/server/src/types/express.d.ts @@ -0,0 +1,11 @@ +import type { User } from "@/types/index.js"; + +declare global { + namespace Express { + interface Request { + file?: Multer.File; + user?: User | undefined; + resource?: any; + } + } +} diff --git a/server/src/types/incident.ts b/server/src/types/incident.ts new file mode 100644 index 000000000..39dfb3d0d --- /dev/null +++ b/server/src/types/incident.ts @@ -0,0 +1,49 @@ +// export type IncidentResolutionType = "automatic" | "manual" | null; + +export const IncidentResolutionTypes = ["automatic", "manual", null] as const; +export type IncidentResolutionType = (typeof IncidentResolutionTypes)[number]; + +export interface Incident { + id: string; + monitorId: string; + teamId: string; + startTime: string; + endTime: string | null; + status: boolean; + message?: string | null; + statusCode?: number | null; + resolutionType: IncidentResolutionType; + resolvedBy?: string | null; + comment?: string | null; + createdAt: string; + updatedAt: string; +} + +export interface IncidentSummaryTopMonitor { + monitorId: string; + monitorName: string | null; + incidentCount: number; +} + +export interface IncidentSummaryItem { + id: string; + monitorId: string; + monitorName: string | null; + status: boolean; + startTime: string; + endTime: string | null; + resolutionType: IncidentResolutionType; + message: string | null; + statusCode: number | null; + createdAt: string; +} + +export interface IncidentSummary { + total: number; + totalActive: number; + totalManualResolutions: number; + totalAutomaticResolutions: number; + avgResolutionTimeHours: number; + topMonitor: IncidentSummaryTopMonitor | null; + latestIncidents: IncidentSummaryItem[]; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts new file mode 100644 index 000000000..ad92ea1fe --- /dev/null +++ b/server/src/types/index.ts @@ -0,0 +1,15 @@ +export * from "@/types/check.js"; +export * from "@/types/monitor.js"; +export * from "@/types/monitorStats.js"; +export * from "@/types/statusPage.js"; +export * from "@/types/network.js"; +export * from "@/types/user.js"; +export * from "@/types/invite.js"; +export * from "@/types/recoveryToken.js"; +export * from "@/types/settings.js"; +export * from "@/types/notification.js"; +export * from "@/types/alert.js"; +export * from "@/types/incident.js"; +export * from "@/types/email.js"; +export * from "@/types/team.js"; +export * from "@/types/maintenanceWindow.js"; diff --git a/server/src/types/invite.ts b/server/src/types/invite.ts new file mode 100644 index 000000000..cdbdb37e5 --- /dev/null +++ b/server/src/types/invite.ts @@ -0,0 +1,12 @@ +import type { UserRole } from "@/types/user.js"; + +export interface Invite { + id: string; + email: string; + teamId: string; + role: UserRole[]; + token: string; + expiry: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/maintenanceWindow.ts b/server/src/types/maintenanceWindow.ts new file mode 100644 index 000000000..33285b8e4 --- /dev/null +++ b/server/src/types/maintenanceWindow.ts @@ -0,0 +1,12 @@ +export interface MaintenanceWindow { + id: string; + monitorId: String; + teamId: string; + active: boolean; + name: string; + repeat: number; + start: string; + end: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts new file mode 100644 index 000000000..c7d99a88a --- /dev/null +++ b/server/src/types/monitor.ts @@ -0,0 +1,122 @@ +import type { Check } from "@/types/check.js"; +export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const; +export type MonitorType = (typeof MonitorTypes)[number]; + +export interface MonitorThresholds { + usage_cpu?: number; + usage_memory?: number; + usage_disk?: number; + usage_temperature?: number; +} + +export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; + +export interface Monitor { + id: string; + userId: string; + teamId: string; + name: string; + description?: string; + status?: boolean; + statusWindow: boolean[]; + statusWindowSize: number; + statusWindowThreshold: number; + type: MonitorType; + ignoreTlsErrors: boolean; + jsonPath?: string; + expectedValue?: string; + matchMethod?: MonitorMatchMethod; + url: string; + port?: number; + isActive: boolean; + interval: number; + uptimePercentage?: number; + notifications: string[]; + secret?: string; + thresholds?: MonitorThresholds; + alertThreshold: number; + cpuAlertThreshold: number; + memoryAlertThreshold: number; + diskAlertThreshold: number; + tempAlertThreshold: number; + selectedDisks: string[]; + gameId?: string; + group: string | null; + createdAt: string; + updatedAt: string; +} + +export interface MonitorsSummary { + totalMonitors: number; + upMonitors: number; + downMonitors: number; + pausedMonitors: number; +} + +export interface MonitorWithChecks extends Monitor { + checks: Check[]; +} + +export interface MonitorsWithChecksByTeamIdResult { + summary: MonitorsSummary | null; + count: number; + monitors: MonitorWithChecks[]; +} + +export interface UptimeDetailsResult { + monitorData: { + monitor: Monitor; + groupedChecks: import("./check.js").GroupedCheck[]; + groupedUpChecks: import("./check.js").GroupedCheck[]; + groupedDownChecks: import("./check.js").GroupedCheck[]; + groupedAvgResponseTime: number; + groupedUptimePercentage: number; + }; + monitorStats: import("./monitorStats.js").MonitorStats | null; +} + +export interface HardwareDetailsResult extends Monitor { + stats: { + aggregateData: { + latestCheck: import("./check.js").Check | null; + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: Array<{ + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: Array<{ + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; + }>; + net: Array<{ + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; + }>; + }>; + }; +} + +export interface PageSpeedDetailsResult { + monitor: Monitor & { + checks: import("./check.js").Check[]; + }; + monitorStats: import("./monitorStats.js").MonitorStats | null; +} diff --git a/server/src/types/monitorStats.ts b/server/src/types/monitorStats.ts new file mode 100644 index 000000000..8f9603529 --- /dev/null +++ b/server/src/types/monitorStats.ts @@ -0,0 +1,14 @@ +export interface MonitorStats { + id: string; + monitorId: string; + avgResponseTime: number; + totalChecks: number; + totalUpChecks: number; + totalDownChecks: number; + uptimePercentage: number; + lastCheckTimestamp: number; + lastResponseTime: number; + timeOfLastFailure?: number; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/network.ts b/server/src/types/network.ts new file mode 100644 index 000000000..6bed40316 --- /dev/null +++ b/server/src/types/network.ts @@ -0,0 +1,112 @@ +import type { + CheckCaptureInfo, + CheckCpuInfo, + CheckDiskInfo, + CheckErrorInfo, + CheckHostInfo, + CheckMemoryInfo, + CheckNetworkInterfaceInfo, + GotTimings, + ILighthouseAudit, + Monitor, + MonitorMatchMethod, + MonitorType, +} from "@/types/index.js"; + +export interface MonitorStatusResponse { + monitorId: string; + teamId: string; + type: MonitorType; + status: boolean; + code: number; + message: string; + responseTime?: number; + payload?: T | null; + timings?: GotTimings; + first_byte_took?: number; + body_read_took?: number; + dns_took?: number; + conn_took?: number; + connect_took?: number; + tls_took?: number; + jsonPath?: string; + matchMethod?: MonitorMatchMethod; + expectedValue?: string; + extracted?: unknown; +} + +export interface PingStatusPayload { + host: string; + numeric_host?: string; + alive: boolean; + time: number; + times?: number[]; + output?: string; + min?: string; + max?: string; + avg?: string; + stddev?: string; + packetLoss?: string; +} + +export type HttpStatusPayload = unknown; + +export interface PageSpeedCategoryScore { + score?: number | null; +} + +export interface PageSpeedStatusPayload { + lighthouseResult?: { + categories?: { + accessibility?: PageSpeedCategoryScore; + "best-practices"?: PageSpeedCategoryScore; + performance?: PageSpeedCategoryScore; + seo?: PageSpeedCategoryScore; + [key: string]: PageSpeedCategoryScore | undefined; + }; + audits?: Record; + }; + [key: string]: unknown; +} + +export interface HardwareStatusMetrics { + cpu?: CheckCpuInfo; + memory?: CheckMemoryInfo; + disk?: CheckDiskInfo[]; + host?: CheckHostInfo; + net?: CheckNetworkInterfaceInfo[]; +} + +export interface HardwareStatusPayload { + data?: HardwareStatusMetrics; + errors?: CheckErrorInfo[] | { errors?: CheckErrorInfo[] }; + capture?: CheckCaptureInfo; + [key: string]: unknown; +} + +export type DockerStatusPayload = Record; + +export interface PortStatusPayload { + success: boolean; +} + +export type GameStatusPayload = Record; + +export interface MonitorPayloadMap { + ping: PingStatusPayload; + http: HttpStatusPayload; + pagespeed: PageSpeedStatusPayload; + hardware: HardwareStatusPayload; + docker: DockerStatusPayload; + port: PortStatusPayload; + game: GameStatusPayload; + default: unknown; +} + +export type StatusChangeResult = { + monitor: Monitor; + statusChanged: boolean; + prevStatus: boolean | undefined; + code: number; + timestamp: number; +}; diff --git a/server/src/types/notification.ts b/server/src/types/notification.ts new file mode 100644 index 000000000..669c840ca --- /dev/null +++ b/server/src/types/notification.ts @@ -0,0 +1,17 @@ +export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const; +export type NotificationChannel = (typeof NotificationChannels)[number]; + +export interface Notification { + id: string; + userId: string; + teamId: string; + type: NotificationChannel; + notificationName: string; + address?: string; + phone?: string; + homeserverUrl?: string; + roomId?: string; + accessToken?: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/recoveryToken.ts b/server/src/types/recoveryToken.ts new file mode 100644 index 000000000..488e1f4c1 --- /dev/null +++ b/server/src/types/recoveryToken.ts @@ -0,0 +1,8 @@ +export interface RecoveryToken { + id: string; + email: string; + token: string; + expiry: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/settings.ts b/server/src/types/settings.ts new file mode 100644 index 000000000..108cdcb72 --- /dev/null +++ b/server/src/types/settings.ts @@ -0,0 +1,32 @@ +export interface SettingsThresholds { + cpu?: number; + memory?: number; + disk?: number; + temperature?: number; +} + +export interface Settings { + id: string; + checkTTL: number; + language: string; + jwtSecret?: string; + pagespeedApiKey?: string; + systemEmailHost?: string; + systemEmailPort?: number; + systemEmailAddress?: string; + systemEmailPassword?: string; + systemEmailUser?: string; + systemEmailConnectionHost?: string; + systemEmailTLSServername?: string; + systemEmailSecure: boolean; + systemEmailPool: boolean; + systemEmailIgnoreTLS: boolean; + systemEmailRequireTLS: boolean; + systemEmailRejectUnauthorized: boolean; + showURL: boolean; + singleton: boolean; + version: number; + globalThresholds?: SettingsThresholds; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts new file mode 100644 index 000000000..64a5ac50b --- /dev/null +++ b/server/src/types/statusPage.ts @@ -0,0 +1,29 @@ +export const StatusPageTypes = ["uptime"] as const; +export type StatusPageType = (typeof StatusPageTypes)[number]; + +export interface StatusPageLogo { + data: Buffer; + contentType: string; +} + +export interface StatusPage { + id: string; + userId: string; + teamId: string; + type: StatusPageType; + companyName: string; + url: string; + timezone?: string; + color: string; + monitors: string[]; + subMonitors: string[]; + originalMonitors?: string[]; + logo?: StatusPageLogo | null; + isPublished: boolean; + showCharts: boolean; + showUptimePercentage: boolean; + showAdminLoginLink: boolean; + customCSS: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/team.ts b/server/src/types/team.ts new file mode 100644 index 000000000..1bca943a0 --- /dev/null +++ b/server/src/types/team.ts @@ -0,0 +1,6 @@ +export interface Team { + id: string; + email: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/user.ts b/server/src/types/user.ts new file mode 100644 index 000000000..41c009791 --- /dev/null +++ b/server/src/types/user.ts @@ -0,0 +1,24 @@ +export const UserRoles = ["user", "admin", "superadmin", "demo"] as const; +export type UserRole = (typeof UserRoles)[number]; + +export interface UserProfileImage { + data?: Buffer; + contentType?: string; +} + +export interface User { + id: string; + firstName: string; + lastName: string; + email: string; + password: string; + avatarImage?: string; + profileImage?: UserProfileImage; + isActive: boolean; + isVerified: boolean; + role: UserRole[]; + teamId: string; + checkTTL?: number; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/utils/AppError.ts b/server/src/utils/AppError.ts new file mode 100644 index 000000000..93baeb78e --- /dev/null +++ b/server/src/utils/AppError.ts @@ -0,0 +1,24 @@ +export interface AppErrorConfig { + message: string; + status?: number; + service?: string | null; + method?: string | null; + details?: any; +} + +export class AppError extends Error { + private status: number; + private service: string | null; + private method: string | null; + private details: any; + + constructor({ message, status = 500, service = null, method = null, details = null }: AppErrorConfig) { + super(message); + this.status = status; + this.service = service; + this.method = method; + this.details = details; + + Error.captureStackTrace(this, this.constructor); + } +} 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.ts similarity index 53% rename from server/src/utils/dataUtils.js rename to server/src/utils/dataUtils.ts index e7fd334dd..0bf3e6416 100755 --- a/server/src/utils/dataUtils.js +++ b/server/src/utils/dataUtils.ts @@ -1,35 +1,33 @@ -const calculatePercentile = (arr, percentile) => { +import type { Check, GroupedCheck, NormalizedCheck, NormalizedUptimeCheck } from "@/types/index.js"; + +const calculatePercentile = (arr: Check[], percentile: number): number => { 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; + if (upper >= sorted.length) return sorted[lower]!.responseTime; + return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight; }; -const calculatePercentileUptimeDetails = (arr, percentile) => { +const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: number): number => { 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; + if (upper >= sorted.length) return sorted[lower]!.avgResponseTime; + return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight; }; -const NormalizeData = (checks, rangeMin, rangeMax) => { +export const NormalizeData = (checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck[] => { 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, @@ -45,19 +43,16 @@ const NormalizeData = (checks, rangeMin, rangeMax) => { }); } }; -const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => { + +export const NormalizeDataUptimeDetails = (checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck[] => { 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, @@ -69,32 +64,7 @@ const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => { return normalizedChecks; } else { return checks.map((check) => { - return { ...check, originalResponseTime: check.responseTime }; + return { ...check, originalAvgResponseTime: check.avgResponseTime }; }); } }; - -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/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.ts similarity index 74% rename from server/src/utils/imageProcessing.js rename to server/src/utils/imageProcessing.ts index 470a5c27e..442f0d6da 100755 --- a/server/src/utils/imageProcessing.js +++ b/server/src/utils/imageProcessing.ts @@ -1,9 +1,6 @@ import sharp from "sharp"; -/** - * Generates a 64 * 64 pixel image from a given image - * @param {} file - */ -const GenerateAvatarImage = async (file) => { + +const GenerateAvatarImage = async (file: Express.Multer.File) => { try { // Resize to target 64 * 64 let resizedImageBuffer = await sharp(file.buffer) diff --git a/server/src/utils/logger.js b/server/src/utils/logger.ts similarity index 50% rename from server/src/utils/logger.js rename to server/src/utils/logger.ts index 0917862fa..4053f4a82 100755 --- a/server/src/utils/logger.js +++ b/server/src/utils/logger.ts @@ -1,46 +1,84 @@ -import { createLogger, format, transports } from "winston"; +import { createLogger, format, transports, Logger as WinstonLogger } from "winston"; +import type { Logform } from "winston"; import dotenv from "dotenv"; +import { EnvConfig } from "@/service/system/settingsService.js"; dotenv.config(); const SERVICE_NAME = "Logger"; -class Logger { +interface LogConfig { + message: string; + service?: string; + method?: string; + details?: Record; + stack?: string; +} + +interface LogEntry extends LogConfig { + level: string; + timestamp: string; +} + +export interface ILogger { + readonly serviceName: string; + info(config: LogConfig): void; + warn(config: LogConfig): void; + error(config: LogConfig): void; + debug(config: LogConfig): void; + cacheLog(entry: LogEntry): void; + getLogs(): LogEntry[]; + buildLogEntry(level: string, config: LogConfig): LogEntry; +} + +class Logger implements ILogger { static SERVICE_NAME = SERVICE_NAME; - constructor({ envSettings }) { + + private logger: WinstonLogger; + private envSettings: Partial; + private logCache: LogEntry[]; + private maxCacheSize: number; + + constructor({ envSettings }: { envSettings: Partial }) { 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); + const consoleFormat = format.printf((info: Logform.TransformableInfo) => { + const { level, service, method, details, timestamp, stack } = info; + let message = info.message as string; + let formattedMessage: string = message; + let formattedDetails: string | undefined; + + if (typeof message === "object" && message !== null) { + formattedMessage = JSON.stringify(message, null, 2); } - if (details instanceof Object) { - details = JSON.stringify(details, null, 2); + if (typeof details === "object" && details !== null) { + formattedDetails = JSON.stringify(details, null, 2); } + let msg = `${timestamp} ${level}:`; service && (msg += ` [${service}]`); method && (msg += `(${method})`); - message && (msg += ` ${message}`); - details && (msg += ` (details: ${details})`); + formattedMessage && (msg += ` ${formattedMessage}`); + formattedDetails && (msg += ` (details: ${formattedDetails})`); - if (typeof stack !== "undefined") { + if (typeof stack === "string") { const stackTrace = stack - ?.split("\n") + .split("\n") .slice(1) // Remove first line (error message) - .map((line) => { + .map((line: string) => { 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]), + line: parseInt(match[3] ?? "0", 10), + column: parseInt(match[4] ?? "0", 10), }; } return line.trim(); }); - stack && (msg += ` (stack: ${JSON.stringify(stackTrace, null, 2)})`); + msg += ` (stack: ${JSON.stringify(stackTrace, null, 2)})`; } return msg; @@ -67,62 +105,31 @@ class Logger { 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) { + info(config: LogConfig) { 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) { + warn(config: LogConfig) { 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) { + error(config: LogConfig) { 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) { + + debug(config: LogConfig) { const logEntry = this.buildLogEntry("debug", config); this.cacheLog(logEntry); this.logger.debug(logEntry); } - cacheLog(entry) { + cacheLog(entry: LogEntry) { this.logCache.push(entry); if (this.logCache.length > this.maxCacheSize) { this.logCache.shift(); @@ -133,7 +140,7 @@ class Logger { return this.logCache; } - buildLogEntry(level, config) { + buildLogEntry(level: string, config: LogConfig): LogEntry { return { level, message: config.message, @@ -151,3 +158,4 @@ export default Logger; // Legacy logger const logger = new Logger({ envSettings: { logLevel: "debug" } }); export { logger }; +export type { LogConfig, LogEntry }; 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.ts similarity index 65% rename from server/src/utils/utils.js rename to server/src/utils/utils.ts index bcb4d8e5e..a82d8734f 100755 --- a/server/src/utils/utils.js +++ b/server/src/utils/utils.ts @@ -1,9 +1,4 @@ -/** - * Converts a request body parameter to a boolean. - * @param {string | boolean} value - * @returns {boolean} - */ -const ParseBoolean = (value) => { +export const ParseBoolean = (value: boolean | string | null | undefined) => { if (value === true || value === "true") { return true; } else if (value === false || value === "false" || value === null || value === undefined) { @@ -11,7 +6,7 @@ const ParseBoolean = (value) => { } }; -const getTokenFromHeaders = (headers) => { +export const getTokenFromHeaders = (headers: Record) => { const authorizationHeader = headers.authorization; if (!authorizationHeader) throw new Error("No auth headers"); @@ -20,5 +15,3 @@ const getTokenFromHeaders = (headers) => { return parts[1]; }; - -export { ParseBoolean, getTokenFromHeaders }; diff --git a/server/src/validation/joi.js b/server/src/validation/joi.ts similarity index 87% rename from server/src/validation/joi.js rename to server/src/validation/joi.ts index fa8dccde2..8871c29e2 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.ts @@ -1,14 +1,14 @@ -import joi from "joi"; -import { ROLES, VALID_ROLES } from "../utils/roleUtils.js"; +import joi, { type CustomHelpers } from "joi"; +import { type UserRole, UserRoles } from "@/types/user.js"; //**************************************** // Custom Validators //**************************************** -const roleValidatior = (role) => (value, helpers) => { - const hasRole = role.some((role) => value.includes(role)); +const roleValidatior = (role: UserRole[]) => (value: UserRole[], helpers: CustomHelpers) => { + const hasRole = role.some((role: UserRole) => value.includes(role)); if (!hasRole) { - throw new joi.ValidationError(`You do not have the required authorization. Required roles: ${role.join(", ")}`); + return helpers.error("any.invalid", { message: `You do not have the required authorization. Required roles: ${role.join(", ")}` }); } return value; }; @@ -17,8 +17,7 @@ const roleValidatior = (role) => (value, helpers) => { // Auth //**************************************** -const passwordPattern = - /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]+$/; +const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.~`|\\/]+$/; const loginValidation = joi.object({ email: joi.string().email().required().lowercase(), @@ -46,7 +45,7 @@ const registrationBodyValidation = joi.object({ .custom((value, helpers) => { const lowercasedValue = value.toLowerCase(); if (value !== lowercasedValue) { - return helpers.message("Email must be in lowercase"); + return helpers.message({ custom: "Email must be in lowercase" }); } return lowercasedValue; }), @@ -120,30 +119,30 @@ const getMonitorByIdQueryValidation = joi.object({ 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"), + filter: joi.string().allow("", null), }); -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 getMonitorsWithChecksQueryValidation = joi.object({ + limit: joi.number().integer().min(1).max(100).optional(), + page: joi.number().integer().min(0).optional(), + rowsPerPage: joi.number().integer().min(1).max(100).optional(), + filter: joi.string().allow("", null).optional(), + field: joi.string().optional(), + order: joi.string().valid("asc", "desc").optional(), + 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")) + ) + .optional(), + explain: joi.boolean().optional(), }); const getCertificateParamValidation = joi.object({ @@ -174,6 +173,8 @@ const createMonitorBodyValidation = joi.object({ expectedValue: joi.string().allow(""), matchMethod: joi.string(), gameId: joi.string().allow(""), + selectedDisks: joi.array().items(joi.string()).optional(), + group: joi.string().max(50).trim().allow(null, "").optional(), }); const createMonitorsBodyValidation = joi.array().items( @@ -203,6 +204,8 @@ const editMonitorBodyValidation = joi.object({ usage_temperature: joi.number(), }), gameId: joi.string(), + selectedDisks: joi.array().items(joi.string()).optional(), + group: joi.string().max(50).trim().allow(null, "").optional(), }); const pauseMonitorParamValidation = joi.object({ @@ -409,7 +412,7 @@ const updateAppSettingsBodyValidation = joi.object({ pagespeedApiKey: joi.string().allow(""), language: joi.string().allow(""), timezone: joi.string().allow(""), - // showURL: joi.bool().required(), + showURL: joi.bool().optional(), systemEmailHost: joi.string().allow(""), systemEmailPort: joi.number().allow(""), systemEmailAddress: joi.string().allow(""), @@ -573,10 +576,10 @@ const createNotificationBodyValidation = joi.object({ "any.required": "Notification name is required", }), - type: joi.string().valid("email", "webhook", "slack", "discord", "pager_duty").required().messages({ + 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, or pager_duty", + "any.only": "Notification type must be email, webhook, slack, discord, pager_duty, or matrix", }), address: joi.when("type", { @@ -604,8 +607,40 @@ const createNotificationBodyValidation = joi.object({ "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(), + }), }); //**************************************** @@ -654,7 +689,7 @@ const editUserByIdBodyValidation = joi.object({ email: joi.string().email().required(), role: joi .array() - .items(joi.string().valid(...VALID_ROLES)) + .items(joi.string().valid(...UserRoles)) .min(1) .required(), }); @@ -665,11 +700,15 @@ const editSuperadminUserByIdBodyValidation = joi.object({ email: joi.string().email().required(), role: joi .array() - .items(joi.string().valid(...VALID_ROLES, ROLES.SUPERADMIN)) + .items(joi.string().valid(...UserRoles)) .min(1) .required(), }); +const editUserPasswordByIdBodyValidation = joi.object({ + password: joi.string().min(8).required().pattern(passwordPattern), +}); + export { roleValidatior, loginValidation, @@ -685,8 +724,7 @@ export { getMonitorByIdQueryValidation, getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, - getMonitorStatsByIdParamValidation, - getMonitorStatsByIdQueryValidation, + getMonitorsWithChecksQueryValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, getCertificateParamValidation, @@ -739,4 +777,5 @@ export { editUserByIdParamValidation, editUserByIdBodyValidation, editSuperadminUserByIdBodyValidation, + editUserPasswordByIdBodyValidation, }; diff --git a/server/test/monitorService.test.ts b/server/test/monitorService.test.ts new file mode 100644 index 000000000..32708a42e --- /dev/null +++ b/server/test/monitorService.test.ts @@ -0,0 +1,230 @@ +import { jest } from "@jest/globals"; +import { MonitorService } from "../src/service/business/monitorService.ts"; +import type { IChecksRepository, IMonitorStatsRepository, IMonitorsRepository, IStatusPagesRepository } from "../src/repositories/index.ts"; + +const createMonitorsRepositoryMock = () => + ({ + findMonitorCountByTeamIdAndType: jest.fn(), + findByTeamId: jest.fn(), + findById: jest.fn(), + findMonitorsSummaryByTeamId: jest.fn(), + findGroupsByTeamId: jest.fn(), + create: jest.fn(), + createBulkMonitors: jest.fn(), + deleteByTeamId: jest.fn(), + }) as unknown as IMonitorsRepository; + +const createChecksRepositoryMock = () => + ({ + findLatestChecksByMonitorIds: jest.fn(), + findDateRangeChecksByMonitor: jest.fn(), + }) as unknown as IChecksRepository; + +const createMonitorStatsRepositoryMock = () => + ({ + findByMonitorId: jest.fn(), + deleteByMonitorId: jest.fn(), + }) as unknown as IMonitorStatsRepository; + +const createStatusPagesRepositoryMock = () => + ({ + removeMonitorFromStatusPages: jest.fn(), + }) as unknown as IStatusPagesRepository; + +const createService = ({ + monitorsRepository = createMonitorsRepositoryMock(), + checksRepository = createChecksRepositoryMock(), + monitorStatsRepository = createMonitorStatsRepositoryMock(), + statusPagesRepository = createStatusPagesRepositoryMock(), +}: { + monitorsRepository?: IMonitorsRepository; + checksRepository?: IChecksRepository; + monitorStatsRepository?: IMonitorStatsRepository; + statusPagesRepository?: IStatusPagesRepository; +} = {}) => { + return new MonitorService({ + db: { + checkModule: { deleteChecks: jest.fn() }, + statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() }, + pageSpeedCheckModule: { deletePageSpeedChecksByMonitorId: jest.fn() }, + notificationModule: { deleteNotificationsByMonitorId: jest.fn() }, + }, + jobQueue: { + addJob: jest.fn(), + updateJob: jest.fn(), + resumeJob: jest.fn(), + pauseJob: jest.fn(), + deleteJob: jest.fn(), + }, + stringService: {}, + emailService: { buildEmail: jest.fn(), sendEmail: jest.fn() }, + papaparse: { parse: jest.fn(), unparse: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + errorService: { + createAuthorizationError: jest.fn(() => new Error("unauthorized")), + createServerError: jest.fn(() => new Error("server")), + createBadRequestError: jest.fn(() => new Error("bad request")), + createNotFoundError: jest.fn(() => new Error("not found")), + }, + games: [], + monitorsRepository, + checksRepository, + monitorStatsRepository, + statusPagesRepository, + }); +}; + +describe("MonitorService", () => { + describe("getMonitorsWithChecksByTeamId", () => { + it("returns monitors enriched with normalized checks", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(2); + (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([ + { id: "m1", name: "Monitor 1", interval: 60000 }, + { id: "m2", name: "Monitor 2", interval: 60000 }, + ]); + + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ + m1: [ + { responseTime: 10, status: true, message: "OK" }, + { responseTime: 20, status: true, message: "OK" }, + ], + m2: [{ responseTime: 50, status: true, message: "OK" }], + }); + + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + + expect(result).toMatchObject({ count: 2 }); + expect(result.monitors).toHaveLength(2); + expect(result.monitors[0]).toHaveProperty("checks"); + expect(result.monitors[0].checks.length).toBeGreaterThan(0); + expect(result.monitors[0].checks[0]).toEqual( + expect.objectContaining({ + responseTime: expect.any(Number), + status: expect.any(Boolean), + message: expect.any(String), + }) + ); + }); + }); + + describe("getMonitorsByTeamId", () => { + it("returns monitors array from repository", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([ + { id: "m1", name: "Monitor 1" }, + { id: "m2", name: "Monitor 2" }, + ]); + const service = createService({ monitorsRepository }); + const result = await service.getMonitorsByTeamId({ teamId: "team" } as any); + expect(result).toHaveLength(2); + expect(result[0]).toHaveProperty("id", "m1"); + }); + }); + + describe("getMonitorsWithChecksByTeamId summary", () => { + it("includes summary and monitors with checks", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); + (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([{ id: "m1", type: "http" }]); + (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ + totalMonitors: 1, + upMonitors: 1, + downMonitors: 0, + pausedMonitors: 0, + }); + + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ m1: [] }); + + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + expect(result).toEqual({ + summary: { totalMonitors: 1, upMonitors: 1, downMonitors: 0, pausedMonitors: 0 }, + count: 1, + monitors: [{ id: "m1", type: "http", checks: [] }], + }); + }); + }); + + describe("getUptimeDetailsById", () => { + it("returns monitorData and monitorStats with expected shape", async () => { + const TEAM_ID = "team"; + const monitor = { + id: "monitor-1", + teamId: TEAM_ID, + name: "Hardware monitor", + interval: 60000, + statusWindow: [], + statusWindowSize: 5, + statusWindowThreshold: 60, + type: "http", + ignoreTlsErrors: false, + url: "https://example.com", + isActive: true, + alertThreshold: 5, + cpuAlertThreshold: 5, + memoryAlertThreshold: 5, + diskAlertThreshold: 5, + tempAlertThreshold: 5, + selectedDisks: [], + notifications: [], + group: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findDateRangeChecksByMonitor as jest.Mock).mockResolvedValue({ + monitorType: "http", + groupedChecks: [{ _id: "2024-01-01", avgResponseTime: 100, totalChecks: 2 }], + groupedUpChecks: [{ _id: "2024-01-01", totalChecks: 2, avgResponseTime: 90 }], + groupedDownChecks: [{ _id: "2024-01-01", totalChecks: 0, avgResponseTime: 0 }], + uptimePercentage: 0.99, + avgResponseTime: 95, + }); + + const monitorStatsRepository = createMonitorStatsRepositoryMock(); + (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue({ + id: "stats-1", + monitorId: monitor.id, + avgResponseTime: 90, + totalChecks: 10, + totalUpChecks: 9, + totalDownChecks: 1, + uptimePercentage: 0.9, + lastCheckTimestamp: 123456789, + lastResponseTime: 80, + timeOfLastFailure: 123456700, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + + const service = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); + const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: "monitor-1", dateRange: "recent" }); + + expect(result).toHaveProperty("monitorData"); + expect(result.monitorData.monitor).toMatchObject({ id: monitor.id, name: monitor.name }); + expect(result.monitorData.groupedChecks[0]).toEqual( + expect.objectContaining({ + _id: expect.any(String), + avgResponseTime: expect.any(Number), + totalChecks: expect.any(Number), + }) + ); + expect(result.monitorStats).toEqual( + expect.objectContaining({ + monitorId: monitor.id, + avgResponseTime: 90, + totalChecks: 10, + totalUpChecks: 9, + totalDownChecks: 1, + }) + ); + }); + }); +}); diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts new file mode 100644 index 000000000..3b179bd91 --- /dev/null +++ b/server/test/superSimpleQueueHelper.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; +import type { Monitor } from "../src/types/monitor.ts"; + +const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }); + +const createHelper = (overrides?: Partial[0]>) => { + const maintenanceWindowsRepository = { + findByMonitorId: jest.fn().mockResolvedValue([]), + }; + const statusServiceMock = { + updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: { id: "m1" }, statusChanged: true, prevStatus: false }), + }; + const helper = new SuperSimpleQueueHelper({ + logger: createLogger(), + networkService: { requestStatus: jest.fn() }, + statusService: statusServiceMock, + notificationsService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + checkService: { buildCheck: jest.fn().mockResolvedValue({}) }, + buffer: { addToBuffer: jest.fn() }, + incidentService: { handleIncident: jest.fn().mockResolvedValue(undefined) }, + maintenanceWindowsRepository, + ...overrides, + }); + return { helper, maintenanceWindowsRepository }; +}; + +describe("SuperSimpleQueueHelper", () => { + describe("getMonitorJob", () => { + it("skips execution when monitor is in maintenance window", async () => { + const { helper } = createHelper(); + const spy = jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(true); + const job = helper.getMonitorJob(); + await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor); + expect(helper["networkService"].requestStatus).not.toHaveBeenCalled(); + expect(helper["logger"].debug).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") }) + ); + spy.mockRestore(); + }); + + it("processes monitor status and notifications when active", async () => { + const networkResponse = { monitor: { id: "m1" }, status: true }; + const updatedMonitor = { id: "m1", status: true }; + const { helper } = createHelper({ + networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, + statusService: { + updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false, code: 200 }), + }, + notificationsService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + }); + jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(false); + const job = helper.getMonitorJob(); + const monitor = { id: "m1", teamId: "team" } as Monitor; + await job(monitor); + expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor); + }); + + it("throws when monitor id is missing", async () => { + const { helper } = createHelper(); + const job = helper.getMonitorJob(); + await expect(job({} as Monitor)).rejects.toThrow("No monitor id"); + expect(helper["logger"].warn).toHaveBeenCalled(); + }); + }); + + describe("isInMaintenanceWindow", () => { + it("returns true when an active window spans now", async () => { + const now = new Date(); + const { helper, maintenanceWindowsRepository } = createHelper(); + maintenanceWindowsRepository.findByMonitorId.mockResolvedValue([ + { + active: true, + start: new Date(now.getTime() - 1000).toISOString(), + end: new Date(now.getTime() + 1000).toISOString(), + repeat: 0, + }, + ]); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true); + }); + + it("returns true when repeat interval advances window into current time", async () => { + const now = Date.now(); + const { helper, maintenanceWindowsRepository } = createHelper(); + maintenanceWindowsRepository.findByMonitorId.mockResolvedValue([ + { + active: true, + start: new Date(now - 7200000).toISOString(), + end: new Date(now - 6600000).toISOString(), + repeat: 3600000, + }, + ]); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true); + }); + + it("returns false when no active windows exist", async () => { + const { helper } = createHelper(); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(false); + }); + }); +}); diff --git a/server/tests/controllers/authController.test.js b/server/tests/controllers/authController.test.js deleted file mode 100755 index 341c8eda6..000000000 --- a/server/tests/controllers/authController.test.js +++ /dev/null @@ -1,962 +0,0 @@ -import { - issueToken, - registerUser, - loginUser, - editUser, - checkSuperadminExists, - requestRecovery, - validateRecovery, - resetPassword, - deleteUser, - getAllUsers, -} from "../../controllers/authController.js"; -import jwt from "jsonwebtoken"; -import { errorMessages, successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; -import { tokenType } from "../../utils/utils.js"; -import logger from "../../utils/logger.js"; - -describe("Auth Controller - issueToken", function () { - let stub; - - afterEach(function () { - sinon.restore(); // Restore stubs after each test - }); - - it("should reject with an error if jwt.sign fails", function () { - const error = new Error("jwt.sign error"); - stub = sinon.stub(jwt, "sign").throws(error); - const payload = { id: "123" }; - const appSettings = { jwtSecret: "my_secret" }; - expect(() => issueToken(payload, tokenType.ACCESS_TOKEN, appSettings)).to.throw(error); - }); - - it("should return a token if jwt.sign is successful and appSettings.jwtTTL is not defined", function () { - const payload = { id: "123" }; - const appSettings = { jwtSecret: "my_secret" }; - const expectedToken = "mockToken"; - - stub = sinon.stub(jwt, "sign").returns(expectedToken); - const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings); - expect(token).to.equal(expectedToken); - }); - - it("should return a token if jwt.sign is successful and appSettings.jwtTTL is defined", function () { - const payload = { id: "123" }; - const appSettings = { jwtSecret: "my_secret", jwtTTL: "1s" }; - const expectedToken = "mockToken"; - - stub = sinon.stub(jwt, "sign").returns(expectedToken); - const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings); - expect(token).to.equal(expectedToken); - }); - - it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is not defined", function () { - const payload = {}; - const appSettings = { refreshTokenSecret: "my_refresh_secret" }; - const expectedToken = "mockRefreshToken"; - - stub = sinon.stub(jwt, "sign").returns(expectedToken); - const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings); - expect(token).to.equal(expectedToken); - }); - - it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is defined", function () { - const payload = {}; - const appSettings = { - refreshTokenSecret: "my_refresh_secret", - refreshTokenTTL: "7d", - }; - const expectedToken = "mockRefreshToken"; - - stub = sinon.stub(jwt, "sign").returns(expectedToken); - const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings); - expect(token).to.equal(expectedToken); - }); -}); - -describe("Auth Controller - registerUser", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { - firstName: "firstname", - lastName: "lastname", - email: "test@test.com", - password: "Uptime1!", - role: ["admin"], - teamId: "123", - inviteToken: "invite", - }, - db: { - checkSuperadmin: sinon.stub(), - getInviteTokenAndDelete: sinon.stub(), - updateAppSettings: sinon.stub(), - insertUser: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub().resolves({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_refresh_secret", - }), - }, - emailService: { - buildAndSendEmail: sinon.stub(), - }, - file: {}, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - sinon.stub(logger, "error"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = {}; - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if checkSuperadmin fails", async function () { - req.db.checkSuperadmin.throws(new Error("checkSuperadmin error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("checkSuperadmin error"); - }); - - it("should reject with an error if getInviteTokenAndDelete fails", async function () { - req.db.checkSuperadmin.returns(true); - req.db.getInviteTokenAndDelete.throws(new Error("getInviteTokenAndDelete error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getInviteTokenAndDelete error"); - }); - - it("should reject with an error if updateAppSettings fails", async function () { - req.db.checkSuperadmin.returns(false); - req.db.updateAppSettings.throws(new Error("updateAppSettings error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("updateAppSettings error"); - }); - - it("should reject with an error if insertUser fails", async function () { - req.db.checkSuperadmin.resolves(false); - req.db.updateAppSettings.resolves(); - req.db.insertUser.rejects(new Error("insertUser error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("insertUser error"); - }); - - it("should reject with an error if settingsService.getSettings fails", async function () { - req.db.checkSuperadmin.resolves(false); - req.db.updateAppSettings.resolves(); - req.db.insertUser.resolves({ _id: "123" }); - req.settingsService.getSettings.rejects(new Error("settingsService.getSettings error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("settingsService.getSettings error"); - }); - - it("should log an error if emailService.buildAndSendEmail fails", async function () { - req.db.checkSuperadmin.resolves(false); - req.db.updateAppSettings.resolves(); - req.db.insertUser.returns({ _id: "123" }); - req.settingsService.getSettings.returns({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_secret", - }); - req.emailService.buildAndSendEmail.rejects(new Error("emailService error")); - await registerUser(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal("emailService error"); - }); - - it("should return a success message and data if all operations are successful", async function () { - const user = { _id: "123" }; - req.db.checkSuperadmin.resolves(false); - req.db.updateAppSettings.resolves(); - req.db.insertUser.returns(user); - req.settingsService.getSettings.returns({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_secret", - }); - req.emailService.buildAndSendEmail.resolves("message-id"); - await registerUser(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_CREATE_USER, - data: { user, token: sinon.match.string, refreshToken: sinon.match.string }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should return a success message and data if all operations are successful and superAdmin true", async function () { - const user = { _id: "123" }; - req.db.checkSuperadmin.resolves(true); - req.db.updateAppSettings.resolves(); - req.db.insertUser.returns(user); - req.settingsService.getSettings.returns({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_secret", - }); - req.emailService.buildAndSendEmail.resolves("message-id"); - await registerUser(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_CREATE_USER, - data: { user, token: sinon.match.string, refreshToken: sinon.match.string }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - loginUser", function () { - let req, res, next, user; - - beforeEach(function () { - req = { - body: { email: "test@example.com", password: "Password123!" }, - db: { - getUserByEmail: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub().resolves({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_refresh_token", - }), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - user = { - _doc: { - email: "test@example.com", - }, - comparePassword: sinon.stub(), - }; - }); - - it("should reject with an error if validation fails", async function () { - req.body = {}; - await loginUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getUserByEmail fails", async function () { - req.db.getUserByEmail.rejects(new Error("getUserByEmail error")); - await loginUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getUserByEmail error"); - }); - - it("should login user successfully", async function () { - req.db.getUserByEmail.resolves(user); - user.comparePassword.resolves(true); - await loginUser(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_LOGIN_USER, - data: { - user: { - email: "test@example.com", - avatarImage: undefined, - }, - token: sinon.match.string, - refreshToken: sinon.match.string, - }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should reject a user with an incorrect password", async function () { - req.body = { - email: "test@test.com", - password: "Password123!", - }; - req.db.getUserByEmail.resolves(user); - user.comparePassword.resolves(false); - await loginUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal(errorMessages.AUTH_INCORRECT_PASSWORD); - }); -}); - -describe("Auth Controller - refreshAuthToken", function () { - let req, res, next, issueTokenStub; - - beforeEach(function () { - req = { - headers: { - "x-refresh-token": "valid_refresh_token", - authorization: "Bearer old_auth_token", - }, - settingsService: { - getSettings: sinon.stub().resolves({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_refresh_secret", - }), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - sinon.stub(jwt, "verify"); - - issueTokenStub = sinon.stub().returns("new_auth_token"); - sinon.replace({ issueToken }, "issueToken", issueTokenStub); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject if no refresh token is provided", async function () { - delete req.headers["x-refresh-token"]; - await refreshAuthToken(req, res, next); - - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal(errorMessages.NO_REFRESH_TOKEN); - expect(next.firstCall.args[0].status).to.equal(401); - }); - - it("should reject if the refresh token is invalid", async function () { - jwt.verify.yields(new Error("invalid token")); - await refreshAuthToken(req, res, next); - - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal(errorMessages.INVALID_REFRESH_TOKEN); - expect(next.firstCall.args[0].status).to.equal(401); - }); - - it("should reject if the refresh token is expired", async function () { - const error = new Error("Token expired"); - error.name = "TokenExpiredError"; - jwt.verify.yields(error); - await refreshAuthToken(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal(errorMessages.EXPIRED_REFRESH_TOKEN); - expect(next.firstCall.args[0].status).to.equal(401); - }); - - it("should reject if settingsService.getSettings fails", async function () { - req.settingsService.getSettings.rejects(new Error("settingsService.getSettings error")); - await refreshAuthToken(req, res, next); - - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("settingsService.getSettings error"); - }); - - it("should generate a new auth token if the refresh token is valid", async function () { - const decodedPayload = { expiresIn: "60" }; - jwt.verify.callsFake(() => { - return decodedPayload; - }); - await refreshAuthToken(req, res, next); - - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_TOKEN_REFRESHED, - data: { - user: decodedPayload, - token: sinon.match.string, - refreshToken: "valid_refresh_token", - }, - }) - ).to.be.true; - }); -}); - -describe("Auth Controller - editUser", function () { - let req, res, next, stub, user; - - beforeEach(function () { - req = { - params: { userId: "123" }, - body: { password: "Password1!", newPassword: "Password2!" }, - headers: { authorization: "Bearer token" }, - user: { _id: "123" }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "my_secret" }), - }, - db: { - getUserByEmail: sinon.stub(), - updateUser: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - stub = sinon.stub(jwt, "verify").returns({ email: "test@example.com" }); - }); - - afterEach(function () { - sinon.restore(); - stub.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = { invalid: 1 }; - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if param.userId !== req.user._id", async function () { - req.params = { userId: "456" }; - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(401); - }); - - it("should reject with an error if !req.body.password and getUserByEmail fails", async function () { - req.db.getUserByEmail.rejects(new Error("getUserByEmail error")); - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getUserByEmail error"); - }); - - it("should reject with an error if user.comparePassword fails", async function () { - req.db.getUserByEmail.returns({ - comparePassword: sinon.stub().rejects(new Error("Bad Password Match")), - }); - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Bad Password Match"); - }); - - it("should reject with an error if user.comparePassword returns false", async function () { - req.db.getUserByEmail.returns({ - comparePassword: sinon.stub().returns(false), - }); - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(401); - expect(next.firstCall.args[0].message).to.equal(errorMessages.AUTH_INCORRECT_PASSWORD); - }); - - it("should edit a user if it receives a proper request", async function () { - const user = { - comparePassword: sinon.stub().resolves(true), - }; - req.db.getUserByEmail.resolves(user); - req.db.updateUser.resolves({ email: "test@example.com" }); - - await editUser(req, res, next); - - expect(req.db.getUserByEmail.calledOnce).to.be.true; - expect(req.db.updateUser.calledOnce).to.be.true; - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_UPDATE_USER, - data: { email: "test@example.com" }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should edit a user if it receives a proper request and both password fields are undefined", async function () { - req.body.password = undefined; - req.body.newPassword = undefined; - req.db.getUserByEmail.resolves(user); - req.db.updateUser.resolves({ email: "test@example.com" }); - - await editUser(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_UPDATE_USER, - data: { email: "test@example.com" }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should reject an edit request if password format is incorrect", async function () { - req.body = { password: "bad_password", newPassword: "bad_password" }; - const user = { - comparePassword: sinon.stub().resolves(true), - }; - req.db.getUserByEmail.resolves(user); - - await editUser(req, res, next); - - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); -}); - -describe("Auth Controller - checkSuperadminExists", function () { - let req, res, next; - - beforeEach(function () { - req = { - db: { - checkSuperadmin: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject with an error if checkSuperadmin fails", async function () { - req.db.checkSuperadmin.rejects(new Error("checkSuperadmin error")); - await checkSuperadminExists(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("checkSuperadmin error"); - }); - - it("should return true if a superadmin exists", async function () { - req.db.checkSuperadmin.resolves(true); - await checkSuperadminExists(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_SUPERADMIN_EXISTS, - data: true, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should return false if a superadmin does not exist", async function () { - req.db.checkSuperadmin.resolves(false); - await checkSuperadminExists(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_SUPERADMIN_EXISTS, - data: false, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - requestRecovery", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { email: "test@test.com" }, - db: { - getUserByEmail: sinon.stub(), - requestRecoveryToken: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub().returns({ clientHost: "http://localhost" }), - }, - emailService: { - buildAndSendEmail: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject with an error if validation fails", async function () { - req.body = {}; - await requestRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getUserByEmail fails", async function () { - req.db.getUserByEmail.rejects(new Error("getUserByEmail error")); - await requestRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getUserByEmail error"); - }); - - it("should throw an error if the user is not found", async function () { - req.db.getUserByEmail.resolves(null); - await requestRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - // expect(next.firstCall.args[0].message).to.equal( - // errorMessages.FRIENDLY_ERROR - // ); - }); - - it("should throw an error if the email is not provided", async function () { - req.body = {}; - await requestRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should return a success message if the email is provided", async function () { - const user = { firstName: "John" }; - const recoveryToken = { token: "recovery-token" }; - const msgId = "message-id"; - req.db.getUserByEmail.resolves(user); - req.db.requestRecoveryToken.resolves(recoveryToken); - req.emailService.buildAndSendEmail.resolves(msgId); - await requestRecovery(req, res, next); - expect(req.db.getUserByEmail.calledOnceWith("test@test.com")).to.be.true; - expect(req.db.requestRecoveryToken.calledOnceWith(req, res)).to.be.true; - expect( - req.emailService.buildAndSendEmail.calledOnceWith( - "passwordResetTemplate", - { - name: "John", - email: "test@test.com", - url: "http://localhost/set-new-password/recovery-token", - }, - "test@test.com", - "Checkmate Password Reset" - ) - ).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN, - data: msgId, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - validateRecovery", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { recoveryToken: "recovery-token" }, - db: { - validateRecoveryToken: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject with an error if validation fails", async function () { - req.body = {}; - await validateRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if validateRecoveryToken fails", async function () { - req.db.validateRecoveryToken.rejects(new Error("validateRecoveryToken error")); - await validateRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("validateRecoveryToken error"); - }); - - it("should return a success message if the token is valid", async function () { - req.db.validateRecoveryToken.resolves(); - await validateRecovery(req, res, next); - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - resetPassword", function () { - let req, res, next, newPasswordValidation, handleValidationError, handleError; - - beforeEach(function () { - req = { - body: { - recoveryToken: "recovery-token", - password: "Password1!", - }, - db: { - resetPassword: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - newPasswordValidation = { - validateAsync: sinon.stub(), - }; - handleValidationError = sinon.stub(); - handleError = sinon.stub(); - }); - - it("should reject with an error if validation fails", async function () { - req.body = { password: "bad_password" }; - await resetPassword(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if resetPassword fails", async function () { - const error = new Error("resetPassword error"); - newPasswordValidation.validateAsync.resolves(); - req.db.resetPassword.rejects(error); - await resetPassword(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("resetPassword error"); - }); - - it("should reset password successfully", async function () { - const user = { _doc: {} }; - const appSettings = { jwtSecret: "my_secret" }; - const token = "token"; - - newPasswordValidation.validateAsync.resolves(); - req.db.resetPassword.resolves(user); - req.settingsService.getSettings.resolves(appSettings); - - await resetPassword(req, res, next); - - expect(req.db.resetPassword.calledOnceWith(req, res)).to.be.true; - expect(req.settingsService.getSettings.calledOnce).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_RESET_PASSWORD, - data: { user: sinon.match.object, token: sinon.match.string }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - deleteUser", function () { - let req, res, next, handleError; - - beforeEach(function () { - req = { - headers: { - authorization: "Bearer token", - }, - db: { - getUserByEmail: sinon.stub(), - getMonitorsByTeamId: sinon.stub(), - deleteJob: sinon.stub(), - deleteChecks: sinon.stub(), - deletePageSpeedChecksByMonitorId: sinon.stub(), - deleteNotificationsByMonitorId: sinon.stub(), - deleteTeam: sinon.stub(), - deleteAllOtherUsers: sinon.stub(), - deleteMonitorsByUserId: sinon.stub(), - deleteUser: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - - sinon.stub(jwt, "decode"); - - handleError = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should throw an error if user is not found", async function () { - jwt.decode.returns({ email: "test@example.com" }); - req.db.getUserByEmail.throws(new Error(errorMessages.DB_USER_NOT_FOUND)); - - await deleteUser(req, res, next); - - expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true; - expect(next.calledOnce).to.be.true; - expect(next.firstCall.args[0].message).to.equal(errorMessages.DB_USER_NOT_FOUND); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); - - it("should delete user and associated data if user is superadmin", async function () { - const user = { - _id: "user_id", - email: "test@example.com", - role: ["superadmin"], - teamId: "team_id", - }; - const monitors = [{ _id: "monitor_id" }]; - - jwt.decode.returns({ email: "test@example.com" }); - req.db.getUserByEmail.resolves(user); - req.db.getMonitorsByTeamId.resolves({ monitors }); - - await deleteUser(req, res, next); - - expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true; - expect( - req.db.getMonitorsByTeamId.calledOnceWith({ - params: { teamId: "team_id" }, - }) - ).to.be.true; - expect(req.jobQueue.deleteJob.calledOnceWith(monitors[0])).to.be.true; - expect(req.db.deleteChecks.calledOnceWith("monitor_id")).to.be.true; - expect(req.db.deletePageSpeedChecksByMonitorId.calledOnceWith("monitor_id")).to.be.true; - expect(req.db.deleteNotificationsByMonitorId.calledOnceWith("monitor_id")).to.be.true; - expect(req.db.deleteTeam.calledOnceWith("team_id")).to.be.true; - expect(req.db.deleteAllOtherUsers.calledOnce).to.be.true; - expect(req.db.deleteMonitorsByUserId.calledOnceWith("user_id")).to.be.true; - expect(req.db.deleteUser.calledOnceWith("user_id")).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_DELETE_USER, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should delete user if user is not superadmin", async function () { - const user = { - _id: "user_id", - email: "test@example.com", - role: ["user"], - teamId: "team_id", - }; - - jwt.decode.returns({ email: "test@example.com" }); - req.db.getUserByEmail.resolves(user); - - await deleteUser(req, res, next); - - expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true; - expect( - req.db.getMonitorsByTeamId.calledOnceWith({ - params: { teamId: "team_id" }, - }) - ).to.be.true; - expect(req.db.deleteUser.calledOnceWith("user_id")).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_DELETE_USER, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should handle errors", async function () { - const error = new Error("Something went wrong"); - const SERVICE_NAME = "AuthController"; - jwt.decode.returns({ email: "test@example.com" }); - req.db.getUserByEmail.rejects(error); - await deleteUser(req, res, next); - expect(next.calledOnce).to.be.true; - expect(next.firstCall.args[0].message).to.equal("Something went wrong"); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - getAllUsers", function () { - let req, res, next; - - beforeEach(function () { - req = { - db: { - getAllUsers: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); // Restore the original methods after each test - }); - - it("should return 200 and all users", async function () { - const allUsers = [{ id: 1, name: "John Doe" }]; - req.db.getAllUsers.resolves(allUsers); - - await getAllUsers(req, res, next); - - expect(req.db.getAllUsers.calledOnce).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: "Got all users", - data: allUsers, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should call next with error when an exception occurs", async function () { - const error = new Error("Something went wrong"); - req.db.getAllUsers.rejects(error); - await getAllUsers(req, res, next); - expect(req.db.getAllUsers.calledOnce).to.be.true; - expect(next.calledOnce).to.be.true; - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); -}); diff --git a/server/tests/controllers/checkController.test.js b/server/tests/controllers/checkController.test.js deleted file mode 100755 index c5fb57d50..000000000 --- a/server/tests/controllers/checkController.test.js +++ /dev/null @@ -1,372 +0,0 @@ -import { createCheck, getChecks, getTeamChecks, deleteChecks, deleteChecksByTeamId, updateChecksTTL } from "../../controllers/checkController.js"; -import jwt from "jsonwebtoken"; -import { errorMessages, successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; -describe("Check Controller - createCheck", function () { - let req, res, next, handleError; - - beforeEach(function () { - req = { - params: {}, - body: {}, - db: { - createCheck: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - handleError = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); // Restore the original methods after each test - }); - - it("should reject with a validation if params are invalid", async function () { - await createCheck(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with a validation error if body is invalid", async function () { - req.params = { - monitorId: "monitorId", - }; - await createCheck(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { - monitorId: "monitorId", - }; - req.body = { - monitorId: "monitorId", - status: true, - responseTime: 100, - statusCode: 200, - message: "message", - }; - req.db.createCheck.rejects(new Error("error")); - await createCheck(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - }); - - it("should return a success message if check is created", async function () { - req.params = { - monitorId: "monitorId", - }; - req.db.createCheck.resolves({ id: "123" }); - req.body = { - monitorId: "monitorId", - status: true, - responseTime: 100, - statusCode: 200, - message: "message", - }; - await createCheck(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.CHECK_CREATE, - data: { id: "123" }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Check Controller - getChecks", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - db: { - getChecks: sinon.stub(), - getChecksCount: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with a validation error if params are invalid", async function () { - await getChecks(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should return a success message if checks are found", async function () { - req.params = { - monitorId: "monitorId", - }; - req.db.getChecks.resolves([{ id: "123" }]); - req.db.getChecksCount.resolves(1); - await getChecks(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.CHECK_GET, - data: { checksCount: 1, checks: [{ id: "123" }] }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { - monitorId: "monitorId", - }; - req.db.getChecks.rejects(new Error("error")); - await getChecks(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - }); -}); - -describe("Check Controller - getTeamChecks", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - db: { - getTeamChecks: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with a validation error if params are invalid", async function () { - await getTeamChecks(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should return 200 and check data on successful validation and data retrieval", async function () { - req.params = { teamId: "1" }; - const checkData = [{ id: 1, name: "Check 1" }]; - req.db.getTeamChecks.resolves(checkData); - - await getTeamChecks(req, res, next); - expect(req.db.getTeamChecks.calledOnceWith(req)).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.CHECK_GET, - data: checkData, - }) - ).to.be.true; - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { teamId: "1" }; - req.db.getTeamChecks.rejects(new Error("Retrieval Error")); - await getTeamChecks(req, res, next); - expect(req.db.getTeamChecks.calledOnceWith(req)).to.be.true; - expect(next.firstCall.args[0]).to.be.an("error"); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); -}); - -describe("Check Controller - deleteChecks", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - db: { - deleteChecks: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - await deleteChecks(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { monitorId: "1" }; - req.db.deleteChecks.rejects(new Error("Deletion Error")); - await deleteChecks(req, res, next); - expect(req.db.deleteChecks.calledOnceWith(req.params.monitorId)).to.be.true; - expect(next.firstCall.args[0]).to.be.an("error"); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); - - it("should delete checks successfully", async function () { - req.params = { monitorId: "123" }; - req.db.deleteChecks.resolves(1); - await deleteChecks(req, res, next); - expect(req.db.deleteChecks.calledOnceWith(req.params.monitorId)).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.CHECK_DELETE, - data: { deletedCount: 1 }, - }) - ).to.be.true; - }); -}); - -describe("Check Controller - deleteChecksByTeamId", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - db: { - deleteChecksByTeamId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - await deleteChecksByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { teamId: "1" }; - req.db.deleteChecksByTeamId.rejects(new Error("Deletion Error")); - await deleteChecksByTeamId(req, res, next); - expect(req.db.deleteChecksByTeamId.calledOnceWith(req.params.teamId)).to.be.true; - expect(next.firstCall.args[0]).to.be.an("error"); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); - - it("should delete checks successfully", async function () { - req.params = { teamId: "123" }; - req.db.deleteChecksByTeamId.resolves(1); - await deleteChecksByTeamId(req, res, next); - expect(req.db.deleteChecksByTeamId.calledOnceWith(req.params.teamId)).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.CHECK_DELETE, - data: { deletedCount: 1 }, - }) - ).to.be.true; - }); -}); - -describe("Check Controller - updateCheckTTL", function () { - let stub, req, res, next; - - beforeEach(function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - - req = { - body: {}, - headers: { authorization: "Bearer token" }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "my_secret" }), - }, - db: { - updateChecksTTL: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - stub.restore(); - }); - - it("should reject if body validation fails", async function () { - await updateChecksTTL(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should throw a JwtError if verification fails", async function () { - stub.restore(); - req.body = { - ttl: 1, - }; - await updateChecksTTL(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - }); - - it("should call next with error if data retrieval fails", async function () { - req.body = { - ttl: 1, - }; - req.db.updateChecksTTL.rejects(new Error("Update Error")); - await updateChecksTTL(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - }); - - it("should update TTL successfully", async function () { - req.body = { - ttl: 1, - }; - req.db.updateChecksTTL.resolves(); - await updateChecksTTL(req, res, next); - expect(req.db.updateChecksTTL.calledOnceWith("123", 1 * 86400)).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.CHECK_UPDATE_TTL, - }) - ).to.be.true; - }); -}); diff --git a/server/tests/controllers/controllerUtils.test.js b/server/tests/controllers/controllerUtils.test.js deleted file mode 100755 index a55318857..000000000 --- a/server/tests/controllers/controllerUtils.test.js +++ /dev/null @@ -1,157 +0,0 @@ -import sinon from "sinon"; - -import { handleValidationError, handleError, fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; -import { expect } from "chai"; -import sslChecker from "ssl-checker"; -import { afterEach } from "node:test"; -import exp from "constants"; - -describe("controllerUtils - handleValidationError", function () { - it("should set status to 422", function () { - const error = {}; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.status).to.equal(422); - }); - - it("should set service to the provided serviceName", function () { - const error = {}; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.service).to.equal(serviceName); - }); - - it("should set message to error.details[0].message if present", function () { - const error = { - details: [{ message: "Detail message" }], - }; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.message).to.equal("Detail message"); - }); - - it("should set message to error.message if error.details is not present", function () { - const error = { - message: "Error message", - }; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.message).to.equal("Error message"); - }); - - it('should set message to "Validation Error" if neither error.details nor error.message is present', function () { - const error = {}; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.message).to.equal("Validation Error"); - }); -}); - -describe("controllerUtils - handleError", function () { - it("should set stats to the provided status if error.code is undefined", function () { - const error = {}; - const serviceName = "TestService"; - const method = "testMethod"; - const status = 400; - const result = handleError(error, serviceName, method, status); - expect(result.status).to.equal(status); - }); - - it("should not overwrite error.code if it is already defined", function () { - const error = { status: 404 }; - const serviceName = "TestService"; - const method = "testMethod"; - const status = 400; - const result = handleError(error, serviceName, method, status); - expect(result.status).to.equal(404); - }); - - it("should set service to the provided serviceName if error.service is undefined", function () { - const error = {}; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.service).to.equal(serviceName); - }); - - it("should not overwrite error.service if it is already defined", function () { - const error = { service: "ExistingService" }; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.service).to.equal("ExistingService"); - }); - - it("should set method to the provided method if error.method is undefined", function () { - const error = {}; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.method).to.equal(method); - }); - - it("should not overwrite error.method if it is already defined", function () { - const error = { method: "existingMethod" }; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.method).to.equal("existingMethod"); - }); - - it("should set code to 500 if error.code is undefined and no code is provided", function () { - const error = {}; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.status).to.equal(500); - }); -}); - -describe("controllerUtils - fetchMonitorCertificate", function () { - let sslChecker, monitor; - - beforeEach(function () { - monitor = { - url: "https://www.google.com", - }; - sslChecker = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if a URL does not parse", async function () { - monitor.url = "invalidurl"; - try { - await fetchMonitorCertificate(sslChecker, monitor); - } catch (error) { - expect(error).to.be.an("error"); - expect(error.message).to.equal("Invalid URL"); - } - }); - - it("should reject with an error if sslChecker throws an error", async function () { - sslChecker.rejects(new Error("Test error")); - try { - await fetchMonitorCertificate(sslChecker, monitor); - } catch (error) { - expect(error).to.be.an("error"); - expect(error.message).to.equal("Test error"); - } - }); - - it("should return a certificate if sslChecker resolves", async function () { - sslChecker.resolves({ validTo: "2022-01-01" }); - const result = await fetchMonitorCertificate(sslChecker, monitor); - expect(result).to.deep.equal({ validTo: "2022-01-01" }); - }); - - it("should throw an error if a ssl-checker returns null", async function () { - sslChecker.returns(null); - await fetchMonitorCertificate(sslChecker, monitor).catch((error) => { - expect(error).to.be.an("error"); - expect(error.message).to.equal("Certificate not found"); - }); - }); -}); diff --git a/server/tests/controllers/inviteController.test.js b/server/tests/controllers/inviteController.test.js deleted file mode 100755 index 635bdf653..000000000 --- a/server/tests/controllers/inviteController.test.js +++ /dev/null @@ -1,202 +0,0 @@ -import { issueInvitation, inviteVerifyController } from "../../controllers/inviteController.js"; -import jwt from "jsonwebtoken"; -import sinon from "sinon"; -import joi from "joi"; -describe("inviteController - issueInvitation", function () { - let req, res, next, stub; - - beforeEach(function () { - req = { - headers: { authorization: "Bearer token" }, - body: { - email: "test@test.com", - role: ["admin"], - teamId: "123", - }, - db: { requestInviteToken: sinon.stub() }, - settingsService: { getSettings: sinon.stub() }, - emailService: { buildAndSendEmail: sinon.stub() }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if role validation fails", async function () { - stub = sinon.stub(jwt, "decode").callsFake(() => { - return { role: ["bad_role"], firstname: "first_name", teamId: "1" }; - }); - await issueInvitation(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0]).to.be.instanceOf(joi.ValidationError); - expect(next.firstCall.args[0].status).to.equal(422); - stub.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - stub = sinon.stub(jwt, "decode").callsFake(() => { - return { role: ["admin"], firstname: "first_name", teamId: "1" }; - }); - req.body = {}; - await issueInvitation(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - stub.restore(); - }); - - it("should reject with an error if DB operations fail", async function () { - stub = sinon.stub(jwt, "decode").callsFake(() => { - return { role: ["admin"], firstname: "first_name", teamId: "1" }; - }); - req.db.requestInviteToken.throws(new Error("DB error")); - await issueInvitation(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - stub.restore(); - }); - - it("should send an invite successfully", async function () { - const token = "token"; - const decodedToken = { - role: "admin", - firstname: "John", - teamId: "team123", - }; - const inviteToken = { token: "inviteToken" }; - const clientHost = "http://localhost"; - - stub = sinon.stub(jwt, "decode").callsFake(() => { - return decodedToken; - }); - req.db.requestInviteToken.resolves(inviteToken); - req.settingsService.getSettings.returns({ clientHost }); - req.emailService.buildAndSendEmail.resolves(); - await issueInvitation(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: "Invite sent", - data: inviteToken, - }) - ).to.be.true; - stub.restore(); - }); - - it("should send an email successfully", async function () { - const token = "token"; - const decodedToken = { - role: "admin", - firstname: "John", - teamId: "team123", - }; - const inviteToken = { token: "inviteToken" }; - const clientHost = "http://localhost"; - - stub = sinon.stub(jwt, "decode").callsFake(() => { - return decodedToken; - }); - req.db.requestInviteToken.resolves(inviteToken); - req.settingsService.getSettings.returns({ clientHost }); - req.emailService.buildAndSendEmail.resolves(); - - await issueInvitation(req, res, next); - expect(req.emailService.buildAndSendEmail.calledOnce).to.be.true; - expect( - req.emailService.buildAndSendEmail.calledWith( - "employeeActivationTemplate", - { - name: "John", - link: "http://localhost/register/inviteToken", - }, - "test@test.com", - "Welcome to Uptime Monitor" - ) - ).to.be.true; - stub.restore(); - }); - - it("should continue executing if sending an email fails", async function () { - const token = "token"; - req.emailService.buildAndSendEmail.rejects(new Error("Email error")); - const decodedToken = { - role: "admin", - firstname: "John", - teamId: "team123", - }; - const inviteToken = { token: "inviteToken" }; - const clientHost = "http://localhost"; - - stub = sinon.stub(jwt, "decode").callsFake(() => { - return decodedToken; - }); - req.db.requestInviteToken.resolves(inviteToken); - req.settingsService.getSettings.returns({ clientHost }); - await issueInvitation(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: "Invite sent", - data: inviteToken, - }) - ).to.be.true; - stub.restore(); - }); -}); - -describe("inviteController - inviteVerifyController", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { token: "token" }, - db: { - getInviteToken: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = {}; - await inviteVerifyController(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getInviteToken.throws(new Error("DB error")); - await inviteVerifyController(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return 200 and invite data when validation and invite retrieval are successful", async function () { - req.db.getInviteToken.resolves({ invite: "data" }); - await inviteVerifyController(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - status: "success", - msg: "Invite verified", - data: { invite: "data" }, - }) - ).to.be.true; - expect(next.called).to.be.false; - }); -}); diff --git a/server/tests/controllers/maintenanceWindowController.test.js b/server/tests/controllers/maintenanceWindowController.test.js deleted file mode 100755 index 0ea07b389..000000000 --- a/server/tests/controllers/maintenanceWindowController.test.js +++ /dev/null @@ -1,422 +0,0 @@ -import { - createMaintenanceWindows, - getMaintenanceWindowById, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindow, - editMaintenanceWindow, -} from "../../controllers/maintenanceWindowController.js"; - -import jwt from "jsonwebtoken"; -import { successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; - -describe("maintenanceWindowController - createMaintenanceWindows", function () { - let req, res, next, stub; - - beforeEach(function () { - req = { - body: { - monitors: ["66ff52e7c5911c61698ac724"], - name: "window", - active: true, - start: "2024-10-11T05:27:13.747Z", - end: "2024-10-11T05:27:14.747Z", - repeat: "123", - }, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - createMaintenanceWindow: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req.body = {}; - await createMaintenanceWindows(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - stub.restore(); - }); - - it("should reject with an error if jwt.verify fails", async function () { - stub = sinon.stub(jwt, "verify").throws(new jwt.JsonWebTokenError()); - await createMaintenanceWindows(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - stub.restore(); - }); - - it("should reject with an error DB operations fail", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req.db.createMaintenanceWindow.throws(new Error("DB error")); - await createMaintenanceWindows(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - stub.restore(); - }); - - it("should return success message if all operations are successful", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - await createMaintenanceWindows(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(201); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_CREATE, - }) - ).to.be.true; - stub.restore(); - }); - - it("should return success message if all operations are successful with active set to undefined", async function () { - req.body.active = undefined; - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - await createMaintenanceWindows(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(201); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_CREATE, - }) - ).to.be.true; - stub.restore(); - }); -}); - -describe("maintenanceWindowController - getMaintenanceWindowById", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: {}, - params: { - id: "123", - }, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - getMaintenanceWindowById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject if param validation fails", async function () { - req.params = {}; - await getMaintenanceWindowById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject if DB operations fail", async function () { - req.db.getMaintenanceWindowById.throws(new Error("DB error")); - await getMaintenanceWindowById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message with data if all operations are successful", async function () { - req.db.getMaintenanceWindowById.returns({ id: "123" }); - await getMaintenanceWindowById(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID, - data: { id: "123" }, - }) - ).to.be.true; - }); -}); - -describe("maintenanceWindowController - getMaintenanceWindowsByTeamId", function () { - let req, res, next, stub; - - beforeEach(function () { - req = { - body: {}, - params: {}, - query: {}, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - getMaintenanceWindowsByTeamId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject if query validation fails", async function () { - req.query = { - invalid: 1, - }; - await getMaintenanceWindowsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject if jwt.verify fails", async function () { - stub = sinon.stub(jwt, "verify").throws(new jwt.JsonWebTokenError()); - await getMaintenanceWindowsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - stub.restore(); - }); - - it("should reject with an error if DB operations fail", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req.db.getMaintenanceWindowsByTeamId.throws(new Error("DB error")); - await getMaintenanceWindowsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - stub.restore(); - }); - - it("should return success message with data if all operations are successful", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req.db.getMaintenanceWindowsByTeamId.returns([{ id: "123" }]); - await getMaintenanceWindowsByTeamId(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM, - data: [{ id: jwt.verify().teamId }], - }) - ).to.be.true; - stub.restore(); - }); -}); - -describe("maintenanceWindowController - getMaintenanceWindowsByMonitorId", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: {}, - params: { - monitorId: "123", - }, - query: {}, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - getMaintenanceWindowsByMonitorId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject if param validation fails", async function () { - req.params = {}; - await getMaintenanceWindowsByMonitorId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMaintenanceWindowsByMonitorId.throws(new Error("DB error")); - await getMaintenanceWindowsByMonitorId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message with data if all operations are successful", async function () { - const data = [{ monitorId: "123" }]; - req.db.getMaintenanceWindowsByMonitorId.returns(data); - await getMaintenanceWindowsByMonitorId(req, res, next); - expect(req.db.getMaintenanceWindowsByMonitorId.calledOnceWith(req.params.monitorId)); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_MONITOR, - data: data, - }) - ).to.be.true; - }); -}); - -describe("maintenanceWindowController - deleteMaintenanceWindow", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: {}, - params: { - id: "123", - }, - query: {}, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - deleteMaintenanceWindowById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject if param validation fails", async function () { - req.params = {}; - await deleteMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.deleteMaintenanceWindowById.throws(new Error("DB error")); - await deleteMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message if all operations are successful", async function () { - await deleteMaintenanceWindow(req, res, next); - expect(req.db.deleteMaintenanceWindowById.calledOnceWith(req.params.id)); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_DELETE, - }) - ).to.be.true; - }); -}); - -describe("maintenanceWindowController - editMaintenanceWindow", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { - active: true, - name: "test", - }, - params: { - id: "123", - }, - query: {}, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - editMaintenanceWindowById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject if param validation fails", async function () { - req.params = {}; - await editMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject if body validation fails", async function () { - req.body = { invalid: 1 }; - await editMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.editMaintenanceWindowById.throws(new Error("DB error")); - await editMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message with data if all operations are successful", async function () { - const data = { id: "123" }; - req.db.editMaintenanceWindowById.returns(data); - - await editMaintenanceWindow(req, res, next); - expect(req.db.editMaintenanceWindowById.calledOnceWith(req.params.id, req.body)); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_EDIT, - data: data, - }) - ).to.be.true; - }); -}); diff --git a/server/tests/controllers/monitorController.test.js b/server/tests/controllers/monitorController.test.js deleted file mode 100755 index 963862579..000000000 --- a/server/tests/controllers/monitorController.test.js +++ /dev/null @@ -1,1116 +0,0 @@ -import { - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorCertificate, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - checkEndpointResolution, - deleteMonitor, - deleteAllMonitors, - editMonitor, - pauseMonitor, - addDemoMonitors, -} from "../../controllers/monitorController.js"; -import jwt from "jsonwebtoken"; -import sinon from "sinon"; -import { successMessages } from "../../utils/messages.js"; -import logger from "../../utils/logger.js"; -import axios from "axios"; -const SERVICE_NAME = "monitorController"; - -describe("Monitor Controller - getAllMonitors", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - body: {}, - db: { - getAllMonitors: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getAllMonitors.throws(new Error("DB error")); - await getAllMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = [{ monitor: "data" }]; - req.db.getAllMonitors.returns(data); - await getAllMonitors(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_ALL, - data: data, - }) - ).to.be.true; - }); -}); -describe("Monitor Controller - getAllMonitorsWithUptimeStats", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - body: {}, - db: { - getAllMonitorsWithUptimeStats: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getAllMonitorsWithUptimeStats.throws(new Error("DB error")); - await getAllMonitorsWithUptimeStats(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = [{ monitor: "data" }]; - req.db.getAllMonitorsWithUptimeStats.returns(data); - await getAllMonitorsWithUptimeStats(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_ALL, - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - getMonitorStatsById", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorStatsById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorStatsById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if query validation fails", async function () { - req.query = { invalid: 1 }; - await getMonitorStatsById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMonitorStatsById.throws(new Error("DB error")); - await getMonitorStatsById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = [{ monitorStats: "data" }]; - req.db.getMonitorStatsById.returns(data); - await getMonitorStatsById(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_STATS_BY_ID, - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - getMonitorCertificate", function () { - let req, res, next, fetchMonitorCertificate; - - beforeEach(function () { - req = { - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - fetchMonitorCertificate = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorCertificate(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getMonitorById operation fails", async function () { - req.db.getMonitorById.throws(new Error("DB error")); - await getMonitorCertificate(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed with a valid cert", async function () { - req.db.getMonitorById.returns({ url: "https://www.google.com" }); - const data = { certificate: "cert", validTo: "2024/08/08" }; - fetchMonitorCertificate.returns(data); - await getMonitorCertificate(req, res, next, fetchMonitorCertificate); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_CERTIFICATE, - data: { certificateDate: new Date(data.validTo) }, - }) - ).to.be.true; - }); - - it("should return an error if fetchMonitorCertificate fails", async function () { - req.db.getMonitorById.returns({ url: "https://www.google.com" }); - fetchMonitorCertificate.throws(new Error("Certificate error")); - await getMonitorCertificate(req, res, next, fetchMonitorCertificate); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Certificate error"); - }); -}); - -describe("Monitor Controller - getMonitorById", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if query param validation fails", async function () { - req.query = { invalid: 1 }; - await getMonitorById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMonitorById.throws(new Error("DB error")); - await getMonitorById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return 404 if a monitor is not found", async function () { - const error = new Error("Monitor not found"); - error.status = 404; - req.db.getMonitorById.throws(error); - await getMonitorById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(404); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = { monitor: "data" }; - req.db.getMonitorById.returns(data); - await getMonitorById(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_BY_ID, - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - getMonitorsAndSummaryByTeamId", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - teamId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorsAndSummaryByTeamId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorsAndSummaryByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if query validation fails", async function () { - req.query = { invalid: 1 }; - await getMonitorsAndSummaryByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMonitorsAndSummaryByTeamId.throws(new Error("DB error")); - await getMonitorsAndSummaryByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = { monitors: "data", summary: "data" }; - req.db.getMonitorsAndSummaryByTeamId.returns(data); - await getMonitorsAndSummaryByTeamId(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(req.params.teamId), - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - getMonitorsByTeamId", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - teamId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorsByTeamId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if query validation fails", async function () { - req.query = { invalid: 1 }; - await getMonitorsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMonitorsByTeamId.throws(new Error("DB error")); - await getMonitorsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = { monitors: "data" }; - req.db.getMonitorsByTeamId.returns(data); - await getMonitorsByTeamId(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(req.params.teamId), - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - createMonitor", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - body: { - userId: "123", - teamId: "123", - name: "test_monitor", - description: "test_monitor_desc", - type: "http", - url: "https://example.com", - notifications: [{ email: "example@example.com" }], - }, - db: { - createMonitor: sinon.stub(), - createNotification: sinon.stub(), - }, - jobQueue: { - addJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = {}; - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB createMonitor operation fail", async function () { - req.db.createMonitor.throws(new Error("DB error")); - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if DB createNotification operation fail", async function () { - req.db.createNotification.throws(new Error("DB error")); - req.db.createMonitor.returns({ _id: "123" }); - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if monitor.save operation fail", async function () { - req.db.createMonitor.returns({ - _id: "123", - save: sinon.stub().throws(new Error("Monitor save error")), - }); - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Monitor save error"); - }); - - it("should throw an error if addJob operation fails", async function () { - req.db.createMonitor.returns({ _id: "123", save: sinon.stub() }); - req.jobQueue.addJob.throws(new Error("Job error")); - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Job error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const monitor = { _id: "123", save: sinon.stub() }; - req.db.createMonitor.returns(monitor); - await createMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(201); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_CREATE, - data: monitor, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - checkEndpointResolution", function () { - let req, res, next, axiosGetStub; - - beforeEach(function () { - req = { query: { monitorURL: "https://example.com" } }; - res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; - next = sinon.stub(); - axiosGetStub = sinon.stub(axios, "get"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should resolve the URL successfully", async function () { - axiosGetStub.resolves({ status: 200, statusText: "OK" }); - await checkEndpointResolution(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - code: 200, - statusText: "OK", - msg: "URL resolved successfully", - }) - ).to.be.true; - expect(next.called).to.be.false; - }); - - it("should return an error if endpoint resolution fails", async function () { - const axiosError = new Error("resolution failed"); - axiosError.code = "ENOTFOUND"; - axiosGetStub.rejects(axiosError); - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const errorPassedToNext = next.getCall(0).args[0]; - expect(errorPassedToNext).to.be.an.instanceOf(Error); - expect(errorPassedToNext.message).to.include("resolution failed"); - expect(errorPassedToNext.code).to.equal("ENOTFOUND"); - expect(errorPassedToNext.status).to.equal(500); - }); - - it("should reject with an error if query validation fails", async function () { - req.query.monitorURL = "invalid-url"; - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const error = next.getCall(0).args[0]; - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - expect(error.message).to.equal('"monitorURL" must be a valid uri'); - }); -}); - -describe("Monitor Controller - deleteMonitor", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - deleteMonitor: sinon.stub(), - deleteChecks: sinon.stub(), - deletePageSpeedChecksByMonitorId: sinon.stub(), - deleteNotificationsByMonitorId: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - sinon.stub(logger, "error"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await deleteMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB deleteMonitor operation fail", async function () { - req.db.deleteMonitor.throws(new Error("DB error")); - await deleteMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should log an error if deleteJob throws an error", async function () { - const error = new Error("Job error"); - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - req.jobQueue.deleteJob.rejects(error); - await deleteMonitor(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); - }); - - it("should log an error if deleteChecks throws an error", async function () { - const error = new Error("Checks error"); - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - req.db.deleteChecks.rejects(error); - await deleteMonitor(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); - }); - - it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async function () { - const error = new Error("PageSpeed error"); - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - req.db.deletePageSpeedChecksByMonitorId.rejects(error); - await deleteMonitor(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); - }); - - it("should log an error if deleteNotificationsByMonitorId throws an error", async function () { - const error = new Error("Notifications error"); - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - req.db.deleteNotificationsByMonitorId.rejects(error); - await deleteMonitor(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); - }); - - it("should return success message if all operations succeed", async function () { - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - await deleteMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_DELETE, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - deleteAllMonitors", function () { - let req, res, next, stub; - - beforeEach(function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req = { - headers: { - authorization: "Bearer token", - }, - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - deleteAllMonitors: sinon.stub(), - deleteChecks: sinon.stub(), - deletePageSpeedChecksByMonitorId: sinon.stub(), - deleteNotificationsByMonitorId: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - sinon.stub(logger, "error"); - }); - - afterEach(function () { - sinon.restore(); - stub.restore(); - }); - - it("should reject with an error if getTokenFromHeaders throws an error", async function () { - req.headers = {}; - await deleteAllMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("No auth headers"); - expect(next.firstCall.args[0].status).to.equal(500); - }); - - it("should reject with an error if token validation fails", async function () { - stub.restore(); - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - await deleteAllMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - }); - - it("should reject with an error if DB deleteAllMonitors operation fail", async function () { - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.throws(new Error("DB error")); - await deleteAllMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should log an error if deleteChecks throws an error", async function () { - const monitors = [{ name: "test_monitor", _id: "123" }]; - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 }); - const error = new Error("Check error"); - req.db.deleteChecks.rejects(error); - await deleteAllMonitors(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}` - ); - }); - - it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async function () { - const monitors = [{ name: "test_monitor", _id: "123" }]; - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 }); - const error = new Error("Pagespeed Check error"); - req.db.deletePageSpeedChecksByMonitorId.rejects(error); - await deleteAllMonitors(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}` - ); - }); - - it("should log an error if deleteNotificationsByMonitorId throws an error", async function () { - const monitors = [{ name: "test_monitor", _id: "123" }]; - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 }); - const error = new Error("Notifications Check error"); - req.db.deleteNotificationsByMonitorId.rejects(error); - await deleteAllMonitors(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}` - ); - }); - - it("should return success message if all operations succeed", async function () { - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.returns({ - monitors: [{ name: "test_monitor", _id: "123" }], - deletedCount: 1, - }); - await deleteAllMonitors(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: "Deleted 1 monitors", - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - editMonitor", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: { - monitorId: "123", - }, - query: {}, - body: { - notifications: [{ email: "example@example.com" }], - }, - db: { - getMonitorById: sinon.stub(), - editMonitor: sinon.stub(), - deleteNotificationsByMonitorId: sinon.stub(), - createNotification: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - addJob: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = { invalid: 1 }; - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getMonitorById operation fails", async function () { - req.db.getMonitorById.throws(new Error("DB error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if editMonitor operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.throws(new Error("DB error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if deleteNotificationsByMonitorId operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns({ _id: "123" }); - req.db.deleteNotificationsByMonitorId.throws(new Error("DB error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if createNotification operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns({ _id: "123" }); - req.db.createNotification.throws(new Error("DB error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if deleteJob operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns({ _id: "123" }); - req.jobQueue.deleteJob.throws(new Error("Job error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Job error"); - }); - - it("should reject with an error if addJob operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns({ _id: "123" }); - req.jobQueue.addJob.throws(new Error("Add Job error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Add Job error"); - }); - - it("should return success message with data if all operations succeed", async function () { - const monitor = { _id: "123" }; - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns(monitor); - await editMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_EDIT, - data: monitor, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - pauseMonitor", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorById: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - addJob: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getMonitorById operation fails", async function () { - req.db.getMonitorById.throws(new Error("DB error")); - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if deleteJob operation fails", async function () { - const monitor = { _id: req.params.monitorId, isActive: true }; - req.db.getMonitorById.returns(monitor); - req.jobQueue.deleteJob.throws(new Error("Delete Job error")); - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Delete Job error"); - }); - - it("should reject with an error if addJob operation fails", async function () { - const monitor = { _id: req.params.monitorId, isActive: false }; - req.db.getMonitorById.returns(monitor); - req.jobQueue.addJob.throws(new Error("Add Job error")); - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Add Job error"); - }); - - it("should reject with an error if monitor.save operation fails", async function () { - const monitor = { - _id: req.params.monitorId, - active: false, - save: sinon.stub().throws(new Error("Save error")), - }; - req.db.getMonitorById.returns(monitor); - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Save error"); - }); - - it("should return success pause message with data if all operations succeed with inactive monitor", async function () { - const monitor = { - _id: req.params.monitorId, - isActive: false, - save: sinon.stub().resolves(), - }; - req.db.getMonitorById.returns(monitor); - await pauseMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_PAUSE, - data: monitor, - }) - ).to.be.true; - }); - - it("should return success resume message with data if all operations succeed with active monitor", async function () { - const monitor = { - _id: req.params.monitorId, - isActive: true, - save: sinon.stub().resolves(), - }; - req.db.getMonitorById.returns(monitor); - await pauseMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_RESUME, - data: monitor, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - addDemoMonitors", function () { - let req, res, next, stub; - - beforeEach(function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { _id: "123", teamId: "123" }; - }); - req = { - headers: { - authorization: "Bearer token", - }, - params: {}, - query: {}, - body: {}, - db: { - addDemoMonitors: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - jobQueue: { - addJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - stub.restore(); - }); - - it("should reject with an error if getTokenFromHeaders fails", async function () { - req.headers = {}; - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("No auth headers"); - expect(next.firstCall.args[0].status).to.equal(500); - }); - - it("should reject with an error if getting settings fails", async function () { - req.settingsService.getSettings.throws(new Error("Settings error")); - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Settings error"); - }); - - it("should reject with an error if JWT validation fails", async function () { - stub.restore(); - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - }); - - it("should reject with an error if addDemoMonitors operation fails", async function () { - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.addDemoMonitors.throws(new Error("DB error")); - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if addJob operation fails", async function () { - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.addDemoMonitors.returns([{ _id: "123" }]); - req.jobQueue.addJob.throws(new Error("Add Job error")); - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Add Job error"); - }); - - it("should return success message with data if all operations succeed", async function () { - const monitors = [{ _id: "123" }]; - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.addDemoMonitors.returns(monitors); - await addDemoMonitors(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_DEMO_ADDED, - data: monitors.length, - }) - ).to.be.true; - }); -}); diff --git a/server/tests/controllers/queueController.test.js b/server/tests/controllers/queueController.test.js deleted file mode 100755 index 6201bdb5a..000000000 --- a/server/tests/controllers/queueController.test.js +++ /dev/null @@ -1,176 +0,0 @@ -import { afterEach } from "node:test"; -import { getMetrics, getJobs, addJob, obliterateQueue } from "../../controllers/queueController.js"; -import { successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; - -describe("Queue Controller - getMetrics", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - jobQueue: { - getMetrics: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should throw an error if getMetrics throws an error", async function () { - req.jobQueue.getMetrics.throws(new Error("getMetrics error")); - await getMetrics(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getMetrics error"); - }); - - it("should return a success message and data if getMetrics is successful", async function () { - const data = { data: "metrics" }; - req.jobQueue.getMetrics.returns(data); - await getMetrics(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.QUEUE_GET_METRICS, - data, - }); - }); -}); - -describe("Queue Controller - getJobs", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - jobQueue: { - getJobStats: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if getJobs throws an error", async function () { - req.jobQueue.getJobStats.throws(new Error("getJobs error")); - await getJobs(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getJobs error"); - }); - - it("should return a success message and data if getJobs is successful", async function () { - const data = { data: "jobs" }; - req.jobQueue.getJobStats.returns(data); - await getJobs(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.QUEUE_GET_METRICS, - data, - }); - }); -}); - -describe("Queue Controller - addJob", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - jobQueue: { - addJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if addJob throws an error", async function () { - req.jobQueue.addJob.throws(new Error("addJob error")); - await addJob(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("addJob error"); - }); - - it("should return a success message if addJob is successful", async function () { - req.jobQueue.addJob.resolves(); - await addJob(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.QUEUE_ADD_JOB, - }); - }); -}); - -describe("Queue Controller - obliterateQueue", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - jobQueue: { - obliterate: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if obliterateQueue throws an error", async function () { - req.jobQueue.obliterate.throws(new Error("obliterateQueue error")); - await obliterateQueue(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("obliterateQueue error"); - }); - - it("should return a success message if obliterateQueue is successful", async function () { - req.jobQueue.obliterate.resolves(); - await obliterateQueue(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.QUEUE_OBLITERATE, - }); - }); -}); diff --git a/server/tests/controllers/settingsController.test.js b/server/tests/controllers/settingsController.test.js deleted file mode 100755 index 721c41c02..000000000 --- a/server/tests/controllers/settingsController.test.js +++ /dev/null @@ -1,109 +0,0 @@ -import { afterEach } from "node:test"; -import { getAppSettings, updateAppSettings } from "../../controllers/settingsController.js"; - -import { successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; - -describe("Settings Controller - getAppSettings", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should throw an error if getSettings throws an error", async function () { - req.settingsService.getSettings.throws(new Error("getSettings error")); - await getAppSettings(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getSettings error"); - }); - - it("should return a success message and data if getSettings is successful", async function () { - const data = { data: "settings" }; - req.settingsService.getSettings.returns(data); - await getAppSettings(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.GET_APP_SETTINGS, - data, - }); - }); -}); - -describe("Settings Controller - updateAppSettings", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: { - updateAppSettings: sinon.stub(), - }, - settingsService: { - reloadSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = { invalid: 1 }; - await updateAppSettings(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if updateAppSettings throws an error", async function () { - req.db.updateAppSettings.throws(new Error("updateAppSettings error")); - await updateAppSettings(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("updateAppSettings error"); - }); - - it("should reject with an error if reloadSettings throws an error", async function () { - req.settingsService.reloadSettings.throws(new Error("reloadSettings error")); - await updateAppSettings(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("reloadSettings error"); - }); - - it("should return a success message and data if updateAppSettings is successful", async function () { - const data = { data: "settings" }; - req.settingsService.reloadSettings.returns(data); - await updateAppSettings(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.UPDATE_APP_SETTINGS, - data, - }); - }); -}); diff --git a/server/tests/controllers/statusPageController.test.js b/server/tests/controllers/statusPageController.test.js deleted file mode 100755 index cada662e0..000000000 --- a/server/tests/controllers/statusPageController.test.js +++ /dev/null @@ -1,127 +0,0 @@ -import sinon from "sinon"; -import { createStatusPage, getStatusPageByUrl } from "../../controllers/statusPageController.js"; - -describe("statusPageController", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - body: {}, - db: { - createStatusPage: sinon.stub(), - getStatusPageByUrl: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createStatusPage", function () { - beforeEach(function () { - req.body = { - companyName: "Test Company", - url: "123456", - timezone: "America/Toronto", - color: "#000000", - theme: "light", - monitors: ["67309ca673788e808884c8ac", "67309ca673788e808884c8ac"], - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should handle a validation error", async function () { - req.body = { - // Invalid data that will trigger validation error - companyName: "", - url: "", - timezone: "", - color: "invalid", - theme: "invalid", - monitors: ["invalid-id"], - }; - try { - await createStatusPage(req, res, next); - } catch (error) { - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.equal("Validation error"); - } - }); - - it("should handle a db error", async function () { - const err = new Error("DB error"); - req.db.createStatusPage.throws(err); - - try { - await createStatusPage(req, res, next); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should insert a properly formatted status page", async function () { - const result = await createStatusPage(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0].success).to.be.true; - }); - }); - - describe("getStatusPageByUrl", function () { - beforeEach(function () { - req.params = { - url: "123456", - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should handle a validation error", async function () { - req.params = { - url: "", - }; - - try { - await getStatusPageByUrl(req, res, next); - } catch (error) { - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.equal("Validation error"); - } - }); - - it("should handle a DB error", async function () { - const err = new Error("DB error"); - req.db.getStatusPageByUrl.throws(err); - - try { - await getStatusPageByUrl(req, res, next); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should return a status page", async function () { - const statusPage = { - _id: "123456", - companyName: "Test Company", - url: "123456", - }; - req.db.getStatusPageByUrl.resolves(statusPage); - const result = await getStatusPageByUrl(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0].success).to.be.true; - expect(res.json.firstCall.args[0].data).to.deep.equal(statusPage); - }); - }); -}); diff --git a/server/tests/db/checkModule.test.js b/server/tests/db/checkModule.test.js deleted file mode 100755 index 6eef12445..000000000 --- a/server/tests/db/checkModule.test.js +++ /dev/null @@ -1,589 +0,0 @@ -import sinon from "sinon"; -import { - createCheck, - getChecksCount, - getChecks, - getTeamChecks, - deleteChecks, - deleteChecksByTeamId, - updateChecksTTL, -} from "../../db/mongo/modules/checkModule.js"; -import Check from "../../db/models/Check.js"; -import Monitor from "../../db/models/Monitor.js"; -import User from "../../db/models/User.js"; -import logger from "../../utils/logger.js"; - -describe("checkModule", function () { - describe("createCheck", function () { - let checkCountDocumentsStub, checkSaveStub, monitorFindByIdStub, monitorSaveStub; - const mockMonitor = { - _id: "123", - uptimePercentage: 0.5, - status: true, - save: () => this, - }; - const mockCheck = { active: true }; - - beforeEach(function () { - checkSaveStub = sinon.stub(Check.prototype, "save"); - checkCountDocumentsStub = sinon.stub(Check, "countDocuments"); - monitorFindByIdStub = sinon.stub(Monitor, "findById"); - monitorSaveStub = sinon.stub(Monitor.prototype, "save"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return undefined early if no monitor is found", async function () { - monitorFindByIdStub.returns(null); - const check = await createCheck({ monitorId: "123" }); - expect(check).to.be.undefined; - }); - - it("should return a check", async function () { - monitorFindByIdStub.returns(mockMonitor); - checkSaveStub.returns(mockCheck); - monitorSaveStub.returns(mockMonitor); - const check = await createCheck({ monitorId: "123", status: true }); - expect(check).to.deep.equal(mockCheck); - }); - - it("should return a check if status is down", async function () { - mockMonitor.status = false; - monitorFindByIdStub.returns(mockMonitor); - checkSaveStub.returns(mockCheck); - monitorSaveStub.returns(mockMonitor); - const check = await createCheck({ monitorId: "123", status: false }); - expect(check).to.deep.equal(mockCheck); - }); - - it("should return a check if uptimePercentage is undefined", async function () { - mockMonitor.uptimePercentage = undefined; - monitorFindByIdStub.returns(mockMonitor); - checkSaveStub.returns(mockCheck); - monitorSaveStub.returns(mockMonitor); - const check = await createCheck({ monitorId: "123", status: true }); - expect(check).to.deep.equal(mockCheck); - }); - - it("should return a check if uptimePercentage is undefined and status is down", async function () { - mockMonitor.uptimePercentage = undefined; - monitorFindByIdStub.returns(mockMonitor); - checkSaveStub.returns(mockCheck); - monitorSaveStub.returns(mockMonitor); - const check = await createCheck({ monitorId: "123", status: false }); - expect(check).to.deep.equal(mockCheck); - }); - - it("should monitor save error", async function () { - const err = new Error("Save Error"); - monitorSaveStub.throws(err); - try { - await createCheck({ monitorId: "123" }); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should handle errors", async function () { - const err = new Error("DB Error"); - checkCountDocumentsStub.throws(err); - try { - await createCheck({ monitorId: "123" }); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getChecksCount", function () { - let checkCountDocumentStub; - - beforeEach(function () { - checkCountDocumentStub = sinon.stub(Check, "countDocuments"); - }); - - afterEach(function () { - checkCountDocumentStub.restore(); - }); - - it("should return count with basic monitorId query", async function () { - const req = { - params: { monitorId: "test123" }, - query: {}, - }; - checkCountDocumentStub.resolves(5); - - const result = await getChecksCount(req); - - expect(result).to.equal(5); - expect(checkCountDocumentStub.calledOnce).to.be.true; - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - }); - }); - - it("should include dateRange in query when provided", async function () { - const req = { - params: { monitorId: "test123" }, - query: { dateRange: "day" }, - }; - checkCountDocumentStub.resolves(3); - - const result = await getChecksCount(req); - - expect(result).to.equal(3); - expect(checkCountDocumentStub.firstCall.args[0]).to.have.property("createdAt"); - expect(checkCountDocumentStub.firstCall.args[0].createdAt).to.have.property("$gte"); - }); - - it('should handle "all" filter correctly', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "all" }, - }; - checkCountDocumentStub.resolves(2); - - const result = await getChecksCount(req); - - expect(result).to.equal(2); - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - status: false, - }); - }); - - it('should handle "down" filter correctly', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "down" }, - }; - checkCountDocumentStub.resolves(2); - - const result = await getChecksCount(req); - - expect(result).to.equal(2); - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - status: false, - }); - }); - - it('should handle "resolve" filter correctly', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "resolve" }, - }; - checkCountDocumentStub.resolves(1); - - const result = await getChecksCount(req); - - expect(result).to.equal(1); - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - status: false, - statusCode: 5000, - }); - }); - - it("should handle unknown filter correctly", async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "unknown" }, - }; - checkCountDocumentStub.resolves(1); - - const result = await getChecksCount(req); - - expect(result).to.equal(1); - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - status: false, - }); - }); - - it("should combine dateRange and filter in query", async function () { - const req = { - params: { monitorId: "test123" }, - query: { - dateRange: "week", - filter: "down", - }, - }; - checkCountDocumentStub.resolves(4); - - const result = await getChecksCount(req); - - expect(result).to.equal(4); - expect(checkCountDocumentStub.firstCall.args[0]).to.have.all.keys("monitorId", "createdAt", "status"); - }); - }); - - describe("getChecks", function () { - let checkFindStub, monitorFindStub; - - beforeEach(function () { - checkFindStub = sinon.stub(Check, "find").returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([{ id: 1 }, { id: 2 }]), - }), - }), - }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return checks with basic monitorId query", async function () { - const req = { - params: { monitorId: "test123" }, - query: {}, - }; - - const result = await getChecks(req); - - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should return checks with limit query", async function () { - const req = { - params: { monitorId: "test123" }, - query: { limit: 10 }, - }; - - const result = await getChecks(req); - - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should handle pagination correctly", async function () { - const req = { - params: { monitorId: "test123" }, - query: { - page: 2, - rowsPerPage: 10, - }, - }; - - const result = await getChecks(req); - - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should handle dateRange filter", async function () { - const req = { - params: { monitorId: "test123" }, - query: { dateRange: "week" }, - }; - const result = await getChecks(req); - - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it('should handle "all" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "all" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it('should handle "down" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "down" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it('should handle "resolve" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "resolve" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it('should handle "unknown" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "unknown" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should handle ascending sort order", async function () { - const req = { - params: { monitorId: "test123" }, - query: { sortOrder: "asc" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should handle error case", async function () { - const req = { - params: { monitorId: "test123" }, - query: {}, - }; - - checkFindStub.throws(new Error("Database error")); - - try { - await getChecks(req); - } catch (error) { - expect(error.message).to.equal("Database error"); - expect(error.service).to.equal("checkModule"); - expect(error.method).to.equal("getChecks"); - } - }); - }); - - describe("getTeamChecks", function () { - let checkFindStub, checkCountDocumentsStub, monitorFindStub; - const mockMonitors = [{ _id: "123" }]; - - beforeEach(function () { - monitorFindStub = sinon.stub(Monitor, "find").returns({ - select: sinon.stub().returns(mockMonitors), - }); - checkCountDocumentsStub = sinon.stub(Check, "countDocuments").returns(2); - checkFindStub = sinon.stub(Check, "find").returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns({ - select: sinon.stub().returns([{ id: 1 }, { id: 2 }]), - }), - }), - }), - }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return checks with basic monitorId query", async function () { - const req = { - params: { teamId: "test123" }, - query: {}, - }; - - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it("should handle pagination correctly", async function () { - const req = { - params: { monitorId: "test123" }, - query: { limit: 1, page: 2, rowsPerPage: 10 }, - }; - - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it("should handle dateRange filter", async function () { - const req = { - params: { monitorId: "test123" }, - query: { dateRange: "week" }, - }; - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it('should handle "all" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "all" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it('should handle "down" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "down" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it('should handle "resolve" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "resolve" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it('should handle "unknown" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "unknown" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it("should handle ascending sort order", async function () { - const req = { - params: { monitorId: "test123" }, - query: { sortOrder: "asc" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it("should handle error case", async function () { - const req = { - params: { monitorId: "test123" }, - query: {}, - }; - - checkFindStub.throws(new Error("Database error")); - - try { - await getTeamChecks(req); - } catch (error) { - expect(error.message).to.equal("Database error"); - expect(error.service).to.equal("checkModule"); - expect(error.method).to.equal("getTeamChecks"); - } - }); - }); - - describe("deleteChecks", function () { - let checkDeleteManyStub; - - beforeEach(function () { - checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return a value if a check is deleted", async function () { - const result = await deleteChecks("123"); - expect(result).to.equal(1); - }); - - it("should handle an error", async function () { - checkDeleteManyStub.throws(new Error("Database error")); - try { - await deleteChecks("123"); - } catch (error) { - expect(error.message).to.equal("Database error"); - expect(error.method).to.equal("deleteChecks"); - } - }); - }); - - describe("deleteChecksByTeamId", function () { - let mockMonitors = [{ _id: 123, save: () => this }]; - let monitorFindStub, monitorSaveStub, checkDeleteManyStub; - - beforeEach(function () { - monitorSaveStub = sinon.stub(Monitor.prototype, "save"); - monitorFindStub = sinon.stub(Monitor, "find").returns(mockMonitors); - checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return a deleted count", async function () { - const result = await deleteChecksByTeamId("123"); - expect(result).to.equal(1); - }); - - it("should handle errors", async function () { - const err = new Error("DB Error"); - monitorFindStub.throws(err); - try { - const result = await deleteChecksByTeamId("123"); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("updateChecksTTL", function () { - let userUpdateManyStub; - let loggerStub; - - beforeEach(function () { - loggerStub = sinon.stub(logger, "error"); - userUpdateManyStub = sinon.stub(User, "updateMany"); - Check.collection = { dropIndex: sinon.stub(), createIndex: sinon.stub() }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return undefined", async function () { - const result = await updateChecksTTL("123", 10); - expect(result).to.be.undefined; - }); - - it("should log an error if dropIndex throws an error", async function () { - const err = new Error("Drop Index Error"); - Check.collection.dropIndex.throws(err); - await updateChecksTTL("123", 10); - expect(loggerStub.calledOnce).to.be.true; - expect(loggerStub.firstCall.args[0].message).to.equal(err.message); - }); - - it("should throw an error if createIndex throws an error", async function () { - const err = new Error("Create Index Error"); - Check.collection.createIndex.throws(err); - try { - await updateChecksTTL("123", 10); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should throw an error if User.updateMany throws an error", async function () { - const err = new Error("Update Many Error"); - userUpdateManyStub.throws(err); - try { - await updateChecksTTL("123", 10); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/hardwareCheckModule.test.js b/server/tests/db/hardwareCheckModule.test.js deleted file mode 100755 index 20ce197fb..000000000 --- a/server/tests/db/hardwareCheckModule.test.js +++ /dev/null @@ -1,136 +0,0 @@ -import sinon from "sinon"; -import HardwareCheck from "../../db/models/HardwareCheck.js"; -import { createHardwareCheck } from "../../db/mongo/modules/hardwareCheckModule.js"; -import Monitor from "../../db/models/Monitor.js"; -import logger from "../../utils/logger.js"; - -const mockHardwareCheck = { - data: { - cpu: { - physical_core: 4, - logical_core: 8, - frequency: 4800, - current_frequency: 1411, - temperature: [45, 50, 46, 47, 45, 50, 46, 47], - free_percent: 0.8552990910595134, - usage_percent: 0.14470090894048657, - }, - memory: { - total_bytes: 16467628032, - available_bytes: 7895044096, - used_bytes: 6599561216, - usage_percent: 0.4008, - }, - disk: [ - { - read_speed_bytes: null, - write_speed_bytes: null, - total_bytes: 931258499072, - free_bytes: 737097256960, - usage_percent: 0.1661, - }, - ], - host: { - os: "linux", - platform: "ubuntu", - kernel_version: "6.8.0-48-generic", - }, - }, - errors: [ - { - metric: ["cpu.temperature"], - err: "unable to read CPU temperature", - }, - ], -}; - -const mockMonitor = { - _id: "123", - uptimePercentage: 1, - status: true, - save: () => this, -}; - -describe("HardwareCheckModule", function () { - let hardwareCheckSaveStub, hardwareCheckCountDocumentsStub, monitorFindByIdStub, loggerStub; - - beforeEach(function () { - loggerStub = sinon.stub(logger, "error"); - hardwareCheckSaveStub = sinon.stub(HardwareCheck.prototype, "save"); - monitorFindByIdStub = sinon.stub(Monitor, "findById"); - hardwareCheckCountDocumentsStub = sinon.stub(HardwareCheck, "countDocuments"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createHardwareCheck", function () { - it("should return a hardware check", async function () { - hardwareCheckSaveStub.resolves(mockHardwareCheck); - monitorFindByIdStub.resolves(mockMonitor); - hardwareCheckCountDocumentsStub.resolves(1); - const hardwareCheck = await createHardwareCheck({ status: true }); - expect(hardwareCheck).to.exist; - expect(hardwareCheck).to.deep.equal(mockHardwareCheck); - }); - - it("should return a hardware check for a check with status false", async function () { - hardwareCheckSaveStub.resolves(mockHardwareCheck); - monitorFindByIdStub.resolves(mockMonitor); - hardwareCheckCountDocumentsStub.resolves(1); - const hardwareCheck = await createHardwareCheck({ status: false }); - expect(hardwareCheck).to.exist; - expect(hardwareCheck).to.deep.equal(mockHardwareCheck); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - monitorFindByIdStub.resolves(mockMonitor); - hardwareCheckSaveStub.rejects(err); - try { - await createHardwareCheck({}); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - - it("should log an error if a monitor is not found", async function () { - monitorFindByIdStub.resolves(null); - const res = await createHardwareCheck({}); - expect(loggerStub.calledOnce).to.be.true; - expect(res).to.be.null; - }); - - it("should handle a monitor with undefined uptimePercentage", async function () { - monitorFindByIdStub.resolves({ ...mockMonitor, uptimePercentage: undefined }); - hardwareCheckSaveStub.resolves(mockHardwareCheck); - hardwareCheckCountDocumentsStub.resolves(1); - const res = await createHardwareCheck({}); - expect(res).to.exist; - }); - - it("should handle a monitor with undefined uptimePercentage and true status", async function () { - monitorFindByIdStub.resolves({ - ...mockMonitor, - uptimePercentage: undefined, - }); - hardwareCheckSaveStub.resolves(mockHardwareCheck); - hardwareCheckCountDocumentsStub.resolves(1); - const res = await createHardwareCheck({ status: true }); - expect(res).to.exist; - }); - - it("should handle a monitor with undefined uptimePercentage and false status", async function () { - monitorFindByIdStub.resolves({ - ...mockMonitor, - uptimePercentage: undefined, - }); - hardwareCheckSaveStub.resolves(mockHardwareCheck); - hardwareCheckCountDocumentsStub.resolves(1); - const res = await createHardwareCheck({ status: false }); - expect(res).to.exist; - }); - }); -}); diff --git a/server/tests/db/inviteModule.test.js b/server/tests/db/inviteModule.test.js deleted file mode 100755 index e8913c983..000000000 --- a/server/tests/db/inviteModule.test.js +++ /dev/null @@ -1,103 +0,0 @@ -import sinon from "sinon"; -import InviteToken from "../../db/models/InviteToken.js"; -import { requestInviteToken, getInviteToken, getInviteTokenAndDelete } from "../../db/mongo/modules/inviteModule.js"; -import { errorMessages } from "../../utils/messages.js"; - -describe("Invite Module", function () { - const mockUserData = { - email: "test@test.com", - teamId: "123", - role: ["admin"], - token: "123", - }; - const mockInviteToken = { _id: 123, time: 123 }; - let inviteTokenDeleteManyStub, inviteTokenSaveStub, inviteTokenFindOneStub, inviteTokenFindOneAndDeleteStub; - - beforeEach(function () { - inviteTokenDeleteManyStub = sinon.stub(InviteToken, "deleteMany"); - inviteTokenSaveStub = sinon.stub(InviteToken.prototype, "save"); - inviteTokenFindOneStub = sinon.stub(InviteToken, "findOne"); - inviteTokenFindOneAndDeleteStub = sinon.stub(InviteToken, "findOneAndDelete"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("requestInviteToken", function () { - it("should return a new invite token", async function () { - inviteTokenDeleteManyStub.resolves(); - inviteTokenSaveStub.resolves(); - const inviteToken = await requestInviteToken(mockUserData); - expect(inviteToken.email).to.equal(mockUserData.email); - expect(inviteToken.role).to.deep.equal(mockUserData.role); - expect(inviteToken.token).to.exist; - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - inviteTokenDeleteManyStub.rejects(err); - try { - await requestInviteToken(mockUserData); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getInviteToken", function () { - it("should return an invite token", async function () { - inviteTokenFindOneStub.resolves(mockInviteToken); - const inviteToken = await getInviteToken(mockUserData.token); - expect(inviteToken).to.deep.equal(mockInviteToken); - }); - - it("should handle a token not found", async function () { - inviteTokenFindOneStub.resolves(null); - try { - await getInviteToken(mockUserData.token); - } catch (error) { - expect(error.message).to.equal(errorMessages.AUTH_INVITE_NOT_FOUND); - } - }); - - it("should handle DB errors", async function () { - const err = new Error("test error"); - inviteTokenFindOneStub.rejects(err); - try { - await getInviteToken(mockUserData.token); - } catch (error) { - expect(error).to.deep.equal(err); - expect(error.method).to.equal("getInviteToken"); - } - }); - }); - - describe("getInviteTokenAndDelete", function () { - it("should return a deleted invite", async function () { - inviteTokenFindOneAndDeleteStub.resolves(mockInviteToken); - const deletedInvite = await getInviteTokenAndDelete(mockUserData.token); - expect(deletedInvite).to.deep.equal(mockInviteToken); - }); - - it("should handle a token not found", async function () { - inviteTokenFindOneAndDeleteStub.resolves(null); - try { - await getInviteTokenAndDelete(mockUserData.token); - } catch (error) { - expect(error.message).to.equal(errorMessages.AUTH_INVITE_NOT_FOUND); - } - }); - - it("should handle DB errors", async function () { - const err = new Error("test error"); - inviteTokenFindOneAndDeleteStub.rejects(err); - try { - await getInviteTokenAndDelete(mockUserData.token); - } catch (error) { - expect(error).to.deep.equal(err); - expect(error.method).to.equal("getInviteTokenAndDelete"); - } - }); - }); -}); diff --git a/server/tests/db/maintenanceWindowModule.test.js b/server/tests/db/maintenanceWindowModule.test.js deleted file mode 100755 index 576c97f84..000000000 --- a/server/tests/db/maintenanceWindowModule.test.js +++ /dev/null @@ -1,255 +0,0 @@ -import sinon from "sinon"; -import MaintenanceWindow from "../../db/models/MaintenanceWindow.js"; -import { - createMaintenanceWindow, - getMaintenanceWindowById, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindowById, - deleteMaintenanceWindowByMonitorId, - deleteMaintenanceWindowByUserId, - editMaintenanceWindowById, -} from "../../db/mongo/modules/maintenanceWindowModule.js"; - -describe("MaintenanceWindow Module", function () { - const mockMaintenanceWindow = { - monitorId: "123", - active: true, - oneTime: true, - start: 1, - end: 20000, - }; - - let mockMaintenanceWindows = [mockMaintenanceWindow]; - let maintenanceWindowSaveStub, - maintenanceWindowFindByIdStub, - maintenanceWindowCountDocumentsStub, - maintenanceWindowFindStub, - maintenanceWindowFindByIdAndDeleteStub, - maintenanceWindowDeleteManyStub, - maintenanceWindowFindByIdAndUpdateStub; - - beforeEach(function () { - maintenanceWindowSaveStub = sinon.stub(MaintenanceWindow.prototype, "save"); - maintenanceWindowFindByIdStub = sinon.stub(MaintenanceWindow, "findById"); - maintenanceWindowCountDocumentsStub = sinon.stub(MaintenanceWindow, "countDocuments"); - maintenanceWindowFindStub = sinon.stub(MaintenanceWindow, "find").returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMaintenanceWindows), - }), - }), - }); - maintenanceWindowFindByIdAndDeleteStub = sinon.stub(MaintenanceWindow, "findByIdAndDelete"); - maintenanceWindowDeleteManyStub = sinon.stub(MaintenanceWindow, "deleteMany"); - maintenanceWindowFindByIdAndUpdateStub = sinon.stub(MaintenanceWindow, "findByIdAndUpdate"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createMaintenanceWindow", function () { - it("should save a new maintenance window", async function () { - maintenanceWindowSaveStub.resolves(mockMaintenanceWindow); - const result = await createMaintenanceWindow(mockMaintenanceWindow); - expect(result).to.deep.equal(mockMaintenanceWindow); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowSaveStub.rejects(err); - try { - await createMaintenanceWindow(mockMaintenanceWindow); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getMaintenanceWindowById", function () { - it("should return a maintenance window", async function () { - maintenanceWindowFindByIdStub.resolves(mockMaintenanceWindow); - const result = await getMaintenanceWindowById(mockMaintenanceWindow.id); - expect(result).to.deep.equal(mockMaintenanceWindow); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowFindByIdStub.rejects(err); - try { - await getMaintenanceWindowById(mockMaintenanceWindow.id); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getMaintenanceWindowsByTeamId", function () { - let query; - - beforeEach(function () { - query = { - active: true, - page: 1, - rowsPerPage: 10, - field: "name", - order: "asc", - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return a list of maintenance windows and count", async function () { - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should return a list of maintenance windows and count with empty query", async function () { - query = undefined; - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should return a list of maintenance windows and count with no pagination provided", async function () { - query.page = undefined; - query.rowsPerPage = undefined; - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should return a list of maintenance windows and count with field and desc order", async function () { - query.order = "desc"; - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should return a list of maintenance windows and count no field", async function () { - query.field = undefined; - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowCountDocumentsStub.rejects(err); - try { - await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getMaintenanceWindowsByMonitorId", function () { - it("should return a list of maintenance windows", async function () { - maintenanceWindowFindStub.resolves(mockMaintenanceWindows); - const result = await getMaintenanceWindowsByMonitorId(mockMaintenanceWindow.monitorId); - expect(result).to.deep.equal(mockMaintenanceWindows); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowFindStub.rejects(err); - try { - await getMaintenanceWindowsByMonitorId(mockMaintenanceWindow.monitorId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteMaintenanceWindowById", function () { - it("should delete a maintenance window", async function () { - maintenanceWindowFindByIdAndDeleteStub.resolves(mockMaintenanceWindow); - const result = await deleteMaintenanceWindowById(mockMaintenanceWindow.id); - expect(result).to.deep.equal(mockMaintenanceWindow); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowFindByIdAndDeleteStub.rejects(err); - try { - await deleteMaintenanceWindowById(mockMaintenanceWindow.id); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteMaintenanceWindowByMonitorId", function () { - it("should return the number of documents deleted", async function () { - maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 }); - const result = await deleteMaintenanceWindowByMonitorId(mockMaintenanceWindow.monitorId); - expect(result).to.deep.equal({ deletedCount: 1 }); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowDeleteManyStub.rejects(err); - try { - await deleteMaintenanceWindowByMonitorId(mockMaintenanceWindow.monitorId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteMaintenanceWindowByUserId", function () { - it("should return the number of documents deleted", async function () { - maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 }); - const result = await deleteMaintenanceWindowByUserId(mockMaintenanceWindow.userId); - expect(result).to.deep.equal({ deletedCount: 1 }); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowDeleteManyStub.rejects(err); - try { - await deleteMaintenanceWindowByUserId(mockMaintenanceWindow.userId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("editMaintenanceWindowById", function () { - it("should return the updated maintenance window", async function () { - maintenanceWindowFindByIdAndUpdateStub.resolves(mockMaintenanceWindow); - const result = await editMaintenanceWindowById(mockMaintenanceWindow.id, mockMaintenanceWindow); - expect(result).to.deep.equal(mockMaintenanceWindow); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowFindByIdAndUpdateStub.rejects(err); - try { - await editMaintenanceWindowById(mockMaintenanceWindow.id, mockMaintenanceWindow); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/monitorModule.test.js b/server/tests/db/monitorModule.test.js deleted file mode 100755 index e59c352cf..000000000 --- a/server/tests/db/monitorModule.test.js +++ /dev/null @@ -1,1865 +0,0 @@ -import sinon from "sinon"; -import Monitor from "../../db/models/Monitor.js"; -import Check from "../../db/models/Check.js"; -import PageSpeedCheck from "../../db/models/PageSpeedCheck.js"; -import HardwareCheck from "../../db/models/HardwareCheck.js"; -import Notification from "../../db/models/Notification.js"; - -import { errorMessages } from "../../utils/messages.js"; -import { - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - deleteMonitor, - deleteAllMonitors, - deleteMonitorsByUserId, - editMonitor, - addDemoMonitors, - calculateUptimeDuration, - getLastChecked, - getLatestResponseTime, - getAverageResponseTime, - getUptimePercentage, - getIncidents, - getMonitorChecks, - processChecksForDisplay, - groupChecksByTime, - calculateGroupStats, -} from "../../db/mongo/modules/monitorModule.js"; - -describe("monitorModule", function () { - let monitorFindStub, - monitorFindByIdStub, - monitorFindByIdAndUpdateStub, - monitorFindByIdAndDeleteStub, - monitorDeleteManyStub, - monitorCountStub, - monitorInsertManyStub, - checkFindStub, - pageSpeedCheckFindStub, - hardwareCheckFindStub; - - beforeEach(function () { - monitorFindStub = sinon.stub(Monitor, "find"); - monitorFindByIdStub = sinon.stub(Monitor, "findById"); - monitorFindByIdAndUpdateStub = sinon.stub(Monitor, "findByIdAndUpdate"); - monitorFindByIdAndDeleteStub = sinon.stub(Monitor, "findByIdAndDelete"); - monitorDeleteManyStub = sinon.stub(Monitor, "deleteMany"); - monitorCountStub = sinon.stub(Monitor, "countDocuments"); - - monitorInsertManyStub = sinon.stub(Monitor, "insertMany"); - checkFindStub = sinon.stub(Check, "find").returns({ - sort: sinon.stub(), - }); - pageSpeedCheckFindStub = sinon.stub(PageSpeedCheck, "find").returns({ - sort: sinon.stub(), - }); - hardwareCheckFindStub = sinon.stub(HardwareCheck, "find").returns({ - sort: sinon.stub(), - }); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("getAllMonitors", function () { - it("should return all monitors", async function () { - const mockMonitors = [ - { _id: "1", name: "Monitor 1", url: "test1.com" }, - { _id: "2", name: "Monitor 2", url: "test2.com" }, - ]; - monitorFindStub.returns(mockMonitors); - const result = await getAllMonitors(); - - expect(result).to.deep.equal(mockMonitors); - expect(monitorFindStub.calledOnce).to.be.true; - expect(monitorFindStub.firstCall.args).to.deep.equal([]); - }); - - it("should handle empty results", async function () { - monitorFindStub.returns([]); - const result = await getAllMonitors(); - expect(result).to.be.an("array").that.is.empty; - }); - - it("should throw error when database fails", async function () { - // Arrange - const error = new Error("Database error"); - error.service = "MonitorModule"; - error.method = "getAllMonitors"; - monitorFindStub.rejects(error); - // Act & Assert - try { - await getAllMonitors(); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err).to.equal(error); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("getAllMonitors"); - } - }); - }); - - describe("getAllMonitorsWithUptimeStats", function () { - it("should return monitors with uptime stats for different time periods", async function () { - // Mock data - const mockMonitors = [ - { - _id: "monitor1", - type: "http", - toObject: () => ({ - _id: "monitor1", - type: "http", - name: "Test Monitor", - }), - }, - ]; - - const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; - - monitorFindStub.resolves(mockMonitors); - checkFindStub.resolves(mockChecks); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(1); - - const monitor = result[0]; - expect(monitor).to.have.property("_id", "monitor1"); - expect(monitor).to.have.property("name", "Test Monitor"); - - // Check uptime percentages exist for all time periods - expect(monitor).to.have.property("1"); - expect(monitor).to.have.property("7"); - expect(monitor).to.have.property("30"); - expect(monitor).to.have.property("90"); - - // Verify uptime percentage calculation (3 successful out of 4 = 75%) - expect(monitor["1"]).to.equal(75); - expect(monitor["7"]).to.equal(75); - expect(monitor["30"]).to.equal(75); - expect(monitor["90"]).to.equal(75); - }); - - it("should return monitors with stats for pagespeed type", async function () { - // Mock data - const mockMonitors = [ - { - _id: "monitor1", - type: "pagespeed", - toObject: () => ({ - _id: "monitor1", - type: "pagespeed", - name: "Test Monitor", - }), - }, - ]; - - const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; - - monitorFindStub.resolves(mockMonitors); - pageSpeedCheckFindStub.resolves(mockChecks); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(1); - - const monitor = result[0]; - expect(monitor).to.have.property("_id", "monitor1"); - expect(monitor).to.have.property("name", "Test Monitor"); - - // Check uptime percentages exist for all time periods - expect(monitor).to.have.property("1"); - expect(monitor).to.have.property("7"); - expect(monitor).to.have.property("30"); - expect(monitor).to.have.property("90"); - - // Verify uptime percentage calculation (3 successful out of 4 = 75%) - expect(monitor["1"]).to.equal(75); - expect(monitor["7"]).to.equal(75); - expect(monitor["30"]).to.equal(75); - expect(monitor["90"]).to.equal(75); - }); - - it("should return monitors with stats for hardware type", async function () { - // Mock data - const mockMonitors = [ - { - _id: "monitor1", - type: "hardware", - toObject: () => ({ - _id: "monitor1", - type: "hardware", - name: "Test Monitor", - }), - }, - ]; - - const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; - - monitorFindStub.resolves(mockMonitors); - hardwareCheckFindStub.resolves(mockChecks); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(1); - - const monitor = result[0]; - expect(monitor).to.have.property("_id", "monitor1"); - expect(monitor).to.have.property("name", "Test Monitor"); - - // Check uptime percentages exist for all time periods - expect(monitor).to.have.property("1"); - expect(monitor).to.have.property("7"); - expect(monitor).to.have.property("30"); - expect(monitor).to.have.property("90"); - - // Verify uptime percentage calculation (3 successful out of 4 = 75%) - expect(monitor["1"]).to.equal(75); - expect(monitor["7"]).to.equal(75); - expect(monitor["30"]).to.equal(75); - expect(monitor["90"]).to.equal(75); - }); - - it("should handle errors appropriately", async function () { - // Setup stub to throw error - monitorFindStub.rejects(new Error("Database error")); - - try { - await getAllMonitorsWithUptimeStats(); - } catch (error) { - expect(error).to.be.an("error"); - expect(error.message).to.equal("Database error"); - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getAllMonitorsWithUptimeStats"); - } - }); - - it("should handle empty monitor list", async function () { - monitorFindStub.resolves([]); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(0); - }); - - it("should handle monitor with no checks", async function () { - const mockMonitors = [ - { - _id: "monitor1", - type: "http", - toObject: () => ({ - _id: "monitor1", - type: "http", - name: "Test Monitor", - }), - }, - ]; - - monitorFindStub.resolves(mockMonitors); - checkFindStub.resolves([]); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result[0]).to.have.property("1", 0); - expect(result[0]).to.have.property("7", 0); - expect(result[0]).to.have.property("30", 0); - expect(result[0]).to.have.property("90", 0); - }); - }); - - describe("calculateUptimeDuration", function () { - let clock; - const NOW = new Date("2024-01-01T12:00:00Z").getTime(); - - beforeEach(function () { - // Fix the current time - clock = sinon.useFakeTimers(NOW); - }); - - afterEach(function () { - clock.restore(); - }); - - it("should return 0 when checks array is empty", function () { - expect(calculateUptimeDuration([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(calculateUptimeDuration(null)).to.equal(0); - }); - - it("should calculate uptime from last down check to most recent check", function () { - const checks = [ - { status: true, createdAt: "2024-01-01T11:00:00Z" }, // Most recent - { status: true, createdAt: "2024-01-01T10:00:00Z" }, - { status: false, createdAt: "2024-01-01T09:00:00Z" }, // Last down - { status: true, createdAt: "2024-01-01T08:00:00Z" }, - ]; - - // Expected: 2 hours (from 09:00 to 11:00) = 7200000ms - expect(calculateUptimeDuration(checks)).to.equal(7200000); - }); - - it("should calculate uptime from first check when no down checks exist", function () { - const checks = [ - { status: true, createdAt: "2024-01-01T11:00:00Z" }, - { status: true, createdAt: "2024-01-01T10:00:00Z" }, - { status: true, createdAt: "2024-01-01T09:00:00Z" }, - ]; - - // Expected: Current time (12:00) - First check (09:00) = 3 hours = 10800000ms - expect(calculateUptimeDuration(checks)).to.equal(10800000); - }); - }); - - describe("getLastChecked", function () { - let clock; - const NOW = new Date("2024-01-01T12:00:00Z").getTime(); - - beforeEach(function () { - // Fix the current time - clock = sinon.useFakeTimers(NOW); - }); - - afterEach(function () { - clock.restore(); - }); - - it("should return 0 when checks array is empty", function () { - expect(getLastChecked([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getLastChecked(null)).to.equal(0); - }); - - it("should return time difference between now and most recent check", function () { - const checks = [ - { createdAt: "2024-01-01T11:30:00Z" }, // 30 minutes ago - { createdAt: "2024-01-01T11:00:00Z" }, - { createdAt: "2024-01-01T10:30:00Z" }, - ]; - - // Expected: 30 minutes = 1800000ms - expect(getLastChecked(checks)).to.equal(1800000); - }); - - it("should handle checks from different days", function () { - const checks = [ - { createdAt: "2023-12-31T12:00:00Z" }, // 24 hours ago - { createdAt: "2023-12-30T12:00:00Z" }, - ]; - - // Expected: 24 hours = 86400000ms - expect(getLastChecked(checks)).to.equal(86400000); - }); - }); - - describe("getLatestResponseTime", function () { - it("should return 0 when checks array is empty", function () { - expect(getLatestResponseTime([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getLatestResponseTime(null)).to.equal(0); - }); - - it("should return response time from most recent check", function () { - const checks = [ - { responseTime: 150, createdAt: "2024-01-01T11:30:00Z" }, // Most recent - { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }, - { responseTime: 250, createdAt: "2024-01-01T10:30:00Z" }, - ]; - - expect(getLatestResponseTime(checks)).to.equal(150); - }); - - it("should handle missing responseTime in checks", function () { - const checks = [{ createdAt: "2024-01-01T11:30:00Z" }, { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }]; - - expect(getLatestResponseTime(checks)).to.equal(0); - }); - }); - - describe("getAverageResponseTime", function () { - it("should return 0 when checks array is empty", function () { - expect(getAverageResponseTime([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getAverageResponseTime(null)).to.equal(0); - }); - - it("should calculate average response time from all checks", function () { - const checks = [ - { responseTime: 100, createdAt: "2024-01-01T11:30:00Z" }, - { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }, - { responseTime: 300, createdAt: "2024-01-01T10:30:00Z" }, - ]; - - // Average: (100 + 200 + 300) / 3 = 200 - expect(getAverageResponseTime(checks)).to.equal(200); - }); - - it("should handle missing responseTime in some checks", function () { - const checks = [ - { responseTime: 100, createdAt: "2024-01-01T11:30:00Z" }, - { createdAt: "2024-01-01T11:00:00Z" }, - { responseTime: 300, createdAt: "2024-01-01T10:30:00Z" }, - ]; - - // Average: (100 + 300) / 2 = 200 - expect(getAverageResponseTime(checks)).to.equal(200); - }); - - it("should return 0 when no checks have responseTime", function () { - const checks = [{ createdAt: "2024-01-01T11:30:00Z" }, { createdAt: "2024-01-01T11:00:00Z" }]; - - expect(getAverageResponseTime(checks)).to.equal(0); - }); - }); - - describe("getUptimePercentage", function () { - it("should return 0 when checks array is empty", function () { - expect(getUptimePercentage([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getUptimePercentage(null)).to.equal(0); - }); - - it("should return 100 when all checks are up", function () { - const checks = [{ status: true }, { status: true }, { status: true }]; - expect(getUptimePercentage(checks)).to.equal(100); - }); - - it("should return 0 when all checks are down", function () { - const checks = [{ status: false }, { status: false }, { status: false }]; - expect(getUptimePercentage(checks)).to.equal(0); - }); - - it("should calculate correct percentage for mixed status checks", function () { - const checks = [{ status: true }, { status: false }, { status: true }, { status: true }]; - // 3 up out of 4 total = 75% - expect(getUptimePercentage(checks)).to.equal(75); - }); - - it("should handle undefined status values", function () { - const checks = [{ status: true }, { status: undefined }, { status: true }]; - // 2 up out of 3 total ≈ 66.67% - expect(getUptimePercentage(checks)).to.equal((2 / 3) * 100); - }); - }); - - describe("getIncidents", function () { - it("should return 0 when checks array is empty", function () { - expect(getIncidents([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getIncidents(null)).to.equal(0); - }); - - it("should return 0 when all checks are up", function () { - const checks = [{ status: true }, { status: true }, { status: true }]; - expect(getIncidents(checks)).to.equal(0); - }); - - it("should count all incidents when all checks are down", function () { - const checks = [{ status: false }, { status: false }, { status: false }]; - expect(getIncidents(checks)).to.equal(3); - }); - - it("should count correct number of incidents for mixed status checks", function () { - const checks = [{ status: true }, { status: false }, { status: true }, { status: false }, { status: true }]; - expect(getIncidents(checks)).to.equal(2); - }); - - it("should handle undefined status values", function () { - const checks = [{ status: true }, { status: undefined }, { status: false }, { status: false }]; - // Only counts explicit false values - expect(getIncidents(checks)).to.equal(2); - }); - }); - - describe("getMonitorChecks", function () { - let mockModel; - - beforeEach(function () { - // Create a mock model with chainable methods - const mockChecks = [ - { monitorId: "123", createdAt: new Date("2024-01-01") }, - { monitorId: "123", createdAt: new Date("2024-01-02") }, - ]; - - mockModel = { - find: sinon.stub().returns({ - sort: sinon.stub().returns(mockChecks), - }), - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return all checks and date-ranged checks", async function () { - // Arrange - const monitorId = "123"; - const dateRange = { - start: new Date("2024-01-01"), - end: new Date("2024-01-02"), - }; - const sortOrder = -1; - - // Act - const result = await getMonitorChecks(monitorId, mockModel, dateRange, sortOrder); - - // Assert - expect(result).to.have.keys(["checksAll", "checksForDateRange"]); - - // Verify find was called with correct parameters - expect(mockModel.find.firstCall.args[0]).to.deep.equal({ monitorId }); - expect(mockModel.find.secondCall.args[0]).to.deep.equal({ - monitorId, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }); - - // Verify sort was called with correct parameters - const sortCalls = mockModel.find().sort.getCalls(); - sortCalls.forEach((call) => { - expect(call.args[0]).to.deep.equal({ createdAt: sortOrder }); - }); - }); - - it("should handle empty results", async function () { - // Arrange - const emptyModel = { - find: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }; - - // Act - const result = await getMonitorChecks( - "123", - emptyModel, - { - start: new Date(), - end: new Date(), - }, - -1 - ); - - // Assert - expect(result.checksAll).to.be.an("array").that.is.empty; - expect(result.checksForDateRange).to.be.an("array").that.is.empty; - }); - - it("should maintain sort order", async function () { - // Arrange - const sortedChecks = [ - { monitorId: "123", createdAt: new Date("2024-01-02") }, - { monitorId: "123", createdAt: new Date("2024-01-01") }, - ]; - - const sortedModel = { - find: sinon.stub().returns({ - sort: sinon.stub().returns(sortedChecks), - }), - }; - - // Act - const result = await getMonitorChecks( - "123", - sortedModel, - { - start: new Date("2024-01-01"), - end: new Date("2024-01-02"), - }, - -1 - ); - - // Assert - expect(result.checksAll[0].createdAt).to.be.greaterThan(result.checksAll[1].createdAt); - expect(result.checksForDateRange[0].createdAt).to.be.greaterThan(result.checksForDateRange[1].createdAt); - }); - }); - - describe("processChecksForDisplay", function () { - let normalizeStub; - - beforeEach(function () { - normalizeStub = sinon.stub(); - }); - - it("should return original checks when numToDisplay is not provided", function () { - const checks = [1, 2, 3, 4, 5]; - const result = processChecksForDisplay(normalizeStub, checks); - expect(result).to.deep.equal(checks); - }); - - it("should return original checks when numToDisplay is greater than checks length", function () { - const checks = [1, 2, 3]; - const result = processChecksForDisplay(normalizeStub, checks, 5); - expect(result).to.deep.equal(checks); - }); - - it("should filter checks based on numToDisplay", function () { - const checks = [1, 2, 3, 4, 5, 6]; - const result = processChecksForDisplay(normalizeStub, checks, 3); - // Should return [1, 3, 5] as n = ceil(6/3) = 2 - expect(result).to.deep.equal([1, 3, 5]); - }); - - it("should handle empty checks array", function () { - const checks = []; - const result = processChecksForDisplay(normalizeStub, checks, 3); - expect(result).to.be.an("array").that.is.empty; - }); - - it("should call normalizeData when normalize is true", function () { - const checks = [1, 2, 3]; - normalizeStub.returns([10, 20, 30]); - - const result = processChecksForDisplay(normalizeStub, checks, null, true); - - expect(normalizeStub.args[0]).to.deep.equal([checks, 1, 100]); - expect(result).to.deep.equal([10, 20, 30]); - }); - - it("should handle both filtering and normalization", function () { - const checks = [1, 2, 3, 4, 5, 6]; - normalizeStub.returns([10, 30, 50]); - - const result = processChecksForDisplay(normalizeStub, checks, 3, true); - - expect(normalizeStub.args[0][0]).to.deep.equal([1, 3, 5]); - expect(result).to.deep.equal([10, 30, 50]); - }); - }); - - describe("groupChecksByTime", function () { - const mockChecks = [ - { createdAt: "2024-01-15T10:30:45Z" }, - { createdAt: "2024-01-15T10:45:15Z" }, - { createdAt: "2024-01-15T11:15:00Z" }, - { createdAt: "2024-01-16T10:30:00Z" }, - ]; - - it("should group checks by hour when dateRange is 'day'", function () { - const result = groupChecksByTime(mockChecks, "day"); - - // Get timestamps for 10:00 and 11:00 on Jan 15 - const time1 = new Date("2024-01-15T10:00:00Z").getTime(); - const time2 = new Date("2024-01-15T11:00:00Z").getTime(); - const time3 = new Date("2024-01-16T10:00:00Z").getTime(); - - expect(Object.keys(result)).to.have.lengthOf(3); - - expect(result[time1].checks).to.have.lengthOf(2); - expect(result[time2].checks).to.have.lengthOf(1); - expect(result[time3].checks).to.have.lengthOf(1); - }); - - it("should group checks by day when dateRange is not 'day'", function () { - const result = groupChecksByTime(mockChecks, "week"); - - expect(Object.keys(result)).to.have.lengthOf(2); - expect(result["2024-01-15"].checks).to.have.lengthOf(3); - expect(result["2024-01-16"].checks).to.have.lengthOf(1); - }); - - it("should handle empty checks array", function () { - const result = groupChecksByTime([], "day"); - expect(result).to.deep.equal({}); - }); - - it("should handle single check", function () { - const singleCheck = [{ createdAt: "2024-01-15T10:30:45Z" }]; - const result = groupChecksByTime(singleCheck, "day"); - - const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); - expect(Object.keys(result)).to.have.lengthOf(1); - expect(result[expectedTime].checks).to.have.lengthOf(1); - }); - - it("should skip invalid dates and process valid ones", function () { - const checksWithInvalidDate = [ - { createdAt: "invalid-date" }, - { createdAt: "2024-01-15T10:30:45Z" }, - { createdAt: null }, - { createdAt: undefined }, - { createdAt: "" }, - ]; - - const result = groupChecksByTime(checksWithInvalidDate, "day"); - - const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); - expect(Object.keys(result)).to.have.lengthOf(1); - expect(result[expectedTime].checks).to.have.lengthOf(1); - expect(result[expectedTime].checks[0].createdAt).to.equal("2024-01-15T10:30:45Z"); - }); - - it("should handle checks in same time group", function () { - const checksInSameHour = [{ createdAt: "2024-01-15T10:15:00Z" }, { createdAt: "2024-01-15T10:45:00Z" }]; - - const result = groupChecksByTime(checksInSameHour, "day"); - - const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); - expect(Object.keys(result)).to.have.lengthOf(1); - expect(result[expectedTime].checks).to.have.lengthOf(2); - }); - }); - - describe("calculateGroupStats", function () { - // Mock getUptimePercentage function - let uptimePercentageStub; - - beforeEach(function () { - uptimePercentageStub = sinon.stub(); - uptimePercentageStub.returns(95); // Default return value - }); - - it("should calculate stats correctly for a group of checks", function () { - const mockGroup = { - time: "2024-01-15", - checks: [ - { status: true, responseTime: 100 }, - { status: false, responseTime: 200 }, - { status: true, responseTime: 300 }, - ], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: (2 / 3) * 100, - totalChecks: 3, - totalIncidents: 1, - avgResponseTime: 200, // (100 + 200 + 300) / 3 - }); - }); - - it("should handle empty checks array", function () { - const mockGroup = { - time: "2024-01-15", - checks: [], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: 0, - totalChecks: 0, - totalIncidents: 0, - avgResponseTime: 0, - }); - }); - - it("should handle missing responseTime values", function () { - const mockGroup = { - time: "2024-01-15", - checks: [{ status: true }, { status: false, responseTime: 200 }, { status: true, responseTime: undefined }], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: (2 / 3) * 100, - totalChecks: 3, - totalIncidents: 1, - avgResponseTime: 200, // 200 / 1 - }); - }); - - it("should handle all checks with status false", function () { - const mockGroup = { - time: "2024-01-15", - checks: [ - { status: false, responseTime: 100 }, - { status: false, responseTime: 200 }, - { status: false, responseTime: 300 }, - ], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: 0, - totalChecks: 3, - totalIncidents: 3, - avgResponseTime: 200, - }); - }); - - it("should handle all checks with status true", function () { - const mockGroup = { - time: "2024-01-15", - checks: [ - { status: true, responseTime: 100 }, - { status: true, responseTime: 200 }, - { status: true, responseTime: 300 }, - ], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: 100, - totalChecks: 3, - totalIncidents: 0, - avgResponseTime: 200, - }); - }); - }); - - describe("getMonitorStatsById", function () { - const now = new Date(); - const oneHourAgo = new Date(now - 3600000); - const twoHoursAgo = new Date(now - 7200000); - - const mockMonitor = { - _id: "monitor123", - type: "http", - name: "Test Monitor", - url: "https://test.com", - toObject: () => ({ - _id: "monitor123", - type: "http", - name: "Test Monitor", - url: "https://test.com", - }), - }; - - const mockMonitorPing = { - _id: "monitor123", - type: "ping", - name: "Test Monitor", - url: "https://test.com", - toObject: () => ({ - _id: "monitor123", - type: "http", - name: "Test Monitor", - url: "https://test.com", - }), - }; - const mockMonitorDocker = { - _id: "monitor123", - type: "docker", - name: "Test Monitor", - url: "https://test.com", - toObject: () => ({ - _id: "monitor123", - type: "http", - name: "Test Monitor", - url: "https://test.com", - }), - }; - - const checkDocs = [ - { - monitorId: "monitor123", - status: true, - responseTime: 100, - createdAt: new Date("2024-01-01T12:00:00Z"), - toObject: function () { - return { - monitorId: this.monitorId, - status: this.status, - responseTime: this.responseTime, - createdAt: this.createdAt, - }; - }, - }, - { - monitorId: "monitor123", - status: true, - responseTime: 150, - createdAt: new Date("2024-01-01T11:00:00Z"), - toObject: function () { - return { - monitorId: this.monitorId, - status: this.status, - responseTime: this.responseTime, - createdAt: this.createdAt, - }; - }, - }, - { - monitorId: "monitor123", - status: false, - responseTime: 200, - createdAt: new Date("2024-01-01T10:00:00Z"), - toObject: function () { - return { - monitorId: this.monitorId, - status: this.status, - responseTime: this.responseTime, - createdAt: this.createdAt, - }; - }, - }, - ]; - const req = { - params: { monitorId: "monitor123" }, - query: { - dateRange: "day", - sortOrder: "desc", - numToDisplay: 10, - normalize: true, - }, - }; - - beforeEach(function () { - checkFindStub.returns({ - sort: () => checkDocs, - }); - monitorFindByIdStub.returns(mockMonitor); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return monitor stats with calculated values, sort order desc", async function () { - req.query.sortOrder = "desc"; - const result = await getMonitorStatsById(req); - expect(result).to.include.keys([ - "_id", - "type", - "name", - "url", - "uptimeDuration", - "lastChecked", - "latestResponseTime", - "periodIncidents", - "periodTotalChecks", - "periodAvgResponseTime", - "periodUptime", - "aggregateData", - ]); - expect(result.latestResponseTime).to.equal(100); - expect(result.periodTotalChecks).to.equal(3); - expect(result.periodIncidents).to.equal(1); - expect(result.periodUptime).to.be.a("number"); - expect(result.aggregateData).to.be.an("array"); - }); - - it("should return monitor stats with calculated values, ping type", async function () { - monitorFindByIdStub.returns(mockMonitorPing); - req.query.sortOrder = "desc"; - const result = await getMonitorStatsById(req); - expect(result).to.include.keys([ - "_id", - "type", - "name", - "url", - "uptimeDuration", - "lastChecked", - "latestResponseTime", - "periodIncidents", - "periodTotalChecks", - "periodAvgResponseTime", - "periodUptime", - "aggregateData", - ]); - expect(result.latestResponseTime).to.equal(100); - expect(result.periodTotalChecks).to.equal(3); - expect(result.periodIncidents).to.equal(1); - expect(result.periodUptime).to.be.a("number"); - expect(result.aggregateData).to.be.an("array"); - }); - - it("should return monitor stats with calculated values, docker type", async function () { - monitorFindByIdStub.returns(mockMonitorDocker); - req.query.sortOrder = "desc"; - const result = await getMonitorStatsById(req); - expect(result).to.include.keys([ - "_id", - "type", - "name", - "url", - "uptimeDuration", - "lastChecked", - "latestResponseTime", - "periodIncidents", - "periodTotalChecks", - "periodAvgResponseTime", - "periodUptime", - "aggregateData", - ]); - expect(result.latestResponseTime).to.equal(100); - expect(result.periodTotalChecks).to.equal(3); - expect(result.periodIncidents).to.equal(1); - expect(result.periodUptime).to.be.a("number"); - expect(result.aggregateData).to.be.an("array"); - }); - - it("should return monitor stats with calculated values", async function () { - req.query.sortOrder = "asc"; - const result = await getMonitorStatsById(req); - expect(result).to.include.keys([ - "_id", - "type", - "name", - "url", - "uptimeDuration", - "lastChecked", - "latestResponseTime", - "periodIncidents", - "periodTotalChecks", - "periodAvgResponseTime", - "periodUptime", - "aggregateData", - ]); - expect(result.latestResponseTime).to.equal(100); - expect(result.periodTotalChecks).to.equal(3); - expect(result.periodIncidents).to.equal(1); - expect(result.periodUptime).to.be.a("number"); - expect(result.aggregateData).to.be.an("array"); - }); - - it("should throw error when monitor is not found", async function () { - monitorFindByIdStub.returns(Promise.resolve(null)); - - const req = { - params: { monitorId: "nonexistent" }, - }; - - try { - await getMonitorStatsById(req); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error).to.be.an("Error"); - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorStatsById"); - } - }); - }); - - describe("getMonitorById", function () { - let notificationFindStub; - let monitorSaveStub; - - beforeEach(function () { - // Create stubs - notificationFindStub = sinon.stub(Notification, "find"); - monitorSaveStub = sinon.stub().resolves(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return monitor with notifications when found", async function () { - // Arrange - const monitorId = "123"; - const mockMonitor = { - _id: monitorId, - name: "Test Monitor", - save: monitorSaveStub, - }; - const mockNotifications = [ - { _id: "notif1", message: "Test notification 1" }, - { _id: "notif2", message: "Test notification 2" }, - ]; - - monitorFindByIdStub.resolves(mockMonitor); - notificationFindStub.resolves(mockNotifications); - - const result = await getMonitorById(monitorId); - expect(result._id).to.equal(monitorId); - expect(result.name).to.equal("Test Monitor"); - expect(monitorFindByIdStub.calledWith(monitorId)).to.be.true; - expect(notificationFindStub.calledWith({ monitorId })).to.be.true; - expect(monitorSaveStub.calledOnce).to.be.true; - }); - - it("should throw 404 error when monitor not found", async function () { - // Arrange - const monitorId = "nonexistent"; - monitorFindByIdStub.resolves(null); - - // Act & Assert - try { - await getMonitorById(monitorId); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.message).to.equal(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); - expect(error.status).to.equal(404); - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorById"); - } - }); - - it("should handle database errors properly", async function () { - // Arrange - const monitorId = "123"; - const dbError = new Error("Database connection failed"); - monitorFindByIdStub.rejects(dbError); - - // Act & Assert - try { - await getMonitorById(monitorId); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorById"); - expect(error.message).to.equal("Database connection failed"); - } - }); - - it("should handle notification fetch errors", async function () { - // Arrange - const monitorId = "123"; - const mockMonitor = { - _id: monitorId, - name: "Test Monitor", - save: monitorSaveStub, - }; - const notificationError = new Error("Notification fetch failed"); - - monitorFindByIdStub.resolves(mockMonitor); - notificationFindStub.rejects(notificationError); - - // Act & Assert - try { - await getMonitorById(monitorId); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorById"); - expect(error.message).to.equal("Notification fetch failed"); - } - }); - - it("should handle monitor save errors", async function () { - // Arrange - const monitorId = "123"; - const mockMonitor = { - _id: monitorId, - name: "Test Monitor", - save: sinon.stub().rejects(new Error("Save failed")), - }; - const mockNotifications = []; - - monitorFindByIdStub.resolves(mockMonitor); - notificationFindStub.resolves(mockNotifications); - - // Act & Assert - try { - await getMonitorById(monitorId); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorById"); - expect(error.message).to.equal("Save failed"); - } - }); - }); - - describe("getMonitorsAndSummaryByTeamId", function () { - it("should return monitors and correct summary counts", async function () { - // Arrange - const teamId = "team123"; - const type = "http"; - const mockMonitors = [ - { teamId, type, status: true, isActive: true }, // up - { teamId, type, status: false, isActive: true }, // down - { teamId, type, status: null, isActive: false }, // paused - { teamId, type, status: true, isActive: true }, // up - ]; - monitorFindStub.resolves(mockMonitors); - - // Act - const result = await getMonitorsAndSummaryByTeamId(teamId, type); - - // Assert - expect(result.monitors).to.have.lengthOf(4); - expect(result.monitorCounts).to.deep.equal({ - up: 2, - down: 1, - paused: 1, - total: 4, - }); - expect(monitorFindStub.calledOnceWith({ teamId, type })).to.be.true; - }); - - it("should return empty results for non-existent team", async function () { - // Arrange - monitorFindStub.resolves([]); - - // Act - const result = await getMonitorsAndSummaryByTeamId("nonexistent", "http"); - - // Assert - expect(result.monitors).to.have.lengthOf(0); - expect(result.monitorCounts).to.deep.equal({ - up: 0, - down: 0, - paused: 0, - total: 0, - }); - }); - - it("should handle database errors", async function () { - // Arrange - const error = new Error("Database error"); - error.service = "MonitorModule"; - error.method = "getMonitorsAndSummaryByTeamId"; - monitorFindStub.rejects(error); - - // Act & Assert - try { - await getMonitorsAndSummaryByTeamId("team123", "http"); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err).to.equal(error); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("getMonitorsAndSummaryByTeamId"); - } - }); - }); - - describe("getMonitorsByTeamId", function () { - beforeEach(function () { - // Chain stubs for Monitor.find().skip().limit().sort() - - // Stub for CHECK_MODEL_LOOKUP model find - checkFindStub.returns({ - sort: sinon.stub().returns({ - limit: sinon.stub().returns([]), - }), - }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return monitors with basic query parameters", async function () { - const mockMonitors = [ - { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, - { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, - ]; - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMonitors), - }), - }), - }); - - const req = { - params: { teamId: "team123" }, - query: { - type: "http", - page: 0, - rowsPerPage: 10, - field: "name", - status: false, - checkOrder: "desc", - }, - }; - - monitorCountStub.resolves(2); - - const result = await getMonitorsByTeamId(req); - - expect(result).to.have.property("monitors"); - expect(result).to.have.property("monitorCount", 2); - }); - - it("should return monitors with basic query parameters", async function () { - const mockMonitors = [ - { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, - { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, - ]; - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMonitors), - }), - }), - }); - - const req = { - params: { teamId: "team123" }, - query: { - type: "http", - page: 0, - rowsPerPage: 10, - field: "name", - status: true, - checkOrder: "asc", - }, - }; - - monitorCountStub.resolves(2); - - const result = await getMonitorsByTeamId(req); - - expect(result).to.have.property("monitors"); - expect(result).to.have.property("monitorCount", 2); - }); - - it("should handle type filter with array input", async function () { - const req = { - params: { teamId: "team123" }, - query: { - type: ["http", "ping"], - }, - }; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }), - }); - monitorCountStub.resolves(0); - - await getMonitorsByTeamId(req); - - expect(Monitor.find.firstCall.args[0]).to.deep.equal({ - teamId: "team123", - type: { $in: ["http", "ping"] }, - }); - }); - - it("should handle text search filter", async function () { - const req = { - params: { teamId: "team123" }, - query: { - filter: "search", - }, - }; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }), - }); - monitorCountStub.resolves(0); - - await getMonitorsByTeamId(req); - - expect(Monitor.find.firstCall.args[0]).to.deep.equal({ - teamId: "team123", - $or: [{ name: { $regex: "search", $options: "i" } }, { url: { $regex: "search", $options: "i" } }], - }); - }); - - it("should handle pagination parameters", async function () { - const req = { - params: { teamId: "team123" }, - query: { - page: 2, - rowsPerPage: 5, - }, - }; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }), - }); - monitorCountStub.resolves(0); - - const result = await getMonitorsByTeamId(req); - expect(result).to.deep.equal({ - monitors: [], - monitorCount: 0, - }); - }); - - it("should handle sorting parameters", async function () { - const req = { - params: { teamId: "team123" }, - query: { - field: "name", - order: "asc", - }, - }; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }), - }); - monitorCountStub.resolves(0); - - await getMonitorsByTeamId(req); - - const result = await getMonitorsByTeamId(req); - expect(result).to.deep.equal({ - monitors: [], - monitorCount: 0, - }); - }); - - it("should return early when limit is -1", async function () { - // Arrange - const req = { - params: { teamId: "team123" }, - query: { - limit: "-1", - }, - }; - - const mockMonitors = [ - { _id: "1", type: "http" }, - { _id: "2", type: "ping" }, - ]; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMonitors), - }), - }), - }); - - monitorCountStub.resolves(2); - - // Act - const result = await getMonitorsByTeamId(req); - - // Assert - expect(result).to.deep.equal({ - monitors: mockMonitors, - monitorCount: 2, - }); - }); - - it("should normalize checks when normalize parameter is provided", async function () { - const req = { - params: { teamId: "team123" }, - query: { normalize: "true" }, - }; - monitorCountStub.resolves(2); - - const mockMonitors = [ - { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, - { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, - ]; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMonitors), - }), - }), - }); - - const result = await getMonitorsByTeamId(req); - expect(result.monitorCount).to.equal(2); - expect(result.monitors).to.have.lengthOf(2); - }); - - it("should handle database errors", async function () { - const req = { - params: { teamId: "team123" }, - query: {}, - }; - - const error = new Error("Database error"); - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().throws(error), - }), - }), - }); - - try { - await getMonitorsByTeamId(req); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("getMonitorsByTeamId"); - expect(err.message).to.equal("Database error"); - } - }); - }); - - describe("createMonitor", function () { - it("should create a monitor without notifications", async function () { - let monitorSaveStub = sinon.stub(Monitor.prototype, "save").resolves(); - - const req = { - body: { - name: "Test Monitor", - url: "http://test.com", - type: "http", - notifications: ["someNotification"], - }, - }; - - const expectedMonitor = { - name: "Test Monitor", - url: "http://test.com", - type: "http", - notifications: undefined, - save: monitorSaveStub, - }; - - const result = await createMonitor(req); - expect(result.name).to.equal(expectedMonitor.name); - expect(result.url).to.equal(expectedMonitor.url); - }); - - it("should handle database errors", async function () { - const req = { - body: { - name: "Test Monitor", - }, - }; - - try { - await createMonitor(req); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("createMonitor"); - } - }); - }); - - describe("deleteMonitor", function () { - it("should delete a monitor successfully", async function () { - const monitorId = "123456789"; - const mockMonitor = { - _id: monitorId, - name: "Test Monitor", - url: "http://test.com", - }; - - const req = { - params: { monitorId }, - }; - - monitorFindByIdAndDeleteStub.resolves(mockMonitor); - - const result = await deleteMonitor(req); - - expect(result).to.deep.equal(mockMonitor); - sinon.assert.calledWith(monitorFindByIdAndDeleteStub, monitorId); - }); - - it("should throw error when monitor not found", async function () { - const monitorId = "nonexistent123"; - const req = { - params: { monitorId }, - }; - - monitorFindByIdAndDeleteStub.resolves(null); - - try { - await deleteMonitor(req); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.message).to.equal(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteMonitor"); - } - }); - - it("should handle database errors", async function () { - const monitorId = "123456789"; - const req = { - params: { monitorId }, - }; - - const dbError = new Error("Database connection error"); - monitorFindByIdAndDeleteStub.rejects(dbError); - - try { - await deleteMonitor(req); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteMonitor"); - } - }); - }); - - describe("deleteAllMonitors", function () { - it("should delete all monitors for a team successfully", async function () { - const teamId = "team123"; - const mockMonitors = [ - { _id: "1", name: "Monitor 1", teamId }, - { _id: "2", name: "Monitor 2", teamId }, - ]; - - monitorFindStub.resolves(mockMonitors); - monitorDeleteManyStub.resolves({ deletedCount: 2 }); - - const result = await deleteAllMonitors(teamId); - - expect(result).to.deep.equal({ - monitors: mockMonitors, - deletedCount: 2, - }); - sinon.assert.calledWith(monitorFindStub, { teamId }); - sinon.assert.calledWith(monitorDeleteManyStub, { teamId }); - }); - - it("should return empty array when no monitors found", async function () { - const teamId = "emptyTeam"; - - monitorFindStub.resolves([]); - monitorDeleteManyStub.resolves({ deletedCount: 0 }); - - const result = await deleteAllMonitors(teamId); - - expect(result).to.deep.equal({ - monitors: [], - deletedCount: 0, - }); - sinon.assert.calledWith(monitorFindStub, { teamId }); - sinon.assert.calledWith(monitorDeleteManyStub, { teamId }); - }); - - it("should handle database errors", async function () { - const teamId = "team123"; - const dbError = new Error("Database connection error"); - monitorFindStub.rejects(dbError); - - try { - await deleteAllMonitors(teamId); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteAllMonitors"); - } - }); - - it("should handle deleteMany errors", async function () { - const teamId = "team123"; - monitorFindStub.resolves([{ _id: "1", name: "Monitor 1" }]); - monitorDeleteManyStub.rejects(new Error("Delete operation failed")); - - try { - await deleteAllMonitors(teamId); - } catch (err) { - expect(err.message).to.equal("Delete operation failed"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteAllMonitors"); - } - }); - }); - - describe("deleteMonitorsByUserId", function () { - beforeEach(function () {}); - - afterEach(function () { - sinon.restore(); - }); - - it("should delete all monitors for a user successfully", async function () { - // Arrange - const userId = "user123"; - const mockResult = { - deletedCount: 3, - acknowledged: true, - }; - - monitorDeleteManyStub.resolves(mockResult); - - // Act - const result = await deleteMonitorsByUserId(userId); - - // Assert - expect(result).to.deep.equal(mockResult); - sinon.assert.calledWith(monitorDeleteManyStub, { userId: userId }); - }); - - it("should return zero deletedCount when no monitors found", async function () { - // Arrange - const userId = "nonexistentUser"; - const mockResult = { - deletedCount: 0, - acknowledged: true, - }; - - monitorDeleteManyStub.resolves(mockResult); - - // Act - const result = await deleteMonitorsByUserId(userId); - - // Assert - expect(result.deletedCount).to.equal(0); - sinon.assert.calledWith(monitorDeleteManyStub, { userId: userId }); - }); - - it("should handle database errors", async function () { - // Arrange - const userId = "user123"; - const dbError = new Error("Database connection error"); - monitorDeleteManyStub.rejects(dbError); - - // Act & Assert - try { - await deleteMonitorsByUserId(userId); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteMonitorsByUserId"); - } - }); - }); - - describe("editMonitor", function () { - it("should edit a monitor successfully", async function () { - // Arrange - const candidateId = "monitor123"; - const candidateMonitor = { - name: "Updated Monitor", - url: "http://updated.com", - type: "http", - notifications: ["someNotification"], - }; - - const expectedUpdateData = { - name: "Updated Monitor", - url: "http://updated.com", - type: "http", - notifications: undefined, - }; - - const mockUpdatedMonitor = { - _id: candidateId, - ...expectedUpdateData, - }; - - monitorFindByIdAndUpdateStub.resolves(mockUpdatedMonitor); - - // Act - const result = await editMonitor(candidateId, candidateMonitor); - - // Assert - expect(result).to.deep.equal(mockUpdatedMonitor); - sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, expectedUpdateData, { - new: true, - }); - }); - - it("should return null when monitor not found", async function () { - // Arrange - const candidateId = "nonexistent123"; - const candidateMonitor = { - name: "Updated Monitor", - }; - - monitorFindByIdAndUpdateStub.resolves(null); - - // Act - const result = await editMonitor(candidateId, candidateMonitor); - - // Assert - expect(result).to.be.null; - sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, { name: "Updated Monitor", notifications: undefined }, { new: true }); - }); - - it("should remove notifications from update data", async function () { - // Arrange - const candidateId = "monitor123"; - const candidateMonitor = { - name: "Updated Monitor", - notifications: ["notification1", "notification2"], - }; - - const expectedUpdateData = { - name: "Updated Monitor", - notifications: undefined, - }; - - monitorFindByIdAndUpdateStub.resolves({ - _id: candidateId, - ...expectedUpdateData, - }); - - // Act - await editMonitor(candidateId, candidateMonitor); - - // Assert - sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, expectedUpdateData, { - new: true, - }); - }); - - it("should handle database errors", async function () { - // Arrange - const candidateId = "monitor123"; - const candidateMonitor = { - name: "Updated Monitor", - }; - - const dbError = new Error("Database connection error"); - monitorFindByIdAndUpdateStub.rejects(dbError); - - // Act & Assert - try { - await editMonitor(candidateId, candidateMonitor); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("editMonitor"); - } - }); - }); - - describe("addDemoMonitors", function () { - it("should add demo monitors successfully", async function () { - // Arrange - const userId = "user123"; - const teamId = "team123"; - monitorInsertManyStub.resolves([{ _id: "123" }]); - const result = await addDemoMonitors(userId, teamId); - expect(result).to.deep.equal([{ _id: "123" }]); - }); - - it("should handle database errors", async function () { - const userId = "user123"; - const teamId = "team123"; - - const dbError = new Error("Database connection error"); - monitorInsertManyStub.rejects(dbError); - - try { - await addDemoMonitors(userId, teamId); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("addDemoMonitors"); - } - }); - }); -}); diff --git a/server/tests/db/notificationModule.test.js b/server/tests/db/notificationModule.test.js deleted file mode 100755 index fbb97f33a..000000000 --- a/server/tests/db/notificationModule.test.js +++ /dev/null @@ -1,76 +0,0 @@ -import sinon from "sinon"; -import Notification from "../../db/models/Notification.js"; -import { createNotification, getNotificationsByMonitorId, deleteNotificationsByMonitorId } from "../../db/mongo/modules/notificationModule.js"; - -describe("notificationModule", function () { - const mockNotification = { - monitorId: "123", - }; - const mockNotifications = [mockNotification]; - let notificationSaveStub, notificationFindStub, notificationDeleteManyStub; - - beforeEach(function () { - notificationSaveStub = sinon.stub(Notification.prototype, "save").resolves(); - notificationFindStub = sinon.stub(Notification, "find").resolves(); - notificationDeleteManyStub = sinon.stub(Notification, "deleteMany").resolves(); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createNotification", function () { - it("should create a new notification", async function () { - const notificationData = { _id: "123", name: "test" }; - notificationSaveStub.resolves(notificationData); - const res = await createNotification(notificationData); - expect(res).to.deep.equal(notificationData); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - notificationSaveStub.rejects(err); - try { - await createNotification(mockNotification); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getNotificationsByMonitorId", function () { - it("should return notifications by monitor ID", async function () { - notificationFindStub.resolves(mockNotifications); - const res = await getNotificationsByMonitorId(mockNotification.monitorId); - expect(res).to.deep.equal(mockNotifications); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - notificationFindStub.rejects(err); - try { - await getNotificationsByMonitorId(mockNotification.monitorId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteNotificationsByMonitorId", function () { - it("should delete notifications by monitor ID", async function () { - notificationDeleteManyStub.resolves({ deletedCount: mockNotifications.length }); - const res = await deleteNotificationsByMonitorId(mockNotification.monitorId); - expect(res).to.deep.equal(mockNotifications.length); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - notificationDeleteManyStub.rejects(err); - try { - await deleteNotificationsByMonitorId(mockNotification.monitorId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/pageSpeedCheckModule.test.js b/server/tests/db/pageSpeedCheckModule.test.js deleted file mode 100755 index 45135135d..000000000 --- a/server/tests/db/pageSpeedCheckModule.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import sinon from "sinon"; -import PageSpeedCheck from "../../db/models/PageSpeedCheck.js"; -import { createPageSpeedCheck, deletePageSpeedChecksByMonitorId } from "../../db/mongo/modules/pageSpeedCheckModule.js"; - -const mockPageSpeedCheck = { - monitorId: "monitorId", - bestPractices: 1, - seo: 1, - performance: 1, -}; - -const mockDeletedResult = { deletedCount: 1 }; - -describe("pageSpeedCheckModule", function () { - let pageSpeedCheckSaveStub, pageSpeedCheckDeleteManyStub; - - beforeEach(function () { - pageSpeedCheckSaveStub = sinon.stub(PageSpeedCheck.prototype, "save"); - pageSpeedCheckDeleteManyStub = sinon.stub(PageSpeedCheck, "deleteMany"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createPageSpeedCheck", function () { - it("should return a page speed check", async function () { - pageSpeedCheckSaveStub.resolves(mockPageSpeedCheck); - const pageSpeedCheck = await createPageSpeedCheck(mockPageSpeedCheck); - expect(pageSpeedCheck).to.deep.equal(mockPageSpeedCheck); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - pageSpeedCheckSaveStub.rejects(err); - try { - await expect(createPageSpeedCheck(mockPageSpeedCheck)); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deletePageSpeedChecksByMonitorId", function () { - it("should return the number of deleted checks", async function () { - pageSpeedCheckDeleteManyStub.resolves(mockDeletedResult); - const result = await deletePageSpeedChecksByMonitorId("monitorId"); - expect(result).to.deep.equal(mockDeletedResult.deletedCount); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - pageSpeedCheckDeleteManyStub.rejects(err); - try { - await expect(deletePageSpeedChecksByMonitorId("monitorId")); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/recoveryModule.test.js b/server/tests/db/recoveryModule.test.js deleted file mode 100755 index 88afa257a..000000000 --- a/server/tests/db/recoveryModule.test.js +++ /dev/null @@ -1,168 +0,0 @@ -import sinon from "sinon"; -import RecoveryToken from "../../db/models/RecoveryToken.js"; -import User from "../../db/models/User.js"; -import { requestRecoveryToken, validateRecoveryToken, resetPassword } from "../../db/mongo/modules/recoveryModule.js"; -import { errorMessages } from "../../utils/messages.js"; - -const mockRecoveryToken = { - email: "test@test.com", - token: "1234567890", -}; - -const mockUser = { - email: "test@test.com", - password: "oldPassword", -}; - -const mockUserWithoutPassword = { - email: "test@test.com", -}; - -// Create a query builder that logs -const createQueryChain = (finalResult, comparePasswordResult = false) => ({ - select: () => ({ - select: async () => { - if (finalResult === mockUser) { - // Return a new object with all required methods - return { - email: "test@test.com", - password: "oldPassword", - comparePassword: sinon.stub().resolves(comparePasswordResult), - save: sinon.stub().resolves(), - }; - } - return finalResult; - }, - }), - // Add methods to the top level too - comparePassword: sinon.stub().resolves(comparePasswordResult), - save: sinon.stub().resolves(), -}); - -describe("recoveryModule", function () { - let deleteManyStub, saveStub, findOneStub, userCompareStub, userSaveStub, userFindOneStub; - let req, res; - - beforeEach(function () { - req = { - body: { email: "test@test.com" }, - }; - deleteManyStub = sinon.stub(RecoveryToken, "deleteMany"); - saveStub = sinon.stub(RecoveryToken.prototype, "save"); - findOneStub = sinon.stub(RecoveryToken, "findOne"); - userCompareStub = sinon.stub(User.prototype, "comparePassword"); - userSaveStub = sinon.stub(User.prototype, "save"); - userFindOneStub = sinon.stub().resolves(); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("requestRecoveryToken", function () { - it("should return a recovery token", async function () { - deleteManyStub.resolves(); - saveStub.resolves(mockRecoveryToken); - const result = await requestRecoveryToken(req, res); - expect(result.email).to.equal(mockRecoveryToken.email); - }); - - it("should handle an error", async function () { - const err = new Error("Test error"); - deleteManyStub.rejects(err); - try { - await requestRecoveryToken(req, res); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("validateRecoveryToken", function () { - it("should return a recovery token if found", async function () { - findOneStub.resolves(mockRecoveryToken); - const result = await validateRecoveryToken(req, res); - expect(result).to.deep.equal(mockRecoveryToken); - }); - - it("should thrown an error if a token is not found", async function () { - findOneStub.resolves(null); - try { - await validateRecoveryToken(req, res); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_TOKEN_NOT_FOUND); - } - }); - - it("should handle DB errors", async function () { - const err = new Error("Test error"); - findOneStub.rejects(err); - try { - await validateRecoveryToken(req, res); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("resetPassword", function () { - beforeEach(function () { - req.body = { - password: "test", - newPassword: "test1", - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should thrown an error if a recovery token is not found", async function () { - findOneStub.resolves(null); - try { - await resetPassword(req, res); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_TOKEN_NOT_FOUND); - } - }); - - it("should throw an error if a user is not found", async function () { - findOneStub.resolves(mockRecoveryToken); - userFindOneStub = sinon.stub(User, "findOne").resolves(null); - try { - await resetPassword(req, res); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND); - } - }); - - it("should throw an error if the passwords match", async function () { - findOneStub.resolves(mockRecoveryToken); - saveStub.resolves(); - userFindOneStub = sinon.stub(User, "findOne").returns(createQueryChain(mockUser, true)); - try { - await resetPassword(req, res); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_RESET_PASSWORD_BAD_MATCH); - } - }); - - it("should return a user without password if successful", async function () { - findOneStub.resolves(mockRecoveryToken); - saveStub.resolves(); - userFindOneStub = sinon - .stub(User, "findOne") - .returns(createQueryChain(mockUser)) // First call will resolve to mockUser - .onSecondCall() - .returns(createQueryChain(mockUserWithoutPassword)); - const result = await resetPassword(req, res); - expect(result).to.deep.equal(mockUserWithoutPassword); - }); - }); -}); diff --git a/server/tests/db/settingsModule.test.js b/server/tests/db/settingsModule.test.js deleted file mode 100755 index 9d1a80c3e..000000000 --- a/server/tests/db/settingsModule.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import sinon from "sinon"; -import { getAppSettings, updateAppSettings } from "../../db/mongo/modules/settingsModule.js"; -import AppSettings from "../../db/models/AppSettings.js"; - -const mockAppSettings = { - appName: "Test App", -}; - -describe("SettingsModule", function () { - let appSettingsFindOneStub, appSettingsFindOneAndUpdateStub; - - beforeEach(function () { - appSettingsFindOneStub = sinon.stub(AppSettings, "findOne"); - appSettingsFindOneAndUpdateStub = sinon.stub(AppSettings, "findOneAndUpdate"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("getAppSettings", function () { - it("should return app settings", async function () { - appSettingsFindOneStub.resolves(mockAppSettings); - const result = await getAppSettings(); - expect(result).to.deep.equal(mockAppSettings); - }); - - it("should handle an error", async function () { - const err = new Error("Test error"); - appSettingsFindOneStub.throws(err); - try { - await getAppSettings(); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("updateAppSettings", function () { - it("should update app settings", async function () { - appSettingsFindOneAndUpdateStub.resolves(mockAppSettings); - const result = await updateAppSettings(mockAppSettings); - expect(result).to.deep.equal(mockAppSettings); - }); - - it("should handle an error", async function () { - const err = new Error("Test error"); - appSettingsFindOneAndUpdateStub.throws(err); - try { - await updateAppSettings(mockAppSettings); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/statusPageModule.test.js b/server/tests/db/statusPageModule.test.js deleted file mode 100755 index 54e1a287f..000000000 --- a/server/tests/db/statusPageModule.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import sinon from "sinon"; -import { createStatusPage, getStatusPageByUrl } from "../../db/mongo/modules/statusPageModule.js"; -import StatusPage from "../../db/models/StatusPage.js"; -import { errorMessages } from "../../utils/messages.js"; - -describe("statusPageModule", function () { - let statusPageFindOneStub, statusPageSaveStub, statusPageFindStub; - - beforeEach(function () { - statusPageSaveStub = sinon.stub(StatusPage.prototype, "save"); - statusPageFindOneStub = sinon.stub(StatusPage, "findOne"); - statusPageFindStub = sinon.stub(StatusPage, "find"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createStatusPage", function () { - it("should throw an error if a non-unique url is provided", async function () { - statusPageFindOneStub.resolves(true); - try { - await createStatusPage({ url: "test" }); - } catch (error) { - expect(error.status).to.equal(400); - expect(error.message).to.equal(errorMessages.STATUS_PAGE_URL_NOT_UNIQUE); - } - }); - - it("should handle duplicate URL errors", async function () { - const err = new Error("test"); - err.code = 11000; - statusPageSaveStub.rejects(err); - try { - await createStatusPage({ url: "test" }); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should return a status page if a unique url is provided", async function () { - statusPageFindOneStub.resolves(null); - statusPageFindStub.resolves([]); - const mockStatusPage = { url: "test" }; - const statusPage = await createStatusPage(mockStatusPage); - expect(statusPage).to.exist; - expect(statusPage.url).to.equal(mockStatusPage.url); - }); - }); - - describe("getStatusPageByUrl", function () { - it("should throw an error if a status page is not found", async function () { - statusPageFindOneStub.resolves(null); - try { - await getStatusPageByUrl("test"); - } catch (error) { - expect(error.status).to.equal(404); - expect(error.message).to.equal(errorMessages.STATUS_PAGE_NOT_FOUND); - } - }); - - it("should return a status page if a status page is found", async function () { - const mockStatusPage = { url: "test" }; - statusPageFindOneStub.resolves(mockStatusPage); - const statusPage = await getStatusPageByUrl(mockStatusPage.url); - expect(statusPage).to.exist; - expect(statusPage).to.deep.equal(mockStatusPage); - }); - }); -}); diff --git a/server/tests/db/userModule.test.js b/server/tests/db/userModule.test.js deleted file mode 100755 index d0f5ecb3e..000000000 --- a/server/tests/db/userModule.test.js +++ /dev/null @@ -1,294 +0,0 @@ -import sinon from "sinon"; -import UserModel from "../../db/models/User.js"; -import TeamModel from "../../db/models/Team.js"; -import { - insertUser, - getUserByEmail, - updateUser, - deleteUser, - deleteTeam, - deleteAllOtherUsers, - getAllUsers, - logoutUser, -} from "../../db/mongo/modules/userModule.js"; -import { errorMessages } from "../../utils/messages.js"; - -const mockUser = { - email: "test@test.com", - password: "password", - role: ["user"], -}; -const mockSuperUser = { - email: "test@test.com", - password: "password", - role: ["superadmin"], -}; -const imageFile = { - image: 1, -}; - -describe("userModule", function () { - let teamSaveStub, - teamFindByIdAndDeleteStub, - userSaveStub, - userFindStub, - userFindOneStub, - userFindByIdAndUpdateStub, - userFindByIdAndDeleteStub, - userDeleteManyStub, - userUpdateOneStub, - generateAvatarImageStub, - parseBooleanStub; - - beforeEach(function () { - teamSaveStub = sinon.stub(TeamModel.prototype, "save"); - teamFindByIdAndDeleteStub = sinon.stub(TeamModel, "findByIdAndDelete"); - userSaveStub = sinon.stub(UserModel.prototype, "save"); - userFindStub = sinon.stub(UserModel, "find"); - userFindOneStub = sinon.stub(UserModel, "findOne"); - userFindByIdAndUpdateStub = sinon.stub(UserModel, "findByIdAndUpdate"); - userFindByIdAndDeleteStub = sinon.stub(UserModel, "findByIdAndDelete"); - userDeleteManyStub = sinon.stub(UserModel, "deleteMany"); - userUpdateOneStub = sinon.stub(UserModel, "updateOne"); - generateAvatarImageStub = sinon.stub().resolves({ image: 2 }); - parseBooleanStub = sinon.stub().returns(true); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("insertUser", function () { - it("should insert a regular user", async function () { - userSaveStub.resolves(mockUser); - userFindOneStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves(mockUser), - }), - }); - const result = await insertUser(mockUser, imageFile, generateAvatarImageStub); - expect(result).to.deep.equal(mockUser); - }); - - it("should insert a superadmin user", async function () { - userSaveStub.resolves(mockSuperUser); - userFindOneStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves(mockSuperUser), - }), - }); - const result = await insertUser(mockSuperUser, imageFile, generateAvatarImageStub); - expect(result).to.deep.equal(mockSuperUser); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userSaveStub.rejects(err); - try { - await insertUser(mockUser, imageFile, generateAvatarImageStub); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - - it("should handle a duplicate key error", async function () { - const err = new Error("test error"); - err.code = 11000; - userSaveStub.rejects(err); - try { - await insertUser(mockUser, imageFile, generateAvatarImageStub); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getUserByEmail", function () { - it("should return a user", async function () { - userFindOneStub.returns({ - select: sinon.stub().resolves(mockUser), - }); - const result = await getUserByEmail(mockUser.email); - expect(result).to.deep.equal(mockUser); - }); - }); - - describe("getUserByEmail", function () { - it("throw an error if a user is not found", async function () { - userFindOneStub.returns({ - select: sinon.stub().resolves(null), - }); - try { - await getUserByEmail(mockUser.email); - } catch (error) { - expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND); - } - }); - }); - - describe("updateUser", function () { - let req, res; - - beforeEach(function () { - req = { - params: { - userId: "testId", - }, - body: { - deleteProfileImage: "false", - email: "test@test.com", - }, - file: { - buffer: "test", - mimetype: "test", - }, - }; - res = {}; - }); - - afterEach(function () {}); - - it("should update a user", async function () { - parseBooleanStub.returns(false); - userFindByIdAndUpdateStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves(mockUser), - }), - }); - const result = await updateUser(req, res, parseBooleanStub, generateAvatarImageStub); - expect(result).to.deep.equal(mockUser); - }); - - it("should delete a user profile image", async function () { - req.body.deleteProfileImage = "true"; - userFindByIdAndUpdateStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves(mockUser), - }), - }); - const result = await updateUser(req, res, parseBooleanStub, generateAvatarImageStub); - expect(result).to.deep.equal(mockUser); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userFindByIdAndUpdateStub.throws(err); - try { - await updateUser(req, res, parseBooleanStub, generateAvatarImageStub); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteUser", function () { - it("should return a deleted user", async function () { - userFindByIdAndDeleteStub.resolves(mockUser); - const result = await deleteUser("testId"); - expect(result).to.deep.equal(mockUser); - }); - - it("should throw an error if a user is not found", async function () { - try { - await deleteUser("testId"); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND); - } - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userFindByIdAndDeleteStub.throws(err); - try { - await deleteUser("testId"); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteTeam", function () { - it("should return true if team deleted", async function () { - teamFindByIdAndDeleteStub.resolves(); - const result = await deleteTeam("testId"); - expect(result).to.equal(true); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - teamFindByIdAndDeleteStub.throws(err); - try { - await deleteTeam("testId"); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteAllOtherUsers", function () { - it("should return true if all other users deleted", async function () { - userDeleteManyStub.resolves(true); - const result = await deleteAllOtherUsers(); - expect(result).to.equal(true); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userDeleteManyStub.throws(err); - try { - await deleteAllOtherUsers(); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getAllUsers", function () { - it("should return all users", async function () { - userFindStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves([mockUser]), - }), - }); - const result = await getAllUsers(); - expect(result).to.deep.equal([mockUser]); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userFindStub.throws(err); - try { - await getAllUsers(); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("logoutUser", function () { - it("should return true if user logged out", async function () { - userUpdateOneStub.resolves(true); - const result = await logoutUser("testId"); - expect(result).to.equal(true); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userUpdateOneStub.throws(err); - try { - await logoutUser("testId"); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/services/emailService.test.js b/server/tests/services/emailService.test.js deleted file mode 100755 index cca86896a..000000000 --- a/server/tests/services/emailService.test.js +++ /dev/null @@ -1,168 +0,0 @@ -import sinon from "sinon"; -import EmailService from "../../service/emailService.js"; - -describe("EmailService - Constructor", function () { - let settingsServiceMock; - let fsMock; - let pathMock; - let compileMock; - let mjml2htmlMock; - let nodemailerMock; - let loggerMock; - - beforeEach(function () { - settingsServiceMock = { - getSettings: sinon.stub().returns({ - systemEmailHost: "smtp.example.com", - systemEmailPort: 465, - systemEmailAddress: "test@example.com", - systemEmailPassword: "password", - }), - }; - - fsMock = { - readFileSync: sinon.stub().returns(""), - }; - - pathMock = { - join: sinon.stub().callsFake((...args) => args.join("/")), - }; - - compileMock = sinon.stub().returns(() => ""); - - mjml2htmlMock = sinon.stub().returns({ html: "" }); - - nodemailerMock = { - createTransport: sinon.stub().returns({ - sendMail: sinon.stub().resolves({ messageId: "12345" }), - }), - }; - - loggerMock = { - error: sinon.stub(), - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should initialize template loaders and email transporter", function () { - const emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); - - // Verify that the settingsService is assigned correctly - expect(emailService.settingsService).to.equal(settingsServiceMock); - - // Verify that the template loaders are initialized - expect(emailService.templateLookup.welcomeEmailTemplate).to.be.a("function"); - expect(emailService.templateLookup.employeeActivationTemplate).to.be.a("function"); - - // Verify that the email transporter is initialized - expect(nodemailerMock.createTransport.calledOnce).to.be.true; - const emailConfig = nodemailerMock.createTransport.getCall(0).args[0]; - expect(emailConfig).to.deep.equal({ - host: "smtp.example.com", - port: 465, - secure: true, - auth: { - user: "test@example.com", - pass: "password", - }, - }); - }); - - it("should have undefined templates if FS fails", function () { - fsMock = { - readFileSync: sinon.stub().throws(new Error("File read error")), - }; - const emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); - expect(loggerMock.error.called).to.be.true; - expect(loggerMock.error.firstCall.args[0].message).to.equal("File read error"); - }); -}); - -describe("EmailService - buildAndSendEmail", function () { - let settingsServiceMock; - let fsMock; - let pathMock; - let compileMock; - let mjml2htmlMock; - let nodemailerMock; - let loggerMock; - let emailService; - - beforeEach(function () { - settingsServiceMock = { - getSettings: sinon.stub().returns({ - systemEmailHost: "smtp.example.com", - systemEmailPort: 465, - systemEmailAddress: "test@example.com", - systemEmailPassword: "password", - }), - }; - - fsMock = { - readFileSync: sinon.stub().returns(""), - }; - - pathMock = { - join: sinon.stub().callsFake((...args) => args.join("/")), - }; - - compileMock = sinon.stub().returns(() => ""); - - mjml2htmlMock = sinon.stub().returns({ html: "" }); - - nodemailerMock = { - createTransport: sinon.stub().returns({ - sendMail: sinon.stub().resolves({ messageId: "12345" }), - }), - }; - - loggerMock = { - error: sinon.stub(), - }; - - emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should build and send email successfully", async function () { - const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); - - expect(messageId).to.equal("12345"); - expect(nodemailerMock.createTransport().sendMail.calledOnce).to.be.true; - }); - - it("should log error if building HTML fails", async function () { - mjml2htmlMock.throws(new Error("MJML error")); - - const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); - expect(loggerMock.error.calledOnce).to.be.true; - expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error"); - }); - - it("should log error if sending email fails", async function () { - nodemailerMock.createTransport().sendMail.rejects(new Error("SMTP error")); - await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); - expect(loggerMock.error.calledOnce).to.be.true; - expect(loggerMock.error.getCall(0).args[0].message).to.equal("SMTP error"); - }); - - it("should log error if both building HTML and sending email fail", async function () { - mjml2htmlMock.throws(new Error("MJML error")); - nodemailerMock.createTransport().sendMail.rejects(new Error("SMTP error")); - - const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); - - expect(messageId).to.be.undefined; - expect(loggerMock.error.calledTwice).to.be.true; - expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error"); - expect(loggerMock.error.getCall(1).args[0].message).to.equal("SMTP error"); - }); - - it("should log an error if buildHtml fails", async function () {}); -}); diff --git a/server/tests/services/jobQueue.test.js b/server/tests/services/jobQueue.test.js deleted file mode 100755 index 155f9119e..000000000 --- a/server/tests/services/jobQueue.test.js +++ /dev/null @@ -1,813 +0,0 @@ -import sinon from "sinon"; -import JobQueue from "../../service/jobQueue.js"; -import { log } from "console"; - -class QueueStub { - constructor(queueName, options) { - this.queueName = queueName; - this.options = options; - this.workers = []; - this.jobs = []; - } - - // Add any methods that are expected to be called on the Queue instance - add(job) { - this.jobs.push(job); - } - - removeRepeatable(id) { - const removedJob = this.jobs.find((job) => job.data._id === id); - this.jobs = this.jobs.filter((job) => job.data._id !== id); - if (removedJob) { - return true; - } - return false; - } - - getRepeatableJobs() { - return this.jobs; - } - async getJobs() { - return this.jobs; - } - - async pause() { - return true; - } - - async obliterate() { - return true; - } -} - -class WorkerStub { - constructor(QUEUE_NAME, workerTask) { - this.queueName = QUEUE_NAME; - this.workerTask = async () => workerTask({ data: { _id: 1 } }); - } - - async close() { - return true; - } -} - -describe("JobQueue", function () { - let settingsService, logger, db, networkService, statusService, notificationService, jobQueue; - - beforeEach(async function () { - settingsService = { getSettings: sinon.stub() }; - statusService = { updateStatus: sinon.stub() }; - notificationService = { handleNotifications: sinon.stub() }; - - logger = { error: sinon.stub(), info: sinon.stub() }; - db = { - getAllMonitors: sinon.stub().returns([]), - getMaintenanceWindowsByMonitorId: sinon.stub().returns([]), - }; - networkService = { getStatus: sinon.stub() }; - jobQueue = await JobQueue.createJobQueue(db, networkService, statusService, notificationService, settingsService, logger, QueueStub, WorkerStub); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createJobQueue", function () { - it("should create a new JobQueue and add jobs for active monitors", async function () { - db.getAllMonitors.returns([ - { id: 1, isActive: true }, - { id: 2, isActive: true }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - // There should be double the jobs, as one is meant to be instantly executed - // And one is meant to be enqueued - expect(jobQueue.queue.jobs.length).to.equal(4); - }); - - it("should reject with an error if an error occurs", async function () { - db.getAllMonitors.throws("Error"); - try { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("createJobQueue"); - } - }); - - it("should reject with an error if an error occurs, should not overwrite error data", async function () { - const error = new Error("Error"); - error.service = "otherService"; - error.method = "otherMethod"; - db.getAllMonitors.throws(error); - - try { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - } catch (error) { - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("Constructor", function () { - it("should construct a new JobQueue with default port and host if not provided", async function () { - settingsService.getSettings.returns({}); - - expect(jobQueue.connection.host).to.equal("127.0.0.1"); - expect(jobQueue.connection.port).to.equal(6379); - }); - - it("should construct a new JobQueue with provided port and host", async function () { - settingsService.getSettings.returns({ redisHost: "localhost", redisPort: 1234 }); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - expect(jobQueue.connection.host).to.equal("localhost"); - expect(jobQueue.connection.port).to.equal(1234); - }); - }); - - describe("isMaintenanceWindow", function () { - it("should throw an error if error occurs", async function () { - db.getMaintenanceWindowsByMonitorId.throws("Error"); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - try { - jobQueue.isInMaintenanceWindow(1); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("createWorker"); - } - }); - - it("should return true if in maintenance window with no repeat", async function () { - db.getMaintenanceWindowsByMonitorId.returns([ - { - active: true, - start: new Date(Date.now() - 1000).toISOString(), - end: new Date(Date.now() + 1000).toISOString(), - repeat: 0, - }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const inWindow = await jobQueue.isInMaintenanceWindow(1); - expect(inWindow).to.be.true; - }); - - it("should return true if in maintenance window with repeat", async function () { - db.getMaintenanceWindowsByMonitorId.returns([ - { - active: true, - start: new Date(Date.now() - 10000).toISOString(), - end: new Date(Date.now() - 5000).toISOString(), - repeat: 1000, - }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const inWindow = await jobQueue.isInMaintenanceWindow(1); - expect(inWindow).to.be.true; - }); - - it("should return false if in end < start", async function () { - db.getMaintenanceWindowsByMonitorId.returns([ - { - active: true, - start: new Date(Date.now() - 5000).toISOString(), - end: new Date(Date.now() - 10000).toISOString(), - repeat: 1000, - }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const inWindow = await jobQueue.isInMaintenanceWindow(1); - expect(inWindow).to.be.false; - }); - - it("should return false if not in maintenance window", async function () { - db.getMaintenanceWindowsByMonitorId.returns([ - { - active: false, - start: new Date(Date.now() - 5000).toISOString(), - end: new Date(Date.now() - 10000).toISOString(), - repeat: 1000, - }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const inWindow = await jobQueue.isInMaintenanceWindow(1); - expect(inWindow).to.be.false; - }); - }); - - describe("createJobHandler", function () { - it("resolve to an error if an error is thrown within", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().throws("Error"); - try { - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.details).to.equal(`Error processing job 1: Error`); - } - }); - - it("should log info if job is in maintenance window", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().returns(true); - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - expect(logger.info.calledOnce).to.be.true; - expect(logger.info.firstCall.args[0].message).to.equal("Monitor 1 is in maintenance window"); - }); - - it("should return if status has not changed", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); - statusService.updateStatus = sinon.stub().returns({ statusChanged: false }); - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true; - }); - - it("should return if status has changed, but prevStatus was undefined (monitor paused)", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); - statusService.updateStatus = sinon.stub().returns({ statusChanged: true, prevStatus: undefined }); - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true; - }); - - it("should call notification service if status changed and monitor was not paused", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); - statusService.updateStatus = sinon.stub().returns({ statusChanged: true, prevStatus: false }); - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - expect(jobQueue.notificationService.handleNotifications.calledOnce).to.be.true; - }); - }); - - describe("getWorkerStats", function () { - it("should throw an error if getRepeatable Jobs fails", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.queue.getRepeatableJobs = async () => { - throw new Error("Error"); - }; - try { - const stats = await jobQueue.getWorkerStats(); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("getWorkerStats"); - } - }); - - it("should throw an error if getRepeatable Jobs fails but respect existing error data", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.queue.getRepeatableJobs = async () => { - const error = new Error("Existing Error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - try { - await jobQueue.getWorkerStats(); - } catch (error) { - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("scaleWorkers", function () { - it("should scale workers to 5 if no workers", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - expect(jobQueue.workers.length).to.equal(5); - }); - - it("should scale workers up", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.scaleWorkers({ - load: 100, - jobs: Array.from({ length: 100 }, (_, i) => i + 1), - }); - expect(jobQueue.workers.length).to.equal(20); - }); - - it("should scale workers down, even with error of worker.close fails", async function () { - WorkerStub.prototype.close = async () => { - throw new Error("Error"); - }; - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - await jobQueue.scaleWorkers({ - load: 100, - jobs: Array.from({ length: 100 }, (_, i) => i + 1), - }); - - const res = await jobQueue.scaleWorkers({ - load: 0, - jobs: [], - }); - expect(jobQueue.workers.length).to.equal(5); - }); - - it("should scale workers down", async function () { - WorkerStub.prototype.close = async () => { - return true; - }; - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - await jobQueue.scaleWorkers({ - load: 40, - jobs: Array.from({ length: 40 }, (_, i) => i + 1), - }); - - const res = await jobQueue.scaleWorkers({ - load: 0, - jobs: [], - }); - expect(jobQueue.workers.length).to.equal(5); - }); - - it("should return false if scaling doesn't happen", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const res = await jobQueue.scaleWorkers({ load: 5 }); - expect(jobQueue.workers.length).to.equal(5); - expect(res).to.be.false; - }); - }); - - describe("getJobs", function () { - it("should return jobs", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const jobs = await jobQueue.getJobs(); - expect(jobs.length).to.equal(0); - }); - - it("should throw an error if getRepeatableJobs fails", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - try { - jobQueue.queue.getRepeatableJobs = async () => { - throw new Error("error"); - }; - - await jobQueue.getJobs(true); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("getJobs"); - } - }); - - it("should throw an error if getRepeatableJobs fails but respect existing error data", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - try { - jobQueue.queue.getRepeatableJobs = async () => { - const error = new Error("Existing error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - - await jobQueue.getJobs(true); - } catch (error) { - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("getJobStats", function () { - it("should return job stats for no jobs", async function () { - const jobStats = await jobQueue.getJobStats(); - expect(jobStats).to.deep.equal({ jobs: [], workers: 5 }); - }); - - it("should return job stats for jobs", async function () { - jobQueue.queue.getJobs = async () => { - return [{ data: { url: "test" }, getState: async () => "completed" }]; - }; - const jobStats = await jobQueue.getJobStats(); - expect(jobStats).to.deep.equal({ - jobs: [{ url: "test", state: "completed" }], - workers: 5, - }); - }); - - it("should reject with an error if mapping jobs fails", async function () { - jobQueue.queue.getJobs = async () => { - return [ - { - data: { url: "test" }, - getState: async () => { - throw new Error("Mapping Error"); - }, - }, - ]; - }; - try { - await jobQueue.getJobStats(); - } catch (error) { - expect(error.message).to.equal("Mapping Error"); - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("getJobStats"); - } - }); - - it("should reject with an error if mapping jobs fails but respect existing error data", async function () { - jobQueue.queue.getJobs = async () => { - return [ - { - data: { url: "test" }, - getState: async () => { - const error = new Error("Mapping Error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }, - }, - ]; - }; - try { - await jobQueue.getJobStats(); - } catch (error) { - expect(error.message).to.equal("Mapping Error"); - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("addJob", function () { - it("should add a job to the queue", async function () { - jobQueue.addJob("test", { url: "test" }); - expect(jobQueue.queue.jobs.length).to.equal(1); - }); - - it("should reject with an error if adding fails", async function () { - jobQueue.queue.add = async () => { - throw new Error("Error adding job"); - }; - try { - await jobQueue.addJob("test", { url: "test" }); - } catch (error) { - expect(error.message).to.equal("Error adding job"); - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("addJob"); - } - }); - - it("should reject with an error if adding fails but respect existing error data", async function () { - jobQueue.queue.add = async () => { - const error = new Error("Error adding job"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - try { - await jobQueue.addJob("test", { url: "test" }); - } catch (error) { - expect(error.message).to.equal("Error adding job"); - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("deleteJob", function () { - it("should delete a job from the queue", async function () { - jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] }); - jobQueue.scaleWorkers = sinon.stub(); - const monitor = { _id: 1 }; - const job = { data: monitor }; - jobQueue.queue.jobs = [job]; - await jobQueue.deleteJob(monitor); - // expect(jobQueue.queue.jobs.length).to.equal(0); - // expect(logger.info.calledOnce).to.be.true; - // expect(jobQueue.getWorkerStats.calledOnce).to.be.true; - // expect(jobQueue.scaleWorkers.calledOnce).to.be.true; - }); - - it("should log an error if job is not found", async function () { - jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] }); - jobQueue.scaleWorkers = sinon.stub(); - const monitor = { _id: 1 }; - const job = { data: monitor }; - jobQueue.queue.jobs = [job]; - await jobQueue.deleteJob({ id_: 2 }); - expect(logger.error.calledOnce).to.be.true; - }); - - it("should reject with an error if removeRepeatable fails", async function () { - jobQueue.queue.removeRepeatable = async () => { - const error = new Error("removeRepeatable error"); - throw error; - }; - - try { - await jobQueue.deleteJob({ _id: 1 }); - } catch (error) { - expect(error.message).to.equal("removeRepeatable error"); - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("deleteJob"); - } - }); - - it("should reject with an error if removeRepeatable fails but respect existing error data", async function () { - jobQueue.queue.removeRepeatable = async () => { - const error = new Error("removeRepeatable error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - - try { - await jobQueue.deleteJob({ _id: 1 }); - } catch (error) { - expect(error.message).to.equal("removeRepeatable error"); - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("getMetrics", function () { - it("should return metrics for the job queue", async function () { - jobQueue.queue.getWaitingCount = async () => 1; - jobQueue.queue.getActiveCount = async () => 2; - jobQueue.queue.getCompletedCount = async () => 3; - jobQueue.queue.getFailedCount = async () => 4; - jobQueue.queue.getDelayedCount = async () => 5; - jobQueue.queue.getRepeatableJobs = async () => [1, 2, 3]; - const metrics = await jobQueue.getMetrics(); - expect(metrics).to.deep.equal({ - waiting: 1, - active: 2, - completed: 3, - failed: 4, - delayed: 5, - repeatableJobs: 3, - }); - }); - - it("should log an error if metrics operations fail", async function () { - jobQueue.queue.getWaitingCount = async () => { - throw new Error("Error"); - }; - await jobQueue.getMetrics(); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal("Error"); - }); - }); - - describe("obliterate", function () { - it("should return true if obliteration is successful", async function () { - jobQueue.queue.pause = async () => true; - jobQueue.getJobs = async () => [{ key: 1, id: 1 }]; - jobQueue.queue.removeRepeatableByKey = async () => true; - jobQueue.queue.remove = async () => true; - jobQueue.queue.obliterate = async () => true; - const obliteration = await jobQueue.obliterate(); - expect(obliteration).to.be.true; - }); - - it("should throw an error if obliteration fails", async function () { - jobQueue.getMetrics = async () => { - throw new Error("Error"); - }; - - try { - await jobQueue.obliterate(); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("obliterate"); - } - }); - - it("should throw an error if obliteration fails but respect existing error data", async function () { - jobQueue.getMetrics = async () => { - const error = new Error("Error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - - try { - await jobQueue.obliterate(); - } catch (error) { - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); -}); diff --git a/server/tests/services/networkService.test.js b/server/tests/services/networkService.test.js deleted file mode 100755 index cb48ed9b2..000000000 --- a/server/tests/services/networkService.test.js +++ /dev/null @@ -1,439 +0,0 @@ -import sinon from "sinon"; -import NetworkService from "../../service/networkService.js"; -import { expect } from "chai"; -import http from "http"; -import { errorMessages } from "../../utils/messages.js"; -describe("Network Service", function () { - let axios, ping, Docker, logger, networkService; - - beforeEach(function () { - axios = { - get: sinon.stub().resolves({ - data: { foo: "bar" }, - status: 200, - }), - }; - Docker = class { - listContainers = sinon.stub().resolves([ - { - Names: ["http://test.com"], - Id: "http://test.com", - }, - ]); - getContainer = sinon.stub().returns({ - inspect: sinon.stub().resolves({ State: { Status: "running" } }), - }); - }; - ping = { - promise: { - probe: sinon.stub().resolves({ response: { alive: true }, responseTime: 100, alive: true }), - }, - }; - logger = { error: sinon.stub() }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - }); - - describe("constructor", function () { - it("should create a new NetworkService instance", function () { - const networkService = new NetworkService(); - expect(networkService).to.be.an.instanceOf(NetworkService); - }); - }); - - describe("timeRequest", function () { - it("should time an asynchronous operation", async function () { - const operation = sinon.stub().resolves("success"); - const { response, responseTime } = await networkService.timeRequest(operation); - expect(response).to.equal("success"); - expect(responseTime).to.be.a("number"); - }); - - it("should handle errors if operation throws error", async function () { - const error = new Error("Test error"); - const operation = sinon.stub().throws(error); - const { response, responseTime } = await networkService.timeRequest(operation); - expect(response).to.be.null; - expect(responseTime).to.be.a("number"); - expect(error.message).to.equal("Test error"); - }); - }); - - describe("requestPing", function () { - it("should return a response object if ping successful", async function () { - const pingResult = await networkService.requestPing({ - data: { url: "http://test.com", _id: "123" }, - }); - expect(pingResult.monitorId).to.equal("123"); - expect(pingResult.type).to.equal("ping"); - expect(pingResult.responseTime).to.be.a("number"); - expect(pingResult.status).to.be.true; - }); - - it("should return a response object if ping unsuccessful", async function () { - const error = new Error("Test error"); - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const pingResult = await networkService.requestPing({ - data: { url: "http://test.com", _id: "123" }, - }); - expect(pingResult.monitorId).to.equal("123"); - expect(pingResult.type).to.equal("ping"); - expect(pingResult.responseTime).to.be.a("number"); - expect(pingResult.status).to.be.false; - expect(pingResult.code).to.equal(networkService.PING_ERROR); - }); - - it("should throw an error if ping cannot resolve", async function () { - const error = new Error("test error"); - networkService.timeRequest = sinon.stub().throws(error); - try { - await networkService.requestPing({ - data: { url: "http://test.com", _id: "123" }, - }); - } catch (error) { - expect(error).to.exist; - expect(error.method).to.equal("requestPing"); - } - }); - }); - - describe("requestHttp", function () { - it("should return a response object if http successful", async function () { - const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; - const httpResult = await networkService.requestHttp(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("http"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.true; - }); - - it("should return a response object if http unsuccessful", async function () { - const error = new Error("Test error"); - error.response = { status: 404 }; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; - const httpResult = await networkService.requestHttp(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("http"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.false; - expect(httpResult.code).to.equal(404); - }); - - it("should return a response object if http unsuccessful with unknown code", async function () { - const error = new Error("Test error"); - error.response = {}; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; - const httpResult = await networkService.requestHttp(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("http"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.false; - expect(httpResult.code).to.equal(networkService.NETWORK_ERROR); - }); - - it("should throw an error if an error occurs", async function () { - const error = new Error("test error"); - networkService.timeRequest = sinon.stub().throws(error); - try { - await networkService.requestHttp({ - data: { url: "http://test.com", _id: "123" }, - }); - } catch (error) { - expect(error).to.exist; - expect(error.method).to.equal("requestHttp"); - } - }); - }); - - describe("requestPagespeed", function () { - it("should return a response object if pagespeed successful", async function () { - const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; - const pagespeedResult = await networkService.requestPagespeed(job); - expect(pagespeedResult.monitorId).to.equal("123"); - expect(pagespeedResult.type).to.equal("pagespeed"); - expect(pagespeedResult.responseTime).to.be.a("number"); - expect(pagespeedResult.status).to.be.true; - }); - - it("should return a response object if pagespeed unsuccessful", async function () { - const error = new Error("Test error"); - error.response = { status: 404 }; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; - const pagespeedResult = await networkService.requestPagespeed(job); - expect(pagespeedResult.monitorId).to.equal("123"); - expect(pagespeedResult.type).to.equal("pagespeed"); - expect(pagespeedResult.responseTime).to.be.a("number"); - expect(pagespeedResult.status).to.be.false; - expect(pagespeedResult.code).to.equal(404); - }); - - it("should return a response object if pagespeed unsuccessful with an unknown code", async function () { - const error = new Error("Test error"); - error.response = {}; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; - const pagespeedResult = await networkService.requestPagespeed(job); - expect(pagespeedResult.monitorId).to.equal("123"); - expect(pagespeedResult.type).to.equal("pagespeed"); - expect(pagespeedResult.responseTime).to.be.a("number"); - expect(pagespeedResult.status).to.be.false; - expect(pagespeedResult.code).to.equal(networkService.NETWORK_ERROR); - }); - - it("should throw an error if pagespeed cannot resolve", async function () { - const error = new Error("test error"); - networkService.timeRequest = sinon.stub().throws(error); - try { - await networkService.requestPagespeed({ - data: { url: "http://test.com", _id: "123" }, - }); - } catch (error) { - expect(error).to.exist; - expect(error.method).to.equal("requestPagespeed"); - } - }); - }); - - describe("requestHardware", function () { - it("should return a response object if hardware successful", async function () { - const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; - const httpResult = await networkService.requestHardware(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("hardware"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.true; - }); - - it("should return a response object if hardware successful and job has a secret", async function () { - const job = { - data: { - url: "http://test.com", - _id: "123", - type: "hardware", - secret: "my_secret", - }, - }; - const httpResult = await networkService.requestHardware(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("hardware"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.true; - }); - - it("should return a response object if hardware unsuccessful", async function () { - const error = new Error("Test error"); - error.response = { status: 404 }; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; - const httpResult = await networkService.requestHardware(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("hardware"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.false; - expect(httpResult.code).to.equal(404); - }); - - it("should return a response object if hardware unsuccessful with unknown code", async function () { - const error = new Error("Test error"); - error.response = {}; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; - const httpResult = await networkService.requestHardware(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("hardware"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.false; - expect(httpResult.code).to.equal(networkService.NETWORK_ERROR); - }); - - it("should throw an error if hardware cannot resolve", async function () { - const error = new Error("test error"); - networkService.timeRequest = sinon.stub().throws(error); - try { - await networkService.requestHardware({ - data: { url: "http://test.com", _id: "123" }, - }); - } catch (error) { - expect(error).to.exist; - expect(error.method).to.equal("requestHardware"); - } - }); - }); - - describe("requestDocker", function () { - it("should return a response object if docker successful", async function () { - const job = { data: { url: "http://test.com", _id: "123", type: "docker" } }; - const dockerResult = await networkService.requestDocker(job); - expect(dockerResult.monitorId).to.equal("123"); - expect(dockerResult.type).to.equal("docker"); - expect(dockerResult.responseTime).to.be.a("number"); - expect(dockerResult.status).to.be.true; - }); - - it("should return a response object with status false if container not running", async function () { - Docker = class { - listContainers = sinon.stub().resolves([ - { - Names: ["/my_container"], - Id: "abc123", - }, - ]); - getContainer = sinon.stub().returns({ - inspect: sinon.stub().resolves({ State: { Status: "stopped" } }), - }); - }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - const job = { data: { url: "abc123", _id: "123", type: "docker" } }; - const dockerResult = await networkService.requestDocker(job); - expect(dockerResult.status).to.be.false; - expect(dockerResult.code).to.equal(200); - }); - - it("should handle an error when fetching the container", async function () { - Docker = class { - listContainers = sinon.stub().resolves([ - { - Names: ["/my_container"], - Id: "abc123", - }, - ]); - getContainer = sinon.stub().returns({ - inspect: sinon.stub().throws(new Error("test error")), - }); - }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - const job = { data: { url: "abc123", _id: "123", type: "docker" } }; - const dockerResult = await networkService.requestDocker(job); - expect(dockerResult.status).to.be.false; - expect(dockerResult.code).to.equal(networkService.NETWORK_ERROR); - }); - - it("should throw an error if operations fail", async function () { - Docker = class { - listContainers = sinon.stub().resolves([ - { - Names: ["/my_container"], - Id: "abc123", - }, - ]); - getContainer = sinon.stub().throws(new Error("test error")); - }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - const job = { data: { url: "abc123", _id: "123", type: "docker" } }; - try { - await networkService.requestDocker(job); - } catch (error) { - expect(error.message).to.equal("test error"); - } - }); - - it("should throw an error if no matching images found", async function () { - Docker = class { - listContainers = sinon.stub().resolves([]); - getContainer = sinon.stub().throws(new Error("test error")); - }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - const job = { data: { url: "abc123", _id: "123", type: "docker" } }; - try { - await networkService.requestDocker(job); - } catch (error) { - expect(error.message).to.equal(errorMessages.DOCKER_NOT_FOUND); - } - }); - }); - - describe("getStatus", function () { - beforeEach(function () { - networkService.requestPing = sinon.stub(); - networkService.requestHttp = sinon.stub(); - networkService.requestPagespeed = sinon.stub(); - networkService.requestHardware = sinon.stub(); - networkService.requestDocker = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should call requestPing if type is ping", async function () { - await networkService.getStatus({ data: { type: "ping" } }); - expect(networkService.requestPing.calledOnce).to.be.true; - expect(networkService.requestDocker.notCalled).to.be.true; - expect(networkService.requestHttp.notCalled).to.be.true; - expect(networkService.requestPagespeed.notCalled).to.be.true; - }); - - it("should call requestHttp if type is http", async function () { - await networkService.getStatus({ data: { type: "http" } }); - expect(networkService.requestPing.notCalled).to.be.true; - expect(networkService.requestDocker.notCalled).to.be.true; - expect(networkService.requestHttp.calledOnce).to.be.true; - expect(networkService.requestPagespeed.notCalled).to.be.true; - }); - - it("should call requestPagespeed if type is pagespeed", async function () { - await networkService.getStatus({ data: { type: "pagespeed" } }); - expect(networkService.requestPing.notCalled).to.be.true; - expect(networkService.requestDocker.notCalled).to.be.true; - expect(networkService.requestHttp.notCalled).to.be.true; - expect(networkService.requestPagespeed.calledOnce).to.be.true; - }); - - it("should call requestHardware if type is hardware", async function () { - await networkService.getStatus({ data: { type: "hardware" } }); - expect(networkService.requestHardware.calledOnce).to.be.true; - expect(networkService.requestDocker.notCalled).to.be.true; - expect(networkService.requestPing.notCalled).to.be.true; - expect(networkService.requestPagespeed.notCalled).to.be.true; - }); - - it("should call requestDocker if type is Docker", async function () { - await networkService.getStatus({ data: { type: "docker" } }); - expect(networkService.requestDocker.calledOnce).to.be.true; - expect(networkService.requestHardware.notCalled).to.be.true; - expect(networkService.requestPing.notCalled).to.be.true; - expect(networkService.requestPagespeed.notCalled).to.be.true; - }); - - it("should throw an error if an unknown type is provided", async function () { - try { - await networkService.getStatus({ data: { type: "unknown" } }); - } catch (error) { - expect(error.service).to.equal("NetworkService"); - expect(error.method).to.equal("getStatus"); - expect(error.message).to.equal("Unsupported type: unknown"); - } - }); - - it("should throw an error if job type is undefined", async function () { - try { - await networkService.getStatus({ data: { type: undefined } }); - } catch (error) { - expect(error.service).to.equal("NetworkService"); - expect(error.method).to.equal("getStatus"); - expect(error.message).to.equal("Unsupported type: unknown"); - } - }); - - it("should throw an error if job is empty", async function () { - try { - await networkService.getStatus({}); - } catch (error) { - expect(error.method).to.equal("getStatus"); - expect(error.message).to.equal("Unsupported type: unknown"); - } - }); - - it("should throw an error if job is null", async function () { - try { - await networkService.getStatus(null); - } catch (error) { - expect(error.service).to.equal("NetworkService"); - expect(error.method).to.equal("getStatus"); - expect(error.message).to.equal("Unsupported type: unknown"); - } - }); - }); -}); diff --git a/server/tests/services/notificationService.test.js b/server/tests/services/notificationService.test.js deleted file mode 100755 index 211cc8b46..000000000 --- a/server/tests/services/notificationService.test.js +++ /dev/null @@ -1,285 +0,0 @@ -import sinon from "sinon"; -import NotificationService from "../../service/notificationService.js"; -import { expect } from "chai"; - -describe("NotificationService", function () { - let emailService, db, logger, notificationService; - - beforeEach(function () { - db = { - getNotificationsByMonitorId: sinon.stub(), - }; - emailService = { - buildAndSendEmail: sinon.stub(), - }; - logger = { - warn: sinon.stub(), - }; - - notificationService = new NotificationService(emailService, db, logger); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("constructor", function () { - it("should create a new instance of NotificationService", function () { - expect(notificationService).to.be.an.instanceOf(NotificationService); - }); - }); - - describe("sendEmail", function () { - it("should send an email notification with Up Template", async function () { - const networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - status: true, - prevStatus: false, - }; - const address = "test@test.com"; - await notificationService.sendEmail(networkResponse, address); - expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; - expect( - notificationService.emailService.buildAndSendEmail.calledWith("serverIsUpTemplate", { monitor: "Test Monitor", url: "http://test.com" }) - ); - }); - - it("should send an email notification with Down Template", async function () { - const networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - status: false, - prevStatus: true, - }; - const address = "test@test.com"; - await notificationService.sendEmail(networkResponse, address); - expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; - }); - - it("should send an email notification with Up Template", async function () { - const networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - status: true, - prevStatus: false, - }; - const address = "test@test.com"; - await notificationService.sendEmail(networkResponse, address); - expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; - }); - }); - - describe("handleNotifications", function () { - it("should handle notifications based on the network response", async function () { - notificationService.sendEmail = sinon.stub(); - const res = await notificationService.handleNotifications({ - monitor: { - type: "email", - address: "www.google.com", - }, - }); - expect(res).to.be.true; - }); - - it("should handle hardware notifications", async function () { - notificationService.sendEmail = sinon.stub(); - const res = await notificationService.handleNotifications({ - monitor: { - type: "hardware", - address: "www.google.com", - }, - }); - expect(res).to.be.true; - }); - - it("should handle an error when getting notifications", async function () { - const testError = new Error("Test Error"); - notificationService.db.getNotificationsByMonitorId.rejects(testError); - await notificationService.handleNotifications({ monitorId: "123" }); - expect(notificationService.logger.warn.calledOnce).to.be.true; - }); - }); - - describe("sendHardwareEmail", function () { - let networkResponse, address, alerts; - - beforeEach(function () { - networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - status: true, - prevStatus: false, - }; - address = "test@test.com"; - alerts = ["test"]; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should send an email notification with Hardware Template", async function () { - emailService.buildAndSendEmail.resolves(true); - const res = await notificationService.sendHardwareEmail(networkResponse, address, alerts); - expect(res).to.be.true; - }); - - it("should return false if no alerts are provided", async function () { - alerts = []; - emailService.buildAndSendEmail.resolves(true); - const res = await notificationService.sendHardwareEmail(networkResponse, address, alerts); - expect(res).to.be.false; - }); - }); - - describe("handleStatusNotifications", function () { - let networkResponse; - - beforeEach(function () { - networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - statusChanged: true, - status: true, - prevStatus: false, - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should handle status notifications", async function () { - db.getNotificationsByMonitorId.resolves([{ type: "email", address: "test@test.com" }]); - const res = await notificationService.handleStatusNotifications(networkResponse); - expect(res).to.be.true; - }); - - it("should return false if status hasn't changed", async function () { - networkResponse.statusChanged = false; - const res = await notificationService.handleStatusNotifications(networkResponse); - expect(res).to.be.false; - }); - - it("should return false if prevStatus is undefined", async function () { - networkResponse.prevStatus = undefined; - const res = await notificationService.handleStatusNotifications(networkResponse); - expect(res).to.be.false; - }); - - it("should handle an error", async function () { - const testError = new Error("Test Error"); - db.getNotificationsByMonitorId.rejects(testError); - try { - await notificationService.handleStatusNotifications(networkResponse); - } catch (error) { - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.equal("Test Error"); - } - }); - }); - - describe("handleHardwareNotifications", function () { - let networkResponse; - - beforeEach(function () { - networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - thresholds: { - usage_cpu: 1, - usage_memory: 1, - usage_disk: 1, - }, - }, - payload: { - data: { - cpu: { - usage_percent: 0.655, - }, - memory: { - usage_percent: 0.783, - }, - disk: [ - { - name: "/dev/sda1", - usage_percent: 0.452, - }, - { - name: "/dev/sdb1", - usage_percent: 0.627, - }, - ], - }, - }, - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("it should return false if no thresholds are set", function () { - it("should return false if no thresholds are set", async function () { - networkResponse.monitor.thresholds = undefined; - const res = await notificationService.handleHardwareNotifications(networkResponse); - expect(res).to.be.false; - }); - - it("should return false if metrics are null", async function () { - networkResponse.payload.data = null; - const res = await notificationService.handleHardwareNotifications(networkResponse); - expect(res).to.be.false; - }); - - it("should return true if request is well formed and thresholds > 0", async function () { - db.getNotificationsByMonitorId.resolves([ - { - type: "email", - address: "test@test.com", - alertThreshold: 1, - cpuAlertThreshold: 1, - memoryAlertThreshold: 1, - diskAlertThreshold: 1, - save: sinon.stub().resolves(), - }, - ]); - const res = await notificationService.handleHardwareNotifications(networkResponse); - expect(res).to.be.true; - }); - - it("should return true if thresholds are exceeded", async function () { - db.getNotificationsByMonitorId.resolves([ - { - type: "email", - address: "test@test.com", - alertThreshold: 1, - cpuAlertThreshold: 1, - memoryAlertThreshold: 1, - diskAlertThreshold: 1, - save: sinon.stub().resolves(), - }, - ]); - networkResponse.monitor.thresholds = { - usage_cpu: 0.01, - usage_memory: 0.01, - usage_disk: 0.01, - }; - const res = await notificationService.handleHardwareNotifications(networkResponse); - expect(res).to.be.true; - }); - }); - }); -}); diff --git a/server/tests/services/settingsService.test.js b/server/tests/services/settingsService.test.js deleted file mode 100755 index 6c9960e5e..000000000 --- a/server/tests/services/settingsService.test.js +++ /dev/null @@ -1,140 +0,0 @@ -import sinon from "sinon"; -import SettingsService from "../../service/settingsService.js"; -import { expect } from "chai"; -import NetworkService from "../../service/networkService.js"; -const SERVICE_NAME = "SettingsService"; - -describe("SettingsService", function () { - let sandbox, mockAppSettings; - - beforeEach(function () { - sandbox = sinon.createSandbox(); - sandbox.stub(process.env, "CLIENT_HOST").value("http://localhost"); - sandbox.stub(process.env, "JWT_SECRET").value("secret"); - sandbox.stub(process.env, "REFRESH_TOKEN_SECRET").value("refreshSecret"); - sandbox.stub(process.env, "DB_TYPE").value("postgres"); - sandbox.stub(process.env, "DB_CONNECTION_STRING").value("postgres://user:pass@localhost/db"); - sandbox.stub(process.env, "REDIS_HOST").value("localhost"); - sandbox.stub(process.env, "REDIS_PORT").value("6379"); - sandbox.stub(process.env, "TOKEN_TTL").value("3600"); - sandbox.stub(process.env, "REFRESH_TOKEN_TTL").value("86400"); - sandbox.stub(process.env, "PAGESPEED_API_KEY").value("apiKey"); - sandbox.stub(process.env, "SYSTEM_EMAIL_HOST").value("smtp.mailtrap.io"); - sandbox.stub(process.env, "SYSTEM_EMAIL_PORT").value("2525"); - sandbox.stub(process.env, "SYSTEM_EMAIL_ADDRESS").value("test@example.com"); - sandbox.stub(process.env, "SYSTEM_EMAIL_PASSWORD").value("password"); - }); - - mockAppSettings = { - settingOne: 123, - settingTwo: 456, - }; - - afterEach(function () { - sandbox.restore(); - sinon.restore(); - }); - - describe("constructor", function () { - it("should construct a new SettingsService", function () { - const settingsService = new SettingsService(mockAppSettings); - expect(settingsService.appSettings).to.equal(mockAppSettings); - }); - }); - - describe("loadSettings", function () { - it("should load settings from DB when environment variables are not set", async function () { - const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; - const appSettings = { findOne: sinon.stub().returns(dbSettings) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = {}; - const result = await settingsService.loadSettings(); - expect(result).to.deep.equal(dbSettings); - }); - - it("should throw an error if settings are not found", async function () { - const appSettings = { findOne: sinon.stub().returns(null) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = null; - - try { - await settingsService.loadSettings(); - } catch (error) { - expect(error.message).to.equal("Settings not found"); - expect(error.service).to.equal(SERVICE_NAME); - expect(error.method).to.equal("loadSettings"); - } - }); - - it("should add its method and service name to error if not present", async function () { - const appSettings = { findOne: sinon.stub().throws(new Error("Test error")) }; - const settingsService = new SettingsService(appSettings); - try { - await settingsService.loadSettings(); - } catch (error) { - expect(error.message).to.equal("Test error"); - expect(error.service).to.equal(SERVICE_NAME); - expect(error.method).to.equal("loadSettings"); - } - }); - - it("should not add its method and service name to error if present", async function () { - const error = new Error("Test error"); - error.method = "otherMethod"; - error.service = "OTHER_SERVICE"; - const appSettings = { findOne: sinon.stub().throws(error) }; - const settingsService = new SettingsService(appSettings); - try { - await settingsService.loadSettings(); - } catch (error) { - expect(error.message).to.equal("Test error"); - expect(error.service).to.equal("OTHER_SERVICE"); - expect(error.method).to.equal("otherMethod"); - } - }); - - it("should merge DB settings with environment variables", async function () { - const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; - const appSettings = { findOne: sinon.stub().returns(dbSettings) }; - const settingsService = new SettingsService(appSettings); - const result = await settingsService.loadSettings(); - expect(result).to.deep.equal(settingsService.settings); - expect(settingsService.settings.logLevel).to.equal("debug"); - expect(settingsService.settings.apiBaseUrl).to.equal("http://localhost"); - }); - }); - - describe("reloadSettings", function () { - it("should call loadSettings", async function () { - const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; - const appSettings = { findOne: sinon.stub().returns(dbSettings) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = {}; - const result = await settingsService.reloadSettings(); - expect(result).to.deep.equal(dbSettings); - }); - }); - - describe("getSettings", function () { - it("should return the current settings", function () { - const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; - const appSettings = { findOne: sinon.stub().returns(dbSettings) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = dbSettings; - const result = settingsService.getSettings(); - expect(result).to.deep.equal(dbSettings); - }); - - it("should throw an error if settings have not been loaded", function () { - const appSettings = { findOne: sinon.stub().returns(null) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = null; - - try { - settingsService.getSettings(); - } catch (error) { - expect(error.message).to.equal("Settings have not been loaded"); - } - }); - }); -}); diff --git a/server/tests/services/statusService.test.js b/server/tests/services/statusService.test.js deleted file mode 100755 index 289a36884..000000000 --- a/server/tests/services/statusService.test.js +++ /dev/null @@ -1,255 +0,0 @@ -import sinon from "sinon"; -import StatusService from "../../service/statusService.js"; -import { afterEach, describe } from "node:test"; - -describe("StatusService", () => { - let db, logger, statusService; - - beforeEach(function () { - db = { - getMonitorById: sinon.stub(), - createCheck: sinon.stub(), - createPagespeedCheck: sinon.stub(), - }; - logger = { - info: sinon.stub(), - error: sinon.stub(), - }; - statusService = new StatusService(db, logger); - }); - - afterEach(() => { - sinon.restore(); - }); - - describe("constructor", () => { - it("should create an instance of StatusService", function () { - expect(statusService).to.be.an.instanceOf(StatusService); - }); - }); - - describe("getStatusString", () => { - it("should return 'up' if status is true", function () { - expect(statusService.getStatusString(true)).to.equal("up"); - }); - - it("should return 'down' if status is false", function () { - expect(statusService.getStatusString(false)).to.equal("down"); - }); - - it("should return 'unknown' if status is undefined or null", function () { - expect(statusService.getStatusString(undefined)).to.equal("unknown"); - }); - }); - - describe("updateStatus", () => { - beforeEach(function () { - // statusService.insertCheck = sinon.stub().resolves; - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should throw an error if an error occurs", async function () { - const error = new Error("Test error"); - statusService.db.getMonitorById = sinon.stub().throws(error); - try { - await statusService.updateStatus({ monitorId: "test", status: true }); - } catch (error) { - expect(error.message).to.equal("Test error"); - } - // expect(statusService.insertCheck.calledOnce).to.be.true; - }); - - it("should return {statusChanged: false} if status hasn't changed", async function () { - statusService.db.getMonitorById = sinon.stub().returns({ status: true }); - const result = await statusService.updateStatus({ - monitorId: "test", - status: true, - }); - expect(result).to.deep.equal({ statusChanged: false }); - // expect(statusService.insertCheck.calledOnce).to.be.true; - }); - - it("should return {statusChanged: true} if status has changed from down to up", async function () { - statusService.db.getMonitorById = sinon.stub().returns({ status: false, save: sinon.stub() }); - const result = await statusService.updateStatus({ - monitorId: "test", - status: true, - }); - expect(result.statusChanged).to.be.true; - expect(result.monitor.status).to.be.true; - expect(result.prevStatus).to.be.false; - // expect(statusService.insertCheck.calledOnce).to.be.true; - }); - - it("should return {statusChanged: true} if status has changed from up to down", async function () { - statusService.db.getMonitorById = sinon.stub().returns({ status: true, save: sinon.stub() }); - const result = await statusService.updateStatus({ - monitorId: "test", - status: false, - }); - expect(result.statusChanged).to.be.true; - expect(result.monitor.status).to.be.false; - expect(result.prevStatus).to.be.true; - // expect(statusService.insertCheck.calledOnce).to.be.true; - }); - }); - - describe("buildCheck", () => { - it("should build a check object", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "test", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: { test: "test" }, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - }); - - it("should build a check object for pagespeed type", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "pagespeed", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: { - lighthouseResult: { - categories: { - accessibility: { score: 1 }, - "best-practices": { score: 1 }, - performance: { score: 1 }, - seo: { score: 1 }, - }, - audits: { - "cumulative-layout-shift": { score: 1 }, - "speed-index": { score: 1 }, - "first-contentful-paint": { score: 1 }, - "largest-contentful-paint": { score: 1 }, - "total-blocking-time": { score: 1 }, - }, - }, - }, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - expect(check.accessibility).to.equal(100); - expect(check.bestPractices).to.equal(100); - expect(check.performance).to.equal(100); - expect(check.seo).to.equal(100); - expect(check.audits).to.deep.equal({ - cls: { score: 1 }, - si: { score: 1 }, - fcp: { score: 1 }, - lcp: { score: 1 }, - tbt: { score: 1 }, - }); - }); - - it("should build a check object for pagespeed type with missing data", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "pagespeed", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: { - lighthouseResult: { - categories: {}, - audits: {}, - }, - }, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - expect(check.accessibility).to.equal(0); - expect(check.bestPractices).to.equal(0); - expect(check.performance).to.equal(0); - expect(check.seo).to.equal(0); - expect(check.audits).to.deep.equal({ - cls: 0, - si: 0, - fcp: 0, - lcp: 0, - tbt: 0, - }); - }); - - it("should build a check for hardware type", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "hardware", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: { data: { cpu: "cpu", memory: "memory", disk: "disk", host: "host" } }, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - expect(check.cpu).to.equal("cpu"); - expect(check.memory).to.equal("memory"); - expect(check.disk).to.equal("disk"); - expect(check.host).to.equal("host"); - }); - - it("should build a check for hardware type with missing data", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "hardware", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: {}, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - expect(check.cpu).to.deep.equal({}); - expect(check.memory).to.deep.equal({}); - expect(check.disk).to.deep.equal({}); - expect(check.host).to.deep.equal({}); - }); - }); - - describe("insertCheck", () => { - it("should log an error if one is thrown", async function () { - const testError = new Error("Test error"); - statusService.db.createCheck = sinon.stub().throws(testError); - try { - await statusService.insertCheck({ monitorId: "test" }); - } catch (error) { - expect(error.message).to.equal(testError.message); - } - expect(statusService.logger.error.calledOnce).to.be.true; - }); - - it("should insert a check into the database", async function () { - await statusService.insertCheck({ monitorId: "test", type: "http" }); - expect(statusService.db.createCheck.calledOnce).to.be.true; - }); - }); -}); diff --git a/server/tests/utils/dataUtils.test.js b/server/tests/utils/dataUtils.test.js deleted file mode 100755 index ec9eabb40..000000000 --- a/server/tests/utils/dataUtils.test.js +++ /dev/null @@ -1,61 +0,0 @@ -import { NormalizeData, calculatePercentile } from "../../utils/dataUtils.js"; -import sinon from "sinon"; - -describe("NormalizeData", function () { - it("should normalize response times when checks length is greater than 1", function () { - const checks = [ - { responseTime: 20, _doc: { id: 1 } }, - { responseTime: 40, _doc: { id: 2 } }, - { responseTime: 60, _doc: { id: 3 } }, - ]; - const rangeMin = 1; - const rangeMax = 100; - - const result = NormalizeData(checks, rangeMin, rangeMax); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(3); - result.forEach((check) => { - expect(check).to.have.property("responseTime").that.is.a("number"); - expect(check).to.have.property("originalResponseTime").that.is.a("number"); - }); - }); - - it("should return checks with original response times when checks length is 1", function () { - const checks = [{ responseTime: 20, _doc: { id: 1 } }]; - const rangeMin = 1; - const rangeMax = 100; - - const result = NormalizeData(checks, rangeMin, rangeMax); - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(1); - expect(result[0]).to.have.property("originalResponseTime", 20); - }); - - it("should handle edge cases with extreme response times", function () { - const checks = [ - { responseTime: 5, _doc: { id: 1 } }, - { responseTime: 95, _doc: { id: 2 } }, - ]; - const rangeMin = 1; - const rangeMax = 100; - - const result = NormalizeData(checks, rangeMin, rangeMax); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(2); - expect(result[0]).to.have.property("responseTime").that.is.at.least(rangeMin); - expect(result[1]).to.have.property("responseTime").that.is.at.most(rangeMax); - }); -}); - -describe("calculatePercentile", function () { - it("should return the lower value when upper is greater than or equal to the length of the sorted array", function () { - const checks = [{ responseTime: 10 }, { responseTime: 20 }, { responseTime: 30 }, { responseTime: 40 }, { responseTime: 50 }]; - - const percentile = 100; - const result = calculatePercentile(checks, percentile); - const expected = 50; - expect(result).to.equal(expected); - }); -}); diff --git a/server/tests/utils/imageProcessing.test.js b/server/tests/utils/imageProcessing.test.js deleted file mode 100755 index 09330c317..000000000 --- a/server/tests/utils/imageProcessing.test.js +++ /dev/null @@ -1,53 +0,0 @@ -import { expect } from "chai"; -import sinon from "sinon"; -import sharp from "sharp"; -import { GenerateAvatarImage } from "../../utils/imageProcessing.js"; - -describe("imageProcessing - GenerateAvatarImage", function () { - it("should resize the image to 64x64 and return a base64 string", async function () { - const file = { - buffer: Buffer.from("test image buffer"), - }; - - // Stub the sharp function - const toBufferStub = sinon.stub().resolves(Buffer.from("resized image buffer")); - const resizeStub = sinon.stub().returns({ toBuffer: toBufferStub }); - const sharpStub = sinon.stub(sharp.prototype, "resize").returns({ toBuffer: toBufferStub }); - - const result = await GenerateAvatarImage(file); - - // Verify the result - const expected = Buffer.from("resized image buffer").toString("base64"); - expect(result).to.equal(expected); - - // Verify that the sharp function was called with the correct arguments - expect(sharpStub.calledOnceWith({ width: 64, height: 64, fit: "cover" })).to.be.true; - expect(toBufferStub.calledOnce).to.be.true; - - // Restore the stubbed functions - sharpStub.restore(); - }); - - it("should throw an error if resizing fails", async function () { - const file = { - buffer: Buffer.from("test image buffer"), - }; - - // Stub the sharp function to throw an error - const toBufferStub = sinon.stub().rejects(new Error("Resizing failed")); - const resizeStub = sinon.stub().returns({ toBuffer: toBufferStub }); - const sharpStub = sinon.stub(sharp.prototype, "resize").returns({ toBuffer: toBufferStub }); - - try { - await GenerateAvatarImage(file); - // If no error is thrown, fail the test - expect.fail("Expected error to be thrown"); - } catch (error) { - // Verify that the error message is correct - expect(error.message).to.equal("Resizing failed"); - } - - // Restore the stubbed functions - sharpStub.restore(); - }); -}); diff --git a/server/tests/utils/logger.test.js b/server/tests/utils/logger.test.js deleted file mode 100755 index 2d5bd82a9..000000000 --- a/server/tests/utils/logger.test.js +++ /dev/null @@ -1,139 +0,0 @@ -import sinon from "sinon"; -import logger from "../../utils/logger.js"; -import { Logger } from "../../utils/logger.js"; -import winston from "winston"; - -describe("Logger", function () { - let infoStub, warnStub, errorStub; - - beforeEach(function () { - infoStub = sinon.stub(logger.logger, "info"); - warnStub = sinon.stub(logger.logger, "warn"); - errorStub = sinon.stub(logger.logger, "error"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("constructor", function () { - let createLoggerStub; - - beforeEach(function () { - createLoggerStub = sinon.stub(winston, "createLogger"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should convert message to JSON string if it is an object", function () { - const logMessage = { key: "value" }; - const expectedMessage = JSON.stringify(logMessage, null, 2); - - createLoggerStub.callsFake((config) => { - const consoleTransport = config.transports[0]; - const logEntry = { - level: "info", - message: logMessage, - timestamp: new Date().toISOString(), - }; - const formattedMessage = consoleTransport.format.transform(logEntry); - expect(formattedMessage).to.include(expectedMessage); - return { log: sinon.spy() }; - }); - - const logger = new Logger(); - logger.logger.info(logMessage); - }); - - it("should convert details to JSON string if it is an object", function () { - const logDetails = { key: "value" }; - const expectedDetails = JSON.stringify(logDetails, null, 2); // Removed .s - - createLoggerStub.callsFake((config) => { - const consoleTransport = config.transports[0]; - const logEntry = { - level: "info", - message: "", // Add empty message since it's required - details: logDetails, - timestamp: new Date().toISOString(), - }; - const formattedMessage = consoleTransport.format.transform(logEntry); - expect(formattedMessage).to.include(expectedDetails); - return { info: sinon.spy() }; // Changed to return info method - }); - - const logger = new Logger(); - logger.logger.info("", { details: logDetails }); // Updated to pass details properly - }); - }); - - describe("info", function () { - it("should log an informational message", function () { - const config = { - message: "Info message", - service: "TestService", - method: "TestMethod", - details: { key: "value" }, - }; - - logger.info(config); - - expect(infoStub.calledOnce).to.be.true; - expect( - infoStub.calledWith(config.message, { - service: config.service, - method: config.method, - details: config.details, - }) - ).to.be.true; - }); - }); - - describe("warn", function () { - it("should log a warning message", function () { - const config = { - message: "Warning message", - service: "TestService", - method: "TestMethod", - details: { key: "value" }, - }; - - logger.warn(config); - - expect(warnStub.calledOnce).to.be.true; - expect( - warnStub.calledWith(config.message, { - service: config.service, - method: config.method, - details: config.details, - }) - ).to.be.true; - }); - }); - - describe("error", function () { - it("should log an error message", function () { - const config = { - message: "Error message", - service: "TestService", - method: "TestMethod", - details: { key: "value" }, - stack: "Error stack trace", - }; - - logger.error(config); - - expect(errorStub.calledOnce).to.be.true; - expect( - errorStub.calledWith(config.message, { - service: config.service, - method: config.method, - details: config.details, - stack: config.stack, - }) - ).to.be.true; - }); - }); -}); diff --git a/server/tests/utils/messages.test.js b/server/tests/utils/messages.test.js deleted file mode 100755 index a7e98c477..000000000 --- a/server/tests/utils/messages.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import { errorMessages, successMessages } from "../../utils/messages.js"; -describe("Messages", function () { - describe("messages - errorMessages", function () { - it("should have a DB_FIND_MONITOR_BY_ID function", function () { - const monitorId = "12345"; - expect(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)).to.equal(`Monitor with id ${monitorId} not found`); - }); - - it("should have a DB_DELETE_CHECKS function", function () { - const monitorId = "12345"; - expect(errorMessages.DB_DELETE_CHECKS(monitorId)).to.equal(`No checks found for monitor with id ${monitorId}`); - }); - }); - - describe("messages - successMessages", function () { - it("should have a MONITOR_GET_BY_USER_ID function", function () { - const userId = "12345"; - expect(successMessages.MONITOR_GET_BY_USER_ID(userId)).to.equal(`Got monitor for ${userId} successfully"`); - }); - - // Add more tests for other success messages as needed - }); -}); diff --git a/server/tests/utils/utils.test.js b/server/tests/utils/utils.test.js deleted file mode 100755 index 2630bb8e6..000000000 --- a/server/tests/utils/utils.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { ParseBoolean, getTokenFromHeaders } from "../../utils/utils.js"; - -describe("utils - ParseBoolean", function () { - it("should return true", function () { - const result = ParseBoolean("true"); - expect(result).to.be.true; - }); - - it("should return false", function () { - const result = ParseBoolean("false"); - expect(result).to.be.false; - }); - - it("should return false", function () { - const result = ParseBoolean(null); - expect(result).to.be.false; - }); - - it("should return false", function () { - const result = ParseBoolean(undefined); - expect(result).to.be.false; - }); -}); - -describe("utils - getTokenFromHeaders", function () { - it("should throw an error if authorization header is missing", function () { - const headers = {}; - expect(() => getTokenFromHeaders(headers)).to.throw("No auth headers"); - }); - - it("should throw an error if authorization header does not start with Bearer", function () { - const headers = { authorization: "Basic abcdef" }; - expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers"); - }); - - it("should return the token if authorization header is correctly formatted", function () { - const headers = { authorization: "Bearer abcdef" }; - expect(getTokenFromHeaders(headers)).to.equal("abcdef"); - }); - - it("should throw an error if authorization header has more than two parts", function () { - const headers = { authorization: "Bearer abc def" }; - expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers"); - }); - - it("should throw an error if authorization header has less than two parts", function () { - const headers = { authorization: "Bearer" }; - expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers"); - }); -}); diff --git a/server/tsconfig.jest.json b/server/tsconfig.jest.json new file mode 100644 index 000000000..80c321cf6 --- /dev/null +++ b/server/tsconfig.jest.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "types": ["jest"], + "noEmit": true, + "resolveJsonModule": true + }, + "include": ["src", "test"] +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 000000000..bdb3a7b0e --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "allowJs": true, + "checkJs": false, + "paths": { + "@/*": ["./src/*"] + }, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "isolatedModules": true + }, + "include": ["src"], + "exclude": ["tests", "dist", "node_modules"] +}