diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..7f80b8533 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +* text eol=lf \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 16273e9d5..9c9ba0c65 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,27 +1,28 @@ -**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)** +**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)** ## Describe your changes -Briefly describe the changes you made and their purpose. +Briefly describe the changes you made and their purpose. ## Write your issue number after "Fixes " -Fixes #123 +Fixes #123 ## Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks. - [ ] (Do not skip this or your PR will be closed) I deployed the application locally. -- [ ] (Do not skip this or your PR will be closed) I have performed a self-review and testing of my code. +- [ ] (Do not skip this or your PR will be closed) I have performed a self-reviewing and testing of my code. - [ ] I have included the issue # in the PR. -- [ ] I have added i18n support to visible strings (instead of `
Add
`, use): +- [ ] I have added i18n support to visible strings (instead of `
Add
`, use): + ```Javascript const { t } = useTranslation();
{t('add')}
``` + - [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed - [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). - [ ] I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions. - [ ] My PR is granular and targeted to one specific feature. - [ ] I ran `npm run format` in server and client directories, which automatically formats your code. - [ ] I took a screenshot or a video and attached to this PR if there is a UI change. - diff --git a/.github/workflows/check-tests.yml b/.github/workflows/check-tests.yml new file mode 100644 index 000000000..8d0688eb2 --- /dev/null +++ b/.github/workflows/check-tests.yml @@ -0,0 +1,23 @@ +name: Test Check (Server) +on: + pull_request: + workflow_dispatch: + +jobs: + test-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: Run server tests + working-directory: server + run: npm test diff --git a/.github/workflows/poeditor-sync.yml b/.github/workflows/poeditor-sync.yml index c9bb92b72..6b6731b2d 100644 --- a/.github/workflows/poeditor-sync.yml +++ b/.github/workflows/poeditor-sync.yml @@ -1,120 +1,138 @@ -name: POEditor Translation Synchronization +name: POEditor Translation Synchronization (disabled) + +# This workflow is intentionally disabled — kept for reference only. +# The full definition is preserved commented-out below. To re-enable, +# delete the stub job and uncomment the section starting at `on:`. on: - # For manual triggering - workflow_dispatch: - inputs: - languages: - description: "Languages to synchronize (comma separated, e.g.: tr,en,es)" - required: false - default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th" - format: - description: "Export format (key_value_json or json)" - required: false - default: "key_value_json" - -permissions: - contents: write - pull-requests: write + workflow_dispatch: jobs: - sync-translations: - runs-on: ubuntu-latest - permissions: - contents: write - issues: write - pull-requests: write + disabled: + if: false + runs-on: ubuntu-latest + steps: + - run: echo "This workflow is disabled. See comments below for the original definition." - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - fetch-depth: 0 +# --------------------------------------------------------------------------- +# Original (disabled) workflow below. Do not uncomment if you don't know what you're doing. +# --------------------------------------------------------------------------- - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: "22" +# on: +# # For manual triggering +# workflow_dispatch: +# inputs: +# languages: +# description: "Languages to synchronize (comma separated, e.g.: tr,en,es)" +# required: false +# default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th" +# format: +# description: "Export format (key_value_json or json)" +# required: false +# default: "key_value_json" - - name: Create package.json for scripts - run: | - mkdir -p .github/scripts - cat > .github/scripts/package.json << EOF - { - "name": "poeditor-scripts", - "version": "1.0.0", - "private": true, - "type": "module", - "dependencies": { - "axios": "^1.6.0", - "fs-extra": "^11.1.1" - } - } - EOF +# permissions: +# contents: write +# pull-requests: write - - name: Install dependencies - run: | - cd .github/scripts - npm install +# jobs: +# sync-translations: +# runs-on: ubuntu-latest +# permissions: +# contents: write +# issues: write +# pull-requests: write - - name: Download translations from POEditor - env: - POEDITOR_API: ${{ secrets.POEDITOR_API }} - POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }} - LANGUAGES: ${{ github.event.inputs.languages || 'tr,en' }} - EXPORT_FORMAT: ${{ github.event.inputs.format || 'key_value_json' }} - run: | - mkdir -p temp - node .github/scripts/download-translations.js +# steps: +# - name: Checkout code +# uses: actions/checkout@v3 +# with: +# token: ${{ secrets.GITHUB_TOKEN }} +# fetch-depth: 0 - - name: Verify translation files - run: | - echo "Verifying translation files..." - for file in temp/*.json; do - echo "Checking $file" - if [ ! -s "$file" ]; then - echo "Error: $file is empty or does not exist" - exit 1 - fi - # Validate JSON format - cat "$file" | jq . > /dev/null || { echo "Error: $file is not valid JSON"; exit 1; } - done - echo "All translation files are valid" +# - name: Setup Node.js +# uses: actions/setup-node@v3 +# with: +# node-version: "22" - - name: Copy translations to project - run: | - mkdir -p client/src/locales - cp -r temp/* client/src/locales/ - echo "Translation files copied to client/src/locales/" +# - name: Create package.json for scripts +# run: | +# mkdir -p .github/scripts +# cat > .github/scripts/package.json << EOF +# { +# "name": "poeditor-scripts", +# "version": "1.0.0", +# "private": true, +# "type": "module", +# "dependencies": { +# "axios": "^1.6.0", +# "fs-extra": "^11.1.1" +# } +# } +# EOF - - name: Get current date - id: date - run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT +# - name: Install dependencies +# run: | +# cd .github/scripts +# npm install - - name: Format client code - run: | - cd client - npm ci - npm run format +# - name: Download translations from POEditor +# env: +# POEDITOR_API: ${{ secrets.POEDITOR_API }} +# POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }} +# LANGUAGES: ${{ github.event.inputs.languages || 'tr,en' }} +# EXPORT_FORMAT: ${{ github.event.inputs.format || 'key_value_json' }} +# run: | +# mkdir -p temp +# node .github/scripts/download-translations.js - - name: Create Pull Request - uses: peter-evans/create-pull-request@v7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "feat: translations updated from POEditor" - title: "🌐 Updated Translations from POEditor" - body: | - This PR contains the latest translations from POEditor. +# - name: Verify translation files +# run: | +# echo "Verifying translation files..." +# for file in temp/*.json; do +# echo "Checking $file" +# if [ ! -s "$file" ]; then +# echo "Error: $file is empty or does not exist" +# exit 1 +# fi +# # Validate JSON format +# cat "$file" | jq . > /dev/null || { echo "Error: $file is not valid JSON"; exit 1; } +# done +# echo "All translation files are valid" - 📅 Update Date: ${{ steps.date.outputs.date }} - 🔄 Updated Languages: ${{ github.event.inputs.languages || 'tr,en' }} +# - name: Copy translations to project +# run: | +# mkdir -p client/src/locales +# cp -r temp/* client/src/locales/ +# echo "Translation files copied to client/src/locales/" - ⚠️ Please review the translations and approve the PR if everything looks correct. - branch: translation-update-${{ github.run_number }} - delete-branch: true - base: develop - add-paths: | - client/src/locales/*.json - committer: GitHub Action - author: GitHub Action +# - name: Get current date +# id: date +# run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT + +# - name: Format client code +# run: | +# cd client +# npm ci +# npm run format + +# - name: Create Pull Request +# uses: peter-evans/create-pull-request@v7 +# with: +# token: ${{ secrets.GITHUB_TOKEN }} +# commit-message: "feat: translations updated from POEditor" +# title: "🌐 Updated Translations from POEditor" +# body: | +# This PR contains the latest translations from POEditor. + +# 📅 Update Date: ${{ steps.date.outputs.date }} +# 🔄 Updated Languages: ${{ github.event.inputs.languages || 'tr,en' }} + +# ⚠️ Please review the translations and approve the PR if everything looks correct. +# branch: translation-update-${{ github.run_number }} +# delete-branch: true +# base: develop +# add-paths: | +# client/src/locales/*.json +# committer: GitHub Action +# author: GitHub Action diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml index 532197710..a42efc4a4 100644 --- a/.github/workflows/production-deploy.yml +++ b/.github/workflows/production-deploy.yml @@ -2,7 +2,7 @@ name: Demo deploy on: push: - branches: ["demo"] + branches: [ "demo" ] workflow_dispatch: jobs: docker-build-and-push-client: @@ -84,7 +84,8 @@ jobs: script: | cd checkmate git pull - cd docker/prod + cd docker/unified docker compose down docker compose pull docker compose up -d + docker system prune -af diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index 84be4e483..e7983f249 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -2,7 +2,7 @@ name: Staging deploy on: push: - branches: ["develop"] + branches: [ "develop" ] jobs: docker-build-and-push-client: @@ -78,13 +78,13 @@ jobs: - name: SSH into server and restart container using Docker Compose uses: appleboy/ssh-action@v1.2.2 with: - host: ${{ secrets.STAGING_SERVER_HOST }} - username: ${{ secrets.STAGING_SERVER_USER }} - key: ${{ secrets.STAGING_SERVER_SSH_KEY }} + host: ${{ secrets.DEMO_SERVER_HOST }} + username: ${{ secrets.DEMO_SERVER_USER }} + key: ${{ secrets.DEMO_SERVER_SSH_KEY }} script: | cd checkmate git pull - cd docker/staging + cd docker/unified docker compose down docker compose pull docker compose up -d diff --git a/CLAUDE.md b/CLAUDE.md index 1535e84cf..9af1a6742 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,6 +105,56 @@ client/src/ - **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling) - **i18n**: i18next + react-i18next (translations via PoEditor) +--- + +## Backend Architecture Patterns + +### Repository Pattern & Separation of Concerns + +The backend enforces a strict three-layer separation between HTTP handling, business logic, and data access: + +``` +Request → Controller → Service → Repository → MongoDB (Mongoose) +``` + +- **Controllers** (`/controllers`) handle HTTP concerns only: parsing request params, calling the appropriate service, and returning a response via the `responseHandler` middleware. They contain no business logic. +- **Services** (`/service/business`) contain all business logic: deciding whether an incident should be created, whether a notification should fire, what state a monitor is in, etc. +- **Repositories** (`/repositories`) are the sole layer that talks to MongoDB through Mongoose. They expose clean, reusable query methods (e.g. `findByMonitorId`, `createCheck`) so that services never construct raw DB queries directly. + +This separation makes each layer independently testable and keeps Mongoose-specific code out of business logic. When adding a new feature, the pattern to follow is: add a repository method for any new DB query, call it from a service, and expose it via a controller route. + +### Monitoring Flow: From Check to Notification + +Background monitoring runs on a scheduled queue, not on the HTTP request cycle. The high-level flow for uptime monitoring is: + +``` +Pulse (cron) → BullMQ Job → StatusService + ├── performs HTTP/port/ping check + ├── saves Check via CheckRepository + ├── evaluates monitor state change + │ └── calls IncidentService (create / resolve incident) + └── calls NotificationService (email, Slack, Discord, webhook) +``` + +1. **Pulse** (cron scheduler) enqueues a job into a **BullMQ** queue for each active monitor at its configured interval. +2. A **BullMQ worker** picks up the job and calls `StatusService`, which performs the actual check (HTTP request, TCP port probe, ping, etc.). +3. The result is persisted as a `Check` document via the repository layer. +4. `StatusService` compares the new result against the monitor's previous state. If the monitor transitions from up → down (or down → up), it delegates to `IncidentService` to open or resolve an `Incident` document. +5. On a state change, `NotificationService` reads the monitor's configured `Notification` documents and dispatches alerts to all enabled channels (email, Discord, Slack, webhooks). + +### Queue System (BullMQ + Redis) + +Redis serves two roles: job queue storage for BullMQ and ephemeral caching. BullMQ manages concurrency, retries, and backpressure for monitoring jobs, ensuring checks are processed reliably even under load. + +- Each monitor type (HTTP, port, ping, infrastructure) maps to its own queue worker so failures in one type don't block others. +- Job scheduling interval is driven by the `interval` field on the `Monitor` model. +- Failed jobs are retried with configurable backoff before being moved to a dead-letter state. +- Redis is also used to cache frequently read data (e.g. aggregated stats) to reduce MongoDB query pressure. + +When working on anything related to check scheduling, incident lifecycle, or notifications, trace the flow starting from the relevant BullMQ worker rather than from the controller layer. + +--- + ## Code Conventions ### Internationalization @@ -142,4 +192,4 @@ Key Mongoose models in `/server/src/db/models/`: - **StatusPage** - Public status pages - **Notification** - Alert configuration (email, Discord, Slack, webhooks) - **MaintenanceWindow** - Scheduled maintenance periods -- **AppSettings** - Global application settings +- **AppSettings** - Global application settings \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 5421ce769..015470a27 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,69 +1,69 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in the **bluewave-uptime** project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community, especially during Hacktoberfest, where we encourage new contributors to join and feel supported. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our community include: - -- Demonstrating empathy and kindness toward other people -- Being respectful of differing opinions, viewpoints, and experiences -- Giving and gracefully accepting constructive feedback -- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -- Focusing on what is best not just for us as individuals, but for the overall community - -Examples of unacceptable behavior include: - -- The use of sexualized language or imagery, and sexual attention or advances of any kind -- Trolling, insulting or derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or email address, without their explicit permission -- Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, especially during Hacktoberfest to ensure a positive experience for all contributors. - -## Scope - -This Code of Conduct applies within all project spaces, and it also applies when an individual is officially representing the project or its community in public spaces. Examples of representing our project include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@bluewavelabs.ca. All complaints will be reviewed and investigated promptly and fairly. - -All project team members are obligated to respect the privacy and security of the reporter of any incident. - -## Enforcement Guidelines - -Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. -**Consequence**: A private, written warning from project maintainers, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. - -### 2. Warning -**Community Impact**: A violation through a single incident or series of actions. -**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. - -### 3. Temporary Ban -**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. -**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban -**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. -**Consequence**: A permanent ban from any sort of public interaction within the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - ---- - -By following this Code of Conduct, we can ensure a welcoming and inclusive environment, encouraging new contributors to engage with the project and the community. Thank you for helping make **bluewave-uptime** a positive space for all! +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in the **checkmate** project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community, especially during Hacktoberfest, where we encourage new contributors to join and feel supported. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, especially during Hacktoberfest to ensure a positive experience for all contributors. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when an individual is officially representing the project or its community in public spaces. Examples of representing our project include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@bluewavelabs.ca. All complaints will be reviewed and investigated promptly and fairly. + +All project team members are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. +**Consequence**: A private, written warning from project maintainers, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning +**Community Impact**: A violation through a single incident or series of actions. +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +--- + +By following this Code of Conduct, we can ensure a welcoming and inclusive environment, encouraging new contributors to engage with the project and the community. Thank you for helping make **checkmate** a positive space for all! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 656523f88..38ca4eeb2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ PS: **We work closely with contributors on our [Discord channel](https://discord Before you dive in, make sure you have these installed: ```bash -# Check Node.js (v16+ required) +# Check Node.js (v20+ required) node --version # Check npm @@ -96,10 +96,16 @@ cd docker/dev Build the Docker images: +Linux: ```bash ./build_images.sh ``` +Windows: +```bash +./build_images.bat +``` + Run MongoDB container: ```bash diff --git a/PULLREQUESTS.md b/PULLREQUESTS.md index d1ee8eea9..71f3e7e0a 100644 --- a/PULLREQUESTS.md +++ b/PULLREQUESTS.md @@ -33,4 +33,4 @@ Ensure you execute `npm run format` before submitting your pull requests in both ### Keep PRs focused -It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a big or work on another feature. +It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a bug or work on another feature. diff --git a/README.es.md b/README.es.md index 21909b2ed..e57efc2e4 100644 --- a/README.es.md +++ b/README.es.md @@ -76,7 +76,7 @@ También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servido Si tienes alguna pregunta, sugerencia o comentario, tienes varias opciones: - [Canal de Discord](https://discord.gg/NAb6H3UTjK) -- [Discusiones en GitHub](https://github.com/bluewave-labs/bluewave-uptime/discussions) +- [Discusiones en GitHub](https://github.com/bluewave-labs/checkmate/discussions) - [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/) ¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti! diff --git a/README.md b/README.md index 07c0e84bd..0402ea5f0 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@

An open source uptime and infrastructure monitoring application

+[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=checkmate) + image This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time. @@ -29,7 +31,6 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul - [📦 Demo](#demo) - [🔗 User's guide](#users-guide) - [🛠️ Installation](#installation) -- [🏁 Translations](#translations) - [🚀 Performance](#performance) - [💚 Questions & Ideas](#questions--ideas) - [🧩 Features](#features) @@ -42,17 +43,23 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul ## Demo -You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel). +You can see the latest build of [Checkmate](https://demo.checkmate.so/) in action. + +The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel). ## User's guide -Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :) +Usage instructions can be found [here](https://checkmate.so/docs). + +## Prerequisites +- [Docker](https://www.docker.com/) installed +- [Git](https://git-scm.com/) installed ## Installation -See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). +See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs). -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. +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), [Cloudzy](https://cloudzy.com/marketplace/checkmate) 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 @@ -60,11 +67,6 @@ If you need to monitor internal HTTPS endpoints with certificates from private C For more documentation, see the [docs directory](./docs/). - -## Translations - -If you would like to use Checkmate in your language, please [go to this page](https://poeditor.com/join/project/lRUoGZFCsJ) and register for the language you would like to translate Checkmate to. - ## Performance Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute: @@ -80,7 +82,7 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb If you have any questions, suggestions or comments, you have several options: - [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred) -- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time) +- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (we check here from time to time) Feel free to ask questions or share your ideas - we'd love to hear from you! @@ -112,6 +114,15 @@ Feel free to ask questions or share your ideas - we'd love to hear from you! If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features). +## Monitor Lifecycle + +1. A monitor executes a check (HTTP / ping / port / hardware via Capture agent) +2. The result is stored (success/failure + response time) +3. Recent check results are evaluated against the monitor's configured status change threshold +4. If the monitor's status change threshold is met and the current status is not equal to the previous status, the monitor's state changes (e.g. `initializing`, `up`, `down`, `breached`) +5. Upon a state change: an incident is either created or resolved, depending on the monitor's current status +6. Notifications are triggered based on configuration + ## Screenshots

@@ -157,27 +168,20 @@ Here's how you can contribute: 0. Star this repo :) 1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag. -2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview). -3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. -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). +2. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. +3. Open an issue if you believe you've encountered a bug. +4. Check for good-first-issue's if you are a newcomer. +5. Make a pull request to add new features/make quality-of-life improvements/fix bugs. +6. 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). -[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date) +[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/Checkmate&Date) ## Our sponsors Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features). - -Also check other developer and contributor-friendly projects of BlueWave: - -- [VerifyWise](https://github.com/bluewave-labs/verifywise), the first open source AI governance platform. -- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), an secure file sharing application, aka dataroom. -- [Guidefox](https://github.com/bluewave-labs/guidefox), an application that helps new users learn how to use your product via hints, tours, popups and banners. diff --git a/client/package-lock.json b/client/package-lock.json index 9cf2824ae..7462d95d7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -83,7 +83,6 @@ "node_modules/@babel/core": { "version": "7.29.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -354,7 +353,6 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -392,7 +390,6 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1162,7 +1159,6 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1267,7 +1263,6 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2308,7 +2303,6 @@ "version": "24.5.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2324,7 +2318,6 @@ "node_modules/@types/react": { "version": "18.3.27", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2446,7 +2439,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2771,7 +2763,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3203,8 +3194,7 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -3586,7 +3576,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4331,7 +4320,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5115,7 +5103,6 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -5763,7 +5750,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5775,7 +5761,6 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5975,8 +5960,7 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6122,11 +6106,10 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.59.0", + "version": "4.57.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6915,7 +6898,6 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7051,7 +7033,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/scripts/migrate-locales.ts b/client/scripts/migrate-locales.ts new file mode 100644 index 000000000..721321882 --- /dev/null +++ b/client/scripts/migrate-locales.ts @@ -0,0 +1,258 @@ +/** + * Locale Migration Script + * + * Restructures all non-English locale files to match en.json's nested key structure. + * Recovers translations via safe key-matching strategies, uses English placeholders + * for unmatched keys. + * + * Usage: npx tsx client/scripts/migrate-locales.ts + * + * SAFETY: en.json is NEVER modified. Originals are backed up before overwriting. + */ + +import fs from "fs"; +import path from "path"; + +const LOCALES_DIR = path.resolve(import.meta.dirname, "../src/locales"); +const BACKUP_DIR = path.join(LOCALES_DIR, "backup"); +const REPORT_PATH = path.join(LOCALES_DIR, "migration-report.json"); + +// ── Helpers ────────────────────────────────────────────────────────────────── + +/** Flatten a nested object into { "a.b.c": value } pairs */ +function flattenKeys(obj: Record, prefix = ""): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + const fullPath = prefix ? `${prefix}.${key}` : key; + if (typeof value === "object" && value !== null && !Array.isArray(value)) { + Object.assign(result, flattenKeys(value as Record, fullPath)); + } else { + result[fullPath] = String(value); + } + } + return result; +} + +/** Set a value at a dot-separated path in a nested object */ +function setNestedValue( + obj: Record, + keyPath: string, + value: string +): void { + const parts = keyPath.split("."); + let current = obj; + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!(part in current) || typeof current[part] !== "object") { + current[part] = {}; + } + current = current[part] as Record; + } + current[parts[parts.length - 1]] = value; +} + +// ── Build mapping indexes from en.json ─────────────────────────────────────── + +function buildIndexes(enFlat: Record) { + const enKeys = Object.keys(enFlat); + + // last segment -> en key paths + const lastSegIndex: Record = {}; + // last 2 segments -> en key paths + const suffixIndex: Record = {}; + + for (const keyPath of enKeys) { + const parts = keyPath.split("."); + const lastSeg = parts[parts.length - 1].toLowerCase(); + if (!lastSegIndex[lastSeg]) lastSegIndex[lastSeg] = []; + lastSegIndex[lastSeg].push(keyPath); + + if (parts.length >= 2) { + const suffix = parts.slice(-2).join(".").toLowerCase(); + if (!suffixIndex[suffix]) suffixIndex[suffix] = []; + suffixIndex[suffix].push(keyPath); + } + } + + return { lastSegIndex, suffixIndex }; +} + +// ── Mapping logic ──────────────────────────────────────────────────────────── + +type MatchStrategy = "exact" | "suffix-2" | "unique-name" | "english-placeholder"; + +interface KeyMatch { + enKey: string; + value: string; + strategy: MatchStrategy; +} + +function buildMigratedLocale( + enFlat: Record, + langFlat: Record, + indexes: ReturnType +): { matches: KeyMatch[]; result: Record } { + const { lastSegIndex, suffixIndex } = indexes; + const matches: KeyMatch[] = []; + const result: Record = {}; + + // Build reverse lookup: for each orphan key in lang, index by last segment and suffix + const langByLastSeg: Record = {}; + const langBySuffix: Record = {}; + + for (const [langKey, langValue] of Object.entries(langFlat)) { + const parts = langKey.split("."); + const lastSeg = parts[parts.length - 1].toLowerCase(); + if (!langByLastSeg[lastSeg]) langByLastSeg[lastSeg] = []; + langByLastSeg[lastSeg].push({ key: langKey, value: langValue }); + + if (parts.length >= 2) { + const suffix = parts.slice(-2).join(".").toLowerCase(); + if (!langBySuffix[suffix]) langBySuffix[suffix] = []; + langBySuffix[suffix].push({ key: langKey, value: langValue }); + } + } + + for (const enKey of Object.keys(enFlat)) { + const enValue = enFlat[enKey]; + const parts = enKey.split("."); + const lastSeg = parts[parts.length - 1].toLowerCase(); + const suffix = parts.length >= 2 ? parts.slice(-2).join(".").toLowerCase() : null; + + // Strategy 1: Exact path match + if (langFlat[enKey] !== undefined) { + const value = langFlat[enKey]; + setNestedValue(result, enKey, value); + matches.push({ enKey, value, strategy: "exact" }); + continue; + } + + // Strategy 2: 2-segment suffix match (lang key suffix matches en key suffix, both unique) + if (suffix) { + const enCandidates = suffixIndex[suffix]; + const langCandidates = langBySuffix[suffix]; + if ( + enCandidates && + enCandidates.length === 1 && + langCandidates && + langCandidates.length === 1 + ) { + const value = langCandidates[0].value; + setNestedValue(result, enKey, value); + matches.push({ enKey, value, strategy: "suffix-2" }); + continue; + } + } + + // Strategy 3: Unique last-segment match + const enCandidates = lastSegIndex[lastSeg]; + const langCandidates = langByLastSeg[lastSeg]; + if ( + enCandidates && + enCandidates.length === 1 && + langCandidates && + langCandidates.length === 1 + ) { + const value = langCandidates[0].value; + setNestedValue(result, enKey, value); + matches.push({ enKey, value, strategy: "unique-name" }); + continue; + } + + // No match — use English placeholder + setNestedValue(result, enKey, enValue); + matches.push({ enKey, value: enValue, strategy: "english-placeholder" }); + } + + return { matches, result }; +} + +// ── Main ───────────────────────────────────────────────────────────────────── + +function main() { + // Load en.json + const enPath = path.join(LOCALES_DIR, "en.json"); + const en = JSON.parse(fs.readFileSync(enPath, "utf8")); + const enFlat = flattenKeys(en); + const enKeyCount = Object.keys(enFlat).length; + const indexes = buildIndexes(enFlat); + + console.log(`English: ${enKeyCount} keys (source of truth — NOT modified)\n`); + + // Find all non-English locale files + const localeFiles = fs + .readdirSync(LOCALES_DIR) + .filter( + (f: string) => + f.endsWith(".json") && f !== "en.json" && f !== "migration-report.json" + ); + + if (localeFiles.length === 0) { + console.log("No locale files to migrate."); + return; + } + + // Create backup directory + if (!fs.existsSync(BACKUP_DIR)) { + fs.mkdirSync(BACKUP_DIR, { recursive: true }); + } + + const report: Record< + string, + { + total: number; + exact: number; + suffix: number; + uniqueName: number; + placeholder: number; + } + > = {}; + + for (const file of localeFiles) { + const lang = file.replace(".json", ""); + const filePath = path.join(LOCALES_DIR, file); + + // Backup original + const backupPath = path.join(BACKUP_DIR, file); + fs.copyFileSync(filePath, backupPath); + + // Load and flatten + const langData = JSON.parse(fs.readFileSync(filePath, "utf8")); + const langFlat = flattenKeys(langData); + + // Migrate + const { matches, result } = buildMigratedLocale(enFlat, langFlat, indexes); + + // Write migrated file + fs.writeFileSync(filePath, JSON.stringify(result, null, "\t") + "\n", "utf8"); + + // Compute stats + const stats = { + total: enKeyCount, + exact: matches.filter((m) => m.strategy === "exact").length, + suffix: matches.filter((m) => m.strategy === "suffix-2").length, + uniqueName: matches.filter((m) => m.strategy === "unique-name").length, + placeholder: matches.filter((m) => m.strategy === "english-placeholder").length, + }; + report[lang] = stats; + + const translated = stats.exact + stats.suffix + stats.uniqueName; + const pct = ((translated / enKeyCount) * 100).toFixed(1); + console.log( + `${lang.padEnd(8)} ${String(translated).padStart(4)} translated (${pct}%) | ` + + `exact: ${stats.exact} suffix: ${stats.suffix} name: ${stats.uniqueName} ` + + `placeholder: ${stats.placeholder}` + ); + } + + // Write report + fs.writeFileSync(REPORT_PATH, JSON.stringify(report, null, "\t") + "\n", "utf8"); + + const totalLangs = localeFiles.length; + console.log( + `\nMigrated ${totalLangs} locale files. Originals backed up to locales/backup/` + ); + console.log(`Report written to ${REPORT_PATH}`); +} + +main(); diff --git a/client/src/Components/common/charts/HeatmapResponseTimeTooltip.tsx b/client/src/Components/common/charts/HeatmapResponseTimeTooltip.tsx index c046e05b5..8bc651c8c 100644 --- a/client/src/Components/common/charts/HeatmapResponseTimeTooltip.tsx +++ b/client/src/Components/common/charts/HeatmapResponseTimeTooltip.tsx @@ -57,7 +57,8 @@ export const HeatmapResponseTimeTooltip = ({ {formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} - {t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms + {t("common.labels.responseTime")}:{" "} + {check?.originalResponseTime?.toFixed() ?? "N/A"} ms Status:{" "} diff --git a/client/src/Components/inputs/Slider.tsx b/client/src/Components/inputs/Slider.tsx index 83037ab54..a17fc8a10 100644 --- a/client/src/Components/inputs/Slider.tsx +++ b/client/src/Components/inputs/Slider.tsx @@ -11,20 +11,26 @@ import type { ResponsiveStyleValue } from "@mui/system"; interface SliderInputProps extends SliderProps { sx?: SliderProps["sx"]; showValue?: boolean; + formatDisplayValue?: (value: number) => string; } export const SliderInput = forwardRef( - ({ sx, showValue = false, ...props }, ref) => { + ({ sx, showValue = false, formatDisplayValue, ...props }, ref) => { const theme = useTheme(); const additionalSx = Array.isArray(sx) ? sx : sx ? [sx] : []; + const displayValue = + showValue && formatDisplayValue && typeof props.value === "number" + ? formatDisplayValue(props.value) + : props.value; + return ( - {showValue && {props.value}} + {showValue && {displayValue}} string; sliderMaxWidth?: ResponsiveStyleValue; } export const SliderWithLabel = forwardRef( ( - { fieldLabel, required, showValue = true, value, sliderMaxWidth = "100%", ...props }, + { + fieldLabel, + required, + showValue = true, + formatDisplayValue, + value, + sliderMaxWidth = "100%", + ...props + }, ref ) => { const theme = useTheme(); @@ -82,6 +97,7 @@ export const SliderWithLabel = forwardRef diff --git a/client/src/Components/monitors/MonitorStatBoxes.tsx b/client/src/Components/monitors/MonitorStatBoxes.tsx index 044bb71ab..ab6160603 100644 --- a/client/src/Components/monitors/MonitorStatBoxes.tsx +++ b/client/src/Components/monitors/MonitorStatBoxes.tsx @@ -25,10 +25,7 @@ export const MonitorStatBoxes = ({ } const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0; - const timeSinceLastFailure = - timeOfLastFailure > 0 - ? Date.now() - timeOfLastFailure - : Date.now() - new Date(monitorStats?.createdAt || 0).getTime(); + const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure : 0; // Determine time since last check const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0; @@ -42,6 +39,12 @@ export const MonitorStatBoxes = ({ const streakTime = prettyMilliseconds(timeSinceLastFailure, options); const lastCheckTime = prettyMilliseconds(timeSinceLastCheck, options); + const isActive = + monitor?.status === "up" || + monitor?.status === "paused" || + monitor?.status === "maintenance" || + monitor?.status === "initializing" || + monitor?.status === "breached"; const palette = getStatusPalette(monitor?.status); return ( @@ -51,8 +54,12 @@ export const MonitorStatBoxes = ({ > { return useMemo(() => { - const defaults = - data?.type === "matrix" - ? { - type: "matrix" as const, - notificationName: data.notificationName || "", - homeserverUrl: data.homeserverUrl || "", - roomId: data.roomId || "", - accessToken: data.accessToken || "", - } - : { - type: (data?.type || "email") as Exclude, - notificationName: data?.notificationName || "", - address: data?.address || "", - }; - + const defaults = buildDefaults(data); return { schema: notificationSchema, defaults }; }, [data]); }; diff --git a/client/src/Hooks/useSettingsForm.ts b/client/src/Hooks/useSettingsForm.ts index 3af8c229e..4b0768d79 100644 --- a/client/src/Hooks/useSettingsForm.ts +++ b/client/src/Hooks/useSettingsForm.ts @@ -1,14 +1,14 @@ import { useMemo } from "react"; import { settingsSchema } from "@/Validation/settings"; import type { Settings } from "@/Types/Settings"; -import type { SettingsFormData } from "@/Validation/settings"; +import type { SettingsFormInput } from "@/Validation/settings"; interface UseSettingsFormOptions { data?: Settings | null; } export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => { return useMemo(() => { - const defaults: SettingsFormData = { + const defaults: SettingsFormInput = { systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false, systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false, systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true, diff --git a/client/src/Hooks/useStatusPageForm.ts b/client/src/Hooks/useStatusPageForm.ts index a079095cc..0011ea761 100644 --- a/client/src/Hooks/useStatusPageForm.ts +++ b/client/src/Hooks/useStatusPageForm.ts @@ -27,12 +27,14 @@ export const useStatusPageForm = ({ companyName: data?.companyName || "", url: data?.url || generateDefaultUrl(), timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + type: data?.type || ["uptime"], color: data?.color || "#4169E1", monitors: data?.monitors || [], isPublished: data?.isPublished ?? false, showCharts: data?.showCharts ?? true, showUptimePercentage: data?.showUptimePercentage ?? true, showAdminLoginLink: data?.showAdminLoginLink ?? false, + showInfrastructure: data?.showInfrastructure ?? false, customCSS: data?.customCSS || "", logo: transformLogo(data?.logo), }; diff --git a/client/src/Pages/Account/components/AddTeamMemberDialog.tsx b/client/src/Pages/Account/components/AddTeamMemberDialog.tsx index 14765eb79..e584d3235 100644 --- a/client/src/Pages/Account/components/AddTeamMemberDialog.tsx +++ b/client/src/Pages/Account/components/AddTeamMemberDialog.tsx @@ -43,6 +43,7 @@ export const AddTeamMemberDialog = ({ const roleOptions: { value: UserRole; label: string }[] = [ { value: "admin", label: t("common.auth.roles.admin") }, { value: "user", label: t("common.auth.roles.user") }, + { value: "demo", label: t("common.auth.roles.demo") }, ]; const handleClose = () => { diff --git a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx index e7c42a1ac..cf72e3d24 100644 --- a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx +++ b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx @@ -48,6 +48,7 @@ export const InviteTeamMemberDialog = ({ const roleOptions: { value: UserRole; label: string }[] = [ { value: "admin", label: t("common.auth.roles.admin") }, { value: "user", label: t("common.auth.roles.user") }, + { value: "demo", label: t("common.auth.roles.demo") }, ]; const handleGenerateToken = async (data: InviteFormData) => { diff --git a/client/src/Pages/Auth/Login/index.tsx b/client/src/Pages/Auth/Login/index.tsx index ec95a0ce3..9de5f1df5 100644 --- a/client/src/Pages/Auth/Login/index.tsx +++ b/client/src/Pages/Auth/Login/index.tsx @@ -1,6 +1,7 @@ import { BaseAuthPage, TextLink } from "@/Components/design-elements"; import { Button, TextField } from "@/Components/inputs"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; @@ -9,7 +10,7 @@ import type { LoginFormData } from "@/Validation/login"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; import { setAuthState } from "@/Features/Auth/authSlice"; -import { usePost } from "@/Hooks/UseApi"; +import { useLazyGet, usePost } from "@/Hooks/UseApi"; const LoginPage = () => { const { t } = useTranslation(); @@ -17,6 +18,17 @@ const LoginPage = () => { const navigate = useNavigate(); const { post, loading } = usePost(); + const [isCheckingAdmin, setIsCheckingAdmin] = useState(true); + const { get } = useLazyGet(); + + useEffect(() => { + get("/auth/users/superadmin").then((res) => { + if (res?.data === false) navigate("/register", { replace: true }); + else setIsCheckingAdmin(false); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const { schema, defaults } = useLoginForm(); const { control, handleSubmit } = useForm({ @@ -24,6 +36,8 @@ const LoginPage = () => { defaultValues: defaults, }); + if (isCheckingAdmin) return null; + const onSubmit = async (data: LoginFormData) => { if (loading) return; @@ -82,12 +96,6 @@ const LoginPage = () => { linkText={t("pages.auth.login.links.forgotPassword.linkText")} href="/forgot-password" /> - ); }; diff --git a/client/src/Pages/Auth/Register/index.tsx b/client/src/Pages/Auth/Register/index.tsx index a2a439023..71603058f 100644 --- a/client/src/Pages/Auth/Register/index.tsx +++ b/client/src/Pages/Auth/Register/index.tsx @@ -1,16 +1,17 @@ import { BaseAuthPage } from "@/Components/design-elements"; import { Button, TextField } from "@/Components/inputs"; +import Alert from "@mui/material/Alert"; import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js"; import { useRegisterForm } from "@/Hooks/useRegisterForm"; import type { RegisterFormData } from "@/Validation/register"; import { useTranslation } from "react-i18next"; -import { usePost, useGet } from "@/Hooks/UseApi"; +import { useLazyGet, usePost } from "@/Hooks/UseApi"; import { setAuthState } from "@/Features/Auth/authSlice"; import { useDispatch } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; import type { AuthResponse } from "@/Types/User"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; interface RegisterPayload { user: Omit; @@ -32,21 +33,23 @@ const RegisterPage = () => { const { post: verifyToken } = usePost<{ token: string }, InviteVerifyResponse>(); const hasVerified = useRef(false); - const { data: superAdminExists, isLoading: isCheckingAdmin } = useGet( - token ? null : "/auth/users/superadmin" - ); + const [isCheckingAdmin, setIsCheckingAdmin] = useState(!token); + const { get } = useLazyGet(); + + useEffect(() => { + if (token) return; + get("/auth/users/superadmin").then((res) => { + if (res?.data === true) navigate("/login", { replace: true }); + else setIsCheckingAdmin(false); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const { control, handleSubmit, setError, reset } = useForm({ resolver: zodResolver(schema), defaultValues: defaults, }); - useEffect(() => { - if (superAdminExists === true) { - navigate("/login", { replace: true }); - } - }, [superAdminExists, navigate]); - useEffect(() => { if (!token || hasVerified.current) return; hasVerified.current = true; @@ -92,6 +95,7 @@ const RegisterPage = () => { title={t("pages.auth.register.title")} subtitle={t("pages.auth.register.subtitle")} > + {!token && {t("pages.auth.register.setupNotice")}} { > { /> } /> - {watchedType !== "matrix" && ( + {watchedType !== "matrix" && + watchedType !== "telegram" && + watchedType !== "pushover" && + watchedType !== "twilio" && ( + ( + + )} + /> + } + /> + )} + {watchedType === "telegram" && ( ( - - )} - /> + + ( + + )} + /> + ( + + )} + /> + + } + /> + )} + {watchedType === "pushover" && ( + + ( + + )} + /> + ( + + )} + /> + + } + /> + )} + {watchedType === "twilio" && ( + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + } /> )} @@ -180,7 +354,7 @@ const NotificationsCreatePage = () => { ( { ( { ( { // Initialize form with schema and defaults const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings }); - const form = useForm({ + const form = useForm({ resolver: zodResolver(schema), defaultValues: defaults, mode: "onChange", @@ -255,28 +256,7 @@ export const SettingsPage = () => { delete (dataToSend as any).systemEmailPassword; } - // Convert undefined to empty string for backend unsetting - const processedData = Object.entries(dataToSend).reduce((acc, [key, value]) => { - const typedKey = key as keyof SettingsFormData; - if (value === undefined) { - (acc as any)[typedKey] = ""; - } else if (typeof value === "object" && value !== null) { - // Handle nested objects like globalThresholds - // Convert 0 to "" for unsetting thresholds - (acc as any)[typedKey] = Object.entries(value).reduce( - (nested, [nestedKey, nestedValue]) => ({ - ...nested, - [nestedKey]: nestedValue === undefined ? "" : nestedValue, - }), - {} - ); - } else { - (acc as any)[typedKey] = value; - } - return acc; - }, {} as Partial); - - const result = await patch("/settings", processedData as SettingsFormData); + const result = await patch("/settings", dataToSend as SettingsFormData); if (result?.success) { // Update API key state from response @@ -462,6 +442,43 @@ export const SettingsPage = () => { /> )} + {/* Check Retention */} + {isAdmin && ( + ( + field.onChange(value)} + valueLabelDisplay="auto" + valueLabelFormat={(value: number) => + value >= CHECK_TTL_SENTINEL + ? t("pages.settings.form.retention.option.days.unlimited") + : `${value}` + } + formatDisplayValue={(value: number) => + value >= CHECK_TTL_SENTINEL + ? t("pages.settings.form.retention.option.days.unlimited") + : `${value}` + } + /> + )} + /> + } + /> + )} + {/* Global Thresholds */} {isAdmin && ( { + const params = new URLSearchParams(); + ["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach( + (type) => params.append("type", type) + ); + return `/monitors/team?${params.toString()}`; +})(); + interface TimezoneOption { _id: string; name: string; @@ -49,11 +58,11 @@ const CreateStatusPage = () => { // Fetch existing status page data when configuring const { data: statusPageData, isLoading: isLoadingStatusPage } = - useGet(isCreate ? null : `/status-page/${url}?type=uptime`); + useGet( + isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure` + ); - const { data: monitorsResponse } = useGet( - "/monitors/team?type=http&type=ping&type=port&type=docker" - ); + const { data: monitorsResponse } = useGet(monitorsUrl); const monitors = monitorsResponse ?? []; const { post, loading: isSubmittingPost } = usePost(); @@ -81,6 +90,24 @@ const CreateStatusPage = () => { reset(defaults); }, [defaults, reset]); + const watchedMonitorIds: string[] = form.watch("monitors") ?? []; + const computedTypes: MonitorDisplayType[] = useMemo(() => { + const selectedMonitors = (watchedMonitorIds ?? []) + .map((id) => monitors.find((m) => m.id === id)) + .filter((m): m is Monitor => m !== undefined); + + const typesSet = new Set(); + selectedMonitors.forEach((m) => { + typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime"); + }); + + return typesSet.size ? Array.from(typesSet) : ["uptime"]; + }, [watchedMonitorIds, monitors]); + + useEffect(() => { + form.setValue("type", computedTypes); + }, [computedTypes]); + const onError = (errors: any) => { logger.debug("Status page validation errors", errors); }; @@ -103,7 +130,6 @@ const CreateStatusPage = () => { const onSubmit = async (data: StatusPageFormData) => { const fd = new FormData(); - fd.append("type", "uptime"); fd.append("isPublished", String(data.isPublished)); if (data.companyName) fd.append("companyName", data.companyName); if (data.url) fd.append("url", data.url); @@ -112,11 +138,16 @@ const CreateStatusPage = () => { fd.append("showCharts", String(data.showCharts)); fd.append("showUptimePercentage", String(data.showUptimePercentage)); fd.append("showAdminLoginLink", String(data.showAdminLoginLink)); + fd.append("showInfrastructure", String(data.showInfrastructure)); data.monitors.forEach((monitorId) => { fd.append("monitors[]", monitorId); }); + data.type.forEach((type) => { + fd.append("type[]", type); + }); + // Handle logo upload if (data.logo === null) { // Signal to remove the logo @@ -303,7 +334,9 @@ const CreateStatusPage = () => { }} > - {monitor.name} + {`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`} { @@ -425,6 +458,23 @@ const CreateStatusPage = () => { /> )} /> + ( + + } + label={t( + "pages.statusPages.form.features.option.showInfrastructure.label" + )} + /> + )} + /> {/* { + const theme = useTheme(); + return ( + + + {label} + + {value} + + ); +}; + +interface MetricItemProps { + label: string; + progress: number; + details?: MetricDetail[]; +} + +const MetricItem = ({ label, progress, details }: MetricItemProps) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + return ( + + + + {label} + + {details && details.length > 0 && ( + + {details.map((detail) => ( + + ))} + + )} + + ); +}; + +type LatestCheck = NonNullable[number]; + +const buildCpuMetric = ( + check: LatestCheck, + t: (key: string) => string +): MetricConfig | null => { + if (!check.cpu || typeof check.cpu.usage_percent !== "number") { + return null; + } + const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER; + return { + key: "cpu", + label: t("pages.statusPages.monitorsList.infrastructure.cpu"), + hasData: true, + progress: usagePercent, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.usage"), + value: `${usagePercent.toFixed(2)}%`, + }, + ], + }; +}; + +const buildMemoryMetric = ( + check: LatestCheck, + t: (key: string) => string +): MetricConfig | null => { + if ( + !check.memory || + typeof check.memory.usage_percent !== "number" || + typeof check.memory.used_bytes !== "number" || + typeof check.memory.total_bytes !== "number" + ) { + return null; + } + return { + key: "memory", + label: t("pages.statusPages.monitorsList.infrastructure.memory"), + hasData: true, + progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.used"), + value: prettyBytes(check.memory.used_bytes ?? 0), + }, + { + label: t("pages.statusPages.monitorsList.infrastructure.total"), + value: prettyBytes(check.memory.total_bytes ?? 0), + }, + ], + }; +}; + +const buildDiskMetric = ( + check: LatestCheck, + t: (key: string) => string +): MetricConfig | null => { + if (!check.disk || check.disk.length === 0) { + return null; + } + const disks = check.disk; + const avgUsagePercent = + (disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) * + PERCENTAGE_MULTIPLIER; + const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0); + const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0); + + return { + key: "disk", + label: t("pages.statusPages.monitorsList.infrastructure.disk"), + hasData: true, + progress: avgUsagePercent, + details: [ + { + label: t("pages.statusPages.monitorsList.infrastructure.used"), + value: prettyBytes(totalUsedBytes), + }, + { + label: t("pages.statusPages.monitorsList.infrastructure.total"), + value: prettyBytes(totalTotalBytes), + }, + ], + }; +}; + +export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0]; + + if (!latestCheck) { + return ( + + {t("pages.statusPages.monitorsList.noData")} + + ); + } + + const metrics: MetricConfig[] = [ + buildCpuMetric(latestCheck, t), + buildMemoryMetric(latestCheck, t), + buildDiskMetric(latestCheck, t), + ].filter((m): m is MetricConfig => m !== null); + + if (metrics.length === 0) { + return ( + + {t("pages.statusPages.monitorsList.noData")} + + ); + } + + return ( + + {metrics.map(({ key, label, progress, details }) => ( + + ))} + + ); +}; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx index b94b20a80..248a69c9b 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList.tsx @@ -5,13 +5,16 @@ import Typography from "@mui/material/Typography"; import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common"; import { StatusLabel, BaseBox } from "@/Components/design-elements"; import { SwitchComponent } from "@/Components/inputs"; +import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics"; -import { useTheme } from "@mui/material/styles"; +import { useTheme, type Theme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import { useState } from "react"; import type { Monitor } from "@/Types/Monitor"; import type { StatusPage } from "@/Types/StatusPage"; +import { getMonitorTypeLabel } from "@/Types/StatusPage"; import type { RootState } from "@/Types/state"; +import { LAYOUT, SPACING } from "@/Utils/Theme/constants"; interface StatusPageMonitor extends Monitor { checks?: Monitor["recentChecks"]; @@ -22,29 +25,124 @@ interface MonitorsListProps { monitors: StatusPageMonitor[]; } -export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { +const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => { + const bg = + monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light; + return { + backgroundColor: bg, + color: theme.palette.background.paper, + padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`, + borderRadius: theme.shape.borderRadius, + }; +}; + +const MonitorHeader = ({ + monitor, + showURL, +}: { + monitor: StatusPageMonitor; + showURL: boolean; +}) => { const theme = useTheme(); const { t } = useTranslation(); - const showURL = useSelector((state: RootState) => state.ui?.showURL); + return ( + + + + + {monitor.name} + + + {getMonitorTypeLabel(monitor.type, t)} + + + {showURL && monitor.url && ( + + {monitor.url} + + )} + + + + ); +}; +const MonitorContent = ({ + monitor, + statusPage, + chartType, +}: { + monitor: StatusPageMonitor; + statusPage: StatusPage; + chartType: string; +}) => { + const theme = useTheme(); + + if (monitor.type === "hardware") { + if (statusPage.showInfrastructure === false) return null; + return ; + } + + if (statusPage.showCharts === false) return null; + + const checks = monitor.checks?.slice().reverse() ?? []; + return ( + + {chartType === "histogram" ? ( + + ) : ( + + )} + + ); +}; + +export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { + const theme = useTheme(); + const { t } = useTranslation(); + const showURL = useSelector((state: RootState) => state.ui?.showURL); const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram"); return ( - - {statusPage.showCharts !== false && ( + + {statusPage.showCharts && ( {t("pages.statusPages.monitorsList.chartTypeHeatmap")} { - setChartType(e.target.checked ? "histogram" : "heatmap"); - }} + onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")} /> {t("pages.statusPages.monitorsList.chartTypeHistogram")} @@ -52,59 +150,22 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => { )} - {monitors?.map((monitor) => { - return ( - - - - - {monitor.name} - - {showURL && ( - - {monitor.url} - - )} - - - - {statusPage.showCharts !== false && ( - - {chartType === "histogram" ? ( - - ) : ( - - )} - - )} - - ); - })} + {monitors.map((monitor) => ( + + + + + ))} ); }; diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx index b5c52b32b..d2043a7f3 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar.tsx @@ -1,35 +1,115 @@ -import { AlertTriangle, CircleCheck } from "lucide-react"; +import { + AlertTriangle, + CircleCheck, + CircleX, + Loader, + PauseCircle, + ShieldAlert, + Wrench, +} from "lucide-react"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { useTranslation } from "react-i18next"; -import type { Theme } from "@mui/material"; import { useTheme } from "@mui/material"; -import type { Monitor } from "@/Types/Monitor"; +import type { Theme } from "@mui/material"; +import type { Monitor, MonitorStatus } from "@/Types/Monitor"; + +interface StatusDisplay { + icon: JSX.Element; + msg?: string; + color?: string; +} const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => { - const monitorsStatus: Record = { + const monitorsStatus: StatusDisplay = { icon: , }; - if (monitors.every((monitor) => monitor.status === "up")) { + // Handle empty monitors array + if (monitors.length === 0) { + monitorsStatus.msg = t("pages.statusPages.statusBar.noMonitors"); + monitorsStatus.color = theme.palette.warning.main; + monitorsStatus.icon = ; + return monitorsStatus; + } + + const allOf = (...statuses: MonitorStatus[]) => + monitors.every((m) => statuses.includes(m.status)); + const someOf = (...statuses: MonitorStatus[]) => + monitors.some((m) => statuses.includes(m.status)); + const noneOf = (...statuses: MonitorStatus[]) => + monitors.every((m) => !statuses.includes(m.status)); + + // All monitors in a single state + if (allOf("up")) { monitorsStatus.msg = t("pages.statusPages.statusBar.allUp"); monitorsStatus.color = theme.palette.success.main; monitorsStatus.icon = ; - return monitorsStatus; - } else if (monitors.every((monitor) => monitor.status === "down")) { + } else if (allOf("breached")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.allBreached"); + monitorsStatus.color = theme.palette.error.main; + monitorsStatus.icon = ; + } else if (allOf("maintenance")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.allMaintenance"); + monitorsStatus.color = theme.palette.warning.main; + monitorsStatus.icon = ; + } else if (allOf("down")) { monitorsStatus.msg = t("pages.statusPages.statusBar.allDown"); monitorsStatus.color = theme.palette.error.main; - return monitorsStatus; - } else if (monitors.some((monitor) => monitor.status === "down")) { + monitorsStatus.icon = ; + } else if (allOf("paused")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.allPaused"); + monitorsStatus.color = theme.palette.warning.main; + monitorsStatus.icon = ; + } else if (allOf("initializing")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.allInitializing"); + monitorsStatus.color = theme.palette.warning.main; + monitorsStatus.icon = ; + + // Breached takes highest priority in mixed states + } else if (someOf("breached") && someOf("down")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.breachedAndDown"); + monitorsStatus.color = theme.palette.error.main; + monitorsStatus.icon = ; + } else if (someOf("breached")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.breached"); + monitorsStatus.color = theme.palette.error.main; + monitorsStatus.icon = ; + + // Maintenance combinations + } else if (someOf("maintenance") && someOf("down")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.maintenanceAndDown"); + monitorsStatus.color = theme.palette.error.main; + monitorsStatus.icon = ; + } else if (someOf("maintenance") && noneOf("down")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.maintenance"); + monitorsStatus.color = theme.palette.warning.main; + monitorsStatus.icon = ; + + // Degraded (some down, no maintenance/breached) + } else if (someOf("down")) { monitorsStatus.msg = t("pages.statusPages.statusBar.degraded"); monitorsStatus.color = theme.palette.warning.main; - return monitorsStatus; + monitorsStatus.icon = ; + + // Some Paused + } else if (someOf("paused")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.partiallyPaused"); + monitorsStatus.color = theme.palette.warning.main; + monitorsStatus.icon = ; + + // Initializing + } else if (someOf("initializing")) { + monitorsStatus.msg = t("pages.statusPages.statusBar.initializing"); + monitorsStatus.color = theme.palette.info.main; + monitorsStatus.icon = ; } else { monitorsStatus.msg = t("pages.statusPages.statusBar.unknown"); monitorsStatus.color = theme.palette.warning.main; - return monitorsStatus; } + + return monitorsStatus; }; interface StatusBarProps { diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 0ca629581..78b7cba41 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -23,16 +23,22 @@ const StatusPageView = () => { const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isPublic = location.pathname.startsWith("/status/public"); - const apiUrl = url ? `/status-page/${url}?type=uptime` : null; + const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null; - const { data, isLoading, error } = useGet(apiUrl); + const { data, isLoading, error } = useGet( + apiUrl, + {}, + { + refreshInterval: 10000, + } + ); const statusPage = data?.statusPage; const monitors = data?.monitors ?? []; if (!statusPage) return null; - if (monitors?.length === 0) { + if (monitors.length === 0) { return ( row.type, + render: (row) => row.type.join(", "), }, { id: "status", diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index c69fe7972..9f7771222 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -1,3 +1,5 @@ +export const CHECK_TTL_SENTINEL = 366; + export interface CheckMetadata { monitorId: string; teamId: string; @@ -125,9 +127,6 @@ export interface Check { timings?: CheckTimings; statusCode: number; message: string; - ack: boolean; - ackAt?: string | null; - expiry: string; cpu?: CheckCpuInfo; memory?: CheckMemoryInfo; disk?: CheckDiskInfo[]; @@ -140,7 +139,6 @@ export interface Check { seo?: number; performance?: number; audits?: CheckAudits; - __v: number; createdAt: string; updatedAt: string; } @@ -243,10 +241,7 @@ export interface ChecksSummary { downChecks: number; } -export type CheckSnapshot = Omit< - Check, - "metadata" | "ack" | "ackAt" | "expiry" | "__v" | "updatedAt" -> & { +export type CheckSnapshot = Omit & { originalResponseTime: number; }; diff --git a/client/src/Types/Notification.ts b/client/src/Types/Notification.ts index 5b9645c9d..9d5b914c0 100644 --- a/client/src/Types/Notification.ts +++ b/client/src/Types/Notification.ts @@ -5,6 +5,10 @@ export const NotificationChannels = [ "webhook", "pager_duty", "matrix", + "teams", + "telegram", + "pushover", + "twilio", ] as const; export type NotificationChannel = (typeof NotificationChannels)[number]; @@ -19,6 +23,8 @@ export interface Notification { homeserverUrl?: string; roomId?: string; accessToken?: string; + accountSid?: string; + twilioPhoneNumber?: string; createdAt: string; updatedAt: string; } diff --git a/client/src/Types/StatusPage.ts b/client/src/Types/StatusPage.ts index 900373f3d..6fe641210 100644 --- a/client/src/Types/StatusPage.ts +++ b/client/src/Types/StatusPage.ts @@ -1,10 +1,31 @@ -import type { Monitor } from "@/Types/Monitor"; +import type { Monitor, MonitorType } from "@/Types/Monitor"; +export type MonitorDisplayType = "uptime" | "infrastructure"; + +export const MONITOR_TYPE_KEYS: Partial> = { + http: "pages.common.monitors.monitorTypes.optionHttp", + ping: "pages.common.monitors.monitorTypes.optionPing", + docker: "pages.common.monitors.monitorTypes.optionDocker", + port: "pages.common.monitors.monitorTypes.optionPort", + game: "pages.common.monitors.monitorTypes.optionGame", + grpc: "pages.common.monitors.monitorTypes.optionGrpc", + websocket: "pages.common.monitors.monitorTypes.optionWebSocket", + hardware: "pages.common.monitors.monitorTypes.optionHardware", + pagespeed: "pages.common.monitors.monitorTypes.optionPagespeed", +}; + +export const getMonitorTypeLabel = ( + type: MonitorType, + t: (key: string) => string +): string => { + const key = MONITOR_TYPE_KEYS[type]; + return key ? t(key) : type; +}; export interface StatusPage { id: string; userId: string; teamId: string; - type: string; + type: MonitorDisplayType[]; companyName: string; url: string; timezone?: string; @@ -20,6 +41,7 @@ export interface StatusPage { showCharts: boolean; showUptimePercentage: boolean; showAdminLoginLink: boolean; + showInfrastructure: boolean; customCSS: string; createdAt: string; updatedAt: string; diff --git a/client/src/Validation/notifications.ts b/client/src/Validation/notifications.ts index e505c695f..9fa2b011a 100644 --- a/client/src/Validation/notifications.ts +++ b/client/src/Validation/notifications.ts @@ -45,6 +45,31 @@ const matrixSchema = baseSchema.extend({ accessToken: z.string().min(1, "Access token is required"), }); +const teamsSchema = baseSchema.extend({ + type: z.literal("teams"), + address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"), +}); + +const telegramSchema = baseSchema.extend({ + type: z.literal("telegram"), + address: z.string().min(1, "Chat ID is required"), + accessToken: z.string().min(1, "Bot token is required"), +}); + +const pushoverSchema = baseSchema.extend({ + type: z.literal("pushover"), + address: z.string().min(1, "User key is required"), + accessToken: z.string().min(1, "App token is required"), +}); + +const twilioSchema = baseSchema.extend({ + type: z.literal("twilio"), + accountSid: z.string().min(1, "Account SID is required"), + accessToken: z.string().min(1, "Auth token is required"), + phone: z.string().min(1, "Recipient phone number is required"), + twilioPhoneNumber: z.string().min(1, "Twilio phone number is required"), +}); + export const notificationSchema = z.discriminatedUnion("type", [ emailSchema, slackSchema, @@ -52,6 +77,10 @@ export const notificationSchema = z.discriminatedUnion("type", [ webhookSchema, pagerDutySchema, matrixSchema, + teamsSchema, + telegramSchema, + pushoverSchema, + twilioSchema, ]); export type NotificationFormData = z.infer; diff --git a/client/src/Validation/settings.ts b/client/src/Validation/settings.ts index 7eddaafe2..cf122f69d 100644 --- a/client/src/Validation/settings.ts +++ b/client/src/Validation/settings.ts @@ -1,3 +1,4 @@ +import { CHECK_TTL_SENTINEL } from "@/Types/Check"; import { z } from "zod"; export const settingsSchema = z.object({ @@ -6,39 +7,42 @@ export const settingsSchema = z.object({ systemEmailRejectUnauthorized: z.boolean(), systemEmailConnectionHost: z .string() - .transform((val) => (val.trim() === "" ? undefined : val.trim())) + .transform((val) => (val.trim() === "" ? null : val.trim())) .optional(), systemEmailSecure: z.boolean().optional(), systemEmailPool: z.boolean().optional(), showURL: z.boolean().optional(), - checkTTL: z.number().int().min(1, "Please enter a value").max(365, "Maximum 365 days"), + checkTTL: z + .number() + .int() + .min(1, "Please enter a value") + .max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`), pagespeedApiKey: z .string() - .transform((val) => (val.trim() === "" ? undefined : val.trim())) + .transform((val) => (val.trim() === "" ? null : val.trim())) .optional(), systemEmailHost: z .string() .regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address") - .transform((val) => (val.trim() === "" ? undefined : val.trim())) + .transform((val) => (val.trim() === "" ? null : val.trim())) .optional(), systemEmailPort: z.number().int().min(1).max(65535).optional(), systemEmailAddress: z - .string() .email("Please enter a valid email address") .or(z.literal("")) - .transform((val) => (val === "" ? undefined : val.toLowerCase().trim())) + .transform((val) => (val === "" ? null : val.toLowerCase().trim())) .optional(), systemEmailUser: z .string() - .transform((val) => (val.trim() === "" ? undefined : val.trim())) + .transform((val) => (val.trim() === "" ? null : val.trim())) .optional(), systemEmailPassword: z .string() - .transform((val) => (val.trim() === "" ? undefined : val.trim())) + .transform((val) => (val.trim() === "" ? null : val.trim())) .optional(), systemEmailTLSServername: z .string() - .transform((val) => (val.trim() === "" ? undefined : val.trim())) + .transform((val) => (val.trim() === "" ? null : val.trim())) .optional(), globalThresholds: z.object({ cpu: z.number().int().min(1).max(100), @@ -48,4 +52,5 @@ export const settingsSchema = z.object({ }), }); +export type SettingsFormInput = z.input; export type SettingsFormData = z.infer; diff --git a/client/src/Validation/statusPage.ts b/client/src/Validation/statusPage.ts index fbe6a0690..43d7198f0 100644 --- a/client/src/Validation/statusPage.ts +++ b/client/src/Validation/statusPage.ts @@ -14,12 +14,16 @@ export const statusPageSchema = z.object({ "URL can only contain lowercase letters, numbers, and hyphens" ), timezone: z.string().optional(), + type: z + .array(z.enum(["uptime", "infrastructure"])) + .min(1, "At least one type is required"), color: z.string().min(1, "Color is required"), monitors: z.array(z.string()).min(1, "At least one monitor is required"), isPublished: z.boolean(), showCharts: z.boolean(), showUptimePercentage: z.boolean(), showAdminLoginLink: z.boolean(), + showInfrastructure: z.boolean(), customCSS: z.string().optional(), logo: z .object({ diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index fa92f2e99..c11543cad 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -288,7 +288,13 @@ const logoImageValidation = joi .optional(); // Make entire object optional const statusPageValidation = joi.object({ - type: joi.string().valid("uptime").required(), + type: joi + .alternatives() + .try( + joi.string().valid("uptime", "infrastructure"), + joi.array().items(joi.string().valid("uptime", "infrastructure")) + ) + .required(), isPublished: joi.bool(), companyName: joi .string() @@ -317,6 +323,7 @@ const statusPageValidation = joi.object({ showUptimePercentage: joi.boolean(), showCharts: joi.boolean(), showAdminLoginLink: joi.boolean(), + showInfrastructure: joi.boolean(), }); const settingsValidation = joi.object({ diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json index 6cec253d8..28624c469 100644 --- a/client/src/locales/ar.json +++ b/client/src/locales/ar.json @@ -1,1108 +1,1305 @@ { - "submit": "", - "title": "", - "distributedStatusHeaderText": "", - "distributedStatusSubHeaderText": "", - "settingsDisabled": "", - "settingsSuccessSaved": "", - "settingsFailedToSave": "", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", - "https": "", - "http": "", - "monitor": "", - "aboutus": "", - "now": "", - "delete": "", - "configure": "", - "responseTime": "", - "ms": "", - "bar": "", - "area": "", - "country": "", - "city": "", - "response": "", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "", - "webhookSendError": "", - "webhookUnsupportedPlatform": "", - "distributedRightCategoryTitle": "", - "distributedStatusServerMonitors": "", - "distributedStatusServerMonitorsDescription": "", - "distributedUptimeCreateSelectURL": "", - "distributedUptimeCreateChecks": "", - "distributedUptimeCreateChecksDescription": "", - "distributedUptimeCreateIncidentNotification": "", - "distributedUptimeCreateIncidentDescription": "", - "distributedUptimeCreateAdvancedSettings": "", - "distributedUptimeDetailsNoMonitorHistory": "", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "", - "notifications": { - "enableNotifications": "", - "testNotification": "", - "addOrEditNotifications": "", - "slack": { - "label": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "discord": { - "label": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "telegram": { - "label": "", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "", - "chatIdLabel": "", - "chatIdPlaceholder": "", - "fieldsRequired": "" - }, - "webhook": { - "label": "", - "description": "", - "urlLabel": "", - "urlPlaceholder": "", - "urlRequired": "" - }, - "testNotificationDevelop": "", - "integrationButton": "", - "testSuccess": "", - "testFailed": "", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [""], - "actionButton": "" - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "", - "add": "", - "monitors": "", - "distributedUptimeStatusCreateStatusPage": "", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "", - "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "", - "distributedUptimeStatus60Days": "", - "distributedUptimeStatus90Days": "", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "", - "incidentsTableDateTime": "", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", - "incidentsOptionsHeaderLastHour": "", - "incidentsOptionsHeaderLastDay": "", - "incidentsOptionsHeaderLastWeek": "", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "", - "infrastructureServerUrlLabel": "", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "", - "cpu": "", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "", - "avgCpuTemperature": "", - "memory": "", - "disk": "", - "uptime": "", - "os": "", - "host": "", - "actions": "", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "", - "integrationsSlackInfo": "", - "integrationsDiscord": "", - "integrationsDiscordInfo": "", - "integrationsZapier": "", - "integrationsZapierInfo": "", - "commonSave": "", - "createYour": "", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", - "timezone": "", - "features": "", - "administrator": "", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "", - "window": "", - "cancel": "", - "message": "", - "low": "", - "high": "", - "statusCode": "", - "date&Time": "", - "type": "", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", - "createA": "", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", - "ago": "", - "companyName": "", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "", - "showUptimePercentage": "", - "removeLogo": "", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "", - "invalidFileFormat": "", - "invalidFileSize": "", - "ClickUpload": "", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", - "EmailDescriptionText": "", - "YourPhoto": "", - "PhotoDescriptionText": "", - "save": "", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", - "DeleteWarningTitle": "", - "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "", - "fallbackPage": "", - "invalidFileType": "", - "uploadFailed": "" - }, - "DeleteAccountTitle": "", - "DeleteAccountButton": "", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "", - "greeting": { - "prepend": "", - "append": "", - "overview": "" - }, - "roles": { - "superAdmin": "", - "admin": "", - "teamMember": "", - "demoUser": "" - }, - "teamPanel": { - "teamMembers": "", - "filter": { - "all": "", - "member": "" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "", - "selectRole": "", - "inviteLink": "", - "cancel": "", - "noMembers": "", - "getToken": "", - "emailToken": "", - "table": { - "name": "", - "email": "", - "role": "", - "created": "" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "", - "resumed": "", - "active": "" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "", - "incidents": "", - "statusPages": "", - "maintenance": "", - "integrations": "", - "settings": "", - "support": "", - "discussions": "", - "docs": "", - "changelog": "", - "profile": "", - "password": "", - "team": "", - "logOut": "", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", - "commonSaving": "", - "navControls": "", - "incidentsPageTitle": "", - "passwordPanel": { - "passwordChangedSuccess": "", - "passwordInputIncorrect": "", - "currentPassword": "", - "enterCurrentPassword": "", - "newPassword": "", - "enterNewPassword": "", - "confirmNewPassword": "", - "passwordRequirements": "", - "saving": "" - }, - "emailSent": "", - "failedToSendEmail": "", - "settingsTestEmailSuccess": "", - "settingsTestEmailFailed": "", - "settingsTestEmailFailedWithReason": "", - "settingsTestEmailUnknownError": "", - "statusMsg": { - "paused": "", - "up": "", - "down": "", - "pending": "" - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "", - "game": "", - "https": "" - }, "common": { - "appName": "", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" + "auth": { + "roles": { + "admin": "مسؤول", + "demo": "عرض توضيحي", + "superadmin": "مسؤول أعلى", + "user": "مستخدم" + } }, - "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "", - "back": "" + "alerts": { + "pageSpeedApiKey": { + "content": "تحذير: لم تقم بإضافة مفتاح Google PageSpeed API بعد. قم بزيارة الإعدادات لإضافته. بدونه، لن تعمل مراقبة PageSpeed." + } + }, + "appName": "", + "breadcrumbs": { + "details": "التفاصيل", + "home": "الرئيسية" + }, + "buttons": { + "addMember": "إضافة عضو", + "cancel": "إلغاء", + "close": "إغلاق", + "configure": "تهيئة", + "confirm": "تأكيد", + "create": "إنشاء", + "delete": "حذف", + "generateToken": "إنشاء رمز", + "incidents": "الحوادث", + "inviteMember": "دعوة عضو", + "pause": "إيقاف مؤقت", + "resume": "استئناف", + "save": "حفظ", + "sendInvite": "إرسال دعوة", + "test": "اختبار", + "testNotifications": "اختبار الإشعارات", + "toggleTheme": "", + "flushQueue": "تفريغ قائمة الانتظار", + "notFound": "الانتقال إلى لوحة التحكم الرئيسية", + "resetPassword": "", + "reset": "", + "clear": "مسح", + "addDemo": "إضافة مراقبات تجريبية", + "removeMonitors": "إزالة المراقبات", + "sendTestEmail": "إرسال بريد تجريبي", + "exportToJSON": "تصدير إلى JSON", + "importFromJSON": "استيراد من JSON", + "clearFilters": "مسح المرشحات", + "removeUser": "إزالة المستخدم" + }, + "charts": { + "labels": { + "averageResponseTime": "متوسط وقت الاستجابة", + "downtime": "وقت التوقف", + "high": "", + "low": "", + "uptime": "وقت التشغيل" }, - "inputs": { - "email": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "invalid": "" - } - }, - "password": { - "label": "", - "rules": { - "length": { - "beginning": "", - "highlighted": "" - }, - "special": { - "beginning": "", - "highlighted": "" - }, - "number": { - "beginning": "", - "highlighted": "" - }, - "uppercase": { - "beginning": "", - "highlighted": "" - }, - "lowercase": { - "beginning": "", - "highlighted": "" - }, - "match": { - "beginning": "", - "highlighted": "" - } - }, - "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" - } - }, - "passwordConfirm": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - }, - "lastName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" + "histogram": { + "avg": "المتوسط: {{value}} مللي ثانية", + "max": "الحد الأقصى: {{value}} مللي ثانية" + } + }, + "dialogs": { + "delete": { + "description": "لا يمكن التراجع عن هذا الإجراء.", + "title": "هل أنت متأكد أنك تريد حذف هذا؟" + } + }, + "labels": { + "active": "نشط", + "paused": "متوقف مؤقتاً", + "na": "غير متوفر", + "resolved": "تم الحل", + "responseTime": "وقت الاستجابة" + }, + "form": { + "role": { + "option": { + "role": { + "label": "الأدوار" } } }, - "errors": { - "validation": "" - }, - "fields": { - "password": { - "errors": { - "incorrect": "" + "name": { + "option": { + "firstName": { + "label": "", + "placeholder": "" + }, + "lastName": { + "label": "", + "placeholder": "" } - }, - "role": { - "errors": { - "min": "" + } + }, + "email": { + "option": { + "email": { + "label": "البريد الإلكتروني", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "" - }, - "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "", - "incorrectPassword": "" - }, + "table": { + "empty": "لا يوجد شيء هنا", + "headers": { + "actions": "", + "dateTime": "التاريخ والوقت", + "message": "الرسالة", + "monitor": "المراقب", + "monitorId": "معرّف المراقب", + "name": "الاسم", + "status": "الحالة", + "type": "", + "url": "الرابط", + "interval": "الفاصل الزمني", + "active": "نشط", + "responseTime": "وقت الاستجابة" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "انقر للرفع", + "dragAndDrop": "", + "supportedFormats": "", + "maxSize": "", + "orDragAndDrop": "أو اسحب وأفلت", "errors": { - "password": { - "incorrect": "" - } - }, - "welcome": "" + "invalidFileSize": "", + "invalidFileFormat": "" + } }, - "registration": { - "heading": { - "superAdmin": "", - "user": "" - }, - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" + "headerStatusPageControls": { + "publicLink": "" + }, + "headerTimeRange": { + "labels": { + "day": "يوم", + "month": "شهر", + "recent": "الأخيرة", + "week": "أسبوع" }, "description": { - "superAdmin": "", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "", - "user": "" - }, - "termsAndPolicies": "", - "links": { - "login": "" - }, - "toasts": { - "success": "" - }, - "welcome": "" + "recent": "عرض الإحصائيات لآخر ساعتين.", + "day": "عرض الإحصائيات لآخر 24 ساعة.", + "week": "عرض الإحصائيات لآخر 7 أيام.", + "month": "عرض الإحصائيات لآخر 30 يوماً." + } }, - "forgotPassword": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" + "offlineBanner": { + "serverUnreachable": "تعذر الوصول إلى الخادم", + "retry": "إعادة المحاولة", + "retrying": "جارٍ إعادة المحاولة...", + "reconnected": "" + }, + "sidebar": { + "menu": { + "uptime": "", + "pagespeed": "", + "infrastructure": "", + "notifications": "", + "checks": "الفحوصات", + "incidents": "", + "statusPages": "", + "maintenance": "", + "logs": "", + "settings": "" }, - "buttons": { - "openEmail": "", - "resetPassword": "" + "bottomMenu": { + "support": "", + "discussions": "", + "docs": "", + "changelog": "" }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "accountMenu": { + "profile": "الملف الشخصي", + "password": "كلمة المرور", + "team": "الفريق" }, - "links": { - "login": "", - "resend": "" + "starPrompt": { + "title": "أضف نجمة لـ Checkmate", + "description": "اطلع على أحدث الإصدارات وساعد في تنمية المجتمع على GitHub" }, - "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" + "authFooter": { + "navControls": "", + "logOut": "", + "roles": { + "superAdmin": "مسؤول أعلى", + "admin": "مسؤول", + "user": "مستخدم", + "demoUser": "" + } + } + }, + "starPrompt": { + "title": "أضف نجمة لـ Checkmate", + "description": "اطلع على أحدث الإصدارات وساعد في تنمية المجتمع على GitHub" + } + }, + "pages": { + "notFound": { + "title": "يا إلهي! لقد أسقطت السوشي!", + "subtitle": "إما أن الرابط غير موجود، أو ليس لديك صلاحية الوصول إليه." + }, + "account": { + "tabs": { + "profile": "الملف الشخصي", + "password": "كلمة المرور", + "team": "الفريق" + }, + "form": { + "name": { + "title": "الاسم", + "description": "تحديث معلوماتك الشخصية" + }, + "photo": { + "title": "صورة الملف الشخصي", + "description": "رفع صورة للملف الشخصي" + }, + "currentPassword": { + "title": "كلمة المرور الحالية", + "description": "أدخل كلمة المرور الحالية للتحقق من هويتك", + "option": { + "label": "كلمة المرور الحالية", + "placeholder": "أدخل كلمة المرور الحالية" + } + }, + "newPassword": { + "title": "كلمة مرور جديدة", + "description": "اختر كلمة مرور قوية مكونة من 8 أحرف على الأقل", + "option": { + "newPassword": { + "label": "كلمة مرور جديدة", + "placeholder": "أدخل كلمة المرور الجديدة" + }, + "confirm": { + "label": "تأكيد كلمة المرور", + "placeholder": "تأكيد كلمة المرور الجديدة" + } + } + }, + "deleteAccount": { + "title": "حذف الحساب", + "description": "هذا الإجراء دائم ولا يمكن التراجع عنه" + } + }, + "team": { + "filter": { + "placeholder": "تصفية حسب الدور", + "all": "", + "admin": "مسؤول", + "member": "" + }, + "table": { + "headers": { + "email": "البريد الإلكتروني", + "role": "الدور", + "created": "" + } + }, + "addMember": { + "title": "تسجيل عضو فريق جديد", + "description": "إنشاء حساب مستخدم جديد. شارك بيانات الاعتماد بشكل آمن بعد الإنشاء." + }, + "invite": { + "title": "دعوة عضو فريق", + "description": "إرسال دعوة للانضمام إلى فريقك", + "email": { + "label": "عنوان البريد الإلكتروني", + "placeholder": "أدخل عنوان البريد الإلكتروني" + }, + "role": { + "label": "الدور", + "placeholder": "اختر دوراً" + }, + "linkLabel": "رابط الدعوة" + } + } + }, + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "", + "highlighted": "" + }, + "lowercase": { + "beginning": "", + "highlighted": "" + }, + "match": { + "beginning": "", + "highlighted": "" + }, + "number": { + "beginning": "", + "highlighted": "" + }, + "special": { + "beginning": "", + "highlighted": "" + }, + "uppercase": { + "beginning": "", + "highlighted": "" + } + }, + "form": { + "option": { + "email": { + "label": "البريد الإلكتروني", + "placeholder": "me@example.com" + }, + "password": { + "label": "كلمة المرور", + "placeholder": "********" + }, + "confirmPassword": { + "label": "تأكيد كلمة المرور" + } + } + } + }, + "login": { + "title": "مرحباً بعودتك إلى Checkmate!", + "subtitle": "سجّل الدخول للمتابعة", + "submit": "تسجيل الدخول", + "links": { + "forgotPassword": { + "text": "نسيت كلمة المرور؟", + "linkText": "إعادة تعيين كلمة المرور" + }, + "register": { + "text": "ليس لديك حساب؟", + "linkText": "سجّل هنا" + } + } + }, + "register": { + "title": "مرحباً بعودتك إلى Checkmate!", + "subtitle": "سجّل للبدء", + "submit": "تسجيل" + }, + "forgotPassword": { + "title": "نسيت كلمة المرور؟", + "subtitle": "لا تقلق، سنرسل لك تعليمات إعادة التعيين.", + "submit": "طلب استرداد", + "links": { + "login": { + "text": "العودة إلى", + "linkText": "تسجيل الدخول" + } + } + }, + "setNewPassword": { + "title": "إعادة تعيين كلمة المرور", + "subtitle": "يجب أن تكون كلمة المرور الجديدة مختلفة عن كلمات المرور المستخدمة سابقاً." + } + }, + "checks": { + "selects": { + "monitor": { + "all": "جميع المراقبات" + }, + "status": { + "all": "الكل", + "down": "متوقف", + "up": "يعمل" + } + }, + "table": { + "empty": "لا توجد فحوصات متوقفة في هذا النطاق الزمني", + "headers": { + "statusCode": "رمز الحالة", + "location": "الموقع" + } + } + }, + "common": { + "monitors": { + "actions": { + "configure": "تهيئة", + "delete": "حذف", + "generateToken": "إنشاء رمز", + "details": "التفاصيل", + "incidents": "الحوادث", + "inviteMember": "دعوة عضو", + "openSite": "فتح الموقع", + "pause": "إيقاف مؤقت", + "resume": "استئناف" + }, + "statBoxes": { + "activeFor": "نشط منذ", + "certificateExpiry": "انتهاء الشهادة", + "lastCheck": "آخر فحص", + "lastResponseTime": "آخر وقت استجابة" + }, + "status": { + "down": "متوقف", + "breached": "تم تجاوز الحد", + "initializing": "جارٍ التهيئة", + "maintenance": "صيانة", + "paused": "متوقف مؤقتاً", + "total": "الإجمالي", + "up": "يعمل" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "ألعاب", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "منفذ", + "optionPagespeed": "PageSpeed", + "optionHardware": "البنية التحتية", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } + }, + "createMonitor": { + "form": { + "advanced": { + "description": "إعدادات اختيارية لحالات الاستخدام المتقدمة", + "option": { + "advancedMatching": { + "label": "استخدام المطابقة المتقدمة" + }, + "expectedValue": { + "label": "القيمة المتوقعة" + }, + "jsonPath": { + "description": "سيتم تقييم هذا التعبير مقابل بيانات JSON للاستجابة وسيتم استخدام النتيجة للمطابقة مع القيمة المتوقعة. راجع jmespath.org لتوثيق لغة الاستعلام.", + "label": "تعبير JSONPath" + }, + "matchMethod": { + "label": "طريقة المطابقة", + "equal": "", + "include": "", + "regex": "" + } + }, + "title": "إعدادات متقدمة" + }, + "frequency": { + "description": "كم مرة تريد التحقق من حالة هذا المراقب؟", + "option": { + "frequency": { + "label": "تكرار الفحص", + "value": { + "fifteenMinutes": "15 دقيقة", + "fifteenSeconds": "", + "fiveMinutes": "5 دقائق", + "fourMinutes": "", + "oneMinute": "", + "tenMinutes": "10 دقائق", + "thirtyMinutes": "30 دقيقة", + "thirtySeconds": "", + "threeMinutes": "", + "twoMinutes": "" + } + } + }, + "title": "تكرار الفحص" + }, + "general": { + "option": { + "container": { + "label": "اسم/معرّف الحاوية", + "placeholder": "my-app أو abcd1234" + }, + "host": { + "label": "المضيف", + "placeholder": "192.168.1.100 أو example.com" + }, + "name": { + "label": "اسم العرض", + "placeholder": "مثال: موقعي الإلكتروني" + }, + "secret": { + "label": "مفتاح التفويض", + "placeholder": "أدخل مفتاحك السري" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "", + "placeholder": "" + }, + "port": { + "label": "المنفذ المراد مراقبته", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "اسم الخدمة", + "placeholder": "مثال: my.service.v1 (اتركه فارغاً للفحص العام)" + }, + "wsUrl": { + "label": "رابط WebSocket", + "placeholder": "wss://example.com/socket" + } + }, + "title": "الإعدادات العامة", + "description": { + "http": "أدخل الرابط أو عنوان IP للمراقبة (مثال: https://example.com/ أو 192.168.1.100) وأضف اسم عرض واضح يظهر في لوحة التحكم.", + "ping": "", + "port": "", + "docker": "", + "game": "", + "pagespeed": "تتبع أداء تحميل الصفحة ومؤشرات Core Web Vitals ودرجات التحسين لموقعك.", + "grpc": "أدخل اسم المضيف ومنفذ خادم gRPC، وحدد اختيارياً اسم خدمة لفحص الصحة، وأضف اسم عرض.", + "websocket": "أدخل رابط WebSocket للمراقبة (مثال: wss://example.com/socket) وأضف اسم عرض.", + "hardware": "مراقبة استخدام CPU والذاكرة والقرص ودرجة الحرارة للبنية التحتية الخاصة بك." + } + }, + "ignoreTls": { + "description": "تهيئة التحقق من شهادة TLS/SSL لاتصالات HTTPS.", + "option": { + "tls": { + "label": "تجاهل أخطاء TLS/SSL" + } + }, + "title": "إعدادات TLS/SSL" + }, + "incidents": { + "description": "يتم استخدام نافذة منزلقة لتحديد متى يتوقف المراقب. ستتغير حالة المراقب فقط عندما تستوفي نسبة الفحوصات في النافذة المنزلقة القيمة المحددة.", + "option": { + "checks": { + "label": "عدد الفحوصات في النافذة المنزلقة" + }, + "percentage": { + "label": "ما نسبة الفحوصات في النافذة المنزلقة التي يجب أن تفشل/تنجح قبل تغيير حالة المراقب؟" + } + }, + "title": "الحوادث" + }, + "notifications": { + "description": "اختر قنوات الإشعارات التي تريد استخدامها", + "title": "الإشعارات" + }, + "type": { + "description": "اختر نوع الفحص المراد تنفيذه", + "optionDockerDescription": "استخدم Docker لمراقبة ما إذا كانت الحاوية تعمل.", + "optionGameDescription": "مراقبة ما إذا كان خادم لعبة معين متصلاً.", + "optionGrpcDescription": "مراقبة خدمات gRPC باستخدام بروتوكول فحص الصحة القياسي.", + "optionWebSocketDescription": "مراقبة نقاط نهاية WebSocket لصحة الاتصال ووقت الاستجابة.", + "optionHttpDescription": "استخدم HTTP(S) لمراقبة موقعك الإلكتروني أو نقطة نهاية API.", + "optionPingDescription": "استخدم ICMP Ping لمراقبة ما إذا كان الخادم متصلاً.", + "optionPortDescription": "مراقبة ما إذا كان منفذ معين على الخادم مفتوحاً.", + "title": "النوع" + }, + "thresholds": { + "title": "حدود التنبيه", + "description": "حدد الحدود التي يجب عندها تشغيل التنبيهات لمراقب الأجهزة هذا.", + "option": { + "cpuThreshold": { + "label": "حد تنبيه CPU (%)" + }, + "memoryThreshold": { + "label": "حد تنبيه الذاكرة (%)" + }, + "diskThreshold": { + "label": "حد تنبيه القرص (%)" + }, + "tempThreshold": { + "label": "حد تنبيه الحرارة (°C)" + } + } + }, + "geoChecks": { + "title": "فحوصات موزعة جغرافياً", + "description": "تشغيل فحوصات من مواقع جغرافية متعددة لمراقبة التوفر والأداء العالمي.", + "option": { + "enabled": { + "label": "تفعيل الفحوصات الموزعة جغرافياً" + }, + "locations": { + "label": "المواقع", + "placeholder": "اختر المواقع", + "options": { + "EU": "أوروبا", + "NA": "أمريكا الشمالية", + "AS": "آسيا", + "SA": "أمريكا الجنوبية", + "AF": "أفريقيا", + "OC": "أوقيانوسيا" + } + }, + "interval": { + "label": "فاصل الفحص", + "value": { + "fiveMinutes": "5 دقائق", + "tenMinutes": "10 دقائق", + "fifteenMinutes": "15 دقيقة", + "thirtyMinutes": "30 دقيقة" + } + } + } + }, + "url": { + "title": "عرض IP/URL للمراقب في صفحة الحالة", + "description": "عرض عنوان IP أو رابط المراقب في صفحة الحالة العامة. إذا تم تعطيله، سيظهر فقط اسم المراقب لحماية المعلومات الحساسة.", + "option": { + "showURL": { + "label": "عرض IP/URL في صفحة الحالة", + "enabled": "مفعّل", + "disabled": "معطّل" + } + } + }, + "stats": { + "title": "سجل المراقبة", + "description": "مسح جميع سجلات وإحصائيات المراقبة لفريقك. هذا الإجراء لا يمكن التراجع عنه.", + "buttonText": "مسح جميع الإحصائيات", + "dialog": { + "title": "مسح جميع سجلات المراقبة؟", + "description": "سيتم حذف جميع سجلات المراقبة والإحصائيات وبيانات الفحص لفريقك نهائياً. لا يمكن التراجع عن هذا الإجراء.", + "confirm": "نعم، مسح جميع الإحصائيات" + } + } + } + }, + "editUser": { + "form": { + "roles": { + "title": "الأدوار", + "description": "تعيين أدوار للمستخدم. يمكن اختيار أدوار متعددة." + } + }, + "dialog": { + "removeUser": { + "title": "إزالة المستخدم", + "content": "هل أنت متأكد أنك تريد إزالة {{name}} من فريقك؟ لا يمكن التراجع عن هذا الإجراء." + } + } + }, + "incidents": { + "dialog": { + "resolveIncident": { + "title": "حل الحادثة", + "option": { + "comment": { + "label": "تعليق (اختياري)", + "placeholder": "أضف تعليقاً حول الحل..." + } + } + }, + "details": { + "analysis": "تحليل الحادثة", + "comment": "تعليق:", + "detailsLabel": "التفاصيل", + "downtime": "وقت التوقف:", + "message": "الرسالة:", + "monitor": "المراقب:", + "overview": "نظرة عامة", + "resolutionDetails": "تفاصيل الحل", + "resolutionType": "النوع:", + "resolutionTypes": { + "automatic": "تلقائي", + "manual": "يدوي" + }, + "resolve": "حل الحادثة", + "resolvedAt": "تم الحل في:", + "resolvedBy": "تم الحل بواسطة:", + "startedAt": "بدأت في:", + "status": "الحالة:", + "statusCode": "رمز الحالة:", + "timeline": "الجدول الزمني", + "title": "تفاصيل الحادثة", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "جميع المراقبات", + "monitor": "المراقب", + "resolutionType": "نوع الحل", + "resolutionTypes": { + "manual": "يدوي", + "automatic": "تلقائي", + "all": "الكل" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "الحوادث النشطة", + "active_zero": "لا توجد حوادث نشطة", + "active_one": "{{count}} حادثة نشطة", + "active_other": "{{count}} حوادث نشطة" + }, + "incidentStats": { + "avgResolutionTime": "متوسط وقت الحل", + "mostAffectedMonitor": "المراقب الأكثر تأثراً", + "title": "إحصائيات الحوادث", + "totalIncidents": "إجمالي الحوادث" + }, + "latestIncidents": { + "title": "أحدث الحوادث" + } + }, + "table": { + "actions": { + "details": "التفاصيل", + "goToMonitor": "الانتقال إلى المراقب", + "resolveManually": "حل يدوياً" + }, + "activeIncidents": "الحوادث النشطة", + "headers": { + "endTime": "وقت الانتهاء", + "resolutionType": "نوع الحل", + "startTime": "", + "statusCode": "رمز الحالة" + }, + "resolvedIncidents": "الحوادث المحلولة", + "status": { + "active": "نشط", + "resolved": "تم الحل" + } + } + }, + "infrastructure": { + "charts": { + "labels": { + "cpu": "استخدام CPU", + "disk": "استخدام القرص", + "memory": "استخدام الذاكرة", + "netBytesRecv": "{{name}} - البايتات المستلمة", + "netBytesSent": "{{name}} - البايتات المرسلة", + "temp": "الحرارة" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "أقصى تردد", + "title": "استخدام CPU", + "upperLabel": "التردد الحالي" + }, + "disk": { + "lowerLabel": "متاح", + "title": "استخدام القرص {{idx}}", + "upperLabel": "مستخدم" + }, + "memory": { + "lowerLabel": "متاح", + "title": "استخدام الذاكرة", + "upperLabel": "مستخدم" + } + }, + "statBoxes": { + "avgCpuTemperature": "", + "cpuFrequency": "", + "cpuLogical": "", + "cpuPhysical": "", + "disk": "القرص", + "memory": "الذاكرة", + "os": "" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "القرص", + "memory": "الذاكرة" + } + }, + "tabs": { + "labels": { + "network": "", + "overview": "نظرة عامة" + } + }, + "fallback": { + "actionButton": "أنشئ مراقباً!", + "checks": "تتبع أداء خوادمك,تحديد الاختناقات وتحسين الاستخدام,ضمان الموثوقية من خلال المراقبة في الوقت الفعلي", + "title": "يُستخدم مراقب البنية التحتية من أجل:" + } + }, + "logs": { + "tabs": { + "diagnostics": "", + "logs": "", + "queue": "" + }, + "logLevelSelect": { + "label": "مستوى السجل" + }, + "jobQueue": "قائمة المهام", + "failedJobs": "المهام الفاشلة", + "noLogs": "لم يتم العثور على سجلات", + "metrics": { + "jobs": "المهام", + "activeJobs": "المهام النشطة", + "failingJobs": "المهام الفاشلة", + "totalRuns": "إجمالي عمليات التشغيل", + "totalFailures": "إجمالي حالات الفشل" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "تأخير حلقة الأحداث", + "uptime": "وقت التشغيل", + "usedHeapSize": "حجم الذاكرة المستخدمة", + "totalHeapSize": "إجمالي حجم الذاكرة", + "osMemoryLimit": "حد ذاكرة نظام التشغيل" + }, + "gauges": { + "heapAllocation": "تخصيص الذاكرة", + "heapUsage": "استخدام الذاكرة", + "heapUtilization": "معدل استخدام الذاكرة", + "instantCpuUsage": "استخدام CPU اللحظي", + "availableMemoryPercentage": "% من الذاكرة المتاحة", + "allocatedPercentage": "% مخصص", + "usedSPercentage": "% من 1 ثانية مستخدمة بواسطة CPU", + "total": "الإجمالي", + "used": "مستخدم" + } + }, + "table": { + "headers": { + "timestamp": "الطابع الزمني", + "level": "المستوى", + "service": "الخدمة", + "method": "الطريقة", + "monitorId": "معرّف المراقب", + "runCount": "عدد التشغيلات", + "failCount": "عدد حالات الفشل", + "lastRunAt": "آخر تشغيل في", + "lockedAt": "مقفل في", + "lastFinishedAt": "آخر انتهاء في", + "lastRunTook": "مدة آخر تشغيل", + "lastFailedAt": "آخر فشل في", + "failReason": "سبب الفشل" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "أنشئ نافذة صيانة!", + "checks": "تحديد فترات الصيانة,التخلص من أي سوء فهم,إيقاف إرسال التنبيهات أثناء نوافذ الصيانة", + "title": "تُستخدم نافذة الصيانة من أجل:" + }, + "table": { + "headers": { + "nextWindow": "", + "repeat": "" + } + }, + "form": { + "general": { + "title": "الإعدادات العامة", + "description": "حدد اسماً وخيار التكرار لنافذة الصيانة الخاصة بك.", + "option": { + "name": { + "label": "الاسم", + "placeholder": "مثال: صيانة أسبوعية" + }, + "repeat": { + "label": "تكرار" + } + } + }, + "startDate": { + "title": "تاريخ البدء", + "description": "اختر تاريخ البدء لنافذة الصيانة الخاصة بك.", + "option": { + "startDate": { + "label": "تاريخ البدء" + } + } + }, + "startTime": { + "title": "وقت البدء", + "description": "حدد وقت البدء والمدة لنافذة الصيانة الخاصة بك. جميع القيم بتوقيت UTC", + "option": { + "duration": { + "label": "المدة" + }, + "startTime": { + "label": "وقت البدء" + } + }, + "monitors": { + "title": "المراقبات", + "description": "المراقبات التي يجب أن تنطبق عليها نافذة الصيانة", + "option": { + "addMonitors": { + "label": "إضافة مراقبات" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "إنشاء قناة", + "checks": "تنبيه الفرق حول التوقف أو مشكلات الأداء,إعلام المهندسين عند حدوث الحوادث,إبقاء المسؤولين على اطلاع بتغييرات النظام", + "title": "تُستخدم قنوات الإشعارات من أجل:" + }, + "form": { + "accessToken": { + "optionAccessToken": "رمز الوصول", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "العنوان الذي سيتم إرسال الإشعارات إليه.", + "optionAddress": "العنوان", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "العنوان" + }, + "homeServer": { + "optionHomeServer": "الخادم الرئيسي", + "placeholder": "example.com" + }, + "matrix": { + "description": "تهيئة اتصال خادم Matrix الرئيسي للإشعارات.", + "title": "تهيئة Matrix" + }, + "notificationName": { + "description": "اسم وصفي لقناة الإشعارات", + "optionName": "اسم القناة", + "placeholder": "مثال: تنبيهات الإنتاج", + "title": "اسم القناة" + }, + "pagerDuty": { + "description": "مفتاح تكامل PagerDuty لتلقي التنبيهات.", + "optionIntegrationKey": "مفتاح التكامل", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "مفتاح التكامل" + }, + "roomId": { + "optionRoomId": "معرّف الغرفة", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "اختر نوع قناة الإشعارات المراد إنشاؤها.", + "optionType": "النوع", + "title": "نوع القناة" + }, + "telegram": { + "title": "تهيئة Telegram", + "description": "", + "optionBotToken": "رمز البوت", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "معرّف المحادثة", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "الوجهة" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "إزاحة التخطيط التراكمية (CLS)", + "fcp": "أول رسم للمحتوى (FCP)", + "lcp": "أكبر رسم للمحتوى (LCP)", + "si": "مؤشر السرعة (SI)", + "tbt": "إجمالي وقت الحظر (TBT)" + }, + "legend": { + "title": "تقرير PageSpeed", + "weight": "الوزن" + }, + "pie": { + "title": "تقرير الأداء" + } + }, + "table": { + "headers": { + "pageSpeedScore": "درجة PageSpeed" + } + }, + "fallback": { + "actionButton": "أنشئ مراقباً!", + "checks": "تقديم تقرير عن تجربة المستخدم للصفحة,المساعدة في تحليل سرعة الصفحة,تقديم اقتراحات لتحسين الصفحة", + "title": "يُستخدم مراقب PageSpeed من أجل:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "المنطقة الزمنية للعرض", + "description": "اختر المنطقة الزمنية المستخدمة لعرض التواريخ والأوقات في جميع أنحاء التطبيق.", + "option": { + "timezone": { + "label": "المنطقة الزمنية للعرض" + } + } + }, + "ui": { + "title": "المظهر", + "description": "التبديل بين الوضع الفاتح والداكن، أو تغيير اللغة، أو تخصيص نوع عرض المخططات.", + "option": { + "theme": { + "label": "وضع السمة", + "light": "فاتح", + "dark": "داكن" + }, + "language": { + "label": "اللغة" + }, + "chartType": { + "label": "نوع المخطط", + "histogram": "مدرج تكراري", + "heatmap": "خريطة حرارية" + } + } + }, + "pagespeed": { + "title": "مفتاح Google PageSpeed API", + "description": "أدخل مفتاح Google PageSpeed API لتفعيل مراقبة Google PageSpeed. انقر على إعادة تعيين لتحديث المفتاح.", + "option": { + "apiKey": { + "label": "مفتاح PageSpeed API", + "labelSet": "تم تعيين مفتاح API. انقر على إعادة تعيين لتغييره.", + "placeholder": "أدخل مفتاح Google PageSpeed API الخاص بك" + } + } + }, + "url": { + "title": "عرض IP/URL للمراقب في صفحة الحالة", + "description": "عرض عنوان IP أو رابط المراقب في صفحة الحالة العامة. إذا تم تعطيله، سيظهر فقط اسم المراقب لحماية المعلومات الحساسة.", + "option": { + "showURL": { + "label": "عرض IP/URL في صفحة الحالة", + "enabled": "مفعّل", + "disabled": "معطّل" + } + } + }, + "stats": { + "title": "سجل المراقبة", + "description": "مسح جميع سجلات وإحصائيات المراقبة لفريقك. هذا الإجراء لا يمكن التراجع عنه.", + "option": { + "clear": { + "label": "مسح جميع الإحصائيات. هذا الإجراء لا يمكن التراجع عنه." + } + }, + "dialog": { + "title": "مسح جميع سجلات المراقبة؟", + "description": "لا يمكن التراجع عن هذا" + } + }, + "retention": { + "title": "الاحتفاظ بالفحوصات", + "description": "حدد مدة الاحتفاظ ببيانات الفحص قبل التنظيف التلقائي.", + "option": { + "days": { + "label": "فترة الاحتفاظ (أيام)", + "unlimited": "غير محدود" + } + } + }, + "thresholds": { + "title": "الحدود العامة", + "description": "تعيين حدود عامة لـ CPU والذاكرة والقرص والحرارة لمراقبة البنية التحتية. تنطبق على جميع مراقبات الأجهزة ما لم يتم تجاوزها.", + "option": { + "cpu": { + "label": "حد CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "حد الذاكرة (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "حد القرص (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "حد الحرارة (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "إعدادات البريد الإلكتروني", + "description": "تهيئة إعدادات البريد الإلكتروني لنظامك. يُستخدم لإرسال الإشعارات والتنبيهات.", + "descriptionTransport": "", + "descriptionTransportLink": "اطلع على المواصفات هنا", + "option": { + "host": { + "label": "مضيف البريد - اسم المضيف أو عنوان IP للاتصال", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "منفذ البريد - المنفذ للاتصال", + "placeholder": "587" + }, + "address": { + "label": "عنوان البريد الإلكتروني - يُستخدم للمصادقة", + "placeholder": "you@example.com" + }, + "user": { + "label": "مستخدم البريد - اسم المستخدم للمصادقة، يتجاوز عنوان البريد إذا تم تحديده", + "placeholder": "اتركه فارغاً إذا لم يكن مطلوباً" + }, + "password": { + "label": "كلمة مرور البريد - كلمة المرور للمصادقة", + "labelSet": "تم تعيين كلمة المرور. انقر على إعادة تعيين لتغييرها.", + "placeholder": "أدخل كلمة المرور" + }, + "tlsServername": { + "label": "اسم خادم TLS - اسم مضيف اختياري للتحقق من TLS عندما يكون المضيف عنوان IP", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "مضيف اتصال البريد - اسم المضيف المستخدم في تحية HELO/EHLO", + "placeholder": "localhost" + }, + "secure": { + "label": "استخدام SSL (موصى به): تشفير الاتصال باستخدام SSL/TLS" + }, + "pool": { + "label": "تفعيل تجميع الاتصالات: إعادة استخدام الاتصالات الموجودة لتحسين الأداء" + }, + "ignoreTLS": { + "label": "تعطيل STARTTLS: عدم استخدام TLS حتى لو كان الخادم يدعمه" + }, + "requireTLS": { + "label": "فرض STARTTLS: يتطلب ترقية TLS، يفشل إذا لم يكن مدعوماً" + }, + "rejectUnauthorized": { + "label": "رفض الشهادات غير الصالحة: رفض الاتصالات ذات الشهادات الموقعة ذاتياً أو غير الموثوقة" + } + } + }, + "demoMonitors": { + "title": "مراقبات تجريبية", + "description": "إضافة مراقبات نموذجية لأغراض العرض التوضيحي." + }, + "removeMonitors": { + "title": "إعادة تعيين النظام", + "description": "إزالة جميع المراقبات من نظامك.", + "dialog": { + "title": "إزالة جميع المراقبات؟", + "description": "لا يمكن التراجع عن هذا." + } + }, + "exportMonitors": { + "title": "تصدير المراقبات" + }, + "importExportMonitors": { + "title": "استيراد / تصدير المراقبات", + "description": "استيراد أو تصدير بيانات المراقبات كملف JSON للنسخ الاحتياطي أو النقل." + }, + "about": { + "title": "حول", + "developedBy": "تم التطوير بواسطة Bluewave Labs" + }, + "validation": { + "errorMessage": "يرجى إصلاح أخطاء التحقق التالية:" + } + } + }, + "statusPages": { + "deleteSuccess": "تم حذف صفحة الحالة بنجاح", + "fallback": { + "title": "تُستخدم صفحة الحالة من أجل:", + "checks": "إيصال حالة النظام للمستخدمين وأصحاب المصلحة,عرض معلومات وقت التشغيل في الوقت الفعلي للعامة,بناء الثقة من خلال مراقبة الخدمة الشفافة,تقليل طلبات الدعم أثناء الحوادث", + "actionButton": "أنشئ صفحة حالة!" + }, + "monitorsList": { + "chartTypeHeatmap": "خريطة حرارية", + "chartTypeHistogram": "مدرج تكراري", + "noData": "لا تتوفر بيانات", + "infrastructure": { + "title": "البنية التحتية", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "الذاكرة", + "disk": "القرص", + "usage": "الاستخدام", + "used": "مستخدم", + "total": "الإجمالي" + }, + "uptime": { + "title": "وقت التشغيل", + "responseTime": "وقت الاستجابة" + } + }, + "statusBar": { + "allDown": "جميع الأنظمة متوقفة", + "allUp": "جميع الأنظمة تعمل بشكل طبيعي", + "degraded": "بعض الأنظمة تواجه مشكلات", + "unknown": "تعذر تحديد حالة النظام" + }, + "table": { + "headers": { + "name": "اسم صفحة الحالة", + "url": "الرابط العام" + }, + "published": "منشور", + "unpublished": "غير منشور" + }, + "title": "صفحات الحالة", + "details": { + "empty": { + "title": "لا يوجد شيء هنا بعد", + "addMonitor": "أضف مراقباً للبدء" + } + }, + "form": { + "access": { + "title": "الوصول", + "description": "إذا كانت صفحة الحالة جاهزة، يمكنك وضع علامة منشور عليها.", + "option": { + "published": { + "name": "منشورة ومرئية للعامة" + } + } + }, + "basicInfo": { + "title": "المعلومات الأساسية", + "description": "حدد اسم الشركة والنطاق الفرعي الذي تشير إليه صفحة الحالة.", + "option": { + "name": { + "label": "اسم الشركة", + "placeholder": "Acme Inc." + }, + "url": { + "label": "عنوان صفحة الحالة", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "المراقبات", + "description": "اختر المراقبات لعرضها في صفحة الحالة.", + "noMonitors": "لم يتم اختيار مراقبات", + "option": { + "monitors": { + "label": "اختر المراقبات", + "placeholder": "ابحث واختر المراقبات..." + } + } + }, + "timezone": { + "title": "المنطقة الزمنية", + "description": "اختر المنطقة الزمنية التي ستُعرض بها صفحة الحالة.", + "option": { + "timezone": { + "label": "المنطقة الزمنية", + "placeholder": "اختر المنطقة الزمنية..." + } + } + }, + "appearance": { + "title": "المظهر", + "description": "حدد المظهر الافتراضي لصفحة الحالة العامة.", + "option": { + "logo": { + "label": "الشعار" + }, + "color": { + "label": "لون العلامة التجارية" + } + } + }, + "features": { + "title": "الميزات", + "description": "تهيئة المعلومات المعروضة في صفحة الحالة.", + "option": { + "showCharts": { + "label": "إظهار مخططات وقت الاستجابة" + }, + "showUptimePercentage": { + "label": "إظهار نسبة وقت التشغيل" + }, + "showAdminLoginLink": { + "label": "إظهار رابط تسجيل دخول المسؤول" + }, + "showInfrastructure": { + "label": "إظهار مقاييس البنية التحتية" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "البحث في المراقبات..." + } + }, + "table": { + "headers": { + "responseTime": "وقت الاستجابة" + } + }, + "fallback": { + "actionButton": "أنشئ مراقباً!", + "checks": "التحقق مما إذا كانت المواقع أو الخوادم متصلة ومتجاوبة,تنبيه الفرق حول التوقف أو مشكلات الأداء,مراقبة نقاط نهاية HTTP والـ Ping والحاويات والمنافذ,تتبع اتجاهات وقت التشغيل والموثوقية التاريخية", + "title": "يُستخدم مراقب وقت التشغيل من أجل:" } } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" - }, - "alertBox": "", - "description": "", - "retryButton": { - "default": "", - "processing": "" - } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" - }, - "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" - }, - "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "testNotification": "", - "dialogDeleteTitle": "", - "dialogDeleteConfirm": "" - }, - "notificationConfig": { - "title": "", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "", - "sendTestNotifications": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "", - "diagnostics": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" - } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "", - "intervalHeader": "" - }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" - }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "", - "deleteSuccess": "", - "deleteFailed": "", - "details": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" - }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "", - "toastEmailRequiredFieldsError": "" - }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" - }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" - }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "" - }, - "title": "", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" - }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" - }, - "globalThresholds": { - "title": "", - "description": "" - } - }, - "statusPageCreate": { - "buttonSave": "" - }, - "incidentsOptionsHeaderFilterResolved": "", - "settingsSave": "", - "statusPageCreateAppearanceTitle": "", - "confirmPassword": "", - "monitorHooks": { - "failureAddDemoMonitors": "", - "successAddDemoMonitors": "" - }, - "settingsAppearance": "", - "settingsDisplayTimezone": "", - "settingsGeneralSettings": "", - "incidentsOptionsHeaderTotalIncidents": "", - "statusPage": { - "deleteSuccess": "", - "deleteFailed": "", - "createSuccess": "", - "updateSuccess": "", - "generalSettings": "", - "contents": "", - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "testNotificationsDisabled": "", - "incidentsTableResolvedAt": "", - "incidentsTableActionResolve": "", - "checkHooks": { - "failureResolveOne": "", - "failureResolveAll": "", - "failureResolveMonitor": "" - }, - "checkFormError": "", - "diagnosticsPage": { - "diagnosticDescription": "", - "statsDescription": "", - "gauges": { - "heapAllocationTitle": "", - "heapAllocationSubtitle": "", - "heapUsageTitle": "", - "heapUsageSubtitle": "", - "heapUtilizationTitle": "", - "heapUtilizationSubtitle": "", - "instantCpuUsageTitle": "", - "instantCpuUsageSubtitle": "" - }, - "stats": { - "eventLoopDelayTitle": "", - "uptimeTitle": "", - "usedHeapSizeTitle": "", - "totalHeapSizeTitle": "", - "osMemoryLimitTitle": "" - } - }, - "pageSpeedLighthouseAPI": "", - "time": { - "threeMinutes": "", - "fiveMinutes": "", - "tenMinutes": "", - "twentyMinutes": "", - "oneHour": "", - "oneDay": "", - "oneWeek": "", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" - }, - "general": { - "noOptionsFound": "" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "pageSpeed": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "editUserPage": { - "form": { - "email": "", - "firstName": "", - "lastName": "", - "role": "", - "save": "" - }, - "table": { - "actionHeader": "", - "roleHeader": "" - }, - "title": "", - "toast": { - "successUserUpdate": "", - "validationErrors": "" - } - }, - "incidentsPageActionResolveMonitor": "", - "incidentsPageActionResolveAll": "", - "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + } } diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json index 837b49803..780a7a723 100644 --- a/client/src/locales/cs.json +++ b/client/src/locales/cs.json @@ -1,1108 +1,1305 @@ { - "submit": "Odeslat", - "title": "", - "distributedStatusHeaderText": "", - "distributedStatusSubHeaderText": "", - "settingsDisabled": "Vypnuto", - "settingsSuccessSaved": "Nastavení bylo úspěšně uloženo", - "settingsFailedToSave": "Nepodařilo se uložit nastavení", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", - "https": "", - "http": "", - "monitor": "", - "aboutus": "", - "now": "", - "delete": "", - "configure": "", - "responseTime": "", - "ms": "", - "bar": "", - "area": "", - "country": "", - "city": "", - "response": "", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "", - "webhookSendError": "", - "webhookUnsupportedPlatform": "", - "distributedRightCategoryTitle": "", - "distributedStatusServerMonitors": "", - "distributedStatusServerMonitorsDescription": "", - "distributedUptimeCreateSelectURL": "", - "distributedUptimeCreateChecks": "", - "distributedUptimeCreateChecksDescription": "", - "distributedUptimeCreateIncidentNotification": "", - "distributedUptimeCreateIncidentDescription": "", - "distributedUptimeCreateAdvancedSettings": "", - "distributedUptimeDetailsNoMonitorHistory": "", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "", - "notifications": { - "enableNotifications": "", - "testNotification": "", - "addOrEditNotifications": "", - "slack": { - "label": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "discord": { - "label": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "telegram": { - "label": "", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "", - "chatIdLabel": "", - "chatIdPlaceholder": "", - "fieldsRequired": "" - }, - "webhook": { - "label": "", - "description": "", - "urlLabel": "", - "urlPlaceholder": "", - "urlRequired": "" - }, - "testNotificationDevelop": "", - "integrationButton": "", - "testSuccess": "", - "testFailed": "", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [""], - "actionButton": "" - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "", - "add": "", - "monitors": "", - "distributedUptimeStatusCreateStatusPage": "", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "", - "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "", - "distributedUptimeStatus60Days": "", - "distributedUptimeStatus90Days": "", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "", - "incidentsTableDateTime": "", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", - "incidentsOptionsHeaderLastHour": "", - "incidentsOptionsHeaderLastDay": "", - "incidentsOptionsHeaderLastWeek": "", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "", - "infrastructureServerUrlLabel": "", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "", - "cpu": "", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "", - "avgCpuTemperature": "", - "memory": "", - "disk": "", - "uptime": "", - "os": "", - "host": "", - "actions": "", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "", - "integrationsSlackInfo": "", - "integrationsDiscord": "", - "integrationsDiscordInfo": "", - "integrationsZapier": "", - "integrationsZapierInfo": "", - "commonSave": "", - "createYour": "", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", - "timezone": "", - "features": "", - "administrator": "", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "", - "window": "", - "cancel": "", - "message": "", - "low": "", - "high": "", - "statusCode": "", - "date&Time": "", - "type": "", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", - "createA": "", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", - "ago": "", - "companyName": "", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "", - "showUptimePercentage": "", - "removeLogo": "", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "", - "invalidFileFormat": "", - "invalidFileSize": "", - "ClickUpload": "", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", - "EmailDescriptionText": "", - "YourPhoto": "", - "PhotoDescriptionText": "", - "save": "", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", - "DeleteWarningTitle": "", - "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "", - "fallbackPage": "", - "invalidFileType": "", - "uploadFailed": "" - }, - "DeleteAccountTitle": "", - "DeleteAccountButton": "", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "", - "greeting": { - "prepend": "", - "append": "", - "overview": "" - }, - "roles": { - "superAdmin": "", - "admin": "", - "teamMember": "", - "demoUser": "" - }, - "teamPanel": { - "teamMembers": "", - "filter": { - "all": "", - "member": "" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "", - "selectRole": "", - "inviteLink": "", - "cancel": "", - "noMembers": "", - "getToken": "", - "emailToken": "", - "table": { - "name": "", - "email": "", - "role": "", - "created": "" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "", - "resumed": "", - "active": "" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "", - "incidents": "", - "statusPages": "", - "maintenance": "", - "integrations": "", - "settings": "", - "support": "", - "discussions": "", - "docs": "", - "changelog": "", - "profile": "", - "password": "", - "team": "", - "logOut": "", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", - "commonSaving": "", - "navControls": "", - "incidentsPageTitle": "", - "passwordPanel": { - "passwordChangedSuccess": "", - "passwordInputIncorrect": "", - "currentPassword": "", - "enterCurrentPassword": "", - "newPassword": "", - "enterNewPassword": "", - "confirmNewPassword": "", - "passwordRequirements": "", - "saving": "" - }, - "emailSent": "", - "failedToSendEmail": "", - "settingsTestEmailSuccess": "", - "settingsTestEmailFailed": "", - "settingsTestEmailFailedWithReason": "", - "settingsTestEmailUnknownError": "", - "statusMsg": { - "paused": "", - "up": "", - "down": "", - "pending": "" - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "", - "game": "", - "https": "" - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "Přepnout mezi světlým a tmavým motivem" + "auth": { + "roles": { + "admin": "Administrátor", + "demo": "Demo", + "superadmin": "Superadministrátor", + "user": "Uživatel" + } }, - "toasts": { - "networkError": "Chyba připojení k síti", - "checkConnection": "Zkontrolujte prosím své připojení k síti", - "unknownError": "Nastala neznámá chyba" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "Pokračovat", - "back": "Zpět" + "alerts": { + "pageSpeedApiKey": { + "content": "Upozornění: Zatím jste nepřidali klíč Google PageSpeed API. Přejděte do Nastavení a přidejte ho. Bez něj nebude monitor PageSpeed fungovat." + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "Podrobnosti", + "home": "Domů" + }, + "buttons": { + "addMember": "Přidat člena", + "cancel": "Zrušit", + "close": "Zavřít", + "configure": "Konfigurovat", + "confirm": "Potvrdit", + "create": "Vytvořit", + "delete": "Smazat", + "generateToken": "Vygenerovat token", + "incidents": "Incidenty", + "inviteMember": "Pozvat člena", + "pause": "Pozastavit", + "resume": "Obnovit", + "save": "Uložit", + "sendInvite": "Odeslat pozvánku", + "test": "Test", + "testNotifications": "Testovat oznámení", + "toggleTheme": "Přepnout mezi světlým a tmavým motivem", + "flushQueue": "Vyprázdnit frontu", + "notFound": "Přejít na hlavní přehled", + "resetPassword": "Obnovit heslo", + "reset": "", + "clear": "Vymazat", + "addDemo": "Přidat ukázkové monitory", + "removeMonitors": "Odebrat monitory", + "sendTestEmail": "Odeslat testovací email", + "exportToJSON": "Exportovat do JSON", + "importFromJSON": "Importovat z JSON", + "clearFilters": "Vymazat filtry", + "removeUser": "Odebrat uživatele" + }, + "charts": { + "labels": { + "averageResponseTime": "Průměrná doba odezvy", + "downtime": "Výpadek", + "high": "", + "low": "", + "uptime": "Dostupnost" }, - "inputs": { - "email": { - "label": "E-mail", - "placeholder": "jan.novak@domena.cz", - "errors": { - "empty": "Zadejte prosím e-mailovou adresu", - "invalid": "Překontrolujte si prosím správnost zadané e-mailové adresy" - } - }, - "password": { - "label": "Heslo", - "rules": { - "length": { - "beginning": "Heslo musí být dlouhé alespoň", - "highlighted": "8 znaků" - }, - "special": { - "beginning": "Heslo musí obsahovat alespoň", - "highlighted": "1 speciální znak" - }, - "number": { - "beginning": "Heslo musí obsahovat alespoň", - "highlighted": "1 číslo" - }, - "uppercase": { - "beginning": "Heslo musí obsahovat alespoň", - "highlighted": "1 velké písmeno" - }, - "lowercase": { - "beginning": "Heslo musí obsahovat alespoň", - "highlighted": "1 malé písmeno" - }, - "match": { - "beginning": "Obě hesla se", - "highlighted": "musí shodovat" - } - }, - "errors": { - "empty": "Zadejte prosím heslo", - "length": "Heslo musí být dlouhé alespoň 8 znaků", - "uppercase": "Heslo musí obsahovat alespoň 1 velké písmeno", - "lowercase": "Heslo musí obsahovat alespoň 1 malé písmeno", - "number": "Heslo musí obsahovat alespoň 1 číslo", - "special": "Heslo musí obsahovat alespoň 1 speciální znak", - "incorrect": "Zadané heslo neodpovídá tomu, co bylo nastaveno" - } - }, - "passwordConfirm": { - "label": "Potvrzení hesla", - "placeholder": "Pro ověření správnosti zadejte heslo ještě jednou", - "errors": { - "empty": "Zadejte prosím své heslo ještě jednou, aby mohla být potvrzena jeho správnost (odhalí překlepy)", - "different": "Zadaná hesla se liší, takže je nejspíše jedno z nich špatně zapsané" - } - }, - "firstName": { - "label": "Jméno", - "placeholder": "Jan", - "errors": { - "empty": "Zadejte prosím své křestní jméno", - "length": "Jméno se musí vejít do 50 znaků", - "pattern": "Součástí jména mohou být pouze písmena, mezery apostrofy nebo spojovníky" - } - }, - "lastName": { - "label": "Příjmení", - "placeholder": "Novák", - "errors": { - "empty": "Zadejte prosím své příjmení", - "length": "Příjmení se musí vejít do 50 znaků", - "pattern": "Součástí příjmení mohou být pouze písmena, mezery apostrofy nebo spojovníky" + "histogram": { + "avg": "Prům.: {{value}} ms", + "max": "Max: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Tuto akci nelze vrátit zpět.", + "title": "Opravdu chcete toto smazat?" + } + }, + "labels": { + "active": "Aktivní", + "paused": "pozastaveno", + "na": "N/A", + "resolved": "Vyřešeno", + "responseTime": "Doba odezvy" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Role" } } }, - "errors": { - "validation": "Nastala chyba při ověřování dat." - }, - "fields": { - "password": { - "errors": { - "incorrect": "" + "name": { + "option": { + "firstName": { + "label": "Jméno", + "placeholder": "Jan" + }, + "lastName": { + "label": "Příjmení", + "placeholder": "Novák" } - }, - "role": { - "errors": { - "min": "" + } + }, + "email": { + "option": { + "email": { + "label": "Email", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "Přihlášení", - "subheadings": { - "stepOne": "Zadejte svůj e-mail", - "stepTwo": "Zadejte své heslo" - }, - "links": { - "forgotPassword": "Zapomněli jste heslo? Obnovte si ho", - "register": "Ještě nemáte účet? Zaregistrujte se", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "Vítejte zpět", - "incorrectPassword": "Nesprávné heslo" - }, + "table": { + "empty": "Zde nic není", + "headers": { + "actions": "", + "dateTime": "Datum a čas", + "message": "Zpráva", + "monitor": "Monitor", + "monitorId": "ID monitoru", + "name": "Název", + "status": "Stav", + "type": "", + "url": "URL", + "interval": "Interval", + "active": "Aktivní", + "responseTime": "Doba odezvy" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "Klikněte pro nahrání", + "dragAndDrop": "", + "supportedFormats": "", + "maxSize": "", + "orDragAndDrop": "nebo přetáhněte soubor", "errors": { - "password": { - "incorrect": "" - } - }, - "welcome": "" + "invalidFileSize": "", + "invalidFileFormat": "" + } }, - "registration": { - "heading": { - "superAdmin": "Vytvoření superadministrátora", - "user": "Registrace" - }, - "subheadings": { - "stepOne": "Zadejte své osobní údaje", - "stepTwo": "Zadejte svůj e-mail", - "stepThree": "Nastavte si heslo" + "headerStatusPageControls": { + "publicLink": "" + }, + "headerTimeRange": { + "labels": { + "day": "Den", + "month": "Měsíc", + "recent": "Nedávné", + "week": "Týden" }, "description": { - "superAdmin": "Pro začátek vytvořte účet superadministrátora", - "user": "Zaregistrujte se jako uživatel a požádejte svého superadministrátora, aby vám přidělil oprávnění k hlídačům" - }, - "gettingStartedButton": { - "superAdmin": "Vytvořit účet superadministrátora", - "user": "Zaregistrovat se jako běžný uživatel" - }, - "termsAndPolicies": "Vytvořením účtu souhlasíte s našimi Podmínkami použití (anglicky) a Podmínkami zpracování osobních údajů (anglicky).", - "links": { - "login": "Máte již vytvořený účet? Přihlašte se" - }, - "toasts": { - "success": "Vítejte! Váš účet byl úspěšně vytvořen." - }, - "welcome": "" + "recent": "Zobrazení statistik za poslední 2 hodiny.", + "day": "Zobrazení statistik za posledních 24 hodin.", + "week": "Zobrazení statistik za posledních 7 dní.", + "month": "Zobrazení statistik za posledních 30 dní." + } }, - "forgotPassword": { - "heading": "Zapomenuté heslo", - "subheadings": { - "stepOne": "Nebojte se, zašleme vám pokyny pro obnovení hesla.", - "stepTwo": "Odkaz pro obnovení hesla byl odeslán na ", - "stepThree": "Nové heslo se musí lišit od těch, které jste již použili.", - "stepFour": "Heslo bylo úspěšně obnoveno. Klepněte na tlačítko a dojde k čarovnému přihlášení." + "offlineBanner": { + "serverUnreachable": "Nelze se spojit se serverem", + "retry": "Zkusit znovu", + "retrying": "Opakuji pokus...", + "reconnected": "Podařilo se úspěšně obnovit spojení se serverem" + }, + "sidebar": { + "menu": { + "uptime": "", + "pagespeed": "", + "infrastructure": "", + "notifications": "", + "checks": "Kontroly", + "incidents": "", + "statusPages": "", + "maintenance": "", + "logs": "", + "settings": "" }, - "buttons": { - "openEmail": "Otevřít e-mailovou aplikaci", - "resetPassword": "Obnovit heslo" + "bottomMenu": { + "support": "", + "discussions": "", + "docs": "", + "changelog": "" }, - "imageAlts": { - "passwordKey": "Ikonka přístupového klíče", - "email": "Ikonka e-mailu", - "lock": "Ikonka zámku", - "passwordConfirm": "Ikonka potvrzeného hesla" + "accountMenu": { + "profile": "Profil", + "password": "Heslo", + "team": "Tým" }, - "links": { - "login": "Vrátit se k Přihlášení", - "resend": "Nedorazil vám e-mail? Klepněte pro opětovné zaslání" + "starPrompt": { + "title": "Ohodnoťte Checkmate", + "description": "Sledujte nejnovější vydání a pomozte rozšířit komunitu na GitHubu" }, - "toasts": { - "sent": "Instrukce vám dorazí na .", - "emailNotFound": "Nepodařilo se nalézt e-mailovou adresu.", - "redirect": "Přesměrování proběhne za ...", - "success": "Heslo bylo úspěšně obnoveno.", - "error": "Obnova hesla se nepodařila. Zkuste to prosím znovu nebo kontaktujte podporu." + "authFooter": { + "navControls": "", + "logOut": "", + "roles": { + "superAdmin": "Superadministrátor", + "admin": "Administrátor", + "user": "Uživatel", + "demoUser": "" + } + } + }, + "starPrompt": { + "title": "Ohodnoťte Checkmate", + "description": "Sledujte nejnovější vydání a pomozte rozšířit komunitu na GitHubu" + } + }, + "pages": { + "notFound": { + "title": "Ale ne! Upustili jste své sushi!", + "subtitle": "Buď tato URL neexistuje, nebo k ní nemáte přístup." + }, + "account": { + "tabs": { + "profile": "Profil", + "password": "Heslo", + "team": "Tým" + }, + "form": { + "name": { + "title": "Jméno", + "description": "Aktualizujte své osobní údaje" + }, + "photo": { + "title": "Profilová fotografie", + "description": "Nahrajte profilový obrázek" + }, + "currentPassword": { + "title": "Aktuální heslo", + "description": "Zadejte aktuální heslo pro ověření totožnosti", + "option": { + "label": "Aktuální heslo", + "placeholder": "Zadejte aktuální heslo" + } + }, + "newPassword": { + "title": "Nové heslo", + "description": "Zvolte silné heslo s alespoň 8 znaky", + "option": { + "newPassword": { + "label": "Nové heslo", + "placeholder": "Zadejte nové heslo" + }, + "confirm": { + "label": "Potvrzení hesla", + "placeholder": "Potvrďte nové heslo" + } + } + }, + "deleteAccount": { + "title": "Smazat účet", + "description": "Tato akce je trvalá a nelze ji vrátit zpět" + } + }, + "team": { + "filter": { + "placeholder": "Filtrovat podle role", + "all": "", + "admin": "Administrátor", + "member": "" + }, + "table": { + "headers": { + "email": "Email", + "role": "Role", + "created": "" + } + }, + "addMember": { + "title": "Registrace nového člena týmu", + "description": "Vytvořte nový uživatelský účet. Po vytvoření bezpečně sdílejte přihlašovací údaje." + }, + "invite": { + "title": "Pozvat člena týmu", + "description": "Odešlete pozvánku k připojení do vašeho týmu", + "email": { + "label": "Emailová adresa", + "placeholder": "Zadejte emailovou adresu" + }, + "role": { + "label": "Role", + "placeholder": "Vyberte roli" + }, + "linkLabel": "Odkaz pro pozvánku" + } + } + }, + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "Heslo musí být dlouhé alespoň", + "highlighted": "8 znaků" + }, + "lowercase": { + "beginning": "Heslo musí obsahovat alespoň", + "highlighted": "1 malé písmeno" + }, + "match": { + "beginning": "Obě hesla se", + "highlighted": "musí shodovat" + }, + "number": { + "beginning": "Heslo musí obsahovat alespoň", + "highlighted": "1 číslo" + }, + "special": { + "beginning": "Heslo musí obsahovat alespoň", + "highlighted": "1 speciální znak" + }, + "uppercase": { + "beginning": "Heslo musí obsahovat alespoň", + "highlighted": "1 velké písmeno" + } + }, + "form": { + "option": { + "email": { + "label": "Email", + "placeholder": "me@example.com" + }, + "password": { + "label": "Heslo", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Potvrzení hesla" + } + } + } + }, + "login": { + "title": "Vítejte zpět v Checkmate!", + "subtitle": "Přihlaste se pro pokračování", + "submit": "Přihlásit se", + "links": { + "forgotPassword": { + "text": "Zapomněli jste heslo?", + "linkText": "Obnovit heslo" + }, + "register": { + "text": "Nemáte účet?", + "linkText": "Zaregistrujte se zde" + } + } + }, + "register": { + "title": "Vítejte v Checkmate!", + "subtitle": "Zaregistrujte se a začněte", + "submit": "Registrovat" + }, + "forgotPassword": { + "title": "Zapomněli jste heslo?", + "subtitle": "Žádný problém, zašleme vám pokyny k obnovení.", + "submit": "Požádat o obnovení", + "links": { + "login": { + "text": "Zpět na", + "linkText": "přihlášení" + } + } + }, + "setNewPassword": { + "title": "Obnovte své heslo", + "subtitle": "Vaše nové heslo musí být odlišné od dříve použitých hesel." + } + }, + "checks": { + "selects": { + "monitor": { + "all": "Všechny monitory" + }, + "status": { + "all": "Vše", + "down": "Nedostupné", + "up": "Dostupné" + } + }, + "table": { + "empty": "Žádné neúspěšné kontroly v tomto období", + "headers": { + "statusCode": "Stavový kód", + "location": "Umístění" + } + } + }, + "common": { + "monitors": { + "actions": { + "configure": "Konfigurovat", + "delete": "Smazat", + "generateToken": "Vygenerovat token", + "details": "Podrobnosti", + "incidents": "Incidenty", + "inviteMember": "Pozvat člena", + "openSite": "Otevřít stránku", + "pause": "Pozastavit", + "resume": "Obnovit" + }, + "statBoxes": { + "activeFor": "Aktivní po dobu", + "certificateExpiry": "Expirace certifikátu", + "lastCheck": "Poslední kontrola", + "lastResponseTime": "Poslední doba odezvy" + }, + "status": { + "down": "nedostupný", + "breached": "překročen práh", + "initializing": "inicializace", + "maintenance": "údržba", + "paused": "pozastaveno", + "total": "celkem", + "up": "dostupný" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Hra", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Port", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infrastruktura", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } + }, + "createMonitor": { + "form": { + "advanced": { + "description": "Volitelná nastavení pro pokročilé použití", + "option": { + "advancedMatching": { + "label": "Použít pokročilé porovnávání" + }, + "expectedValue": { + "label": "Očekávaná hodnota" + }, + "jsonPath": { + "description": "Tento výraz bude vyhodnocen proti odpovědi ve formátu JSON a výsledek bude použit k porovnání s očekávanou hodnotou. Dokumentaci dotazovacího jazyka naleznete na jmespath.org.", + "label": "Výraz JSONPath" + }, + "matchMethod": { + "label": "Metoda porovnání", + "equal": "", + "include": "", + "regex": "" + } + }, + "title": "Pokročilá nastavení" + }, + "frequency": { + "description": "Jak často chcete kontrolovat stav tohoto monitoru?", + "option": { + "frequency": { + "label": "Frekvence kontroly", + "value": { + "fifteenMinutes": "15 minut", + "fifteenSeconds": "", + "fiveMinutes": "5 minut", + "fourMinutes": "", + "oneMinute": "", + "tenMinutes": "10 minut", + "thirtyMinutes": "30 minut", + "thirtySeconds": "", + "threeMinutes": "", + "twoMinutes": "" + } + } + }, + "title": "Frekvence kontroly" + }, + "general": { + "option": { + "container": { + "label": "Název/ID kontejneru", + "placeholder": "my-app nebo abcd1234" + }, + "host": { + "label": "Hostitel", + "placeholder": "192.168.1.100 nebo example.com" + }, + "name": { + "label": "Zobrazovaný název", + "placeholder": "např. Moje stránka" + }, + "secret": { + "label": "Autorizační tajný klíč", + "placeholder": "Zadejte svůj tajný klíč" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "", + "placeholder": "" + }, + "port": { + "label": "Port ke sledování", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Název služby", + "placeholder": "např. my.service.v1 (nechte prázdné pro celkový stav)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Obecná nastavení", + "description": { + "http": "Zadejte URL nebo IP adresu ke sledování (např. https://example.com/ nebo 192.168.1.100) a přidejte jasný zobrazovaný název, který se zobrazí na přehledu.", + "ping": "", + "port": "", + "docker": "", + "game": "", + "pagespeed": "Sledujte výkon načítání stránky, metriky Core Web Vitals a hodnocení optimalizace vašeho webu.", + "grpc": "Zadejte název hostitele a port gRPC serveru, volitelně zadejte název služby pro kontrolu stavu a přidejte zobrazovaný název.", + "websocket": "Zadejte WebSocket URL ke sledování (např. wss://example.com/socket) a přidejte zobrazovaný název.", + "hardware": "Sledujte využití CPU, paměti, disku a teplotu vaší infrastruktury." + } + }, + "ignoreTls": { + "description": "Nastavte ověřování TLS/SSL certifikátů pro HTTPS připojení.", + "option": { + "tls": { + "label": "Ignorovat chyby TLS/SSL" + } + }, + "title": "Nastavení TLS/SSL" + }, + "incidents": { + "description": "Klouzavé okno se používá k určení, kdy je monitor nedostupný. Stav monitoru se změní pouze tehdy, když procento kontrol v klouzavém okně dosáhne zadané hodnoty.", + "option": { + "checks": { + "label": "Počet kontrol v klouzavém okně" + }, + "percentage": { + "label": "Jaké procento kontrol v klouzavém okně musí selhat/uspět, než se změní stav monitoru?" + } + }, + "title": "Incidenty" + }, + "notifications": { + "description": "Vyberte kanály pro oznámení, které chcete použít", + "title": "Oznámení" + }, + "type": { + "description": "Vyberte typ kontroly, která se má provádět", + "optionDockerDescription": "Použijte Docker ke sledování, zda je kontejner spuštěn.", + "optionGameDescription": "Sledujte, zda je konkrétní herní server online.", + "optionGrpcDescription": "Sledujte gRPC služby pomocí standardního protokolu pro kontrolu stavu.", + "optionWebSocketDescription": "Sledujte WebSocket koncové body pro stav připojení a dobu odezvy.", + "optionHttpDescription": "Použijte HTTP(S) ke sledování vašeho webu nebo API koncového bodu.", + "optionPingDescription": "Použijte ICMP Ping ke sledování, zda je server online.", + "optionPortDescription": "Sledujte, zda je konkrétní port na serveru otevřen.", + "title": "Typ" + }, + "thresholds": { + "title": "Prahové hodnoty upozornění", + "description": "Definujte prahové hodnoty, při kterých se mají spouštět upozornění pro tento monitor infrastruktury.", + "option": { + "cpuThreshold": { + "label": "Prahová hodnota upozornění CPU (%)" + }, + "memoryThreshold": { + "label": "Prahová hodnota upozornění paměti (%)" + }, + "diskThreshold": { + "label": "Prahová hodnota upozornění disku (%)" + }, + "tempThreshold": { + "label": "Prahová hodnota upozornění teploty (°C)" + } + } + }, + "geoChecks": { + "title": "Geograficky distribuované kontroly", + "description": "Spouštějte kontroly z více geografických lokací pro sledování globální dostupnosti a výkonu.", + "option": { + "enabled": { + "label": "Povolit geograficky distribuované kontroly" + }, + "locations": { + "label": "Lokace", + "placeholder": "Vyberte lokace", + "options": { + "EU": "Evropa", + "NA": "Severní Amerika", + "AS": "Asie", + "SA": "Jižní Amerika", + "AF": "Afrika", + "OC": "Oceánie" + } + }, + "interval": { + "label": "Interval kontroly", + "value": { + "fiveMinutes": "5 minut", + "tenMinutes": "10 minut", + "fifteenMinutes": "15 minut", + "thirtyMinutes": "30 minut" + } + } + } + }, + "url": { + "title": "IP/URL monitoru na stavové stránce", + "description": "Zobrazí IP adresu nebo URL monitoru na veřejné stavové stránce. Pokud je tato možnost zakázána, zobrazí se pouze název monitoru pro ochranu citlivých informací.", + "option": { + "showURL": { + "label": "Zobrazit IP/URL na stavové stránce", + "enabled": "Povoleno", + "disabled": "Zakázáno" + } + } + }, + "stats": { + "title": "Historie monitoru", + "description": "Vymazat veškerou historii sledování a statistiky vašeho týmu. Tato akce je nevratná.", + "buttonText": "Vymazat všechny statistiky", + "dialog": { + "title": "Vymazat veškerou historii sledování?", + "description": "Toto trvale smaže veškerou historii sledování, statistiky a data kontrol vašeho týmu. Tuto akci nelze vrátit zpět.", + "confirm": "Ano, vymazat všechny statistiky" + } + } + } + }, + "editUser": { + "form": { + "roles": { + "title": "Role", + "description": "Přiřaďte role uživateli. Lze vybrat více rolí." + } + }, + "dialog": { + "removeUser": { + "title": "Odebrat uživatele", + "content": "Opravdu chcete odebrat uživatele {{name}} z vašeho týmu? Tuto akci nelze vrátit zpět." + } + } + }, + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Vyřešit incident", + "option": { + "comment": { + "label": "Komentář (volitelné)", + "placeholder": "Přidejte komentář k vyřešení..." + } + } + }, + "details": { + "analysis": "Analýza incidentu", + "comment": "Komentář:", + "detailsLabel": "Podrobnosti", + "downtime": "Výpadek:", + "message": "Zpráva:", + "monitor": "Monitor:", + "overview": "Přehled", + "resolutionDetails": "Podrobnosti řešení", + "resolutionType": "Typ:", + "resolutionTypes": { + "automatic": "Automatický", + "manual": "Manuální" + }, + "resolve": "Vyřešit incident", + "resolvedAt": "Vyřešeno v:", + "resolvedBy": "Vyřešil:", + "startedAt": "Začátek:", + "status": "Stav:", + "statusCode": "Stavový kód:", + "timeline": "Časová osa", + "title": "Podrobnosti incidentu", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Všechny monitory", + "monitor": "Monitor", + "resolutionType": "Typ řešení", + "resolutionTypes": { + "manual": "Manuální", + "automatic": "Automatický", + "all": "Vše" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Aktivní incidenty", + "active_zero": "Žádné aktivní incidenty", + "active_one": "{{count}} aktivní incident", + "active_other": "{{count}} aktivních incidentů" + }, + "incidentStats": { + "avgResolutionTime": "Průměrná doba řešení", + "mostAffectedMonitor": "Nejvíce postižený monitor", + "title": "Statistiky incidentů", + "totalIncidents": "Celkem incidentů" + }, + "latestIncidents": { + "title": "Nejnovější incidenty" + } + }, + "table": { + "actions": { + "details": "Podrobnosti", + "goToMonitor": "Přejít na monitor", + "resolveManually": "Vyřešit ručně" + }, + "activeIncidents": "Aktivní incidenty", + "headers": { + "endTime": "Čas ukončení", + "resolutionType": "Typ řešení", + "startTime": "", + "statusCode": "Stavový kód" + }, + "resolvedIncidents": "Vyřešené incidenty", + "status": { + "active": "Aktivní", + "resolved": "Vyřešeno" + } + } + }, + "infrastructure": { + "charts": { + "labels": { + "cpu": "Využití CPU", + "disk": "Využití disku", + "memory": "Využití paměti", + "netBytesRecv": "{{name}} - přijaté bajty", + "netBytesSent": "{{name}} - odeslané bajty", + "temp": "Teplota" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Maximální frekvence", + "title": "Využití CPU", + "upperLabel": "Aktuální frekvence" + }, + "disk": { + "lowerLabel": "Volné", + "title": "Využití disku {{idx}}", + "upperLabel": "Použité" + }, + "memory": { + "lowerLabel": "Volné", + "title": "Využití paměti", + "upperLabel": "Použité" + } + }, + "statBoxes": { + "avgCpuTemperature": "", + "cpuFrequency": "", + "cpuLogical": "", + "cpuPhysical": "", + "disk": "Disk", + "memory": "Paměť", + "os": "" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Disk", + "memory": "Paměť" + } + }, + "tabs": { + "labels": { + "network": "", + "overview": "Přehled" + } + }, + "fallback": { + "actionButton": "Vytvořte monitor!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Monitor infrastruktury slouží k:" + } + }, + "logs": { + "tabs": { + "diagnostics": "", + "logs": "", + "queue": "" + }, + "logLevelSelect": { + "label": "Úroveň logování" + }, + "jobQueue": "Fronta úloh", + "failedJobs": "Neúspěšné úlohy", + "noLogs": "Žádné logy nebyly nalezeny", + "metrics": { + "jobs": "Úlohy", + "activeJobs": "Aktivní úlohy", + "failingJobs": "Selhávající úlohy", + "totalRuns": "Celkem spuštění", + "totalFailures": "Celkem selhání" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Zpoždění smyčky událostí", + "uptime": "Doba provozu", + "usedHeapSize": "Použitá velikost haldy", + "totalHeapSize": "Celková velikost haldy", + "osMemoryLimit": "Limit paměti OS" + }, + "gauges": { + "heapAllocation": "Alokace haldy", + "heapUsage": "Využití haldy", + "heapUtilization": "Utilizace haldy", + "instantCpuUsage": "Okamžité využití CPU", + "availableMemoryPercentage": "% dostupné paměti", + "allocatedPercentage": "% alokováno", + "usedSPercentage": "% z 1s využito CPU", + "total": "Celkem", + "used": "Použité" + } + }, + "table": { + "headers": { + "timestamp": "Časové razítko", + "level": "Úroveň", + "service": "Služba", + "method": "Metoda", + "monitorId": "ID monitoru", + "runCount": "Počet spuštění", + "failCount": "Počet selhání", + "lastRunAt": "Poslední spuštění", + "lockedAt": "Uzamčeno v", + "lastFinishedAt": "Naposledy dokončeno", + "lastRunTook": "Poslední běh trval", + "lastFailedAt": "Poslední selhání", + "failReason": "Důvod selhání" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Vytvořte okno údržby!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Okno údržby slouží k:" + }, + "table": { + "headers": { + "nextWindow": "", + "repeat": "" + } + }, + "form": { + "general": { + "title": "Obecná nastavení", + "description": "Nastavte název a možnost opakování pro okno údržby.", + "option": { + "name": { + "label": "Název", + "placeholder": "např. Týdenní údržba" + }, + "repeat": { + "label": "Opakování" + } + } + }, + "startDate": { + "title": "Datum zahájení", + "description": "Vyberte datum zahájení okna údržby.", + "option": { + "startDate": { + "label": "Datum zahájení" + } + } + }, + "startTime": { + "title": "Čas zahájení", + "description": "Nastavte čas zahájení a dobu trvání okna údržby. Všechny hodnoty jsou v UTC", + "option": { + "duration": { + "label": "Doba trvání" + }, + "startTime": { + "label": "Čas zahájení" + } + }, + "monitors": { + "title": "Monitory", + "description": "Monitory, na které se má okno údržby vztahovat", + "option": { + "addMonitors": { + "label": "Přidat monitory" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "Vytvořit kanál", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Kanály oznámení slouží k:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Přístupový token", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "Adresa, na kterou budou zasílána oznámení.", + "optionAddress": "Adresa", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Adresa" + }, + "homeServer": { + "optionHomeServer": "Domovský server", + "placeholder": "example.com" + }, + "matrix": { + "description": "Nastavte připojení k domovskému serveru Matrix pro oznámení.", + "title": "Konfigurace Matrix" + }, + "notificationName": { + "description": "Popisný název kanálu oznámení", + "optionName": "Název kanálu", + "placeholder": "např. Produkční upozornění", + "title": "Název kanálu" + }, + "pagerDuty": { + "description": "Váš integrační klíč PagerDuty pro příjem upozornění.", + "optionIntegrationKey": "Integrační klíč", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Integrační klíč" + }, + "roomId": { + "optionRoomId": "ID místnosti", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Vyberte typ kanálu oznámení, který chcete vytvořit.", + "optionType": "Typ", + "title": "Typ kanálu" + }, + "telegram": { + "title": "Konfigurace Telegramu", + "description": "", + "optionBotToken": "Token bota", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "ID chatu", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Cíl" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "Kumulativní posun rozvržení (CLS)", + "fcp": "První vykreslení obsahu (FCP)", + "lcp": "Největší vykreslení obsahu (LCP)", + "si": "Index rychlosti (SI)", + "tbt": "Celková doba blokování (TBT)" + }, + "legend": { + "title": "Zpráva PageSpeed", + "weight": "Váha" + }, + "pie": { + "title": "Zpráva o výkonu" + } + }, + "table": { + "headers": { + "pageSpeedScore": "Hodnocení PageSpeed" + } + }, + "fallback": { + "actionButton": "Vytvořte monitor!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "Monitor PageSpeed slouží k:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Zobrazované časové pásmo", + "description": "Vyberte časové pásmo používané pro zobrazení dat a časů v celé aplikaci.", + "option": { + "timezone": { + "label": "Zobrazované časové pásmo" + } + } + }, + "ui": { + "title": "Vzhled", + "description": "Přepínejte mezi světlým a tmavým režimem, měňte jazyk nebo upravte typ zobrazení grafů.", + "option": { + "theme": { + "label": "Režim motivu", + "light": "Světlý", + "dark": "Tmavý" + }, + "language": { + "label": "Jazyk" + }, + "chartType": { + "label": "Typ grafu", + "histogram": "Histogram", + "heatmap": "Heatmapa" + } + } + }, + "pagespeed": { + "title": "Klíč Google PageSpeed API", + "description": "Zadejte klíč Google PageSpeed API pro aktivaci sledování Google PageSpeed. Klikněte na Obnovit pro aktualizaci klíče.", + "option": { + "apiKey": { + "label": "Klíč PageSpeed API", + "labelSet": "Klíč API je nastaven. Klikněte na Obnovit pro jeho změnu.", + "placeholder": "Zadejte klíč Google PageSpeed API" + } + } + }, + "url": { + "title": "IP/URL monitoru na stavové stránce", + "description": "Zobrazí IP adresu nebo URL monitoru na veřejné stavové stránce. Pokud je tato možnost zakázána, zobrazí se pouze název monitoru pro ochranu citlivých informací.", + "option": { + "showURL": { + "label": "Zobrazit IP/URL na stavové stránce", + "enabled": "Povoleno", + "disabled": "Zakázáno" + } + } + }, + "stats": { + "title": "Historie monitoru", + "description": "Vymazat veškerou historii sledování a statistiky vašeho týmu. Tato akce je nevratná.", + "option": { + "clear": { + "label": "Vymazat všechny statistiky. Tato akce je nevratná." + } + }, + "dialog": { + "title": "Vymazat veškerou historii sledování?", + "description": "Tuto akci nelze vrátit zpět" + } + }, + "retention": { + "title": "Uchovávání kontrol", + "description": "Nastavte, jak dlouho se mají uchovávat data kontrol před automatickým vymazáním.", + "option": { + "days": { + "label": "Doba uchovávání (dny)", + "unlimited": "Neomezeně" + } + } + }, + "thresholds": { + "title": "Globální prahové hodnoty", + "description": "Nastavte globální prahové hodnoty CPU, paměti, disku a teploty pro sledování infrastruktury. Platí pro všechny monitory infrastruktury, pokud nejsou přepsány.", + "option": { + "cpu": { + "label": "Prahová hodnota CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Prahová hodnota paměti (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Prahová hodnota disku (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Prahová hodnota teploty (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Nastavení emailu", + "description": "Nastavte emailovou konfiguraci pro váš systém. Používá se k odesílání oznámení a upozornění.", + "descriptionTransport": "", + "descriptionTransportLink": "Zobrazit specifikace zde", + "option": { + "host": { + "label": "Emailový hostitel - název hostitele nebo IP adresa pro připojení", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Emailový port - port pro připojení", + "placeholder": "587" + }, + "address": { + "label": "Emailová adresa - použitá pro ověření", + "placeholder": "you@example.com" + }, + "user": { + "label": "Emailový uživatel - uživatelské jméno pro ověření, přepíše emailovou adresu, pokud je zadáno", + "placeholder": "Nechte prázdné, pokud není vyžadováno" + }, + "password": { + "label": "Emailové heslo - heslo pro ověření", + "labelSet": "Heslo je nastaveno. Klikněte na Obnovit pro jeho změnu.", + "placeholder": "Zadejte heslo" + }, + "tlsServername": { + "label": "Název TLS serveru - volitelný název hostitele pro TLS validaci, když je hostitel IP", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "Hostitel emailového připojení - název hostitele pro HELO/EHLO pozdrav", + "placeholder": "localhost" + }, + "secure": { + "label": "Použít SSL (doporučeno): Šifrovat připojení pomocí SSL/TLS" + }, + "pool": { + "label": "Povolit sdílení připojení: Opakované použití stávajících připojení pro vyšší výkon" + }, + "ignoreTLS": { + "label": "Zakázat STARTTLS: Nepoužívat TLS, i když to server podporuje" + }, + "requireTLS": { + "label": "Vynutit STARTTLS: Vyžadovat upgrade TLS, selhat pokud není podporován" + }, + "rejectUnauthorized": { + "label": "Odmítnout neplatné certifikáty: Odmítnout připojení se sebou podepsanými nebo nedůvěryhodnými certifikáty" + } + } + }, + "demoMonitors": { + "title": "Ukázkové monitory", + "description": "Přidejte ukázkové monitory pro demonstrační účely." + }, + "removeMonitors": { + "title": "Obnovení systému", + "description": "Odeberte všechny monitory z vašeho systému.", + "dialog": { + "title": "Odebrat všechny monitory?", + "description": "Tuto akci nelze vrátit zpět." + } + }, + "exportMonitors": { + "title": "Exportovat monitory" + }, + "importExportMonitors": { + "title": "Import / Export monitorů", + "description": "Importujte nebo exportujte data monitorů jako JSON soubor pro zálohu nebo přenos." + }, + "about": { + "title": "O aplikaci", + "developedBy": "Vyvinulo Bluewave Labs" + }, + "validation": { + "errorMessage": "Opravte prosím následující chyby validace:" + } + } + }, + "statusPages": { + "deleteSuccess": "Stavová stránka byla úspěšně smazána", + "fallback": { + "title": "Stavová stránka slouží k:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "Vytvořte stavovou stránku!" + }, + "monitorsList": { + "chartTypeHeatmap": "Heatmapa", + "chartTypeHistogram": "Histogram", + "noData": "Žádná data nejsou k dispozici", + "infrastructure": { + "title": "Infrastruktura", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Paměť", + "disk": "Disk", + "usage": "Využití", + "used": "Použité", + "total": "Celkem" + }, + "uptime": { + "title": "Dostupnost", + "responseTime": "Doba odezvy" + } + }, + "statusBar": { + "allDown": "Všechny systémy jsou nedostupné", + "allUp": "Všechny systémy fungují", + "degraded": "Některé systémy mají problémy", + "unknown": "Nelze určit stav systému" + }, + "table": { + "headers": { + "name": "Název stavové stránky", + "url": "Veřejná URL" + }, + "published": "Publikováno", + "unpublished": "Nepublikováno" + }, + "title": "Stavové stránky", + "details": { + "empty": { + "title": "Zatím zde nic není", + "addMonitor": "Přidejte monitor pro začátek" + } + }, + "form": { + "access": { + "title": "Přístup", + "description": "Pokud je vaše stavová stránka připravena, můžete ji označit jako publikovanou.", + "option": { + "published": { + "name": "Publikováno a viditelné pro veřejnost" + } + } + }, + "basicInfo": { + "title": "Základní informace", + "description": "Zadejte název společnosti a subdoménu, na kterou stavová stránka odkazuje.", + "option": { + "name": { + "label": "Název společnosti", + "placeholder": "Acme s.r.o." + }, + "url": { + "label": "Adresa vaší stavové stránky", + "placeholder": "moje-stavova-stranka" + } + } + }, + "monitors": { + "title": "Monitory", + "description": "Vyberte monitory, které se mají zobrazit na vaší stavové stránce.", + "noMonitors": "Žádné monitory nebyly vybrány", + "option": { + "monitors": { + "label": "Vybrat monitory", + "placeholder": "Hledejte a vyberte monitory..." + } + } + }, + "timezone": { + "title": "Časové pásmo", + "description": "Vyberte časové pásmo, ve kterém se bude zobrazovat stavová stránka.", + "option": { + "timezone": { + "label": "Časové pásmo", + "placeholder": "Vyberte časové pásmo..." + } + } + }, + "appearance": { + "title": "Vzhled", + "description": "Definujte výchozí vzhled vaší veřejné stavové stránky.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Barva značky" + } + } + }, + "features": { + "title": "Funkce", + "description": "Nastavte, jaké informace se zobrazí na stavové stránce.", + "option": { + "showCharts": { + "label": "Zobrazit grafy doby odezvy" + }, + "showUptimePercentage": { + "label": "Zobrazit procento dostupnosti" + }, + "showAdminLoginLink": { + "label": "Zobrazit odkaz pro přihlášení administrátora" + }, + "showInfrastructure": { + "label": "Zobrazit metriky infrastruktury" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Hledat monitory..." + } + }, + "table": { + "headers": { + "responseTime": "Doba odezvy" + } + }, + "fallback": { + "actionButton": "Vytvořte monitor!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Monitor dostupnosti slouží k:" } } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "Podařilo se úspěšně obnovit spojení se serverem", - "stillUnreachable": "Server je stále nedosažitelný. Zkuste to prosím později." - }, - "alertBox": "Chyba připojení k serveru", - "description": "Nepodařilo se připojit k serveru. Zkontrolujte si prosím internetové připojení a pokud problém přetrvává, ověřte konfiguraci aplikace Checkmate.", - "retryButton": { - "default": "Zkusit se znovu připojit", - "processing": "Probíhá připojování…" - } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" - }, - "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" - }, - "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "testNotification": "", - "dialogDeleteTitle": "", - "dialogDeleteConfirm": "" - }, - "notificationConfig": { - "title": "", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "", - "sendTestNotifications": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "", - "diagnostics": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" - } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "", - "intervalHeader": "" - }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" - }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "", - "deleteSuccess": "", - "deleteFailed": "", - "details": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" - }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "", - "toastEmailRequiredFieldsError": "" - }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" - }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" - }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "" - }, - "title": "", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" - }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" - }, - "globalThresholds": { - "title": "", - "description": "" - } - }, - "statusPageCreate": { - "buttonSave": "" - }, - "incidentsOptionsHeaderFilterResolved": "", - "settingsSave": "", - "statusPageCreateAppearanceTitle": "", - "confirmPassword": "", - "monitorHooks": { - "failureAddDemoMonitors": "", - "successAddDemoMonitors": "" - }, - "settingsAppearance": "", - "settingsDisplayTimezone": "", - "settingsGeneralSettings": "", - "incidentsOptionsHeaderTotalIncidents": "", - "statusPage": { - "deleteSuccess": "", - "deleteFailed": "", - "createSuccess": "", - "updateSuccess": "", - "generalSettings": "", - "contents": "", - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "testNotificationsDisabled": "", - "incidentsTableResolvedAt": "", - "incidentsTableActionResolve": "", - "checkHooks": { - "failureResolveOne": "", - "failureResolveAll": "", - "failureResolveMonitor": "" - }, - "checkFormError": "", - "diagnosticsPage": { - "diagnosticDescription": "", - "statsDescription": "", - "gauges": { - "heapAllocationTitle": "", - "heapAllocationSubtitle": "", - "heapUsageTitle": "", - "heapUsageSubtitle": "", - "heapUtilizationTitle": "", - "heapUtilizationSubtitle": "", - "instantCpuUsageTitle": "", - "instantCpuUsageSubtitle": "" - }, - "stats": { - "eventLoopDelayTitle": "", - "uptimeTitle": "", - "usedHeapSizeTitle": "", - "totalHeapSizeTitle": "", - "osMemoryLimitTitle": "" - } - }, - "pageSpeedLighthouseAPI": "", - "time": { - "threeMinutes": "", - "fiveMinutes": "", - "tenMinutes": "", - "twentyMinutes": "", - "oneHour": "", - "oneDay": "", - "oneWeek": "", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" - }, - "general": { - "noOptionsFound": "" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "pageSpeed": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "editUserPage": { - "form": { - "email": "", - "firstName": "", - "lastName": "", - "role": "", - "save": "" - }, - "table": { - "actionHeader": "", - "roleHeader": "" - }, - "title": "", - "toast": { - "successUserUpdate": "", - "validationErrors": "" - } - }, - "incidentsPageActionResolveMonitor": "", - "incidentsPageActionResolveAll": "", - "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + } } diff --git a/client/src/locales/de.json b/client/src/locales/de.json index fd3918378..34755aaba 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -1,1140 +1,1305 @@ { - "submit": "Absenden", - "title": "Titel", - "distributedStatusHeaderText": "Echtzeit Abdeckung", - "distributedStatusSubHeaderText": "Genutzt von Millionen von Geräten weltweit, siehst Du die Systemleistung nach globaler Region, Land oder Stadt", - "settingsDisabled": "Inaktiv", - "settingsSuccessSaved": "Einstellungen erfolgreich gespeichert", - "settingsFailedToSave": "Fehler beim Speichern der Einstellungen", - "settingsStatsCleared": "Statistiken gelöscht", - "settingsFailedToClearStats": "Fehler beim löschen der Statistiken", - "settingsMonitorsDeleted": "Alle Monitore erfolgreich gelöscht.", - "settingsFailedToDeleteMonitors": "Fehler beim Löschen aller Monitore", - "starPromptTitle": "Stern vergeben", - "starPromptDescription": "Sieh dir die neuesten Veröffentlichungen an und hilf der Community auf GitHub", - "https": "HTTPS", - "http": "HTTP", - "monitor": "monitor", - "aboutus": "Über uns", - "now": "Jetzt", - "delete": "Löschen", - "configure": "Einstellungen", - "responseTime": "Antwortzeit", - "ms": "ms", - "bar": "Bar", - "area": "Bereich", - "country": "Land", - "city": "Stadt", - "response": "Antwort", - "monitorStatusUp": "Monitor {name} ({url}) ist jetzt ONLINE und antwortet", - "monitorStatusDown": "Monitor {name} ({url}) ist jetzt OFFLINE und antwortet nicht", - "webhookSendSuccess": "Webhook-Benachrichtigung erfolgreich gesendet", - "webhookSendError": "Fehler beim Senden der Webhook-Benachrichtigung an {platform}", - "webhookUnsupportedPlatform": "Nicht unterstützte Plattform: {platform}", - "distributedRightCategoryTitle": "Monitor", - "distributedStatusServerMonitors": "Server Monitore", - "distributedStatusServerMonitorsDescription": "Überwache den Status der zugehörigen Server", - "distributedUptimeCreateSelectURL": "Hier kannst Du die URL und Typ des Hosts auswählen", - "distributedUptimeCreateChecks": "Durchzuführende Prüfungen", - "distributedUptimeCreateChecksDescription": "Du kannst jederzeit Überprüfungen hinzufügen oder entfernen, nachdem Du Deine Seite hinzugefügt hast.\n", - "distributedUptimeCreateIncidentNotification": "Ereignisbenachrichtigungen", - "distributedUptimeCreateIncidentDescription": "Wenn es ein Ereignis gibt, benachrichtige die Benutzer.", - "distributedUptimeCreateAdvancedSettings": "Erweiterte Einstellungen", - "distributedUptimeDetailsNoMonitorHistory": "Es gibt noch keine Überprüfungshistorie für diesen Monitor.", - "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Zuletzt aktualisiert", - "notifications": { - "enableNotifications": "Aktiviere {{platform}} Benachrichtigungen\n", - "testNotification": "Test Benachrichtigung", - "addOrEditNotifications": "Benachrichtigungen hinzufügen oder bearbeiten", - "slack": { - "label": "Slack", - "description": "Um Slack-Benachrichtigungen zu aktivieren, erstelle eine Slack-App und aktiviere eingehende Webhooks. Danach gib einfach die Webhook-URL hier ein.", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "Slack webhook URL wird benötigt" - }, - "discord": { - "label": "Discord", - "description": "Um Daten von Checkmate an einen Discord-Kanal über Discord-Benachrichtigungen mithilfe von Webhooks zu senden, kannst Du die eingehenden Webhooks von Discord verwenden.", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "Discord webhook URL wird benötigt" - }, - "telegram": { - "label": "Telegram", - "description": "Um Telegram-Benachrichtigungen zu aktivieren, erstelle einen Telegram-Bot mit BotFather, einem offiziellen Bot zum Erstellen und Verwalten von Telegram-Bots. Dann hole dir das API-Token und die Chat-ID und notiere sie hier.", - "tokenLabel": "Dein bot token", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "Deine Chat ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "Telegram token und Chat-ID werden benötigt" - }, - "webhook": { - "label": "Webhooks", - "description": "Du kannst einen benutzerdefinierten Webhook einrichten, um Benachrichtigungen zu erhalten, wenn Ereignisse auftreten.", - "urlLabel": "Webhook URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "Webhook URL wird benötigt" - }, - "testNotificationDevelop": "Testbenachrichtigung 2", - "integrationButton": "Benachrichtigungsintegration", - "testSuccess": "Testbenachrichtigung erfolgreich gesendet!", - "testFailed": "Senden der Testbenachrichtigung fehlgeschlagen", - "unsupportedType": "Nicht unterstützter Benachrichtigungstyp", - "networkError": "Netzwerkfehler aufgetreten", - "fallback": { - "title": "Ein Benachrichtigungskanal wird verwendet, um:", - "checks": [ - "Teams über Ausfallzeiten oder Leistungsprobleme informieren", - "Informieren Sie Techniker über Vorfälle", - "Informieren Sie Administratoren über Systemänderungen" - ], - "actionButton": "Lassen Sie uns Ihren ersten Benachrichtigungskanal erstellen!" - }, - "createButton": "Benachrichtigungskanal erstellen", - "createTitle": "Benachrichtigungskanal", - "create": { - "success": "Benachrichtigung erfolgreich erstellt", - "failed": "Benachrichtigung konnte nicht erstellt werden" - }, - "fetch": { - "success": "Benachrichtigungen erfolgreich abgerufen", - "failed": "Abrufen der Benachrichtigungen fehlgeschlagen" - }, - "delete": { - "success": "Benachrichtigung erfolgreich gelöscht", - "failed": "Benachrichtigung konnte nicht gelöscht werden" - }, - "edit": { - "success": "Benachrichtigung erfolgreich aktualisiert", - "failed": "Benachrichtigung konnte nicht aktualisiert werden" - }, - "test": { - "success": "Test Benachrichtigung erfolgreich gesendet", - "failed": "Fehler beim senden der Test Benachrichtigung" - } - }, - "testLocale": "testLocale", - "add": "Hinzufügen", - "monitors": "Monitore", - "distributedUptimeStatusCreateStatusPage": "Statusseite", - "distributedUptimeStatusCreateStatusPageAccess": "Zugriff", - "distributedUptimeStatusCreateStatusPageReady": "Wenn deine Statusseite bereit ist, kannst du sie als veröffentlicht markieren.", - "distributedUptimeStatusBasicInfoHeader": "Grundlegende Informationen", - "distributedUptimeStatusBasicInfoDescription": "Definiere den Firmennamen und die Subdomain, auf die deine Statusseite verweist.", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "Lade ein Logo für deine Statusseite hoch", - "distributedUptimeStatusLogoUploadButton": "Logo hochladen", - "distributedUptimeStatusStandardMonitorsHeader": "Standardmonitore", - "distributedUptimeStatusStandardMonitorsDescription": "Füge Standardmonitore zu deiner Statusseite hinzu.", - "distributedUptimeStatusCreateYour": "Erstelle Dein", - "distributedUptimeStatusEditYour": "Ändere Dein", - "distributedUptimeStatusPublishedLabel": "Veröffentlicht und für die Öffentlichkeit sichtbar", - "distributedUptimeStatusCompanyNameLabel": "Firmenname", - "distributedUptimeStatusPageAddressLabel": "Die Adresse deiner Statusseite", - "distributedUptimeStatus30Days": "30 Tage", - "distributedUptimeStatus60Days": "60 Tage", - "distributedUptimeStatus90Days": "90 Tage", - "distributedUptimeStatusPageNotSetUp": "Eine Statusseite ist nicht eingerichtet.", - "distributedUptimeStatusContactAdmin": "Bitte kontaktiere deinen Administrator", - "distributedUptimeStatusPageNotPublic": "Diese Statusseite ist nicht öffentlich.", - "distributedUptimeStatusPageDeleteDialog": "Möchtest du diese Statusseite löschen?", - "distributedUptimeStatusPageDeleteConfirm": "Ja, Statusseite löschen", - "distributedUptimeStatusPageDeleteDescription": "Sobald sie gelöscht ist, kann deine Statusseite nicht wiederhergestellt werden.", - "distributedUptimeStatusDevices": "Geräte", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "UPT Burned", - "distributedUptimeStatusUptLogo": "Upt Logo", - "incidentsTableNoIncidents": "Keine Ereignisse aufgezeichnet", - "incidentsTablePaginationLabel": "Ereignisse", - "incidentsTableMonitorName": "Monitor Name", - "incidentsTableStatus": "Status", - "incidentsTableDateTime": "Datum & Zeit", - "incidentsTableStatusCode": "Status Code", - "incidentsTableMessage": "Nachricht", - "incidentsOptionsHeader": "Ereignisse für", - "incidentsOptionsHeaderFilterBy": "Gefiltert nach:", - "incidentsOptionsHeaderFilterAll": "Alle", - "incidentsOptionsHeaderFilterDown": "offline", - "incidentsOptionsHeaderFilterCannotResolve": "Kann nicht aufgelöst werden", - "incidentsOptionsHeaderShow": "Anzeigen:", - "incidentsOptionsHeaderLastHour": "Letzte Stunde", - "incidentsOptionsHeaderLastDay": "Letzter Tag", - "incidentsOptionsHeaderLastWeek": "Letzte Woche", - "incidentsOptionsPlaceholderAllServers": "Alle Server", - "infrastructureCreateYour": "Erstelle dein", - "infrastructureCreateGeneralSettingsDescription": "Hier kannst du die URL des Hosts auswählen, zusammen mit dem Namen und dem Autorisierungs-token, um eine Verbindung zum Server-Agenten herzustellen.", - "infrastructureServerRequirement": "Der Server, den du überwachst, muss die folgende Software ausführen:", - "infrastructureCustomizeAlerts": "Benachrichtigungen anpassen", - "infrastructureAlertNotificationDescription": "Sende eine Benachrichtigung an Benutzer, wenn Schwellenwerte einen bestimmten Prozentsatz überschreiten.", - "infrastructureCreateMonitor": "Erstelle Infrastrukturmonitor", - "infrastructureProtocol": "Protokoll", - "infrastructureServerUrlLabel": "Server URL", - "infrastructureDisplayNameLabel": "Anzeigename", - "infrastructureAuthorizationSecretLabel": "Autorisierungs-token", - "gb": "GB", - "mb": "MB", - "mem": "Speicher", - "memoryUsage": "Speicher Nutzung", - "cpu": "CPU", - "cpuUsage": "CPU Nutzung", - "cpuTemperature": "CPU Temperatur", - "diskUsage": "Disk benutzt", - "used": "benutzt", - "total": "Total", - "cores": "Kerne", - "frequency": "Frequenz", - "status": "Status", - "cpuPhysical": "CPU (Physical)", - "cpuLogical": "CPU (Logical)", - "cpuFrequency": "CPU Frequenz", - "avgCpuTemperature": "Durchschn. CPU Termperatur", - "memory": "Speicher", - "disk": "Disk", - "uptime": "Uptime", - "os": "OS", - "host": "Host", - "actions": "Aktionen", - "integrations": "Integrationen", - "integrationsPrism": "Verbinde Prism mit deinem favorisierten Gerät", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Verbinde dich mit Slack und sieh Ereignisse in einem Kanal", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Verbinde dich mit Discord und sieh Ereignisse direkt in einem Kanal", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Sende alle Ereignisse an Zapier und sieh sie dann überall", - "commonSave": "Speichern", - "createYour": "Erstelle Deinen", - "createMonitor": "Erstelle Monitor", - "pause": "Pausieren", - "resume": "Weiter", - "editing": "Bearbeiten...", - "url": "URL", - "access": "Zugriff", - "timezone": "Zeitzone", - "features": "Funktionen", - "administrator": "Administrator?", - "loginHere": "Hier anmelden", - "displayName": "Anzeigename", - "urlMonitor": "zu überwachende URL", - "portToMonitor": "zu überwachender Port", - "websiteMonitoring": "Websiten-Überwachung", - "websiteMonitoringDescription": "Verwende HTTP(s), um deine Website oder API-Endpunkt zu überwachen.", - "pingMonitoring": "Ping-Überwachung", - "pingMonitoringDescription": "Überprüfe, ob dein Server verfügbar ist oder nicht.", - "dockerContainerMonitoring": "Docker-Container-Überwachung", - "dockerContainerMonitoringDescription": "Überprüfe, ob dein Docker-Container läuft oder nicht.", - "portMonitoring": "Port-Überwachung", - "portMonitoringDescription": "Überprüfe, ob dein Port offen ist oder nicht.", - "createMaintenanceWindow": "Erstelle Wartungsfenster", - "createMaintenance": "Erstelle Wartungsfenster", - "editMaintenance": "Ändere Wartungsfenster", - "maintenanceWindowName": "Name des Wartungsfensters", - "friendlyNameInput": "Freundlicher Name", - "friendlyNamePlaceholder": "Wartung um __ : __ für ___ Minuten", - "maintenanceRepeat": "Wartungsfenster wiederholen", - "maintenance": "Wartungsfenster", - "duration": "Dauer", - "addMonitors": "Monitor hinzufügen", - "window": "Fenster", - "cancel": "Abbrechen", - "message": "Nachricht", - "low": "niedrig", - "high": "hoch", - "statusCode": "Status Code", - "date&Time": "Datum & Zeit", - "type": "Art", - "statusPageName": "Name der Statusseite", - "publicURL": "Öffentliche URL", - "repeat": "Wiederholen", - "edit": "Ändern", - "createA": "Erstelle ein", - "remove": "Löschen", - "maintenanceWindowDescription": "Pings werden während dieses Zeitraums nicht gesendet", - "startTime": "Anfangszeit", - "timeZoneInfo": "Alle Daten und Zeiten sind in der Zeitzone GMT+0.", - "monitorsToApply": "Monitore, auf die das Wartungsfenster angewendet werden soll", - "nextWindow": "Nächstes Fenster", - "notFoundButton": "Gehe zum Haupt-Dashboard", - "pageSpeedConfigureSettingsDescription": "Hier kannst du die URL des Hosts auswählen, zusammen mit der Art des Monitors.", - "monitorDisplayName": "Anzeigename des Monitors", - "whenNewIncident": "Wenn es ein neues Ereignis gibt,", - "notifySMS": "Benachrichtige per SMS (kommt bald)", - "notifyEmails": "Benachrichtige auch per E-Mail an mehrere Adressen (kommt bald)", - "seperateEmails": "Du kannst mehrere E-Mails mit einem Komma trennen", - "checkFrequency": "Überprüfungsfrequenz", - "matchMethod": "Übereinstimmungsmethode", - "expectedValue": "Erwarteter Wert", - "deleteDialogTitle": "Möchtest du diesen Monitor wirklich löschen?", - "deleteDialogDescription": "Sobald dieser Monitor gelöscht ist, kann er nicht wiederhergestellt werden.", - "pageSpeedMonitor": "PageSpeed-Monitor", - "shown": "Gezeigt", - "ago": "vor", - "companyName": "Name der Firma", - "pageSpeedDetailsPerformanceReport": "Die Werte sind geschätzt und können variieren.", - "pageSpeedDetailsPerformanceReportCalculator": "Rechner öffnen", - "checkingEvery": "Überprüfen alle", - "statusPageCreateSettings": "Wenn Ihre Statusseite fertig ist, können Sie sie als veröffentlicht markieren.", - "basicInformation": "Grundlegende Informationen", - "statusPageCreateBasicInfoDescription": "Definieren Sie den Firmennamen und die Subdomäne, auf die Ihre Statusseite verweist.", - "statusPageCreateSelectTimeZoneDescription": "Wählen Sie die Zeitzone aus, in der Ihre Statusseite angezeigt wird.", - "statusPageCreateAppearanceDescription": "Definieren Sie das Standard-Erscheinungsbild Ihrer öffentlichen Statusseite.", - "statusPageCreateSettingsCheckboxLabel": "Veröffentlicht und für die Öffentlichkeit sichtbar", - "statusPageCreateBasicInfoStatusPageAddress": "Ihre Statusseitenadresse", - "statusPageCreateTabsContent": "Statusseitenserver", - "statusPageCreateTabsContentDescription": "Sie können Ihrer Statusseite beliebig viele Server hinzufügen, die Sie überwachen. Sie die Reihenfolge der Server ändern.", - "statusPageCreateTabsContentFeaturesDescription": "Mehr Details auf der Statusseite anzeigen", - "showCharts": "Diagramme anzeigen", - "showUptimePercentage": "Verfügbarkeitsprozentsatz anzeigen", - "removeLogo": "Entferne Logo", - "statusPageStatus": "Eine öffentliche Statusseite ist nicht eingerichtet.", - "statusPageStatusContactAdmin": "Bitte wenden Sie sich an Ihren Administrator", - "statusPageStatusNotPublic": "Diese Statusseite ist nicht öffentlich.", - "statusPageStatusNoPage": "Hier gibt es keine Statusseite.", - "statusPageStatusServiceStatus": "Dienststatus", - "deleteStatusPage": "Möchten Sie diese Statusseite löschen?", - "deleteStatusPageConfirm": "Ja, Statusseite löschen", - "deleteStatusPageDescription": "Nach dem Löschen kann Ihre Statusseite nicht mehr abgerufen werden.", - "uptimeCreate": "Der erwartete Wert wird zum Abgleich mit dem Antwortergebnis verwendet und die Übereinstimmung bestimmt den Status.", - "uptimeCreateJsonPath": "Dieser Ausdruck wird anhand der JSON-Antwortdaten ausgewertet und das Ergebnis wird zum Abgleich mit dem erwarteten Wert verwendet. Siehe", - "uptimeCreateJsonPathQuery": "für die Dokumentation der Abfragesprache.", - "maintenanceTableActionMenuDialogTitle": "Möchten Sie dieses Wartungsfenster wirklich entfernen?", - "infrastructureEditYour": "Bearbeiten Sie Ihre", - "infrastructureEditMonitor": "Infrastrukturmonitor speichern", - "infrastructureMonitorCreated": "Infrastrukturmonitor erfolgreich erstellt!", - "infrastructureMonitorUpdated": "Infrastrukturmonitor erfolgreich aktualisiert!", - "errorInvalidTypeId": "Ungültiger Benachrichtigungstyp angegeben", - "errorInvalidFieldId": "Ungültige Feld-ID angegeben", - "inviteNoTokenFound": "Kein Einladungstoken gefunden", - "pageSpeedWarning": "Warnung: Sie haben noch keinen Google PageSpeed API-Schlüssel hinzugefügt. Ohne diesen funktioniert der PageSpeed-Monitor nicht.", - "pageSpeedLearnMoreLink": "Hier klicken", - "pageSpeedAddApiKey": "um einen API key einzutragen", - "update": "Update", - "invalidFileFormat": "Nicht unterstützes Dateiformat", - "invalidFileSize": "Dateigröße zu groß!", - "ClickUpload": "Zum Hochladen klicken", - "DragandDrop": "drag and drop", - "MaxSize": "Maximalgröße", - "SupportedFormats": "Unterstützte Formate", - "FirstName": "Vorname", - "LastName": "Zuname", - "EmailDescriptionText": "Dies ist Ihre aktuelle E-Mail-Adresse – sie kann nicht geändert werden.", - "YourPhoto": "Profilphoto", - "PhotoDescriptionText": "Dieses Foto wird auf Ihrer Profilseite angezeigt.", - "save": "Speichern", - "DeleteDescriptionText": "Dadurch werden das Konto und alle zugehörigen Daten vom Server entfernt. Dieser Vorgang kann nicht rückgängig gemacht werden.", - "DeleteAccountWarning": "Wenn Sie Ihr Konto löschen, können Sie sich nicht mehr anmelden und alle Ihre Daten werden gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden.", - "DeleteWarningTitle": "Account wirklich löschen?", - "bulkImport": { - "title": "Massenimport", - "selectFileTips": "Wählen Sie die CSV-Datei zum Hochladen aus", - "selectFileDescription": "Sie können unsere oder Beispiel herunterladen.", - "selectFile": "Datei auswählen", - "parsingFailed": "Parsen fehlgeschlagen", - "uploadSuccess": "Monitore erfolgreich erstellt!", - "validationFailed": "Prüfung fehlgeschlagen", - "noFileSelected": "Keine Datei ausgewählt", - "fallbackPage": "Importieren Sie eine Datei, um eine Liste von Servern gleichzeitig hochzuladen", - "invalidFileType": "Ungültiger Dateityp", - "uploadFailed": "Upload fehlgeschlagen" - }, - "DeleteAccountTitle": "Account löschen", - "DeleteAccountButton": "Account löschen", - "publicLink": "Öffentlicher Link", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "Reset", - "ignoreTLSError": "TLS/SSL Fehler ignorieren", - "tlsErrorIgnored": "TLS/SSL Fehler ignoriert", - "ignoreTLSErrorDescription": "TLS/SSL Fehler ignorieren und Verfügbarkeit weiter prüfen", - "createNew": "Neu anlegen", - "greeting": { - "prepend": "Hallo", - "append": "Der Nachmittag gehört Ihnen – machen wir ihn episch!", - "overview": "Hier ist eine Übersicht Ihrer {{type}}-Monitore." - }, - "roles": { - "superAdmin": "Super admin", - "admin": "Admin", - "teamMember": "Team Mitglied", - "demoUser": "Demo user" - }, - "teamPanel": { - "teamMembers": "Team Mitglied", - "filter": { - "all": "Alle", - "member": "Mitglied" - }, - "inviteTeamMember": "Team Mirglied einladen", - "inviteNewTeamMember": "Neues Team Mirglied einladen", - "inviteDescription": "Wenn Sie ein neues Teammitglied hinzufügen, erhält dieses Zugriff auf alle Monitore.", - "email": "Email", - "selectRole": "Rolle auswählen", - "inviteLink": "Einladungslink", - "cancel": "Abbrechen", - "noMembers": "Es gibt keine Team Mitglieder mit dieser Rolle", - "getToken": "Token holen", - "emailToken": "E-mail token", - "table": { - "name": "Name", - "email": "Email", - "role": "Rolle", - "created": "Angelegt" - }, - "addTeamMember": { - "addMemberMenu": "Teammitglied hinzufügen", - "title": "Neues Teammitglied registrieren", - "description": "Einen neuen Benutzer anlegen und Zugangsdaten mit ihm teilen. Diese Methode gibt dem Mitglied direkt Zugriff auf alle Überwachungen.", - "addButton": "Mitglied hinzufügen" - }, - "register": "Teammitglied registrieren", - "registerToast": { - "success": "Benutzer erstellt, Zugangsdaten auf sichere Weise weitergeben.", - "dbUserExists": "Der Benutzer existiert bereits.", - "unknownError": "Unbekannter Fehler aufgetreten." - }, - "registerTeamMember": { - "title": "Teammitglied registrieren", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "Bitte einen Namen eingeben", - "pattern": "Der Name darf nur Buchstaben, Leerzeichen, Apostrophe oder Bindestriche enthalten" - } - }, - "lastName": { - "errors": { - "empty": "Bitte einen Vornamen eingeben", - "pattern": "Der Vorname darf nur Buchstaben, Leerzeichen, Apostrophe oder Bindestriche enthalten" - } - }, - "email": { - "errors": { - "empty": "Zum Fortsetzen bitte eine E-Mail-Adresse eingeben", - "invalid": "Bitte die Gültigkeit der E-Mail-Adresse erneut prüfen" - } - }, - "role": { - "errors": { - "empty": "Eine Rolle wird benötigt" - } - } - } - } - } - }, - "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", - "resumed": "Wiederaufgenommen", - "active": "Aktiv" - }, - "menu": { - "uptime": "Uptime", - "pagespeed": "Pagespeed", - "infrastructure": "Infrastruktur", - "incidents": "Vorfälle", - "statusPages": "Statusseite", - "maintenance": "Wartung", - "integrations": "Integrationen", - "settings": "Einstellungen", - "support": "Support", - "discussions": "Diskussionen", - "docs": "Anleitungen", - "changelog": "Änderungen", - "profile": "Profil", - "password": "Passwort", - "team": "Team", - "logOut": "Abmelden", - "notifications": "Benachrichtigungen", - "logs": "Logs" - }, - "settingsEmailUser": "E-Mail-Benutzer – Benutzername zur Authentifizierung, überschreibt die E-Mail-Adresse, falls angegeben", - "state": "Zustand", - "statusBreadCrumbsStatusPages": "Statusseite", - "statusBreadCrumbsDetails": "Details", - "commonSaving": "Speichere...", - "navControls": "Steuerungen", - "incidentsPageTitle": "Ereignisse", - "passwordPanel": { - "passwordChangedSuccess": "Dein Passwort wurde erfolgreich geändert.", - "passwordInputIncorrect": "Die Passworteingabe war nicht korrekt.", - "currentPassword": "Aktuelles Passwort", - "enterCurrentPassword": "Aktuelles Passwort eingeben", - "newPassword": "Neues Passwort", - "enterNewPassword": "Neues Passwort eingeben", - "confirmNewPassword": "Neues Passwort wiederholen", - "passwordRequirements": "Das Passwort muss mindestens 8 Zeichen, einen Großbuchstaben, eine Zahl und ein Sonderzeichen enthalten.", - "saving": "Speichere..." - }, - "emailSent": "Email erfolgreich gesendet", - "failedToSendEmail": "Email konnte nicht gesendet werden", - "settingsTestEmailSuccess": "Test-Email erfolgreich gesendet", - "settingsTestEmailFailed": "Test-Email konnte nicht gesendet werden", - "settingsTestEmailFailedWithReason": "Email konnte nicht gesendet werden: {{reason}}", - "settingsTestEmailUnknownError": "Unbekannter Fehler", - "statusMsg": { - "paused": "Monitoring pausiert", - "up": "Ihre Site ist online.", - "down": "Ihre Site ist nicht erreichbar.", - "pending": "Ausstehend..." - }, - "uptimeGeneralInstructions": { - "http": "Geben Sie die zu überwachende URL oder IP ein (z. B. https://example.com/ oder 192.168.1.100) und fügen Sie einen eindeutigen Anzeigenamen hinzu, der auf dem Dashboard angezeigt wird.", - "ping": "Geben Sie die IP-Adresse oder den Hostnamen ein, der angepingt werden soll (z. B. 192.168.1.100 oder example.com), und fügen Sie einen eindeutigen Anzeigenamen hinzu, der auf dem Dashboard angezeigt wird.", - "docker": "Geben Sie die Docker-ID Ihres Containers ein. Docker-IDs müssen die vollständige 64-Zeichen-ID sein. Sie können docker inspect ausführen, um die vollständige Container-ID abzurufen.", - "port": "Geben Sie die URL oder IP des Servers, die Portnummer und einen eindeutigen Anzeigenamen ein, der auf dem Dashboard angezeigt wird.", - "game": "IP Adresse oder Hostnamen und Port für Ping eingeben (z.B. 192.168.1.100 oder beispiel.de) und Spieletyp auswählen.", - "https": "Zu überwachende Adresse oder IP (z.B. https://beispiel.de/ oder 192.168.1.100) eingeben und einen klaren Anzeigenamen, der im Dashboard erscheint." - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "Schaltet hell und dunkel um" + "auth": { + "roles": { + "admin": "Admin", + "demo": "Demo", + "superadmin": "Super-Admin", + "user": "Benutzer" + } }, - "toasts": { - "networkError": "Netzwerkfehler", - "checkConnection": "Bitte Verbindung prüfen", - "unknownError": "Unbekannter Fehler" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "Weiter", - "back": "Zurück" + "alerts": { + "pageSpeedApiKey": { + "content": "Warnung: Sie haben noch keinen Google PageSpeed API-Schlüssel hinzugefügt. Besuchen Sie die Einstellungen, um einen hinzuzufügen. Ohne diesen funktioniert der PageSpeed-Monitor nicht." + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "Details", + "home": "Startseite" + }, + "buttons": { + "addMember": "Mitglied hinzufügen", + "cancel": "Abbrechen", + "close": "Schließen", + "configure": "Konfigurieren", + "confirm": "Bestätigen", + "create": "Erstellen", + "delete": "Löschen", + "generateToken": "Token generieren", + "incidents": "Vorfälle", + "inviteMember": "Mitglied einladen", + "pause": "Pausieren", + "resume": "Fortsetzen", + "save": "Speichern", + "sendInvite": "Einladung senden", + "test": "Testen", + "testNotifications": "Benachrichtigungen testen", + "toggleTheme": "Schaltet hell und dunkel um", + "flushQueue": "Warteschlange leeren", + "notFound": "Zum Hauptdashboard", + "resetPassword": "Passwort zurücksetzen", + "reset": "Zurücksetzen", + "clear": "Löschen", + "addDemo": "Demo-Monitore hinzufügen", + "removeMonitors": "Monitore entfernen", + "sendTestEmail": "Test-E-Mail senden", + "exportToJSON": "Als JSON exportieren", + "importFromJSON": "Aus JSON importieren", + "clearFilters": "Filter zurücksetzen", + "removeUser": "Benutzer entfernen" + }, + "charts": { + "labels": { + "averageResponseTime": "Durchschnittliche Antwortzeit", + "downtime": "Ausfallzeit", + "high": "hoch", + "low": "niedrig", + "uptime": "Verfügbarkeit" }, - "inputs": { - "email": { - "label": "Email", - "placeholder": "jordan.ellis@domain.com", - "errors": { - "empty": "Email adresse eingeben um fortzufahren", - "invalid": "Bitte überprüfen Sie die Gültigkeit der eingegebenen E-Mail-Adresse erneut" - } - }, - "password": { - "label": "Passwort", - "rules": { - "length": { - "beginning": "Muß mindestens", - "highlighted": "8 Zeichen lang" - }, - "special": { - "beginning": "Muß mindestens", - "highlighted": "ein Sonderzeichen enthalten" - }, - "number": { - "beginning": "Muß mindestens", - "highlighted": "eine Zahl enthalten" - }, - "uppercase": { - "beginning": "Muß mindestens", - "highlighted": "einen Großbuchstaben enthalten" - }, - "lowercase": { - "beginning": "Muß mindestens", - "highlighted": "einen Kleinbuchstaben enthalten" - }, - "match": { - "beginning": "Passwort und Kennwort bestätigen", - "highlighted": "muß übereinstimmen" - } - }, - "errors": { - "empty": "Bitte Passwort eingeben", - "length": "Passwort muss mindestens 8 Zeichen lang sein", - "uppercase": "Passwort muss mindestens einen Großbuchstaben enthalten", - "lowercase": "Passwort muss mindestens einen Kleinbuchstaben enthalten", - "number": "Passwort muss mindestens eine Zahl enthalten", - "special": "Passwort muss mindestens ein Sonderzeichen enthalten", - "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" - } - }, - "passwordConfirm": { - "label": "Passwort bestätigen", - "placeholder": "Passwort nochmal eingeben", - "errors": { - "empty": "Passwort nochmal eingeben", - "different": "Die Passwörter stimmen nicht überein" - } - }, - "firstName": { - "label": "Name", - "placeholder": "Jordan", - "errors": { - "empty": "Bitte Name eingeben", - "length": "Name darf nicht mehr als 50 Zeichen lang sein", - "pattern": "Name darf nur aus Buchstaben, Leerstellen, Apostroph und Bindestrichen bestehen" - } - }, - "lastName": { - "label": "Zuname", - "placeholder": "Ellis", - "errors": { - "empty": "Bitte Zuname eingeben", - "length": "Zuname darf nicht länger als 50 Zeichen sein", - "pattern": "Zuname darf nur aus Buchstaben, Leerstellen, Apostroph und Bindestrichen bestehen" + "histogram": { + "avg": "Durchschn.: {{value}} ms", + "max": "Max.: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Diese Aktion kann nicht rückgängig gemacht werden.", + "title": "Möchten Sie dies wirklich löschen?" + } + }, + "labels": { + "active": "Aktiv", + "paused": "pausiert", + "na": "N/A", + "resolved": "Gelöst", + "responseTime": "Antwortzeit" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Rollen" } } }, - "errors": { - "validation": "Fehler bei der prüfung der Daten" - }, - "fields": { - "password": { - "errors": { - "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" + "name": { + "option": { + "firstName": { + "label": "Name", + "placeholder": "Jordan" + }, + "lastName": { + "label": "Zuname", + "placeholder": "Ellis" } - }, - "role": { - "errors": { - "min": "Mindestens eine Rolle ist erforderlich" + } + }, + "email": { + "option": { + "email": { + "label": "E-Mail", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "Anmelden", - "subheadings": { - "stepOne": "Email eingeben", - "stepTwo": "Passwort eingeben" - }, - "links": { - "forgotPassword": "Passwort vergessen?", - "register": "Noch keinen Account?", - "forgotPasswordLink": "Passwort zurücksetzen", - "registerLink": "Hier registrieren" - }, - "toasts": { - "success": "Willkommen zurück. Erfolgreich angemeldet.", - "incorrectPassword": "Falsches Passwort" - }, + "table": { + "empty": "Nichts vorhanden", + "headers": { + "actions": "Aktionen", + "dateTime": "Datum & Uhrzeit", + "message": "Nachricht", + "monitor": "Monitor", + "monitorId": "Monitor-ID", + "name": "Name", + "status": "Status", + "type": "Art", + "url": "URL", + "interval": "Intervall", + "active": "Aktiv", + "responseTime": "Antwortzeit" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "Zum Hochladen klicken", + "dragAndDrop": "drag and drop", + "supportedFormats": "Unterstützte Formate", + "maxSize": "Maximalgröße", + "orDragAndDrop": "oder per Drag & Drop", "errors": { - "password": { - "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" - } - }, - "welcome": "Willkommen zurück bei Checkmate!" + "invalidFileSize": "Dateigröße zu groß!", + "invalidFileFormat": "Nicht unterstützes Dateiformat" + } }, - "registration": { - "heading": { - "superAdmin": "Super Admin anlegen", - "user": "Anmelden" - }, - "subheadings": { - "stepOne": "Geben Sie Ihre persönlichen Daten ein", - "stepTwo": "Email eingeben", - "stepThree": "Passwort eingeben" + "headerStatusPageControls": { + "publicLink": "Öffentlicher Link" + }, + "headerTimeRange": { + "labels": { + "day": "Tag", + "month": "Monat", + "recent": "Aktuell", + "week": "Woche" }, "description": { - "superAdmin": "Super admin account anlegen um zu beginnen", - "user": "Melden Sie sich als Benutzer an und bitten Sie den Superadministrator um Zugriff auf Ihre Monitore" - }, - "gettingStartedButton": { - "superAdmin": "Super admin account anlegen", - "user": "Normalen Benutzer anlegen" - }, - "termsAndPolicies": "Indem Sie ein Konto erstellen, stimmen Sie unseren Servicebedingungen und Datenschutzrichtlinien zu.", - "links": { - "login": "Du hast bereits einen Benutzer? Anmelden" - }, - "toasts": { - "success": "Willkommen! Benutzer erfolgreich angelegt" - }, - "welcome": "Willkommen bei Checkmate!" - }, - "forgotPassword": { - "heading": "Passwort vergessen?", - "subheadings": { - "stepOne": "Kein Problem. Wir schicken Dir Infos zum zurücksetzen.", - "stepTwo": "Link zum Password zurücksetzen an geschickt. ", - "stepThree": "Das neue Passwort darf nicht gleich wie frühere Passwörter sein.", - "stepFour": "Das Passwort wurde zurückgesetzt. Unten klicken zum Anmelden." - }, - "buttons": { - "openEmail": "Email app öffnen", - "resetPassword": "Passwort zurücksetzen" - }, - "imageAlts": { - "passwordKey": "Password key icon", - "email": "Email icon", - "lock": "Lock icon", - "passwordConfirm": "Password confirm icon" - }, - "links": { - "login": "Zurück zum Anmelden", - "resend": "Keine Email bekommen? Klicken um nochmal zu senden" - }, - "toasts": { - "sent": "Anleitung an gesendet.", - "emailNotFound": "Email nicht gefunden.", - "redirect": "Umleitung in ...", - "success": "Passwort erfolgreich zurückgesetzt.", - "error": "Kennwort konnte nicht zurückgesetzt werden. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support." + "recent": "Statistiken der letzten 2 Stunden.", + "day": "Statistiken der letzten 24 Stunden.", + "week": "Statistiken der letzten 7 Tage.", + "month": "Statistiken der letzten 30 Tage." } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "Verbindung zum Server wiederhergestellt.", - "stillUnreachable": "Server immer noch nicht erreichbar. Bitte später erneut versuchen." + }, + "offlineBanner": { + "serverUnreachable": "Server nicht erreichbar", + "retry": "Erneut versuchen", + "retrying": "Wird erneut versucht...", + "reconnected": "Verbindung zum Server wiederhergestellt." + }, + "sidebar": { + "menu": { + "uptime": "Verfügbarkeit", + "pagespeed": "Pagespeed", + "infrastructure": "Infrastruktur", + "notifications": "Benachrichtigungen", + "checks": "Checks", + "incidents": "Vorfälle", + "statusPages": "Statusseite", + "maintenance": "Wartung", + "logs": "Protokolle", + "settings": "Einstellungen" }, - "alertBox": "Server Verbindungsfehler", - "description": "Wir können keine Verbindung zum Server herstellen. Bitte überprüfen Sie Ihre Internetverbindung oder Ihre Bereitstellungskonfiguration, falls das Problem weiterhin besteht.", - "retryButton": { - "default": "Verbindung erneut versuchen", - "processing": "Verbinde..." + "bottomMenu": { + "support": "Support", + "discussions": "Diskussionen", + "docs": "Anleitungen", + "changelog": "Änderungen" + }, + "accountMenu": { + "profile": "Profil", + "password": "Passwort", + "team": "Team" + }, + "starPrompt": { + "title": "Checkmate bewerten", + "description": "Entdecken Sie die neuesten Versionen und helfen Sie der Community auf GitHub" + }, + "authFooter": { + "navControls": "Steuerungen", + "logOut": "Abmelden", + "roles": { + "superAdmin": "Super-Admin", + "admin": "Admin", + "user": "Benutzer", + "demoUser": "Demo-Benutzer" + } } + }, + "starPrompt": { + "title": "Checkmate bewerten", + "description": "Entdecken Sie die neuesten Versionen und helfen Sie der Community auf GitHub" } }, - "createNotifications": { - "title": "Benachrichtigungskanal erstellen", - "nameSettings": { - "title": "Name", - "description": "Ein beschreibender Name für Ihre Integration.", - "nameLabel": "Name", - "namePlaceholder": "z.B. Slack Benachrichtigung" + "pages": { + "notFound": { + "title": "Oh nein! Sie haben Ihr Sushi fallen lassen!", + "subtitle": "Entweder existiert die URL nicht oder Sie haben keinen Zugriff darauf." }, - "typeSettings": { - "title": "Typ", - "description": "Wählen Sie den Typ des Benachrichtigungskanals aus, den Sie erstellen möchten.", - "typeLabel": "Typ" - }, - "emailSettings": { - "title": "Email", - "description": "Empfänger Email adresse", - "emailLabel": "Email addresse", - "emailPlaceholder": "z.B. john@example.com" - }, - "slackSettings": { - "title": "Slack", - "description": "Slack webhook hier konfigurieren", - "webhookLabel": "Slack webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "PagerDuty hier konfigurieren", - "integrationKeyLabel": "Integration key", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discord", - "description": "Discord webhook hier konfigurieren", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "webhookSettings": { - "title": "Webhook", - "description": "Webhook hier konfigurieren", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "testNotification": "Testbenachrichtigung", - "dialogDeleteTitle": "Möchten Sie diese Benachrichtigung wirklich löschen?", - "dialogDeleteConfirm": "Löschen" - }, - "notificationConfig": { - "title": "Benachrichtigungen", - "description": "Wählen Sie die Benachrichtigungskanäle aus, die Sie verwenden möchten" - }, - "monitorStatus": { - "checkingEvery": "Überprüfung alle {{interval}}", - "withCaptureAgent": "mit Capture-Agent {{version}}", - "up": "Online", - "down": "Offline", - "paused": "pausiert" - }, - "advancedMatching": "Erweiterter Abgleich", - "sendTestNotifications": "Test Benachrichtigung senden", - "selectAll": "Alle auswählen", - "showAdminLoginLink": "Link „Administrator? Hier anmelden“ auf der Statusseite anzeigen", - "logsPage": { - "title": "Logs", - "description": "Systemprotokolle – letzte 1000 Zeilen", - "tabs": { - "queue": "Warteschlange", - "logs": "Server logs", - "diagnostics": "Diagnose" - }, - "toast": { - "fetchLogsSuccess": "Protokolle erfolgreich abgerufen" - }, - "logLevelSelect": { - "title": "Protokollebene", - "values": { - "all": "Alle", - "info": "Info", - "warn": "Warnung", - "error": "Fehler", - "debug": "Debug" + "account": { + "tabs": { + "profile": "Profil", + "password": "Passwort", + "team": "Team" + }, + "form": { + "name": { + "title": "Name", + "description": "Aktualisieren Sie Ihre persönlichen Informationen" + }, + "photo": { + "title": "Profilbild", + "description": "Laden Sie ein Profilbild hoch" + }, + "currentPassword": { + "title": "Aktuelles Passwort", + "description": "Geben Sie Ihr aktuelles Passwort ein, um Ihre Identität zu bestätigen", + "option": { + "label": "Aktuelles Passwort", + "placeholder": "Aktuelles Passwort eingeben" + } + }, + "newPassword": { + "title": "Neues Passwort", + "description": "Wählen Sie ein sicheres Passwort mit mindestens 8 Zeichen", + "option": { + "newPassword": { + "label": "Neues Passwort", + "placeholder": "Neues Passwort eingeben" + }, + "confirm": { + "label": "Passwort bestätigen", + "placeholder": "Neues Passwort bestätigen" + } + } + }, + "deleteAccount": { + "title": "Konto löschen", + "description": "Diese Aktion ist dauerhaft und kann nicht rückgängig gemacht werden" + } + }, + "team": { + "filter": { + "placeholder": "Nach Rolle filtern", + "all": "Alle", + "admin": "Admin", + "member": "Mitglied" + }, + "table": { + "headers": { + "email": "E-Mail", + "role": "Rolle", + "created": "Angelegt" + } + }, + "addMember": { + "title": "Neues Teammitglied registrieren", + "description": "Erstellen Sie ein neues Benutzerkonto. Teilen Sie die Zugangsdaten nach der Erstellung sicher mit." + }, + "invite": { + "title": "Teammitglied einladen", + "description": "Senden Sie eine Einladung, Ihrem Team beizutreten", + "email": { + "label": "E-Mail-Adresse", + "placeholder": "E-Mail-Adresse eingeben" + }, + "role": { + "label": "Rolle", + "placeholder": "Rolle auswählen" + }, + "linkLabel": "Einladungslink" + } } - } - }, - "queuePage": { - "title": "Warteschlange", - "refreshButton": "Auffrischen", - "flushButton": "Warteschlange leeren", - "jobTable": { - "title": "Aufgaben in der Warteschlange", - "idHeader": "Monitor ID", - "urlHeader": "URL", - "typeHeader": "Typ", - "activeHeader": "Aktiv", - "lockedAtHeader": "Gesperrt bei", - "runCountHeader": "Anzahl Läufe", - "failCountHeader": "Anzahl Fehler", - "lastRunHeader": "Letzter Lauf um", - "lastFinishedAtHeader": "Zuletzt abgeschlossen um", - "lastRunTookHeader": "Letzter Lauf dauerte", - "intervalHeader": "Invervall" }, - "metricsTable": { - "title": "Warteschlangenmetriken", - "metricHeader": "Metrisch", - "valueHeader": "Wert" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "Muß mindestens", + "highlighted": "8 Zeichen lang" + }, + "lowercase": { + "beginning": "Muß mindestens", + "highlighted": "einen Kleinbuchstaben enthalten" + }, + "match": { + "beginning": "Passwort und Kennwort bestätigen", + "highlighted": "muß übereinstimmen" + }, + "number": { + "beginning": "Muß mindestens", + "highlighted": "eine Zahl enthalten" + }, + "special": { + "beginning": "Muß mindestens", + "highlighted": "ein Sonderzeichen enthalten" + }, + "uppercase": { + "beginning": "Muß mindestens", + "highlighted": "einen Großbuchstaben enthalten" + } + }, + "form": { + "option": { + "email": { + "label": "E-Mail", + "placeholder": "me@example.com" + }, + "password": { + "label": "Passwort", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Passwort bestätigen" + } + } + } + }, + "login": { + "title": "Willkommen zurück bei Checkmate!", + "subtitle": "Melden Sie sich an, um fortzufahren", + "submit": "Anmelden", + "links": { + "forgotPassword": { + "text": "Passwort vergessen?", + "linkText": "Passwort zurücksetzen" + }, + "register": { + "text": "Noch kein Konto?", + "linkText": "Hier registrieren" + } + } + }, + "register": { + "title": "Willkommen bei Checkmate!", + "subtitle": "Registrieren Sie sich, um loszulegen", + "submit": "Registrieren" + }, + "forgotPassword": { + "title": "Passwort vergessen?", + "subtitle": "Keine Sorge, wir senden Ihnen Anweisungen zum Zurücksetzen.", + "submit": "Wiederherstellung anfordern", + "links": { + "login": { + "text": "Zurück zum", + "linkText": "Login" + } + } + }, + "setNewPassword": { + "title": "Passwort zurücksetzen", + "subtitle": "Ihr neues Passwort muss sich von zuvor verwendeten Passwörtern unterscheiden." + } }, - "failedJobTable": { - "title": "Anzahl Fehler", - "monitorIdHeader": "Monitor ID", - "monitorUrlHeader": "Monitor URL", - "failCountHeader": "Anzahl Fehler", - "failedAtHeader": "Zuletzt fehlgeschlagen um", - "failReasonHeader": "Fehlergrund" - } - }, - "export": { - "title": "Monitore exportieren", - "success": "Monitore erfolgreich exportiert", - "failed": "Fehler beim exportieren der Monitore" - }, - "monitorActions": { - "title": "Export/Import", - "import": "Monitore importieren", - "export": "Monitore exportieren", - "deleteSuccess": "Monitor erfolgreich gelöscht", - "deleteFailed": "Monitor konnte nicht gelöscht werden", - "details": "Details" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "Entwickelt von Bluewave Labs", - "labelVersion": "Version", - "title": "Über" + "checks": { + "selects": { + "monitor": { + "all": "Alle Monitore" + }, + "status": { + "all": "Alle", + "down": "Ausgefallen", + "up": "Erreichbar" + } + }, + "table": { + "empty": "Keine ausgefallenen Checks in diesem Zeitraum", + "headers": { + "statusCode": "Statuscode", + "location": "Standort" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "Demo Monitor hinzufügen", - "description": "Beispiel Monitor zur Demonstration hinzufügen.", - "title": "Demo Monitore" + "common": { + "monitors": { + "actions": { + "configure": "Konfigurieren", + "delete": "Löschen", + "generateToken": "Token generieren", + "details": "Details", + "incidents": "Vorfälle", + "inviteMember": "Mitglied einladen", + "openSite": "Seite öffnen", + "pause": "Pausieren", + "resume": "Fortsetzen" + }, + "statBoxes": { + "activeFor": "Aktiv seit", + "certificateExpiry": "Zertifikatablauf", + "lastCheck": "Letzter Check", + "lastResponseTime": "Letzte Antwortzeit" + }, + "status": { + "down": "ausgefallen", + "breached": "Schwellenwert überschritten", + "initializing": "wird initialisiert", + "maintenance": "Wartung", + "paused": "pausiert", + "total": "gesamt", + "up": "erreichbar" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Spiel", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Port", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infrastruktur", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "emailSettings": { - "buttonSendTestEmail": "Test-Email senden", - "description": "Konfigurieren Sie die E-Mail-Einstellungen für Ihr System. Diese werden zum Senden von Benachrichtigungen und Warnungen verwendet.", - "descriptionTransport": "Dies erstellt einen SMTP-Transport für NodeMailer", - "labelAddress": "E-Mail-Adresse - Wird zur Authentifizierung verwendet", - "labelConnectionHost": "E-Mail-Verbindungshost – Hostname zur Verwendung in der HELO/EHLO-Begrüßung", - "labelHost": "E-Mail-Host – Hostname oder IP-Adresse, mit der eine Verbindung hergestellt werden soll", - "labelIgnoreTLS": "STARTTLS abschalten: TLS nicht nutzen, selbst wenn der Server es unterstützt", - "labelPassword": "Email password - Password für Anmeldung", - "labelPasswordSet": "Passwort erstellt. Reset drücken um es zu ändern.", - "labelPool": "Verbindungspooling aktivieren: Vorhandene Verbindungen wiederverwenden, um die Leistung zu verbessern", - "labelPort": "Email port - Port zum verbinden", - "labelRejectUnauthorized": "Ungültige Zertifikate ablehnen. Verbindungen mit self-signed oder nicht vertrauenswürdigen Zertifikaten ablehnen.", - "labelRequireTLS": "STARTTLS erzwingen: TLS-Upgrade erforderlich, schlägt fehl, wenn nicht unterstützt", - "labelSecure": "SSL verwenden (empfohlen): Verbindung mit SSL/TLS verschlüsseln", - "labelTLSServername": "TLS-Servername – Optionaler Hostname für die TLS-Validierung, wenn der Host eine IP ist", - "labelUser": "E-Mail-Benutzer – Benutzername zur Authentifizierung, überschreibt die E-Mail-Adresse, falls angegeben", - "linkTransport": "Spezifikationen finden Sie hier", - "placeholderUser": "Leer lassen, wenn nicht benötigt", - "title": "Email", - "toastEmailRequiredFieldsError": "E-Mail-Adresse, Host, Port und Passwort sind erforderlich" + "createMonitor": { + "form": { + "advanced": { + "description": "Optionale Einstellungen für erweiterte Anwendungsfälle", + "option": { + "advancedMatching": { + "label": "Erweiterten Abgleich verwenden" + }, + "expectedValue": { + "label": "Erwarteter Wert" + }, + "jsonPath": { + "description": "Dieser Ausdruck wird gegen die JSON-Antwortdaten ausgewertet und das Ergebnis wird mit dem erwarteten Wert verglichen. Siehe jmespath.org für die Dokumentation der Abfragesprache.", + "label": "JSONPath-Ausdruck" + }, + "matchMethod": { + "label": "Abgleichmethode", + "equal": "Gleich", + "include": "Enthält", + "regex": "Regex" + } + }, + "title": "Erweiterte Einstellungen" + }, + "frequency": { + "description": "Wie oft möchten Sie den Status dieses Monitors überprüfen?", + "option": { + "frequency": { + "label": "Prüffrequenz", + "value": { + "fifteenMinutes": "15 Minuten", + "fifteenSeconds": "15 Sekunden", + "fiveMinutes": "5 Minuten", + "fourMinutes": "4 Minuten", + "oneMinute": "Eine Minute", + "tenMinutes": "10 Minuten", + "thirtyMinutes": "30 Minuten", + "thirtySeconds": "30 Sekunden", + "threeMinutes": "3 Minuten", + "twoMinutes": "Zwei Minuten" + } + } + }, + "title": "Prüffrequenz" + }, + "general": { + "option": { + "container": { + "label": "Containername/-ID", + "placeholder": "my-app oder abcd1234" + }, + "host": { + "label": "Host", + "placeholder": "192.168.1.100 oder example.com" + }, + "name": { + "label": "Anzeigename", + "placeholder": "z. B. Meine Website" + }, + "secret": { + "label": "Autorisierungsschlüssel", + "placeholder": "Geben Sie Ihren geheimen Schlüssel ein" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "Zu überwachende URL", + "placeholder": "localhost" + }, + "port": { + "label": "Zu überwachender Port", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Dienstname", + "placeholder": "z. B. my.service.v1 (leer lassen für allgemeinen Status)" + }, + "wsUrl": { + "label": "WebSocket-URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Allgemeine Einstellungen", + "description": { + "http": "Geben Sie die zu überwachende URL oder IP ein (z. B. https://example.com/ oder 192.168.1.100) und fügen Sie einen eindeutigen Anzeigenamen hinzu, der auf dem Dashboard erscheint.", + "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.", + "port": "Geben Sie die URL oder IP des Servers, die Portnummer und einen eindeutigen Anzeigenamen ein, 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.", + "game": "IP Adresse oder Hostnamen und Port für Ping eingeben (z.B. 192.168.1.100 oder beispiel.de) und Spieletyp auswählen.", + "pagespeed": "Verfolgen Sie die Ladezeit, Core Web Vitals und Optimierungswerte Ihrer Website.", + "grpc": "Geben Sie den Hostnamen und Port des gRPC-Servers ein, optional einen Dienstnamen für den Health-Check, und fügen Sie einen Anzeigenamen hinzu.", + "websocket": "Geben Sie die zu überwachende WebSocket-URL ein (z. B. wss://example.com/socket) und fügen Sie einen Anzeigenamen hinzu.", + "hardware": "Überwachen Sie CPU-, Arbeitsspeicher-, Festplattenauslastung und Temperatur Ihrer Infrastruktur." + } + }, + "ignoreTls": { + "description": "Konfigurieren Sie die TLS/SSL-Zertifikatsvalidierung für HTTPS-Verbindungen.", + "option": { + "tls": { + "label": "TLS/SSL-Fehler ignorieren" + } + }, + "title": "TLS/SSL-Einstellungen" + }, + "incidents": { + "description": "Ein gleitendes Fenster wird verwendet, um festzustellen, wann ein Monitor ausfällt. Der Status eines Monitors ändert sich erst, wenn der Prozentsatz der Checks im gleitenden Fenster den angegebenen Wert erreicht.", + "option": { + "checks": { + "label": "Anzahl der Checks im gleitenden Fenster" + }, + "percentage": { + "label": "Welcher Prozentsatz der Checks im gleitenden Fenster muss fehlschlagen/erfolgreich sein, bevor sich der Monitor-Status ändert?" + } + }, + "title": "Vorfälle" + }, + "notifications": { + "description": "Wählen Sie die Benachrichtigungskanäle aus, die Sie verwenden möchten", + "title": "Benachrichtigungen" + }, + "type": { + "description": "Wählen Sie die Art der durchzuführenden Prüfung", + "optionDockerDescription": "Docker verwenden, um zu überwachen, ob ein Container läuft.", + "optionGameDescription": "Überwachen, ob ein bestimmter Spieleserver online ist.", + "optionGrpcDescription": "gRPC-Dienste mit dem Standard Health Checking Protocol überwachen.", + "optionWebSocketDescription": "WebSocket-Endpunkte auf Verbindungszustand und Antwortzeit überwachen.", + "optionHttpDescription": "HTTP(S) verwenden, um Ihre Website oder Ihren API-Endpunkt zu überwachen.", + "optionPingDescription": "ICMP-Ping verwenden, um zu überwachen, ob ein Server online ist.", + "optionPortDescription": "Überwachen, ob ein bestimmter Port auf einem Server geöffnet ist.", + "title": "Typ" + }, + "thresholds": { + "title": "Alarmschwellenwerte", + "description": "Definieren Sie die Schwellenwerte, bei denen Alarme für diesen Hardware-Monitor ausgelöst werden sollen.", + "option": { + "cpuThreshold": { + "label": "CPU-Alarmschwelle (%)" + }, + "memoryThreshold": { + "label": "Arbeitsspeicher-Alarmschwelle (%)" + }, + "diskThreshold": { + "label": "Festplatten-Alarmschwelle (%)" + }, + "tempThreshold": { + "label": "Temperatur-Alarmschwelle (°C)" + } + } + }, + "geoChecks": { + "title": "Geo-verteilte Checks", + "description": "Führen Sie Checks von mehreren geografischen Standorten aus, um die globale Verfügbarkeit und Leistung zu überwachen.", + "option": { + "enabled": { + "label": "Geo-verteilte Checks aktivieren" + }, + "locations": { + "label": "Standorte", + "placeholder": "Standorte auswählen", + "options": { + "EU": "Europa", + "NA": "Nordamerika", + "AS": "Asien", + "SA": "Südamerika", + "AF": "Afrika", + "OC": "Ozeanien" + } + }, + "interval": { + "label": "Prüfintervall", + "value": { + "fiveMinutes": "5 Minuten", + "tenMinutes": "10 Minuten", + "fifteenMinutes": "15 Minuten", + "thirtyMinutes": "30 Minuten" + } + } + } + }, + "url": { + "title": "Monitor-IP/URL auf Statusseite", + "description": "Zeigt die IP-Adresse oder URL des Monitors auf der öffentlichen Statusseite an. Wenn deaktiviert, wird nur der Monitor-Name angezeigt, um sensible Informationen zu schützen.", + "option": { + "showURL": { + "label": "IP/URL auf Statusseite anzeigen", + "enabled": "Aktiviert", + "disabled": "Deaktiviert" + } + } + }, + "stats": { + "title": "Monitor-Verlauf", + "description": "Löschen Sie alle Überwachungsverläufe und Statistiken für Ihr Team. Diese Aktion ist unwiderruflich.", + "buttonText": "Alle Statistiken löschen", + "dialog": { + "title": "Alle Überwachungsverläufe löschen?", + "description": "Dies wird alle Überwachungsverläufe, Statistiken und Check-Daten für Ihr Team dauerhaft löschen. Diese Aktion kann nicht rückgängig gemacht werden.", + "confirm": "Ja, alle Statistiken löschen" + } + } + } }, - "pageSpeedSettings": { - "description": "Geben Sie Ihren Google PageSpeed API-Schlüssel ein, um die Google PageSpeed-Überwachung zu aktivieren. Klicken Sie auf „Zurücksetzen“, um den Schlüssel zu aktualisieren.", - "labelApiKeySet": "API key erstellt. Reset drücken um ihn zu ändern.", - "labelApiKey": "PageSpeed API key", - "title": "Google PageSpeed API key" + "editUser": { + "form": { + "roles": { + "title": "Rollen", + "description": "Weisen Sie dem Benutzer Rollen zu. Es können mehrere Rollen ausgewählt werden." + } + }, + "dialog": { + "removeUser": { + "title": "Benutzer entfernen", + "content": "Möchten Sie {{name}} wirklich aus Ihrem Team entfernen? Diese Aktion kann nicht rückgängig gemacht werden." + } + } }, - "saveButtonLabel": "Speichern", - "statsSettings": { - "clearAllStatsButton": "Alle Statistiken löschen", - "clearAllStatsDescription": "Alle Statistiken löschen. Dies ist irreversibel.", - "clearAllStatsDialogConfirm": "Ja, alle Statistiken löschen", - "clearAllStatsDialogDescription": "Nach dem Entfernen können der Überwachungsverlauf und die Statistiken nicht mehr abgerufen werden.", - "clearAllStatsDialogTitle": "Möchten Sie alle Statistiken löschen?", - "description": "Legen Sie fest, wie lange Sie historische Daten aufbewahren möchten. Sie können auch alle vorhandenen Daten löschen.", - "labelTTL": "Die Tage, an denen Sie den Überwachungsverlauf beibehalten möchten.", - "labelTTLOptional": "0 für unendlich", - "title": "Monitorverlauf" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Vorfall lösen", + "option": { + "comment": { + "label": "Kommentar (optional)", + "placeholder": "Kommentar zur Lösung hinzufügen..." + } + } + }, + "details": { + "analysis": "Vorfallanalyse", + "comment": "Kommentar:", + "detailsLabel": "Details", + "downtime": "Ausfallzeit:", + "message": "Nachricht:", + "monitor": "Monitor:", + "overview": "Übersicht", + "resolutionDetails": "Lösungsdetails", + "resolutionType": "Typ:", + "resolutionTypes": { + "automatic": "Automatisch", + "manual": "Manuell" + }, + "resolve": "Vorfall lösen", + "resolvedAt": "Gelöst am:", + "resolvedBy": "Gelöst von:", + "startedAt": "Begonnen am:", + "status": "Status:", + "statusCode": "Statuscode:", + "timeline": "Zeitverlauf", + "title": "Vorfalldetails", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Alle Monitore", + "monitor": "Monitor", + "resolutionType": "Lösungstyp", + "resolutionTypes": { + "manual": "Manuell", + "automatic": "Automatisch", + "all": "Alle" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Aktive Vorfälle", + "active_zero": "Keine aktiven Vorfälle", + "active_one": "{{count}} aktiver Vorfall", + "active_other": "{{count}} aktive Vorfälle" + }, + "incidentStats": { + "avgResolutionTime": "Durchschn. Lösungszeit", + "mostAffectedMonitor": "Am meisten betroffener Monitor", + "title": "Vorfallstatistiken", + "totalIncidents": "Vorfälle gesamt" + }, + "latestIncidents": { + "title": "Neueste Vorfälle" + } + }, + "table": { + "actions": { + "details": "Details", + "goToMonitor": "Zum Monitor", + "resolveManually": "Manuell lösen" + }, + "activeIncidents": "Aktive Vorfälle", + "headers": { + "endTime": "Endzeit", + "resolutionType": "Lösungstyp", + "startTime": "Anfangszeit", + "statusCode": "Statuscode" + }, + "resolvedIncidents": "Gelöste Vorfälle", + "status": { + "active": "Aktiv", + "resolved": "Gelöst" + } + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "Alle Monitore löschen", - "description": "Alle Monitore löschen", - "dialogConfirm": "Ja, alle Monitore löschen", - "dialogDescription": "Gelöschte Monitore können nicht wiederhergestellt werden.", - "dialogTitle": "Wirklich alle Monitore löschen?", - "title": "System zurücksetzen" - }, - "timezoneSettings": { - "description": "Wählen Sie die Zeitzone aus, die zum Anzeigen von Datum und Uhrzeit in der gesamten Anwendung verwendet wird.", - "label": "Zeitzone", - "title": "Zeitzone" - }, - "title": "Einstellungen", - "uiSettings": { - "description": "Wechseln Sie zwischen hellem und dunklem Modus oder ändern Sie die Sprache der Benutzeroberfläche.", - "labelLanguage": "Sprache", - "labelTheme": "Themenmodus", - "title": "Aussehen" - }, - "urlSettings": { - "description": "Zeigt die IP-Adresse oder URL des Monitors auf der öffentlichen Statusseite an. Wenn diese Option deaktiviert ist, wird zum Schutz vertraulicher Informationen nur der Monitorname angezeigt.", - "label": "IP/URL auf der Statusseite anzeigen", - "selectDisabled": "Inaktiv", - "selectEnabled": "Aktiv", - "title": "Überwachen Sie IP/URL auf der Statusseite" - }, - "globalThresholds": { - "title": "Globale Schwellenwerte", - "description": "Globale CPU-, Speicher-, Festplatten- und Temperaturschwellenwerte festlegen. Wenn ein Wert angegeben ist, wird er automatisch für die Überwachung genutzt." - } - }, - "statusPageCreate": { - "buttonSave": "Speichern" - }, - "incidentsOptionsHeaderFilterResolved": "Gelöst", - "settingsSave": "Speichern", - "statusPageCreateAppearanceTitle": "Aussehen", - "confirmPassword": "Passwort bestätigen", - "monitorHooks": { - "failureAddDemoMonitors": "Demo Monitor konnte nicht hinzugefügt werden", - "successAddDemoMonitors": "Demo Monitor erfolgreich hinzugefügt" - }, - "settingsAppearance": "Aussehen", - "settingsDisplayTimezone": "Zeitzone anzeigen", - "settingsGeneralSettings": "Allgemeine Einstellungen", - "incidentsOptionsHeaderTotalIncidents": "Gesamtanzahl der Vorfälle", - "statusPage": { - "deleteSuccess": "Statusseite erfolgreich gelöscht", - "deleteFailed": "Löschen der Statusseite fehlgeschlagen", - "createSuccess": "Statusseite erfolgreich erstellt", - "updateSuccess": "Statusseite erfolgreich aktualisiert", - "generalSettings": "Allgemeine Einstellungen", - "contents": "Inhalt", - "fallback": { - "checks": [ - "Überwachen und zeigen Sie den Zustand Ihrer Dienste in Echtzeit an.", - "Verfolgen Sie mehrere Dienste und teilen Sie deren Status.", - "Informieren Sie Nutzer über Ausfälle und Leistung." - ], - "title": "Eine Statusseite wird verwendet, um:", - "actionButton": "Lassen Sie uns Ihre erste Statusseite erstellen!" - } - }, - "testNotificationsDisabled": "Für diesen Monitor sind keine Benachrichtigungen eingerichtet. Sie müssen eine hinzufügen, indem Sie auf die Schaltfläche „Konfigurieren“ klicken.", - "incidentsTableResolvedAt": "Gelöst am", - "incidentsTableActionResolve": "Lösen", - "checkHooks": { - "failureResolveOne": "Der Vorfall konnte nicht gelöst werden.", - "failureResolveAll": "Es konnten nicht alle Vorfälle gelöst werden.", - "failureResolveMonitor": "Monitorvorfälle konnten nicht behoben werden." - }, - "checkFormError": "Bitte überprüfen Sie das Formular auf Fehler.", - "diagnosticsPage": { - "diagnosticDescription": "Systemdiagnose", - "statsDescription": "Systemstatistiken", - "gauges": { - "heapAllocationTitle": "Heap-Zuweisung", - "heapAllocationSubtitle": "% des verfügbaren Speichers", - "heapUsageTitle": "Heap-Nutzung", - "heapUsageSubtitle": "% des verfügbaren Speichers", - "heapUtilizationTitle": "Heap-Auslastung", - "heapUtilizationSubtitle": "% der zugewiesenen", - "instantCpuUsageTitle": "Sofortige CPU-Auslastung", - "instantCpuUsageSubtitle": "% der von der CPU genutzten 1s" - }, - "stats": { - "eventLoopDelayTitle": "Ereignisschleifenverzögerung", - "uptimeTitle": "Betriebszeit", - "usedHeapSizeTitle": "Verwendete Heap-Größe", - "totalHeapSizeTitle": "Gesamtgröße des Heapspeichers", - "osMemoryLimitTitle": "Betriebssystem-Speicherlimit" - } - }, - "pageSpeedLighthouseAPI": "Verwenden Sie die Lighthouse PageSpeed API, um Ihre Website zu überwachen", - "time": { - "threeMinutes": "3 Minuten", - "fiveMinutes": "5 Minuten", - "tenMinutes": "10 Minuten", - "twentyMinutes": "20 Minuten", - "oneHour": "1 Stunde", - "oneDay": "1 Tag", - "oneWeek": "1 Woche", - "fourMinutes": "4 Minuten", - "oneMinute": "Eine Minute", - "twoMinutes": "Zwei Minuten", - "fifteenSeconds": "15 Sekunden", - "thirtySeconds": "30 Sekunden" - }, - "general": { - "noOptionsFound": "Keine {{unit}} gefunden" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [ - "Verfolgen Sie die Leistung Ihrer Server", - "Identifizieren Sie Engpässe und optimieren Sie die Nutzung", - "Sorgen Sie für Zuverlässigkeit durch Echtzeitüberwachung" - ], - "title": "Ein Infrastrukturmonitor wird verwendet, um:", - "actionButton": "Lassen Sie uns Ihren ersten Infrastrukturmonitor erstellen!" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [ - "Markieren Sie Ihre Wartungszeiträume.", - "Beseitigen Sie Missverständnisse.", - "Beenden Sie das Senden von Warnmeldungen in Wartungsfenstern." - ], - "title": "Ein Wartungsfenster wird verwendet, um:", - "actionButton": "Lassen Sie uns Ihr erstes Wartungsfenster erstellen!" - } - }, - "pageSpeed": { - "fallback": { - "checks": [ - "Berichten Sie über die Benutzerfreundlichkeit einer Seite.", - "Helfen Sie bei der Analyse der Webseitengeschwindigkeit.", - "Geben Sie Verbesserungsvorschläge für die Seite." - ], - "title": "Ein PageSpeed-Monitor wird verwendet, um:", - "actionButton": "Lassen Sie uns Ihren ersten PageSpeed-Monitor erstellen!" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [ - "Prüfen Sie, ob Websites oder Server online und reaktionsfähig sind.", - "Benachrichtigen Sie Ihre Teams über Ausfallzeiten oder Leistungsprobleme.", - "Überwachen Sie HTTP-Endpunkte, Pings, Container und Ports.", - "Verfolgen Sie historische Verfügbarkeits- und Zuverlässigkeitstrends." - ], - "title": "Ein Uptime-Monitor wird verwendet, um:", - "actionButton": "Lassen Sie uns Ihren ersten Uptime-Monitor erstellen!" - } - }, - "editUserPage": { - "form": { - "email": "E-Mail", - "firstName": "Vorname", - "lastName": "Nachname", - "role": "Rollen", - "save": "Speichern" - }, - "table": { - "actionHeader": "Aktion", - "roleHeader": "Rolle" - }, - "title": "Benutzer bearbeiten", - "toast": { - "successUserUpdate": "Benutzer erfolgreich aktualisiert", - "validationErrors": "Validierungsfehler" - } - }, - "incidentsPageActionResolveMonitor": "Monitor-Vorfälle beheben", - "incidentsPageActionResolveAll": "Alle Vorfälle lösen", - "matchMethodOptions": { - "equal": "Gleich", - "equalPlaceholder": "Erfolg", - "include": "Enthält", - "includePlaceholder": "OK", - "regex": "Regex", - "regexPlaceholder": "^(success|ok)$", - "text": "Übereinstummungsmethode" - }, - "monitorType": { - "docker": { - "label": "Container Name/ID", - "namePlaceholder": "Mein Container", - "placeholder": "Meine-App" - }, - "http": { - "label": "Zu überwachende URL", - "namePlaceholder": "Google", - "placeholder": "google.de" - }, - "ping": { - "label": "Zu überwachende IP-Adresse", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" - }, - "port": { - "label": "Zu überwachende URL", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" - }, - "game": { - "label": "Zu überwachende URL", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "JSON Pfad" - }, - "bytesPerSecond": "Bytes pro Sekunde", - "bytesReceived": "Empfangene Bytes", - "bytesSent": "Gesendete Bytes", - "chooseGame": "Spiel auswählen", - "createMonitorPage": { - "incidentConfigDescription": "Mithilfe eines gleitenden Fensters wird ermittelt, wann ein Monitor ausfällt. Der Status eines Monitors ändert sich nur, wenn der Prozentsatz der Prüfungen im gleitenden Fenster den angegebenen Wert erreicht.", - "incidentConfigStatusWindowLabel": "Wie viele Prüfungen sollen im gleitenden Fenster ausgeführt werden?", - "incidentConfigStatusWindowThresholdLabel": "Wie viel Prozent der Prüfungen im gleitenden Fenster sollen fehlschlagen/erfolgreich sein, bevor sich der Status ändert?", - "incidentConfigTitle": "Vorfälle", - "incidentConfigDescriptionV2": "Anzahl der aufeinanderfolgenden Prüfungen, die zum Ändern des Monitorstatus erforderlich sind. Maximal 25", - "incidentConfigStatusCheckNumber": "Anzahl der Prüfungen bevor sich er Status ändert", - "intervalTitle": "Prüfintervall", - "intervalDescription": "Wie oft soll die Überwachung geprüft werden." - }, - "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." + "charts": { + "labels": { + "cpu": "CPU-Auslastung", + "disk": "Festplattenauslastung", + "memory": "Arbeitsspeicherauslastung", + "netBytesRecv": "{{name}} - Empfangene Bytes", + "netBytesSent": "{{name}} - Gesendete Bytes", + "temp": "Temp." + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Max. Frequenz", + "title": "CPU-Auslastung", + "upperLabel": "Aktuelle Frequenz" + }, + "disk": { + "lowerLabel": "Frei", + "title": "Festplatte {{idx}} Auslastung", + "upperLabel": "Belegt" + }, + "memory": { + "lowerLabel": "Frei", + "title": "Arbeitsspeicherauslastung", + "upperLabel": "Belegt" + } + }, + "statBoxes": { + "avgCpuTemperature": "Durchschn. CPU Termperatur", + "cpuFrequency": "CPU Frequenz", + "cpuLogical": "CPU (logisch)", + "cpuPhysical": "CPU (physisch)", + "disk": "Festplatte", + "memory": "Arbeitsspeicher", + "os": "Betriebssystem" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Festplatte", + "memory": "Arbeitsspeicher" + } + }, + "tabs": { + "labels": { + "network": "Netzwerk", + "overview": "Übersicht" + } + }, + "fallback": { + "actionButton": "Monitor erstellen!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Ein Infrastruktur-Monitor wird verwendet, um:" + } + }, + "logs": { + "tabs": { + "diagnostics": "Diagnose", + "logs": "Server logs", + "queue": "Warteschlange" + }, + "logLevelSelect": { + "label": "Log-Level" + }, + "jobQueue": "Job-Warteschlange", + "failedJobs": "Fehlgeschlagene Jobs", + "noLogs": "Keine Protokolle gefunden", + "metrics": { + "jobs": "Jobs", + "activeJobs": "Aktive Jobs", + "failingJobs": "Fehlschlagende Jobs", + "totalRuns": "Ausführungen gesamt", + "totalFailures": "Fehler gesamt" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Event-Loop-Verzögerung", + "uptime": "Betriebszeit", + "usedHeapSize": "Verwendete Heap-Größe", + "totalHeapSize": "Gesamte Heap-Größe", + "osMemoryLimit": "OS-Speicherlimit" + }, + "gauges": { + "heapAllocation": "Heap-Zuweisung", + "heapUsage": "Heap-Nutzung", + "heapUtilization": "Heap-Auslastung", + "instantCpuUsage": "Momentane CPU-Auslastung", + "availableMemoryPercentage": "% des verfügbaren Speichers", + "allocatedPercentage": "% zugewiesen", + "usedSPercentage": "% von 1s durch CPU genutzt", + "total": "Gesamt", + "used": "Belegt" + } + }, + "table": { + "headers": { + "timestamp": "Zeitstempel", + "level": "Stufe", + "service": "Dienst", + "method": "Methode", + "monitorId": "Monitor-ID", + "runCount": "Ausführungen", + "failCount": "Fehlerzahl", + "lastRunAt": "Letzter Lauf am", + "lockedAt": "Gesperrt am", + "lastFinishedAt": "Zuletzt abgeschlossen am", + "lastRunTook": "Letzter Lauf dauerte", + "lastFailedAt": "Letzter Fehler am", + "failReason": "Fehlergrund" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Wartungsfenster erstellen!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Ein Wartungsfenster wird verwendet, um:" + }, + "table": { + "headers": { + "nextWindow": "Nächstes Fenster", + "repeat": "Wiederholen" + } + }, + "form": { + "general": { + "title": "Allgemeine Einstellungen", + "description": "Legen Sie einen Namen und eine Wiederholungsoption für Ihr Wartungsfenster fest.", + "option": { + "name": { + "label": "Name", + "placeholder": "z. B. Wöchentliche Wartung" + }, + "repeat": { + "label": "Wiederholen" + } + } + }, + "startDate": { + "title": "Startdatum", + "description": "Wählen Sie das Startdatum für Ihr Wartungsfenster.", + "option": { + "startDate": { + "label": "Startdatum" + } + } + }, + "startTime": { + "title": "Startzeit", + "description": "Legen Sie die Startzeit und Dauer für Ihr Wartungsfenster fest. Alle Werte sind in UTC", + "option": { + "duration": { + "label": "Dauer" + }, + "startTime": { + "label": "Startzeit" + } + }, + "monitors": { + "title": "Monitore", + "description": "Monitore, für die das Wartungsfenster gelten soll", + "option": { + "addMonitors": { + "label": "Monitore hinzufügen" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "Kanal erstellen", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Benachrichtigungskanäle werden verwendet, um:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Zugriffstoken", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "Die Adresse, an die Benachrichtigungen gesendet werden.", + "optionAddress": "Adresse", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Adresse" + }, + "homeServer": { + "optionHomeServer": "Home-Server", + "placeholder": "example.com" + }, + "matrix": { + "description": "Konfigurieren Sie Ihre Matrix-Homeserver-Verbindung für Benachrichtigungen.", + "title": "Matrix-Konfiguration" + }, + "notificationName": { + "description": "Ein beschreibender Name für den Benachrichtigungskanal", + "optionName": "Kanalname", + "placeholder": "z. B. Produktionsalarme", + "title": "Kanalname" + }, + "pagerDuty": { + "description": "Ihr PagerDuty-Integrationsschlüssel für den Empfang von Alarmen.", + "optionIntegrationKey": "Integrationsschlüssel", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Integrationsschlüssel" + }, + "roomId": { + "optionRoomId": "Raum-ID", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Wählen Sie den Typ des zu erstellenden Benachrichtigungskanals.", + "optionType": "Typ", + "title": "Kanaltyp" + }, + "telegram": { + "title": "Telegram-Konfiguration", + "description": "Um Telegram-Benachrichtigungen zu aktivieren, erstelle einen Telegram-Bot mit BotFather, einem offiziellen Bot zum Erstellen und Verwalten von Telegram-Bots. Dann hole dir das API-Token und die Chat-ID und notiere sie hier.", + "optionBotToken": "Bot-Token", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "Chat-ID", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Ziel" + } + } + }, + "pageSpeed": { + "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)" + }, + "legend": { + "title": "PageSpeed-Bericht", + "weight": "Gewichtung" + }, + "pie": { + "title": "Leistungsbericht" + } + }, + "table": { + "headers": { + "pageSpeedScore": "PageSpeed-Wert" + } + }, + "fallback": { + "actionButton": "Monitor erstellen!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "Ein PageSpeed-Monitor wird verwendet, um:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Anzeigesprache Zeitzone", + "description": "Wählen Sie die Zeitzone, die für die Anzeige von Datum und Uhrzeit in der Anwendung verwendet wird.", + "option": { + "timezone": { + "label": "Anzeige-Zeitzone" + } + } + }, + "ui": { + "title": "Erscheinungsbild", + "description": "Wechseln Sie zwischen hellem und dunklem Modus, ändern Sie die Sprache oder passen Sie den Diagrammtyp an.", + "option": { + "theme": { + "label": "Theme-Modus", + "light": "Hell", + "dark": "Dunkel" + }, + "language": { + "label": "Sprache" + }, + "chartType": { + "label": "Diagrammtyp", + "histogram": "Histogramm", + "heatmap": "Heatmap" + } + } + }, + "pagespeed": { + "title": "Google PageSpeed API-Schlüssel", + "description": "Geben Sie Ihren Google PageSpeed API-Schlüssel ein, um die Google PageSpeed-Überwachung zu aktivieren. Klicken Sie auf Zurücksetzen, um den Schlüssel zu aktualisieren.", + "option": { + "apiKey": { + "label": "PageSpeed API-Schlüssel", + "labelSet": "API-Schlüssel ist gesetzt. Klicken Sie auf Zurücksetzen, um ihn zu ändern.", + "placeholder": "Geben Sie Ihren Google PageSpeed API-Schlüssel ein" + } + } + }, + "url": { + "title": "Monitor-IP/URL auf Statusseite", + "description": "Zeigt die IP-Adresse oder URL des Monitors auf der öffentlichen Statusseite an. Wenn deaktiviert, wird nur der Monitor-Name angezeigt, um sensible Informationen zu schützen.", + "option": { + "showURL": { + "label": "IP/URL auf Statusseite anzeigen", + "enabled": "Aktiviert", + "disabled": "Deaktiviert" + } + } + }, + "stats": { + "title": "Monitor-Verlauf", + "description": "Löschen Sie alle Überwachungsverläufe und Statistiken für Ihr Team. Diese Aktion ist unwiderruflich.", + "option": { + "clear": { + "label": "Alle Statistiken löschen. Dies ist unwiderruflich." + } + }, + "dialog": { + "title": "Alle Überwachungsverläufe löschen?", + "description": "Dies kann nicht rückgängig gemacht werden" + } + }, + "retention": { + "title": "Check-Aufbewahrung", + "description": "Legen Sie fest, wie lange Check-Daten aufbewahrt werden, bevor sie automatisch bereinigt werden.", + "option": { + "days": { + "label": "Aufbewahrungsdauer (Tage)", + "unlimited": "Unbegrenzt" + } + } + }, + "thresholds": { + "title": "Globale Schwellenwerte", + "description": "Legen Sie globale CPU-, Arbeitsspeicher-, Festplatten- und Temperaturschwellenwerte für die Infrastrukturüberwachung fest. Diese gelten für alle Hardware-Monitore, sofern nicht überschrieben.", + "option": { + "cpu": { + "label": "CPU-Schwellenwert (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Arbeitsspeicher-Schwellenwert (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Festplatten-Schwellenwert (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Temperatur-Schwellenwert (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "E-Mail-Einstellungen", + "description": "Konfigurieren Sie die E-Mail-Einstellungen für Ihr System. Dies wird zum Senden von Benachrichtigungen und Alarmen verwendet.", + "descriptionTransport": "Dies erstellt einen SMTP-Transport für NodeMailer", + "descriptionTransportLink": "Spezifikationen hier ansehen", + "option": { + "host": { + "label": "E-Mail-Host - Hostname oder IP-Adresse für die Verbindung", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "E-Mail-Port - Port für die Verbindung", + "placeholder": "587" + }, + "address": { + "label": "E-Mail-Adresse - Für die Authentifizierung verwendet", + "placeholder": "you@example.com" + }, + "user": { + "label": "E-Mail-Benutzer - Benutzername für die Authentifizierung, überschreibt E-Mail-Adresse wenn angegeben", + "placeholder": "Leer lassen, wenn nicht erforderlich" + }, + "password": { + "label": "E-Mail-Passwort - Passwort für die Authentifizierung", + "labelSet": "Passwort ist gesetzt. Klicken Sie auf Zurücksetzen, um es zu ändern.", + "placeholder": "Geben Sie Ihr Passwort ein" + }, + "tlsServername": { + "label": "TLS-Servername - Optionaler Hostname für TLS-Validierung wenn Host eine IP ist", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "E-Mail-Verbindungshost - Hostname für die HELO/EHLO-Begrüßung", + "placeholder": "localhost" + }, + "secure": { + "label": "SSL verwenden (empfohlen): Verbindung mit SSL/TLS verschlüsseln" + }, + "pool": { + "label": "Verbindungspooling aktivieren: Bestehende Verbindungen zur Leistungsverbesserung wiederverwenden" + }, + "ignoreTLS": { + "label": "STARTTLS deaktivieren: TLS nicht verwenden, auch wenn der Server es unterstützt" + }, + "requireTLS": { + "label": "STARTTLS erzwingen: TLS-Upgrade erforderlich, Fehler wenn nicht unterstützt" + }, + "rejectUnauthorized": { + "label": "Ungültige Zertifikate ablehnen: Verbindungen mit selbstsignierten oder nicht vertrauenswürdigen Zertifikaten ablehnen" + } + } + }, + "demoMonitors": { + "title": "Demo-Monitore", + "description": "Beispiel-Monitore zu Demonstrationszwecken hinzufügen." + }, + "removeMonitors": { + "title": "System zurücksetzen", + "description": "Alle Monitore aus Ihrem System entfernen.", + "dialog": { + "title": "Alle Monitore entfernen?", + "description": "Dies kann nicht rückgängig gemacht werden." + } + }, + "exportMonitors": { + "title": "Monitore exportieren" + }, + "importExportMonitors": { + "title": "Monitore importieren / exportieren", + "description": "Importieren oder exportieren Sie Ihre Monitor-Daten als JSON-Datei zur Sicherung oder Übertragung." + }, + "about": { + "title": "Über", + "developedBy": "Entwickelt von Bluewave Labs" + }, + "validation": { + "errorMessage": "Bitte beheben Sie die folgenden Validierungsfehler:" + } + } + }, + "statusPages": { + "deleteSuccess": "Statusseite erfolgreich gelöscht", + "fallback": { + "title": "Eine Statusseite wird verwendet, um:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "Statusseite erstellen!" + }, + "monitorsList": { + "chartTypeHeatmap": "Heatmap", + "chartTypeHistogram": "Histogramm", + "noData": "Keine Daten verfügbar", + "infrastructure": { + "title": "Infrastruktur", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Arbeitsspeicher", + "disk": "Festplatte", + "usage": "Auslastung", + "used": "Belegt", + "total": "Gesamt" + }, + "uptime": { + "title": "Verfügbarkeit", + "responseTime": "Antwortzeit" + } + }, + "statusBar": { + "allDown": "Alle Systeme sind ausgefallen", + "allUp": "Alle Systeme funktionsfähig", + "degraded": "Einige Systeme haben Probleme", + "unknown": "Systemstatus kann nicht ermittelt werden" + }, + "table": { + "headers": { + "name": "Name der Statusseite", + "url": "Öffentliche URL" + }, + "published": "Veröffentlicht", + "unpublished": "Unveröffentlicht" + }, + "title": "Statusseiten", + "details": { + "empty": { + "title": "Hier gibt es noch nichts", + "addMonitor": "Fügen Sie einen Monitor hinzu, um zu beginnen" + } + }, + "form": { + "access": { + "title": "Zugriff", + "description": "Wenn Ihre Statusseite bereit ist, können Sie sie als veröffentlicht markieren.", + "option": { + "published": { + "name": "Veröffentlicht und öffentlich sichtbar" + } + } + }, + "basicInfo": { + "title": "Grundinformationen", + "description": "Definieren Sie den Firmennamen und die Subdomain, auf die Ihre Statusseite verweist.", + "option": { + "name": { + "label": "Firmenname", + "placeholder": "Muster GmbH" + }, + "url": { + "label": "Ihre Statusseiten-Adresse", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "Monitore", + "description": "Wählen Sie die Monitore aus, die auf Ihrer Statusseite angezeigt werden sollen.", + "noMonitors": "Keine Monitore ausgewählt", + "option": { + "monitors": { + "label": "Monitore auswählen", + "placeholder": "Monitore suchen und auswählen..." + } + } + }, + "timezone": { + "title": "Zeitzone", + "description": "Wählen Sie die Zeitzone, in der Ihre Statusseite angezeigt wird.", + "option": { + "timezone": { + "label": "Zeitzone", + "placeholder": "Zeitzone auswählen..." + } + } + }, + "appearance": { + "title": "Erscheinungsbild", + "description": "Definieren Sie das Standardaussehen Ihrer öffentlichen Statusseite.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Markenfarbe" + } + } + }, + "features": { + "title": "Funktionen", + "description": "Konfigurieren Sie, welche Informationen auf Ihrer Statusseite angezeigt werden.", + "option": { + "showCharts": { + "label": "Antwortzeit-Diagramme anzeigen" + }, + "showUptimePercentage": { + "label": "Verfügbarkeitsprozentsatz anzeigen" + }, + "showAdminLoginLink": { + "label": "Admin-Login-Link anzeigen" + }, + "showInfrastructure": { + "label": "Infrastruktur-Metriken anzeigen" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Monitore suchen..." + } + }, + "table": { + "headers": { + "responseTime": "Antwortzeit" + } + }, + "fallback": { + "actionButton": "Monitor erstellen!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Ein Verfügbarkeitsmonitor wird verwendet, um:" + } } } } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 5f582cac5..eb9a34339 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -343,6 +343,7 @@ "register": { "title": "Welcome back to Checkmate!", "subtitle": "Sign up to get started", + "setupNotice": "This page is only available during initial setup. Once a super admin is registered, additional users can be invited from team settings.", "submit": "Register" }, "forgotPassword": { @@ -397,16 +398,28 @@ "activeFor": "Active for", "certificateExpiry": "Certificate expiry", "lastCheck": "Last check", - "lastResponseTime": "Last response time" + "lastResponseTime": "Last response time", + "serviceIsDown": "Service is down" }, "status": { "down": "down", - "breached": "breached", + "breached": "threshold exceeded", "initializing": "initializing", "maintenance": "maintenance", "paused": "paused", "total": "total", "up": "up" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Game", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Port", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infrastructure", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" } } }, @@ -534,19 +547,12 @@ }, "type": { "description": "Select the type of check to perform", - "optionDocker": "Docker", "optionDockerDescription": "Use Docker to monitor if a container is running.", - "optionGame": "Game", "optionGameDescription": "Monitor if a specific game server is online.", - "optionGrpc": "gRPC", "optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.", - "optionWebSocket": "WebSocket", "optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.", - "optionHttp": "HTTP(S)", "optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.", - "optionPing": "Ping", "optionPingDescription": "Use ICMP Ping to monitor if a server is online.", - "optionPort": "Port", "optionPortDescription": "Monitor if a specific port on a server is open.", "title": "Type" }, @@ -947,6 +953,34 @@ "description": "Select the type of notification channel to create.", "optionType": "Type", "title": "Channel type" + }, + "telegram": { + "title": "Telegram configuration", + "description": "Configure your Telegram bot to send notifications.", + "optionBotToken": "Bot token", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "Chat ID", + "placeholderChatId": "-1001234567890" + }, + "pushover": { + "title": "Pushover configuration", + "description": "Configure Pushover to receive push notifications on your devices.", + "optionAppToken": "Application API token", + "placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi", + "optionUserKey": "User key", + "placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG" + }, + "twilio": { + "title": "Twilio SMS configuration", + "description": "Configure Twilio to send SMS notifications to a phone number.", + "optionAccountSid": "Account SID", + "placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "optionAuthToken": "Auth token", + "placeholderAuthToken": "your_auth_token", + "optionFromNumber": "From number (Twilio)", + "placeholderFromNumber": "+15551234567", + "optionToNumber": "To number (recipient)", + "placeholderToNumber": "+15559876543" } }, "table": { @@ -1041,7 +1075,7 @@ }, "stats": { "title": "Monitor history", - "description": "Define how long you want to retain historical data. You can also clear all existing data.", + "description": "Clear all monitoring history and statistics for your team. This action is irreversible.", "option": { "clear": { "label": "Clear all stats. This is irreversible." @@ -1052,6 +1086,16 @@ "description": "This cannot be undone" } }, + "retention": { + "title": "Check retention", + "description": "Set how long check data is retained before being automatically cleaned up.", + "option": { + "days": { + "label": "Retention period (days)", + "unlimited": "Unlimited" + } + } + }, "thresholds": { "title": "Global Thresholds", "description": "Set global CPU, Memory, Disk, and Temperature thresholds for infrastructure monitoring. These apply to all hardware monitors unless overridden.", @@ -1168,12 +1212,38 @@ }, "monitorsList": { "chartTypeHeatmap": "Heatmap", - "chartTypeHistogram": "Histogram" + "chartTypeHistogram": "Histogram", + "noData": "No data available", + "infrastructure": { + "title": "Infrastructure", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Memory", + "disk": "Disk", + "usage": "Usage", + "used": "Used", + "total": "Total" + }, + "uptime": { + "title": "Uptime", + "responseTime": "Response time" + } }, "statusBar": { - "allDown": "All systems are down", "allUp": "All systems operational", + "allBreached": "All systems have exceeded thresholds", + "allMaintenance": "All systems are under maintenance", + "allDown": "All systems are down", + "allPaused": "All systems are paused", + "allInitializing": "All systems are initializing", + "breachedAndDown": "Some systems have exceeded thresholds and are down", + "breached": "Some systems have exceeded thresholds", "degraded": "Some systems are experiencing issues", + "maintenanceAndDown": "Some systems are down during maintenance", + "maintenance": "Some systems are under maintenance", + "partiallyPaused": "Some systems are paused", + "initializing": "Some systems are initializing", + "noMonitors": "No monitors have been configured", "unknown": "Unable to determine system status" }, "table": { @@ -1260,6 +1330,9 @@ }, "showAdminLoginLink": { "label": "Show admin login link" + }, + "showInfrastructure": { + "label": "Show infrastructure metrics" } } } diff --git a/client/src/locales/es.json b/client/src/locales/es.json index ed6a98c5d..2006e1cea 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -1,1108 +1,1305 @@ { - "submit": "Enviar", - "title": "Titulo", - "distributedStatusHeaderText": "Cobertura en tiempo real, para dispositivos reales", - "distributedStatusSubHeaderText": "Potenciado por millones de dispositivos alrededor del mundo, vea el rendimiento del sistema por región global, país o ciudad", - "settingsDisabled": "Deshabilitado", - "settingsSuccessSaved": "Ajustes guardados correctamente", - "settingsFailedToSave": "Fallo al guardar los ajustes", - "settingsStatsCleared": "Estadísticas eliminadas correctamente", - "settingsFailedToClearStats": "Fallo al eliminar las estadísticas", - "settingsMonitorsDeleted": "Todos los monitores eliminados correctamente", - "settingsFailedToDeleteMonitors": "Fallo al eliminar todos los monitores", - "starPromptTitle": "", - "starPromptDescription": "Vea las últimas versiones y ayude a incrementar la comunidad en Github", - "https": "HTTPS", - "http": "HTTP", - "monitor": "monitor", - "aboutus": "Acerca de", - "now": "Ahora", - "delete": "Eliminar", - "configure": "Configurar", - "responseTime": "Tiempo de Respuesta", - "ms": "ms", - "bar": "Barra", - "area": "Área", - "country": "PAIS", - "city": "CIUDAD", - "response": "RESPUESTA", - "monitorStatusUp": "El monitor {name} ({url}) está ahora activo y respondiendo", - "monitorStatusDown": "El monitor {name} ({url}) está inactivo y no está respondiendo", - "webhookSendSuccess": "Notificación webhook enviada correctamente", - "webhookSendError": "Error al enviar notificación webhook a {platform}", - "webhookUnsupportedPlatform": "Plataforma no soportada: {platform}", - "distributedRightCategoryTitle": "Monitor", - "distributedStatusServerMonitors": "Monitores de Servidores", - "distributedStatusServerMonitorsDescription": "Monitoree el estado de los servidores relacionados", - "distributedUptimeCreateSelectURL": "Aquí puede seleccionar la URL del anfitrión, junto con el tipo de monitor.", - "distributedUptimeCreateChecks": "Verificaciones a realizar", - "distributedUptimeCreateChecksDescription": "Usted siempre puede agregar o eliminar verificaciones luego de añadir su sitio.", - "distributedUptimeCreateIncidentNotification": "Notificaciones de incidentes", - "distributedUptimeCreateIncidentDescription": "Cuando ocurra un incidente, notificar a usuarios.", - "distributedUptimeCreateAdvancedSettings": "Ajustes avanzados", - "distributedUptimeDetailsNoMonitorHistory": "No hay historial de verificaciones para este monitor aún.", - "distributedUptimeDetailsStatusHeaderUptime": "Tiempo de funcionamiento:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Actualizado por última vez", - "notifications": { - "enableNotifications": "Habilitar notificaciones para {{platform}}", - "testNotification": "Notificación de prueba", - "addOrEditNotifications": "Agregar o eliminar notificaciones", - "slack": { - "label": "Slack", - "description": "Para habilitar notificaciones de Slack, cree una aplicación de Slack y habilite los webhooks entrantes. Luego de eso, simplemente introduzca la URL del webhook aquí.", - "webhookLabel": "URL del webhook", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "discord": { - "label": "Discord", - "description": "Para enviar datos a un canal de Discord desde Checkmate a través de notificaciones de Discord utilizando webhooks, puede utilizar la característica de webhooks entrantes de Discord.", - "webhookLabel": "URL de Webhooks de Discord", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "telegram": { - "label": "Telegram", - "description": "Para habilitar las notificaciones de Telegram, cree un bot de Telegram utilizando BotFather, un bot oficial para crear y administrar bots de Telegram. Entonces, obtenga una clave API y un ID de chat y escríbalos aquí.", - "tokenLabel": "La clave de su bot", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "El ID de su Chat", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "" - }, - "webhook": { - "label": "Webhooks", - "description": "Puede establecer un webhook personalizado para recibir notificaciones cuando ocurra un incidente.", - "urlLabel": "URL del webhook", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "" - }, - "testNotificationDevelop": "Notificación de prueba 2", - "integrationButton": "", - "testSuccess": "", - "testFailed": "", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [""], - "actionButton": "" - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "testLocale", - "add": "Agregar", - "monitors": "monitores", - "distributedUptimeStatusCreateStatusPage": "página de estado", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "", - "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "30 dias", - "distributedUptimeStatus60Days": "60 días", - "distributedUptimeStatus90Days": "90 dias", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "Dispositivos", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "Incidentes", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "Estado", - "incidentsTableDateTime": "", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "Mensaje", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "Todo", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "Mostrar:", - "incidentsOptionsHeaderLastHour": "", - "incidentsOptionsHeaderLastDay": "", - "incidentsOptionsHeaderLastWeek": "", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "Protocolo", - "infrastructureServerUrlLabel": "", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "GB", - "mb": "MB", - "mem": "Mem", - "memoryUsage": "", - "cpu": "CPU", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "", - "used": "", - "total": "Total", - "cores": "Nucleos", - "frequency": "Frecuencia", - "status": "", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "", - "avgCpuTemperature": "", - "memory": "", - "disk": "", - "uptime": "", - "os": "SO", - "host": "", - "actions": "", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "", - "commonSave": "Guardar", - "createYour": "", - "createMonitor": "", - "pause": "Pausar", - "resume": "Resumir", - "editing": "Editando...", - "url": "URL", - "access": "Acceso", - "timezone": "", - "features": "", - "administrator": "", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "Duración", - "addMonitors": "", - "window": "ventana", - "cancel": "Cancelar", - "message": "Mensaje", - "low": "bajo", - "high": "alto", - "statusCode": "", - "date&Time": "", - "type": "", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", - "createA": "", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", - "ago": "", - "companyName": "", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "", - "showUptimePercentage": "", - "removeLogo": "", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "", - "invalidFileFormat": "", - "invalidFileSize": "", - "ClickUpload": "", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", - "EmailDescriptionText": "", - "YourPhoto": "", - "PhotoDescriptionText": "", - "save": "", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", - "DeleteWarningTitle": "", - "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "", - "fallbackPage": "", - "invalidFileType": "", - "uploadFailed": "" - }, - "DeleteAccountTitle": "", - "DeleteAccountButton": "", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "", - "greeting": { - "prepend": "", - "append": "", - "overview": "" - }, - "roles": { - "superAdmin": "", - "admin": "", - "teamMember": "", - "demoUser": "" - }, - "teamPanel": { - "teamMembers": "", - "filter": { - "all": "", - "member": "" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "", - "selectRole": "", - "inviteLink": "", - "cancel": "", - "noMembers": "", - "getToken": "", - "emailToken": "", - "table": { - "name": "", - "email": "", - "role": "", - "created": "" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "", - "resumed": "", - "active": "" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "", - "incidents": "", - "statusPages": "", - "maintenance": "", - "integrations": "", - "settings": "", - "support": "", - "discussions": "", - "docs": "", - "changelog": "", - "profile": "", - "password": "", - "team": "", - "logOut": "", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", - "commonSaving": "", - "navControls": "", - "incidentsPageTitle": "", - "passwordPanel": { - "passwordChangedSuccess": "", - "passwordInputIncorrect": "", - "currentPassword": "", - "enterCurrentPassword": "", - "newPassword": "", - "enterNewPassword": "", - "confirmNewPassword": "", - "passwordRequirements": "", - "saving": "" - }, - "emailSent": "", - "failedToSendEmail": "", - "settingsTestEmailSuccess": "", - "settingsTestEmailFailed": "", - "settingsTestEmailFailedWithReason": "", - "settingsTestEmailUnknownError": "", - "statusMsg": { - "paused": "", - "up": "", - "down": "", - "pending": "" - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "", - "game": "", - "https": "" - }, "common": { - "appName": "", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" + "auth": { + "roles": { + "admin": "Administrador", + "demo": "Demostración", + "superadmin": "Superadministrador", + "user": "Usuario" + } }, - "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "", - "back": "" + "alerts": { + "pageSpeedApiKey": { + "content": "Advertencia: Aún no ha agregado una clave de API de Google PageSpeed. Visite Configuración para agregar una. Sin ella, el monitor de PageSpeed no funcionará." + } + }, + "appName": "", + "breadcrumbs": { + "details": "Detalles", + "home": "Inicio" + }, + "buttons": { + "addMember": "Agregar miembro", + "cancel": "Cancelar", + "close": "Cerrar", + "configure": "Configurar", + "confirm": "Confirmar", + "create": "Crear", + "delete": "Eliminar", + "generateToken": "Generar token", + "incidents": "Incidentes", + "inviteMember": "Invitar miembro", + "pause": "Pausar", + "resume": "Reanudar", + "save": "Guardar", + "sendInvite": "Enviar invitación", + "test": "Probar", + "testNotifications": "Probar notificaciones", + "toggleTheme": "", + "flushQueue": "Vaciar cola", + "notFound": "Ir al panel principal", + "resetPassword": "", + "reset": "", + "clear": "Limpiar", + "addDemo": "Agregar monitores de demostración", + "removeMonitors": "Eliminar monitores", + "sendTestEmail": "Enviar correo de prueba", + "exportToJSON": "Exportar a JSON", + "importFromJSON": "Importar desde JSON", + "clearFilters": "Limpiar filtros", + "removeUser": "Eliminar usuario" + }, + "charts": { + "labels": { + "averageResponseTime": "Tiempo de respuesta promedio", + "downtime": "Tiempo de inactividad", + "high": "alto", + "low": "bajo", + "uptime": "Tiempo de actividad" }, - "inputs": { - "email": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "invalid": "" - } - }, - "password": { - "label": "", - "rules": { - "length": { - "beginning": "", - "highlighted": "" - }, - "special": { - "beginning": "", - "highlighted": "" - }, - "number": { - "beginning": "", - "highlighted": "" - }, - "uppercase": { - "beginning": "", - "highlighted": "" - }, - "lowercase": { - "beginning": "", - "highlighted": "" - }, - "match": { - "beginning": "", - "highlighted": "" - } - }, - "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" - } - }, - "passwordConfirm": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - }, - "lastName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" + "histogram": { + "avg": "Prom: {{value}} ms", + "max": "Máx: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Esta acción no se puede deshacer.", + "title": "¿Está seguro de que desea eliminar esto?" + } + }, + "labels": { + "active": "Activo", + "paused": "pausado", + "na": "N/D", + "resolved": "Resuelto", + "responseTime": "Tiempo de respuesta" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Roles" } } }, - "errors": { - "validation": "" - }, - "fields": { - "password": { - "errors": { - "incorrect": "" + "name": { + "option": { + "firstName": { + "label": "", + "placeholder": "" + }, + "lastName": { + "label": "", + "placeholder": "" } - }, - "role": { - "errors": { - "min": "" + } + }, + "email": { + "option": { + "email": { + "label": "Correo electrónico", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "" - }, - "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "", - "incorrectPassword": "" - }, + "table": { + "empty": "No hay nada aquí", + "headers": { + "actions": "", + "dateTime": "Fecha y hora", + "message": "Mensaje", + "monitor": "Monitor", + "monitorId": "ID de monitor", + "name": "Nombre", + "status": "Estado", + "type": "", + "url": "URL", + "interval": "Intervalo", + "active": "Activo", + "responseTime": "Tiempo de respuesta" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "Haga clic para subir", + "dragAndDrop": "", + "supportedFormats": "", + "maxSize": "", + "orDragAndDrop": "o arrastre y suelte", "errors": { - "password": { - "incorrect": "" - } - }, - "welcome": "" + "invalidFileSize": "", + "invalidFileFormat": "" + } }, - "registration": { - "heading": { - "superAdmin": "", - "user": "" - }, - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" + "headerStatusPageControls": { + "publicLink": "" + }, + "headerTimeRange": { + "labels": { + "day": "Día", + "month": "Mes", + "recent": "Reciente", + "week": "Semana" }, "description": { - "superAdmin": "", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "", - "user": "" - }, - "termsAndPolicies": "", - "links": { - "login": "" - }, - "toasts": { - "success": "" - }, - "welcome": "" + "recent": "Mostrando estadísticas de las últimas 2 horas.", + "day": "Mostrando estadísticas de las últimas 24 horas.", + "week": "Mostrando estadísticas de los últimos 7 días.", + "month": "Mostrando estadísticas de los últimos 30 días." + } }, - "forgotPassword": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" + "offlineBanner": { + "serverUnreachable": "No se puede conectar al servidor", + "retry": "Reintentar", + "retrying": "Reintentando...", + "reconnected": "" + }, + "sidebar": { + "menu": { + "uptime": "", + "pagespeed": "", + "infrastructure": "", + "notifications": "", + "checks": "Verificaciones", + "incidents": "", + "statusPages": "", + "maintenance": "", + "logs": "", + "settings": "" }, - "buttons": { - "openEmail": "", - "resetPassword": "" + "bottomMenu": { + "support": "", + "discussions": "", + "docs": "", + "changelog": "" }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "accountMenu": { + "profile": "Perfil", + "password": "Contraseña", + "team": "Equipo" }, - "links": { - "login": "", - "resend": "" + "starPrompt": { + "title": "Dale estrella a Checkmate", + "description": "Vea las últimas versiones y ayude a crecer la comunidad en GitHub" }, - "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" + "authFooter": { + "navControls": "", + "logOut": "", + "roles": { + "superAdmin": "Superadministrador", + "admin": "Administrador", + "user": "Usuario", + "demoUser": "" + } + } + }, + "starPrompt": { + "title": "Dale estrella a Checkmate", + "description": "Vea las últimas versiones y ayude a crecer la comunidad en GitHub" + } + }, + "pages": { + "notFound": { + "title": "¡Oh no! ¡Se le cayó el sushi!", + "subtitle": "La URL no existe o no tiene acceso a ella." + }, + "account": { + "tabs": { + "profile": "Perfil", + "password": "Contraseña", + "team": "Equipo" + }, + "form": { + "name": { + "title": "Nombre", + "description": "Actualice su información personal" + }, + "photo": { + "title": "Foto de perfil", + "description": "Suba una foto de perfil" + }, + "currentPassword": { + "title": "Contraseña actual", + "description": "Ingrese su contraseña actual para verificar su identidad", + "option": { + "label": "Contraseña actual", + "placeholder": "Ingrese la contraseña actual" + } + }, + "newPassword": { + "title": "Nueva contraseña", + "description": "Elija una contraseña segura de al menos 8 caracteres", + "option": { + "newPassword": { + "label": "Nueva contraseña", + "placeholder": "Ingrese la nueva contraseña" + }, + "confirm": { + "label": "Confirmar contraseña", + "placeholder": "Confirmar nueva contraseña" + } + } + }, + "deleteAccount": { + "title": "Eliminar cuenta", + "description": "Esta acción es permanente y no se puede deshacer" + } + }, + "team": { + "filter": { + "placeholder": "Filtrar por rol", + "all": "", + "admin": "Administrador", + "member": "" + }, + "table": { + "headers": { + "email": "Correo electrónico", + "role": "Rol", + "created": "" + } + }, + "addMember": { + "title": "Registrar nuevo miembro del equipo", + "description": "Cree una nueva cuenta de usuario. Comparta las credenciales de forma segura después de la creación." + }, + "invite": { + "title": "Invitar miembro del equipo", + "description": "Envíe una invitación para unirse a su equipo", + "email": { + "label": "Dirección de correo electrónico", + "placeholder": "Ingrese la dirección de correo electrónico" + }, + "role": { + "label": "Rol", + "placeholder": "Seleccione un rol" + }, + "linkLabel": "Enlace de invitación" + } + } + }, + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "", + "highlighted": "" + }, + "lowercase": { + "beginning": "", + "highlighted": "" + }, + "match": { + "beginning": "", + "highlighted": "" + }, + "number": { + "beginning": "", + "highlighted": "" + }, + "special": { + "beginning": "", + "highlighted": "" + }, + "uppercase": { + "beginning": "", + "highlighted": "" + } + }, + "form": { + "option": { + "email": { + "label": "Correo electrónico", + "placeholder": "me@example.com" + }, + "password": { + "label": "Contraseña", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Confirmar contraseña" + } + } + } + }, + "login": { + "title": "¡Bienvenido de nuevo a Checkmate!", + "subtitle": "Inicie sesión para continuar", + "submit": "Iniciar sesión", + "links": { + "forgotPassword": { + "text": "¿Olvidó su contraseña?", + "linkText": "Restablecer contraseña" + }, + "register": { + "text": "¿No tiene una cuenta?", + "linkText": "Regístrese aquí" + } + } + }, + "register": { + "title": "¡Bienvenido de nuevo a Checkmate!", + "subtitle": "Regístrese para comenzar", + "submit": "Registrarse" + }, + "forgotPassword": { + "title": "¿Olvidó su contraseña?", + "subtitle": "No se preocupe, le enviaremos instrucciones para restablecer.", + "submit": "Solicitar recuperación", + "links": { + "login": { + "text": "Regresar a", + "linkText": "iniciar sesión" + } + } + }, + "setNewPassword": { + "title": "Restablezca su contraseña", + "subtitle": "Su nueva contraseña debe ser diferente a las contraseñas utilizadas anteriormente." + } + }, + "checks": { + "selects": { + "monitor": { + "all": "Todos los monitores" + }, + "status": { + "all": "Todos", + "down": "Caído", + "up": "Activo" + } + }, + "table": { + "empty": "No hay verificaciones caídas en este rango de tiempo", + "headers": { + "statusCode": "Código de estado", + "location": "Ubicación" + } + } + }, + "common": { + "monitors": { + "actions": { + "configure": "Configurar", + "delete": "Eliminar", + "generateToken": "Generar token", + "details": "Detalles", + "incidents": "Incidentes", + "inviteMember": "Invitar miembro", + "openSite": "Abrir sitio", + "pause": "Pausar", + "resume": "Reanudar" + }, + "statBoxes": { + "activeFor": "Activo desde hace", + "certificateExpiry": "Vencimiento del certificado", + "lastCheck": "Última verificación", + "lastResponseTime": "Último tiempo de respuesta" + }, + "status": { + "down": "caído", + "breached": "umbral excedido", + "initializing": "inicializando", + "maintenance": "mantenimiento", + "paused": "pausado", + "total": "total", + "up": "activo" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Juego", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Puerto", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infraestructura", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } + }, + "createMonitor": { + "form": { + "advanced": { + "description": "Configuración opcional para casos de uso avanzados", + "option": { + "advancedMatching": { + "label": "Usar coincidencia avanzada" + }, + "expectedValue": { + "label": "Valor esperado" + }, + "jsonPath": { + "description": "Esta expresión se evaluará contra los datos JSON de la respuesta y el resultado se usará para comparar con el valor esperado. Consulte jmespath.org para documentación del lenguaje de consulta.", + "label": "Expresión JSONPath" + }, + "matchMethod": { + "label": "Método de coincidencia", + "equal": "", + "include": "", + "regex": "" + } + }, + "title": "Configuración avanzada" + }, + "frequency": { + "description": "¿Con qué frecuencia desea verificar el estado de este monitor?", + "option": { + "frequency": { + "label": "Frecuencia de verificación", + "value": { + "fifteenMinutes": "15 minutos", + "fifteenSeconds": "", + "fiveMinutes": "5 minutos", + "fourMinutes": "", + "oneMinute": "", + "tenMinutes": "10 minutos", + "thirtyMinutes": "30 minutos", + "thirtySeconds": "", + "threeMinutes": "", + "twoMinutes": "" + } + } + }, + "title": "Frecuencia de verificación" + }, + "general": { + "option": { + "container": { + "label": "Nombre/ID de contenedor", + "placeholder": "my-app o abcd1234" + }, + "host": { + "label": "Host", + "placeholder": "192.168.1.100 o example.com" + }, + "name": { + "label": "Nombre para mostrar", + "placeholder": "p. ej. Mi sitio web" + }, + "secret": { + "label": "Secreto de autorización", + "placeholder": "Ingrese su clave secreta" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "", + "placeholder": "" + }, + "port": { + "label": "Puerto a monitorear", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Nombre del servicio", + "placeholder": "p. ej. my.service.v1 (dejar vacío para estado general)" + }, + "wsUrl": { + "label": "URL de WebSocket", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Configuración general", + "description": { + "http": "Ingrese la URL o IP a monitorear (p. ej., https://example.com/ o 192.168.1.100) y agregue un nombre para mostrar que aparecerá en el panel.", + "ping": "", + "port": "", + "docker": "", + "game": "", + "pagespeed": "Rastree el rendimiento de carga de página, Core Web Vitals y puntuaciones de optimización de su sitio web.", + "grpc": "Ingrese el nombre de host y puerto del servidor gRPC, opcionalmente especifique un nombre de servicio para la verificación de salud, y agregue un nombre para mostrar.", + "websocket": "Ingrese la URL de WebSocket a monitorear (p. ej., wss://example.com/socket) y agregue un nombre para mostrar.", + "hardware": "Monitoree el uso de CPU, memoria, disco y temperatura de su infraestructura." + } + }, + "ignoreTls": { + "description": "Configure la validación de certificados TLS/SSL para conexiones HTTPS.", + "option": { + "tls": { + "label": "Ignorar errores de TLS/SSL" + } + }, + "title": "Configuración de TLS/SSL" + }, + "incidents": { + "description": "Se utiliza una ventana deslizante para determinar cuándo un monitor cae. El estado de un monitor solo cambiará cuando el porcentaje de verificaciones en la ventana deslizante cumpla con el valor especificado.", + "option": { + "checks": { + "label": "Número de verificaciones en la ventana deslizante" + }, + "percentage": { + "label": "¿Qué porcentaje de verificaciones en la ventana deslizante deben fallar/tener éxito antes de que cambie el estado del monitor?" + } + }, + "title": "Incidentes" + }, + "notifications": { + "description": "Seleccione los canales de notificación que desea usar", + "title": "Notificaciones" + }, + "type": { + "description": "Seleccione el tipo de verificación a realizar", + "optionDockerDescription": "Use Docker para monitorear si un contenedor está en ejecución.", + "optionGameDescription": "Monitoree si un servidor de juegos específico está en línea.", + "optionGrpcDescription": "Monitoree servicios gRPC usando el protocolo estándar de verificación de salud.", + "optionWebSocketDescription": "Monitoree endpoints de WebSocket para verificar la salud de la conexión y el tiempo de respuesta.", + "optionHttpDescription": "Use HTTP(S) para monitorear su sitio web o endpoint de API.", + "optionPingDescription": "Use ICMP Ping para monitorear si un servidor está en línea.", + "optionPortDescription": "Monitoree si un puerto específico de un servidor está abierto.", + "title": "Tipo" + }, + "thresholds": { + "title": "Umbrales de alerta", + "description": "Defina los umbrales en los que se deben activar las alertas para este monitor de hardware.", + "option": { + "cpuThreshold": { + "label": "Umbral de alerta de CPU (%)" + }, + "memoryThreshold": { + "label": "Umbral de alerta de memoria (%)" + }, + "diskThreshold": { + "label": "Umbral de alerta de disco (%)" + }, + "tempThreshold": { + "label": "Umbral de alerta de temperatura (°C)" + } + } + }, + "geoChecks": { + "title": "Verificaciones geodistribuidas", + "description": "Ejecute verificaciones desde múltiples ubicaciones geográficas para monitorear la disponibilidad y el rendimiento global.", + "option": { + "enabled": { + "label": "Habilitar verificaciones geodistribuidas" + }, + "locations": { + "label": "Ubicaciones", + "placeholder": "Seleccionar ubicaciones", + "options": { + "EU": "Europa", + "NA": "Norteamérica", + "AS": "Asia", + "SA": "Sudamérica", + "AF": "África", + "OC": "Oceanía" + } + }, + "interval": { + "label": "Intervalo de verificación", + "value": { + "fiveMinutes": "5 minutos", + "tenMinutes": "10 minutos", + "fifteenMinutes": "15 minutos", + "thirtyMinutes": "30 minutos" + } + } + } + }, + "url": { + "title": "IP/URL del monitor en la página de estado", + "description": "Muestre la dirección IP o URL del monitor en la página de estado pública. Si está deshabilitado, solo se mostrará el nombre del monitor para proteger información sensible.", + "option": { + "showURL": { + "label": "Mostrar IP/URL en la página de estado", + "enabled": "Habilitado", + "disabled": "Deshabilitado" + } + } + }, + "stats": { + "title": "Historial del monitor", + "description": "Limpie todo el historial de monitoreo y estadísticas de su equipo. Esta acción es irreversible.", + "buttonText": "Limpiar todas las estadísticas", + "dialog": { + "title": "¿Limpiar todo el historial de monitoreo?", + "description": "Esto eliminará permanentemente todo el historial de monitoreo, estadísticas y datos de verificación de su equipo. Esta acción no se puede deshacer.", + "confirm": "Sí, limpiar todas las estadísticas" + } + } + } + }, + "editUser": { + "form": { + "roles": { + "title": "Roles", + "description": "Asigne roles al usuario. Se pueden seleccionar múltiples roles." + } + }, + "dialog": { + "removeUser": { + "title": "Eliminar usuario", + "content": "¿Está seguro de que desea eliminar a {{name}} de su equipo? Esta acción no se puede deshacer." + } + } + }, + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Resolver incidente", + "option": { + "comment": { + "label": "Comentario (opcional)", + "placeholder": "Agregue un comentario sobre la resolución..." + } + } + }, + "details": { + "analysis": "Análisis del incidente", + "comment": "Comentario:", + "detailsLabel": "Detalles", + "downtime": "Tiempo de inactividad:", + "message": "Mensaje:", + "monitor": "Monitor:", + "overview": "Resumen", + "resolutionDetails": "Detalles de la resolución", + "resolutionType": "Tipo:", + "resolutionTypes": { + "automatic": "Automático", + "manual": "Manual" + }, + "resolve": "Resolver incidente", + "resolvedAt": "Resuelto a las:", + "resolvedBy": "Resuelto por:", + "startedAt": "Iniciado a las:", + "status": "Estado:", + "statusCode": "Código de estado:", + "timeline": "Línea de tiempo", + "title": "Detalles del incidente", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Todos los monitores", + "monitor": "Monitor", + "resolutionType": "Tipo de resolución", + "resolutionTypes": { + "manual": "Manual", + "automatic": "Automático", + "all": "Todos" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Incidentes activos", + "active_zero": "Sin incidentes activos", + "active_one": "{{count}} incidente activo", + "active_other": "{{count}} incidentes activos" + }, + "incidentStats": { + "avgResolutionTime": "Tiempo prom. de resolución", + "mostAffectedMonitor": "Monitor más afectado", + "title": "Estadísticas de incidentes", + "totalIncidents": "Total de incidentes" + }, + "latestIncidents": { + "title": "Últimos incidentes" + } + }, + "table": { + "actions": { + "details": "Detalles", + "goToMonitor": "Ir al monitor", + "resolveManually": "Resolver manualmente" + }, + "activeIncidents": "Incidentes activos", + "headers": { + "endTime": "Hora de fin", + "resolutionType": "Tipo de resolución", + "startTime": "", + "statusCode": "Código de estado" + }, + "resolvedIncidents": "Incidentes resueltos", + "status": { + "active": "Activo", + "resolved": "Resuelto" + } + } + }, + "infrastructure": { + "charts": { + "labels": { + "cpu": "Uso de CPU", + "disk": "Uso de disco", + "memory": "Uso de memoria", + "netBytesRecv": "{{name}} - Bytes recibidos", + "netBytesSent": "{{name}} - Bytes enviados", + "temp": "Temp" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Frecuencia máxima", + "title": "Uso de CPU", + "upperLabel": "Frecuencia actual" + }, + "disk": { + "lowerLabel": "Libre", + "title": "Uso de disco {{idx}}", + "upperLabel": "Usado" + }, + "memory": { + "lowerLabel": "Libre", + "title": "Uso de memoria", + "upperLabel": "Usado" + } + }, + "statBoxes": { + "avgCpuTemperature": "", + "cpuFrequency": "", + "cpuLogical": "", + "cpuPhysical": "", + "disk": "Disco", + "memory": "Memoria", + "os": "SO" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Disco", + "memory": "Memoria" + } + }, + "tabs": { + "labels": { + "network": "", + "overview": "Resumen" + } + }, + "fallback": { + "actionButton": "¡Cree un monitor!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Un monitor de infraestructura se usa para:" + } + }, + "logs": { + "tabs": { + "diagnostics": "", + "logs": "", + "queue": "" + }, + "logLevelSelect": { + "label": "Nivel de registro" + }, + "jobQueue": "Cola de trabajos", + "failedJobs": "Trabajos fallidos", + "noLogs": "No se encontraron registros", + "metrics": { + "jobs": "Trabajos", + "activeJobs": "Trabajos activos", + "failingJobs": "Trabajos con fallas", + "totalRuns": "Ejecuciones totales", + "totalFailures": "Fallas totales" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Retraso del bucle de eventos", + "uptime": "Tiempo de actividad", + "usedHeapSize": "Tamaño de heap usado", + "totalHeapSize": "Tamaño total de heap", + "osMemoryLimit": "Límite de memoria del SO" + }, + "gauges": { + "heapAllocation": "Asignación de heap", + "heapUsage": "Uso de heap", + "heapUtilization": "Utilización de heap", + "instantCpuUsage": "Uso instantáneo de CPU", + "availableMemoryPercentage": "% de memoria disponible", + "allocatedPercentage": "% asignado", + "usedSPercentage": "% de 1s usado por CPU", + "total": "Total", + "used": "Usado" + } + }, + "table": { + "headers": { + "timestamp": "Marca de tiempo", + "level": "Nivel", + "service": "Servicio", + "method": "Método", + "monitorId": "ID de monitor", + "runCount": "Conteo de ejecuciones", + "failCount": "Conteo de fallas", + "lastRunAt": "Última ejecución", + "lockedAt": "Bloqueado a las", + "lastFinishedAt": "Última finalización", + "lastRunTook": "Última ejecución tomó", + "lastFailedAt": "Última falla", + "failReason": "Razón de falla" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "¡Cree una ventana de mantenimiento!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Una ventana de mantenimiento se usa para:" + }, + "table": { + "headers": { + "nextWindow": "", + "repeat": "" + } + }, + "form": { + "general": { + "title": "Configuración general", + "description": "Establezca un nombre y opción de repetición para su ventana de mantenimiento.", + "option": { + "name": { + "label": "Nombre", + "placeholder": "p. ej. Mantenimiento semanal" + }, + "repeat": { + "label": "Repetir" + } + } + }, + "startDate": { + "title": "Fecha de inicio", + "description": "Seleccione la fecha de inicio de su ventana de mantenimiento.", + "option": { + "startDate": { + "label": "Fecha de inicio" + } + } + }, + "startTime": { + "title": "Hora de inicio", + "description": "Establezca la hora de inicio y duración de su ventana de mantenimiento. Todos los valores están en UTC", + "option": { + "duration": { + "label": "Duración" + }, + "startTime": { + "label": "Hora de inicio" + } + }, + "monitors": { + "title": "Monitores", + "description": "Monitores a los que se debe aplicar la ventana de mantenimiento", + "option": { + "addMonitors": { + "label": "Agregar monitores" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "Crear un canal", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Los canales de notificación se usan para:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Token de acceso", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "La dirección donde se enviarán las notificaciones.", + "optionAddress": "Dirección", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Dirección" + }, + "homeServer": { + "optionHomeServer": "Servidor principal", + "placeholder": "example.com" + }, + "matrix": { + "description": "Configure la conexión de su servidor Matrix para notificaciones.", + "title": "Configuración de Matrix" + }, + "notificationName": { + "description": "Un nombre descriptivo para el canal de notificación", + "optionName": "Nombre del canal", + "placeholder": "p. ej. Alertas de producción", + "title": "Nombre del canal" + }, + "pagerDuty": { + "description": "Su clave de integración de PagerDuty para recibir alertas.", + "optionIntegrationKey": "Clave de integración", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Clave de integración" + }, + "roomId": { + "optionRoomId": "ID de sala", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Seleccione el tipo de canal de notificación a crear.", + "optionType": "Tipo", + "title": "Tipo de canal" + }, + "telegram": { + "title": "Configuración de Telegram", + "description": "Para habilitar las notificaciones de Telegram, cree un bot de Telegram utilizando BotFather, un bot oficial para crear y administrar bots de Telegram. Entonces, obtenga una clave API y un ID de chat y escríbalos aquí.", + "optionBotToken": "Token del bot", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "ID de chat", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Destino" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "Cambio acumulativo de diseño (CLS)", + "fcp": "Primera pintura con contenido (FCP)", + "lcp": "Mayor pintura con contenido (LCP)", + "si": "Índice de velocidad (SI)", + "tbt": "Tiempo total de bloqueo (TBT)" + }, + "legend": { + "title": "Reporte de PageSpeed", + "weight": "Peso" + }, + "pie": { + "title": "Reporte de rendimiento" + } + }, + "table": { + "headers": { + "pageSpeedScore": "Puntuación de PageSpeed" + } + }, + "fallback": { + "actionButton": "¡Cree un monitor!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "Un monitor de PageSpeed se usa para:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Zona horaria de visualización", + "description": "Seleccione la zona horaria utilizada para mostrar fechas y horas en toda la aplicación.", + "option": { + "timezone": { + "label": "Zona horaria de visualización" + } + } + }, + "ui": { + "title": "Apariencia", + "description": "Cambie entre modo claro y oscuro, cambie el idioma o personalice el tipo de visualización de gráficos.", + "option": { + "theme": { + "label": "Modo de tema", + "light": "Claro", + "dark": "Oscuro" + }, + "language": { + "label": "Idioma" + }, + "chartType": { + "label": "Tipo de gráfico", + "histogram": "Histograma", + "heatmap": "Mapa de calor" + } + } + }, + "pagespeed": { + "title": "Clave de API de Google PageSpeed", + "description": "Ingrese su clave de API de Google PageSpeed para habilitar el monitoreo de Google PageSpeed. Haga clic en Restablecer para actualizar la clave.", + "option": { + "apiKey": { + "label": "Clave de API de PageSpeed", + "labelSet": "La clave de API está configurada. Haga clic en Restablecer para cambiarla.", + "placeholder": "Ingrese su clave de API de Google PageSpeed" + } + } + }, + "url": { + "title": "IP/URL del monitor en la página de estado", + "description": "Muestre la dirección IP o URL del monitor en la página de estado pública. Si está deshabilitado, solo se mostrará el nombre del monitor para proteger información sensible.", + "option": { + "showURL": { + "label": "Mostrar IP/URL en la página de estado", + "enabled": "Habilitado", + "disabled": "Deshabilitado" + } + } + }, + "stats": { + "title": "Historial del monitor", + "description": "Limpie todo el historial de monitoreo y estadísticas de su equipo. Esta acción es irreversible.", + "option": { + "clear": { + "label": "Limpiar todas las estadísticas. Esto es irreversible." + } + }, + "dialog": { + "title": "¿Limpiar todo el historial de monitoreo?", + "description": "Esto no se puede deshacer" + } + }, + "retention": { + "title": "Retención de verificaciones", + "description": "Establezca cuánto tiempo se retienen los datos de verificación antes de ser limpiados automáticamente.", + "option": { + "days": { + "label": "Período de retención (días)", + "unlimited": "Ilimitado" + } + } + }, + "thresholds": { + "title": "Umbrales globales", + "description": "Establezca umbrales globales de CPU, memoria, disco y temperatura para monitoreo de infraestructura. Se aplican a todos los monitores de hardware a menos que se anulen.", + "option": { + "cpu": { + "label": "Umbral de CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Umbral de memoria (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Umbral de disco (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Umbral de temperatura (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Configuración de correo electrónico", + "description": "Configure los ajustes de correo electrónico de su sistema. Se usa para enviar notificaciones y alertas.", + "descriptionTransport": "", + "descriptionTransportLink": "Ver especificaciones aquí", + "option": { + "host": { + "label": "Host de correo - Nombre de host o dirección IP para conectar", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Puerto de correo - Puerto de conexión", + "placeholder": "587" + }, + "address": { + "label": "Dirección de correo - Usada para autenticación", + "placeholder": "you@example.com" + }, + "user": { + "label": "Usuario de correo - Nombre de usuario para autenticación, reemplaza la dirección de correo si se especifica", + "placeholder": "Dejar vacío si no es requerido" + }, + "password": { + "label": "Contraseña de correo - Contraseña para autenticación", + "labelSet": "La contraseña está configurada. Haga clic en Restablecer para cambiarla.", + "placeholder": "Ingrese su contraseña" + }, + "tlsServername": { + "label": "Nombre de servidor TLS - Nombre de host opcional para validación TLS cuando el host es una IP", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "Host de conexión de correo - Nombre de host a usar en el saludo HELO/EHLO", + "placeholder": "localhost" + }, + "secure": { + "label": "Usar SSL (recomendado): Cifrar la conexión usando SSL/TLS" + }, + "pool": { + "label": "Habilitar agrupación de conexiones: Reutilizar conexiones existentes para mejorar el rendimiento" + }, + "ignoreTLS": { + "label": "Deshabilitar STARTTLS: No usar TLS aunque el servidor lo soporte" + }, + "requireTLS": { + "label": "Forzar STARTTLS: Requerir actualización TLS, fallar si no es soportado" + }, + "rejectUnauthorized": { + "label": "Rechazar certificados inválidos: Rechazar conexiones con certificados autofirmados o no confiables" + } + } + }, + "demoMonitors": { + "title": "Monitores de demostración", + "description": "Agregue monitores de ejemplo con fines demostrativos." + }, + "removeMonitors": { + "title": "Reinicio del sistema", + "description": "Elimine todos los monitores de su sistema.", + "dialog": { + "title": "¿Eliminar todos los monitores?", + "description": "Esto no se puede deshacer." + } + }, + "exportMonitors": { + "title": "Exportar monitores" + }, + "importExportMonitors": { + "title": "Importar / Exportar monitores", + "description": "Importe o exporte los datos de sus monitores como archivo JSON para respaldo o transferencia." + }, + "about": { + "title": "Acerca de", + "developedBy": "Desarrollado por Bluewave Labs" + }, + "validation": { + "errorMessage": "Corrija los siguientes errores de validación:" + } + } + }, + "statusPages": { + "deleteSuccess": "Página de estado eliminada exitosamente", + "fallback": { + "title": "Una página de estado se usa para:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "¡Cree una página de estado!" + }, + "monitorsList": { + "chartTypeHeatmap": "Mapa de calor", + "chartTypeHistogram": "Histograma", + "noData": "No hay datos disponibles", + "infrastructure": { + "title": "Infraestructura", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Memoria", + "disk": "Disco", + "usage": "Uso", + "used": "Usado", + "total": "Total" + }, + "uptime": { + "title": "Tiempo de actividad", + "responseTime": "Tiempo de respuesta" + } + }, + "statusBar": { + "allDown": "Todos los sistemas están caídos", + "allUp": "Todos los sistemas operativos", + "degraded": "Algunos sistemas están experimentando problemas", + "unknown": "No se puede determinar el estado del sistema" + }, + "table": { + "headers": { + "name": "Nombre de la página de estado", + "url": "URL pública" + }, + "published": "Publicado", + "unpublished": "No publicado" + }, + "title": "Páginas de estado", + "details": { + "empty": { + "title": "Aún no hay nada aquí", + "addMonitor": "Agregue un monitor para comenzar" + } + }, + "form": { + "access": { + "title": "Acceso", + "description": "Si su página de estado está lista, puede marcarla como publicada.", + "option": { + "published": { + "name": "Publicado y visible al público" + } + } + }, + "basicInfo": { + "title": "Información básica", + "description": "Defina el nombre de la empresa y el subdominio al que apunta su página de estado.", + "option": { + "name": { + "label": "Nombre de la empresa", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Dirección de su página de estado", + "placeholder": "mi-pagina-de-estado" + } + } + }, + "monitors": { + "title": "Monitores", + "description": "Seleccione los monitores a mostrar en su página de estado.", + "noMonitors": "No hay monitores seleccionados", + "option": { + "monitors": { + "label": "Seleccionar monitores", + "placeholder": "Buscar y seleccionar monitores..." + } + } + }, + "timezone": { + "title": "Zona horaria", + "description": "Seleccione la zona horaria en la que se mostrará su página de estado.", + "option": { + "timezone": { + "label": "Zona horaria", + "placeholder": "Seleccionar zona horaria..." + } + } + }, + "appearance": { + "title": "Apariencia", + "description": "Defina la apariencia predeterminada de su página de estado pública.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Color de marca" + } + } + }, + "features": { + "title": "Funcionalidades", + "description": "Configure qué información se muestra en su página de estado.", + "option": { + "showCharts": { + "label": "Mostrar gráficos de tiempo de respuesta" + }, + "showUptimePercentage": { + "label": "Mostrar porcentaje de tiempo de actividad" + }, + "showAdminLoginLink": { + "label": "Mostrar enlace de inicio de sesión de administrador" + }, + "showInfrastructure": { + "label": "Mostrar métricas de infraestructura" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Buscar monitores..." + } + }, + "table": { + "headers": { + "responseTime": "Tiempo de respuesta" + } + }, + "fallback": { + "actionButton": "¡Cree un monitor!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Un monitor de tiempo de actividad se usa para:" } } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" - }, - "alertBox": "", - "description": "", - "retryButton": { - "default": "", - "processing": "" - } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" - }, - "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" - }, - "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "testNotification": "", - "dialogDeleteTitle": "", - "dialogDeleteConfirm": "" - }, - "notificationConfig": { - "title": "", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "", - "sendTestNotifications": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "", - "diagnostics": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" - } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "", - "intervalHeader": "" - }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" - }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "", - "deleteSuccess": "", - "deleteFailed": "", - "details": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" - }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "", - "toastEmailRequiredFieldsError": "" - }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" - }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" - }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "" - }, - "title": "", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" - }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" - }, - "globalThresholds": { - "title": "", - "description": "" - } - }, - "statusPageCreate": { - "buttonSave": "" - }, - "incidentsOptionsHeaderFilterResolved": "", - "settingsSave": "", - "statusPageCreateAppearanceTitle": "", - "confirmPassword": "", - "monitorHooks": { - "failureAddDemoMonitors": "", - "successAddDemoMonitors": "" - }, - "settingsAppearance": "", - "settingsDisplayTimezone": "", - "settingsGeneralSettings": "", - "incidentsOptionsHeaderTotalIncidents": "", - "statusPage": { - "deleteSuccess": "", - "deleteFailed": "", - "createSuccess": "", - "updateSuccess": "", - "generalSettings": "", - "contents": "", - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "testNotificationsDisabled": "", - "incidentsTableResolvedAt": "", - "incidentsTableActionResolve": "", - "checkHooks": { - "failureResolveOne": "", - "failureResolveAll": "", - "failureResolveMonitor": "" - }, - "checkFormError": "", - "diagnosticsPage": { - "diagnosticDescription": "", - "statsDescription": "", - "gauges": { - "heapAllocationTitle": "", - "heapAllocationSubtitle": "", - "heapUsageTitle": "", - "heapUsageSubtitle": "", - "heapUtilizationTitle": "", - "heapUtilizationSubtitle": "", - "instantCpuUsageTitle": "", - "instantCpuUsageSubtitle": "" - }, - "stats": { - "eventLoopDelayTitle": "", - "uptimeTitle": "", - "usedHeapSizeTitle": "", - "totalHeapSizeTitle": "", - "osMemoryLimitTitle": "" - } - }, - "pageSpeedLighthouseAPI": "", - "time": { - "threeMinutes": "", - "fiveMinutes": "", - "tenMinutes": "", - "twentyMinutes": "", - "oneHour": "", - "oneDay": "", - "oneWeek": "", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" - }, - "general": { - "noOptionsFound": "" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "pageSpeed": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "editUserPage": { - "form": { - "email": "", - "firstName": "", - "lastName": "", - "role": "", - "save": "" - }, - "table": { - "actionHeader": "", - "roleHeader": "" - }, - "title": "", - "toast": { - "successUserUpdate": "", - "validationErrors": "" - } - }, - "incidentsPageActionResolveMonitor": "", - "incidentsPageActionResolveAll": "", - "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + } } diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json index 4949817c7..d56cb5bda 100644 --- a/client/src/locales/fi.json +++ b/client/src/locales/fi.json @@ -1,1108 +1,1305 @@ { - "submit": "Lähetä", - "title": "Otsikko", - "distributedStatusHeaderText": "Reaaliaikainen kattavuus oikeilla laitteilla", - "distributedStatusSubHeaderText": "Miljoonien laitteiden voimin ympäri maailman näet järjestelmän suorituskyvyn maailman alueittain, maittain tai kaupungeittain", - "settingsDisabled": "Poistettu käytöstä", - "settingsSuccessSaved": "", - "settingsFailedToSave": "", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", - "https": "HTTPS", - "http": "HTTP", - "monitor": "Seurain", - "aboutus": "Tietoja meistä", - "now": "Nyt", - "delete": "Poista", - "configure": "Määritä", - "responseTime": "Vasteaika", - "ms": "ms", - "bar": "Palkki", - "area": "Alue", - "country": "MAA", - "city": "KAUPUNKI", - "response": "Vastaus", - "monitorStatusUp": "Valvontakohde {name} ({url}) on nyt toiminnassa ja vastaa", - "monitorStatusDown": "Valvontakohde {name} ({url}) ei ole toiminnassa, eikä vastaa", - "webhookSendSuccess": "Webhook-ilmoitus lähetettiin onnistuneesti", - "webhookSendError": "Virhe lähetettäessä webhook-ilmoitusta palveluun {platform}", - "webhookUnsupportedPlatform": "Ei tuettu palvelu: {platform}", - "distributedRightCategoryTitle": "Valvonta", - "distributedStatusServerMonitors": "Palvelin valvonta", - "distributedStatusServerMonitorsDescription": "Valvo palvelimien tilaa", - "distributedUptimeCreateSelectURL": "Tässä voit valita isäntäkohteen URL-osoitteen ja valvontatyypin.", - "distributedUptimeCreateChecks": "Suoritettavat tarkistukset", - "distributedUptimeCreateChecksDescription": "Voit aina lisätä tai poistaa tarkistuksia sivuston lisäämisen jälkeen.", - "distributedUptimeCreateIncidentNotification": "Häiriöilmoitukset", - "distributedUptimeCreateIncidentDescription": "Jos ilmenee häiriö, ilmoita käyttäjille.", - "distributedUptimeCreateAdvancedSettings": "Lisäasetukset", - "distributedUptimeDetailsNoMonitorHistory": "Tälle valvontakohteelle ei ole vielä tarkistushistoriaa.", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Viimeksi päivitetty", - "notifications": { - "enableNotifications": "", - "testNotification": "Testi-ilmoitus", - "addOrEditNotifications": "Lisää tai muokkaa ilmoituksia", - "slack": { - "label": "Slack", - "description": "", - "webhookLabel": "Webhookin URL", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "discord": { - "label": "Discord", - "description": "", - "webhookLabel": "Discord Webhookin URL-osoite", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "" - }, - "telegram": { - "label": "Telegram", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "" - }, - "webhook": { - "label": "Webhookit", - "description": "", - "urlLabel": "Webhookin URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "" - }, - "testNotificationDevelop": "Testi-ilmoitus 2", - "integrationButton": "", - "testSuccess": "Testi ilmoituksen lähetys onnistui!", - "testFailed": "Testi ilmoituksen lähetys epäonnistui", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [""], - "actionButton": "" - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "", - "add": "Lisää", - "monitors": "Seuraimet", - "distributedUptimeStatusCreateStatusPage": "", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "Yrityksen nimi", - "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "30 päivää", - "distributedUptimeStatus60Days": "60 päivää", - "distributedUptimeStatus90Days": "90 päivää", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "Ota yhteys järjestelmävalvojaasi", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "UPT logo", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "Tila", - "incidentsTableDateTime": "Päivämäärä ja aika", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "Viesti", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "Kaikki", - "incidentsOptionsHeaderFilterDown": "Alhaalla", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "Näytä:", - "incidentsOptionsHeaderLastHour": "Viime tunti", - "incidentsOptionsHeaderLastDay": "Viime päivä", - "incidentsOptionsHeaderLastWeek": "Viime viikko", - "incidentsOptionsPlaceholderAllServers": "Kaikki palvelimet", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "Protokolla", - "infrastructureServerUrlLabel": "Palvelimen URL-osoite", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "Muistinkäyttö", - "cpu": "", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "Tila", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "", - "avgCpuTemperature": "", - "memory": "Muisti", - "disk": "", - "uptime": "", - "os": "", - "host": "", - "actions": "", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "", - "integrationsDiscord": "", - "integrationsDiscordInfo": "", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "", - "commonSave": "Tallenna", - "createYour": "", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", - "timezone": "Aikavyöhyke", - "features": "", - "administrator": "", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "", - "window": "", - "cancel": "Peruuttaa", - "message": "Viesti", - "low": "", - "high": "", - "statusCode": "Tilakoodi", - "date&Time": "Päivämäärä ja aika", - "type": "", - "statusPageName": "", - "publicURL": "Julkinen URL-osoite", - "repeat": "", - "edit": "Muokkaa", - "createA": "", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "Ilmoita tekstiviestillä (tulossa pian)", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", - "ago": "sitten", - "companyName": "Yrityksen nimi", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "Näytä kaaviot", - "showUptimePercentage": "", - "removeLogo": "Poista logo", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "Ota yhteyttä järjestelmänvalvojaan", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "Muokkaa omaa", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "Päivitä", - "invalidFileFormat": "", - "invalidFileSize": "Tiedoston koko on liian suuri!", - "ClickUpload": "", - "DragandDrop": "raahaa ja pudota", - "MaxSize": "Enimmäiskoko", - "SupportedFormats": "Tuetut muodot", - "FirstName": "Etunimi", - "LastName": "Sukunimi", - "EmailDescriptionText": "Tämä on nykyinen sähköpostiosoitteesi — sitä ei voi vaihtaa.", - "YourPhoto": "Profiilikuva", - "PhotoDescriptionText": "", - "save": "Tallenna", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", - "DeleteWarningTitle": "Haluatko todella poistaa tämän tilin?", - "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "Valitse tiedosto", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "Ei valittua tiedostoa", - "fallbackPage": "", - "invalidFileType": "", - "uploadFailed": "" - }, - "DeleteAccountTitle": "Poista tili", - "DeleteAccountButton": "Poista tili", - "publicLink": "Julkinen linkki", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "Palauta", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "Luo uusi", - "greeting": { - "prepend": "Hei", - "append": "", - "overview": "" - }, - "roles": { - "superAdmin": "Pääylläpitäjä", - "admin": "Ylläpitäjä", - "teamMember": "Tiimin jäsen", - "demoUser": "Demokäyttäjä" - }, - "teamPanel": { - "teamMembers": "Tiimin jäsenet", - "filter": { - "all": "Kaikki", - "member": "Jäsen" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "Sähköposti", - "selectRole": "Valitse rooli", - "inviteLink": "Kutsulinkki", - "cancel": "Peruuttaa", - "noMembers": "", - "getToken": "Hae tunniste", - "emailToken": "Sähköpostitunnus", - "table": { - "name": "Nimi", - "email": "Sähköposti", - "role": "Rooli", - "created": "Luotu" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "Tauota", - "resumed": "Jatka", - "active": "" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "Infrastruktuuri", - "incidents": "", - "statusPages": "Tilasivut", - "maintenance": "Huolto", - "integrations": "", - "settings": "Asetukset", - "support": "Tuki", - "discussions": "Keskustelut", - "docs": "Dokumentit", - "changelog": "Muutosloki", - "profile": "Profiili", - "password": "Salasana", - "team": "Tiimi", - "logOut": "Kirjaudu ulos", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "Tila", - "statusBreadCrumbsStatusPages": "Tilasivut", - "statusBreadCrumbsDetails": "Tiedot", - "commonSaving": "Tallennetaan...", - "navControls": "Ohjaimet", - "incidentsPageTitle": "Häiriöt", - "passwordPanel": { - "passwordChangedSuccess": "Salasanasi vaihdettiin onnistuneesti.", - "passwordInputIncorrect": "", - "currentPassword": "Nykyinen salasana", - "enterCurrentPassword": "Kirjoita nykyinen salasanasi", - "newPassword": "Uusi salasana", - "enterNewPassword": "Kirjoita uusi salasana", - "confirmNewPassword": "Vahvista uusi salasana", - "passwordRequirements": "Uuden salasanan on oltava vähintään 8 merkkiä pitkä ja sen on sisältävä vähintään yksi iso kirjain, yksi pieni kirjain, yksi numero sekä yksi erikoismerkki.", - "saving": "Tallennetaan..." - }, - "emailSent": "Sähköpostin lähetys onnistui", - "failedToSendEmail": "Sähköpostin lähetys epäonnistui", - "settingsTestEmailSuccess": "Testisähköpostin lähetys onnistui", - "settingsTestEmailFailed": "Testisähköpostin lähetys epäonnistui", - "settingsTestEmailFailedWithReason": "Testisähköpostin lähetys epäonnistui: {{reason}}", - "settingsTestEmailUnknownError": "Tuntematon virhe", - "statusMsg": { - "paused": "Valvonta on keskeytetty.", - "up": "Sivusi on ylhäällä.", - "down": "Sivusi on alhaalla.", - "pending": "Odottaa..." - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "", - "game": "", - "https": "" - }, "common": { - "appName": "", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" + "auth": { + "roles": { + "admin": "Ylläpitäjä", + "demo": "Demo", + "superadmin": "Pääylläpitäjä", + "user": "Käyttäjä" + } }, - "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "", - "back": "" + "alerts": { + "pageSpeedApiKey": { + "content": "Varoitus: Et ole vielä lisännyt Google PageSpeed API -avainta. Siirry Asetuksiin lisätäksesi sellainen. Ilman sitä PageSpeed-monitorointi ei toimi." + } + }, + "appName": "", + "breadcrumbs": { + "details": "Tiedot", + "home": "Etusivu" + }, + "buttons": { + "addMember": "Lisää jäsen", + "cancel": "Peruuta", + "close": "Sulje", + "configure": "Määritä", + "confirm": "Vahvista", + "create": "Luo", + "delete": "Poista", + "generateToken": "Luo tunniste", + "incidents": "Häiriöt", + "inviteMember": "Kutsu jäsen", + "pause": "Keskeytä", + "resume": "Jatka", + "save": "Tallenna", + "sendInvite": "Lähetä kutsu", + "test": "Testaa", + "testNotifications": "Testaa ilmoitukset", + "toggleTheme": "", + "flushQueue": "Tyhjennä jono", + "notFound": "Siirry pääsivulle", + "resetPassword": "", + "reset": "Palauta", + "clear": "Tyhjennä", + "addDemo": "Lisää demomonitorit", + "removeMonitors": "Poista monitorit", + "sendTestEmail": "Lähetä testisähköposti", + "exportToJSON": "Vie JSON-muotoon", + "importFromJSON": "Tuo JSON-muodosta", + "clearFilters": "Tyhjennä suodattimet", + "removeUser": "Poista käyttäjä" + }, + "charts": { + "labels": { + "averageResponseTime": "Keskimääräinen vasteaika", + "downtime": "Käyttökatko", + "high": "", + "low": "", + "uptime": "Käytettävyys" }, - "inputs": { - "email": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "invalid": "" - } - }, - "password": { - "label": "", - "rules": { - "length": { - "beginning": "", - "highlighted": "" - }, - "special": { - "beginning": "", - "highlighted": "" - }, - "number": { - "beginning": "", - "highlighted": "" - }, - "uppercase": { - "beginning": "", - "highlighted": "" - }, - "lowercase": { - "beginning": "", - "highlighted": "" - }, - "match": { - "beginning": "", - "highlighted": "" - } - }, - "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" - } - }, - "passwordConfirm": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - }, - "lastName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" + "histogram": { + "avg": "Keskim.: {{value}} ms", + "max": "Maks.: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Tätä toimintoa ei voi kumota.", + "title": "Haluatko varmasti poistaa tämän?" + } + }, + "labels": { + "active": "Aktiivinen", + "paused": "keskeytetty", + "na": "Ei saatavilla", + "resolved": "Ratkaistu", + "responseTime": "Vasteaika" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Roolit" } } }, - "errors": { - "validation": "" - }, - "fields": { - "password": { - "errors": { - "incorrect": "" + "name": { + "option": { + "firstName": { + "label": "", + "placeholder": "" + }, + "lastName": { + "label": "", + "placeholder": "" } - }, - "role": { - "errors": { - "min": "" + } + }, + "email": { + "option": { + "email": { + "label": "Sähköposti", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "" - }, - "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "", - "incorrectPassword": "" - }, + "table": { + "empty": "Täällä ei ole mitään", + "headers": { + "actions": "", + "dateTime": "Päivämäärä ja aika", + "message": "Viesti", + "monitor": "Monitori", + "monitorId": "Monitorin tunnus", + "name": "Nimi", + "status": "Tila", + "type": "", + "url": "Osoite", + "interval": "Aikaväli", + "active": "Aktiivinen", + "responseTime": "Vasteaika" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "Napsauta ladataksesi", + "dragAndDrop": "raahaa ja pudota", + "supportedFormats": "Tuetut muodot", + "maxSize": "Enimmäiskoko", + "orDragAndDrop": "tai vedä ja pudota", "errors": { - "password": { - "incorrect": "" - } - }, - "welcome": "" + "invalidFileSize": "Tiedoston koko on liian suuri!", + "invalidFileFormat": "" + } }, - "registration": { - "heading": { - "superAdmin": "", - "user": "" - }, - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" + "headerStatusPageControls": { + "publicLink": "Julkinen linkki" + }, + "headerTimeRange": { + "labels": { + "day": "Päivä", + "month": "Kuukausi", + "recent": "Viimeisimmät", + "week": "Viikko" }, "description": { - "superAdmin": "", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "", - "user": "" - }, - "termsAndPolicies": "", - "links": { - "login": "" - }, - "toasts": { - "success": "" - }, - "welcome": "" + "recent": "Näytetään tilastot viimeisiltä 2 tunnilta.", + "day": "Näytetään tilastot viimeisiltä 24 tunnilta.", + "week": "Näytetään tilastot viimeisiltä 7 päivältä.", + "month": "Näytetään tilastot viimeisiltä 30 päivältä." + } }, - "forgotPassword": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" + "offlineBanner": { + "serverUnreachable": "Palvelimeen ei saada yhteyttä", + "retry": "Yritä uudelleen", + "retrying": "Yritetään uudelleen...", + "reconnected": "" + }, + "sidebar": { + "menu": { + "uptime": "", + "pagespeed": "", + "infrastructure": "Infrastruktuuri", + "notifications": "", + "checks": "Tarkistukset", + "incidents": "", + "statusPages": "Tilasivut", + "maintenance": "Huolto", + "logs": "", + "settings": "Asetukset" }, - "buttons": { - "openEmail": "", - "resetPassword": "" + "bottomMenu": { + "support": "Tuki", + "discussions": "Keskustelut", + "docs": "Dokumentit", + "changelog": "Muutosloki" }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "accountMenu": { + "profile": "Profiili", + "password": "Salasana", + "team": "Tiimi" }, - "links": { - "login": "", - "resend": "" + "starPrompt": { + "title": "Lisää Checkmate suosikkeihin", + "description": "Katso uusimmat julkaisut ja auta kasvattamaan yhteisöä GitHubissa" }, - "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" + "authFooter": { + "navControls": "Ohjaimet", + "logOut": "Kirjaudu ulos", + "roles": { + "superAdmin": "Pääylläpitäjä", + "admin": "Ylläpitäjä", + "user": "Käyttäjä", + "demoUser": "Demokäyttäjä" + } + } + }, + "starPrompt": { + "title": "Lisää Checkmate suosikkeihin", + "description": "Katso uusimmat julkaisut ja auta kasvattamaan yhteisöä GitHubissa" + } + }, + "pages": { + "notFound": { + "title": "Voi ei! Sushisi putosi!", + "subtitle": "Joko URL-osoitetta ei ole olemassa tai sinulla ei ole pääsyä siihen." + }, + "account": { + "tabs": { + "profile": "Profiili", + "password": "Salasana", + "team": "Tiimi" + }, + "form": { + "name": { + "title": "Nimi", + "description": "Päivitä henkilökohtaiset tietosi" + }, + "photo": { + "title": "Profiilikuva", + "description": "Lataa profiilikuva" + }, + "currentPassword": { + "title": "Nykyinen salasana", + "description": "Syötä nykyinen salasanasi henkilöllisyytesi vahvistamiseksi", + "option": { + "label": "Nykyinen salasana", + "placeholder": "Syötä nykyinen salasana" + } + }, + "newPassword": { + "title": "Uusi salasana", + "description": "Valitse vahva salasana, jossa on vähintään 8 merkkiä", + "option": { + "newPassword": { + "label": "Uusi salasana", + "placeholder": "Syötä uusi salasana" + }, + "confirm": { + "label": "Vahvista salasana", + "placeholder": "Vahvista uusi salasana" + } + } + }, + "deleteAccount": { + "title": "Poista tili", + "description": "Tämä toiminto on pysyvä eikä sitä voi kumota" + } + }, + "team": { + "filter": { + "placeholder": "Suodata roolin mukaan", + "all": "Kaikki", + "admin": "Ylläpitäjä", + "member": "Jäsen" + }, + "table": { + "headers": { + "email": "Sähköposti", + "role": "Rooli", + "created": "Luotu" + } + }, + "addMember": { + "title": "Rekisteröi uusi tiimin jäsen", + "description": "Luo uusi käyttäjätili. Jaa tunnukset turvallisesti luomisen jälkeen." + }, + "invite": { + "title": "Kutsu tiimin jäsen", + "description": "Lähetä kutsu liittyä tiimiisi", + "email": { + "label": "Sähköpostiosoite", + "placeholder": "Syötä sähköpostiosoite" + }, + "role": { + "label": "Rooli", + "placeholder": "Valitse rooli" + }, + "linkLabel": "Kutsulinkki" + } + } + }, + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "", + "highlighted": "" + }, + "lowercase": { + "beginning": "", + "highlighted": "" + }, + "match": { + "beginning": "", + "highlighted": "" + }, + "number": { + "beginning": "", + "highlighted": "" + }, + "special": { + "beginning": "", + "highlighted": "" + }, + "uppercase": { + "beginning": "", + "highlighted": "" + } + }, + "form": { + "option": { + "email": { + "label": "Sähköposti", + "placeholder": "me@example.com" + }, + "password": { + "label": "Salasana", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Vahvista salasana" + } + } + } + }, + "login": { + "title": "Tervetuloa takaisin Checkmateen!", + "subtitle": "Kirjaudu sisään jatkaaksesi", + "submit": "Kirjaudu sisään", + "links": { + "forgotPassword": { + "text": "Unohditko salasanan?", + "linkText": "Nollaa salasana" + }, + "register": { + "text": "Eikö sinulla ole tiliä?", + "linkText": "Rekisteröidy täällä" + } + } + }, + "register": { + "title": "Tervetuloa takaisin Checkmateen!", + "subtitle": "Rekisteröidy aloittaaksesi", + "submit": "Rekisteröidy" + }, + "forgotPassword": { + "title": "Unohditko salasanasi?", + "subtitle": "Ei hätää, lähetämme sinulle nollausohjeet.", + "submit": "Pyydä palautusta", + "links": { + "login": { + "text": "Palaa", + "linkText": "kirjautumiseen" + } + } + }, + "setNewPassword": { + "title": "Nollaa salasanasi", + "subtitle": "Uuden salasanasi on oltava eri kuin aiemmin käytetyt salasanat." + } + }, + "checks": { + "selects": { + "monitor": { + "all": "Kaikki monitorit" + }, + "status": { + "all": "Kaikki", + "down": "Alhaalla", + "up": "Ylhäällä" + } + }, + "table": { + "empty": "Ei alhaalla-tarkistuksia tällä aikavälillä", + "headers": { + "statusCode": "Tilakoodi", + "location": "Sijainti" + } + } + }, + "common": { + "monitors": { + "actions": { + "configure": "Määritä", + "delete": "Poista", + "generateToken": "Luo tunniste", + "details": "Tiedot", + "incidents": "Häiriöt", + "inviteMember": "Kutsu jäsen", + "openSite": "Avaa sivusto", + "pause": "Keskeytä", + "resume": "Jatka" + }, + "statBoxes": { + "activeFor": "Aktiivinen", + "certificateExpiry": "Varmenteen vanheneminen", + "lastCheck": "Viimeisin tarkistus", + "lastResponseTime": "Viimeisin vasteaika" + }, + "status": { + "down": "alhaalla", + "breached": "kynnysarvo ylitetty", + "initializing": "alustamassa", + "maintenance": "huollossa", + "paused": "keskeytetty", + "total": "yhteensä", + "up": "ylhäällä" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Peli", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Portti", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infrastruktuuri", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } + }, + "createMonitor": { + "form": { + "advanced": { + "description": "Valinnaisia asetuksia edistyneille käyttötapauksille", + "option": { + "advancedMatching": { + "label": "Käytä edistynyttä vastaavuutta" + }, + "expectedValue": { + "label": "Odotettu arvo" + }, + "jsonPath": { + "description": "Tämä lauseke arvioidaan vastauksen JSON-dataa vasten ja tulosta käytetään odotetun arvon vertailuun. Katso jmespath.org kyselykielen dokumentaatio.", + "label": "JSONPath-lauseke" + }, + "matchMethod": { + "label": "Vertailutapa", + "equal": "", + "include": "", + "regex": "" + } + }, + "title": "Lisäasetukset" + }, + "frequency": { + "description": "Kuinka usein haluat tarkistaa tämän monitorin tilan?", + "option": { + "frequency": { + "label": "Tarkistusväli", + "value": { + "fifteenMinutes": "15 minuuttia", + "fifteenSeconds": "", + "fiveMinutes": "5 minuuttia", + "fourMinutes": "", + "oneMinute": "", + "tenMinutes": "10 minuuttia", + "thirtyMinutes": "30 minuuttia", + "thirtySeconds": "", + "threeMinutes": "", + "twoMinutes": "" + } + } + }, + "title": "Tarkistusväli" + }, + "general": { + "option": { + "container": { + "label": "Kontin nimi/tunnus", + "placeholder": "my-app tai abcd1234" + }, + "host": { + "label": "Isäntä", + "placeholder": "192.168.1.100 tai example.com" + }, + "name": { + "label": "Näyttönimi", + "placeholder": "esim. Oma verkkosivusto" + }, + "secret": { + "label": "Valtuutusavain", + "placeholder": "Syötä salausavaimesi" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "", + "placeholder": "" + }, + "port": { + "label": "Monitoroitava portti", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Palvelun nimi", + "placeholder": "esim. my.service.v1 (jätä tyhjäksi yleistä tilaa varten)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Yleiset asetukset", + "description": { + "http": "Syötä monitoroitava URL tai IP (esim. https://example.com/ tai 192.168.1.100) ja lisää selkeä näyttönimi, joka näkyy hallintapaneelissa.", + "ping": "", + "port": "", + "docker": "", + "game": "", + "pagespeed": "Seuraa sivun latausnopeutta, Core Web Vitals -arvoja ja optimointipisteitä verkkosivustollasi.", + "grpc": "Syötä gRPC-palvelimen isäntänimi ja portti, määritä halutessasi palvelun nimi terveystarkistusta varten ja lisää näyttönimi.", + "websocket": "Syötä monitoroitava WebSocket URL (esim. wss://example.com/socket) ja lisää näyttönimi.", + "hardware": "Monitoroi infrastruktuurisi CPU:n, muistin ja levyn käyttöä sekä lämpötilaa." + } + }, + "ignoreTls": { + "description": "Määritä TLS/SSL-varmenteen vahvistus HTTPS-yhteyksiä varten.", + "option": { + "tls": { + "label": "Ohita TLS/SSL-virheet" + } + }, + "title": "TLS/SSL-asetukset" + }, + "incidents": { + "description": "Liukuva ikkuna määrittää, milloin monitori katsotaan alhaalla olevaksi. Monitorin tila muuttuu vasta, kun tarkistusten prosenttiosuus liukuvassa ikkunassa saavuttaa määritellyn arvon.", + "option": { + "checks": { + "label": "Tarkistusten määrä liukuvassa ikkunassa" + }, + "percentage": { + "label": "Kuinka suuren osuuden tarkistuksista liukuvassa ikkunassa on epäonnistuttava/onnistuttava ennen kuin monitorin tila muuttuu?" + } + }, + "title": "Häiriöt" + }, + "notifications": { + "description": "Valitse ilmoituskanavat, joita haluat käyttää", + "title": "Ilmoitukset" + }, + "type": { + "description": "Valitse suoritettavan tarkistuksen tyyppi", + "optionDockerDescription": "Käytä Dockeria monitoroimaan, onko kontti käynnissä.", + "optionGameDescription": "Monitoroi, onko tietty pelipalvelin verkossa.", + "optionGrpcDescription": "Monitoroi gRPC-palveluita standardin Health Checking Protocol -protokollan avulla.", + "optionWebSocketDescription": "Monitoroi WebSocket-päätepisteitä yhteyden kunnon ja vasteajan osalta.", + "optionHttpDescription": "Käytä HTTP(S)-protokollaa verkkosivustosi tai API-päätepisteen monitorointiin.", + "optionPingDescription": "Käytä ICMP Ping -komentoa monitoroimaan, onko palvelin verkossa.", + "optionPortDescription": "Monitoroi, onko tietty palvelimen portti auki.", + "title": "Tyyppi" + }, + "thresholds": { + "title": "Hälytysrajat", + "description": "Määritä kynnysarvot, joiden ylittyessä hälytykset laukaistaan tälle laitemonitorille.", + "option": { + "cpuThreshold": { + "label": "CPU-hälytysraja (%)" + }, + "memoryThreshold": { + "label": "Muistin hälytysraja (%)" + }, + "diskThreshold": { + "label": "Levyn hälytysraja (%)" + }, + "tempThreshold": { + "label": "Lämpötilan hälytysraja (°C)" + } + } + }, + "geoChecks": { + "title": "Maantieteellisesti hajautetut tarkistukset", + "description": "Suorita tarkistuksia useista maantieteellisistä sijainneista globaalin saatavuuden ja suorituskyvyn monitoroimiseksi.", + "option": { + "enabled": { + "label": "Ota käyttöön maantieteellisesti hajautetut tarkistukset" + }, + "locations": { + "label": "Sijainnit", + "placeholder": "Valitse sijainnit", + "options": { + "EU": "Eurooppa", + "NA": "Pohjois-Amerikka", + "AS": "Aasia", + "SA": "Etelä-Amerikka", + "AF": "Afrikka", + "OC": "Oseania" + } + }, + "interval": { + "label": "Tarkistusväli", + "value": { + "fiveMinutes": "5 minuuttia", + "tenMinutes": "10 minuuttia", + "fifteenMinutes": "15 minuuttia", + "thirtyMinutes": "30 minuuttia" + } + } + } + }, + "url": { + "title": "Monitorin IP/URL tilasivulla", + "description": "Näytä monitorin IP-osoite tai URL julkisella tilasivulla. Jos se on poistettu käytöstä, vain monitorin nimi näytetään arkaluonteisten tietojen suojaamiseksi.", + "option": { + "showURL": { + "label": "Näytä IP/URL tilasivulla", + "enabled": "Käytössä", + "disabled": "Pois käytöstä" + } + } + }, + "stats": { + "title": "Monitorointihistoria", + "description": "Tyhjennä kaikki tiimisi monitorointihistoria ja tilastot. Tätä toimintoa ei voi kumota.", + "buttonText": "Tyhjennä kaikki tilastot", + "dialog": { + "title": "Tyhjennetäänkö kaikki monitorointihistoria?", + "description": "Tämä poistaa pysyvästi kaikki tiimisi monitorointihistorian, tilastot ja tarkistustiedot. Tätä toimintoa ei voi kumota.", + "confirm": "Kyllä, tyhjennä kaikki tilastot" + } + } + } + }, + "editUser": { + "form": { + "roles": { + "title": "Roolit", + "description": "Määritä käyttäjälle roolit. Useita rooleja voi valita." + } + }, + "dialog": { + "removeUser": { + "title": "Poista käyttäjä", + "content": "Haluatko varmasti poistaa käyttäjän {{name}} tiimistäsi? Tätä toimintoa ei voi kumota." + } + } + }, + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Ratkaise häiriö", + "option": { + "comment": { + "label": "Kommentti (valinnainen)", + "placeholder": "Lisää kommentti ratkaisusta..." + } + } + }, + "details": { + "analysis": "Häiriöanalyysi", + "comment": "Kommentti:", + "detailsLabel": "Tiedot", + "downtime": "Käyttökatko:", + "message": "Viesti:", + "monitor": "Monitori:", + "overview": "Yleiskatsaus", + "resolutionDetails": "Ratkaisun tiedot", + "resolutionType": "Tyyppi:", + "resolutionTypes": { + "automatic": "Automaattinen", + "manual": "Manuaalinen" + }, + "resolve": "Ratkaise häiriö", + "resolvedAt": "Ratkaistu:", + "resolvedBy": "Ratkaisija:", + "startedAt": "Alkanut:", + "status": "Tila:", + "statusCode": "Tilakoodi:", + "timeline": "Aikajana", + "title": "Häiriön tiedot", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Kaikki monitorit", + "monitor": "Monitori", + "resolutionType": "Ratkaisun tyyppi", + "resolutionTypes": { + "manual": "Manuaalinen", + "automatic": "Automaattinen", + "all": "Kaikki" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Aktiiviset häiriöt", + "active_zero": "Ei aktiivisia häiriöitä", + "active_one": "{{count}} aktiivinen häiriö", + "active_other": "{{count}} aktiivista häiriötä" + }, + "incidentStats": { + "avgResolutionTime": "Keskim. ratkaisuaika", + "mostAffectedMonitor": "Eniten häiriöitä kokenut monitori", + "title": "Häiriötilastot", + "totalIncidents": "Häiriöitä yhteensä" + }, + "latestIncidents": { + "title": "Viimeisimmät häiriöt" + } + }, + "table": { + "actions": { + "details": "Tiedot", + "goToMonitor": "Siirry monitoriin", + "resolveManually": "Ratkaise manuaalisesti" + }, + "activeIncidents": "Aktiiviset häiriöt", + "headers": { + "endTime": "Päättymisaika", + "resolutionType": "Ratkaisun tyyppi", + "startTime": "", + "statusCode": "Tilakoodi" + }, + "resolvedIncidents": "Ratkaistut häiriöt", + "status": { + "active": "Aktiivinen", + "resolved": "Ratkaistu" + } + } + }, + "infrastructure": { + "charts": { + "labels": { + "cpu": "CPU:n käyttö", + "disk": "Levyn käyttö", + "memory": "Muistin käyttö", + "netBytesRecv": "{{name}} - tavua vastaanotettu", + "netBytesSent": "{{name}} - tavua lähetetty", + "temp": "Lämpö" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Enimmäistaajuus", + "title": "CPU:n käyttö", + "upperLabel": "Nykyinen taajuus" + }, + "disk": { + "lowerLabel": "Vapaa", + "title": "Levy {{idx}} käyttö", + "upperLabel": "Käytetty" + }, + "memory": { + "lowerLabel": "Vapaa", + "title": "Muistin käyttö", + "upperLabel": "Käytetty" + } + }, + "statBoxes": { + "avgCpuTemperature": "", + "cpuFrequency": "", + "cpuLogical": "", + "cpuPhysical": "", + "disk": "Levy", + "memory": "Muisti", + "os": "" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Levy", + "memory": "Muisti" + } + }, + "tabs": { + "labels": { + "network": "", + "overview": "Yleiskatsaus" + } + }, + "fallback": { + "actionButton": "Luo monitori!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Infrastruktuurimonitorilla voit:" + } + }, + "logs": { + "tabs": { + "diagnostics": "", + "logs": "", + "queue": "" + }, + "logLevelSelect": { + "label": "Lokitaso" + }, + "jobQueue": "Työnojono", + "failedJobs": "Epäonnistuneet työt", + "noLogs": "Lokeja ei löytynyt", + "metrics": { + "jobs": "Työt", + "activeJobs": "Aktiiviset työt", + "failingJobs": "Epäonnistuvat työt", + "totalRuns": "Suoritukset yhteensä", + "totalFailures": "Epäonnistumiset yhteensä" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Tapahtumasilmukan viive", + "uptime": "Käytettävyys", + "usedHeapSize": "Käytetty keon koko", + "totalHeapSize": "Keon kokonaiskoko", + "osMemoryLimit": "Käyttöjärjestelmän muistiraja" + }, + "gauges": { + "heapAllocation": "Keon varaus", + "heapUsage": "Keon käyttö", + "heapUtilization": "Keon hyödyntäminen", + "instantCpuUsage": "Hetkellinen CPU:n käyttö", + "availableMemoryPercentage": "% vapaasta muistista", + "allocatedPercentage": "% varattu", + "usedSPercentage": "% sekunnista CPU:n käytössä", + "total": "Yhteensä", + "used": "Käytetty" + } + }, + "table": { + "headers": { + "timestamp": "Aikaleima", + "level": "Taso", + "service": "Palvelu", + "method": "Metodi", + "monitorId": "Monitorin tunnus", + "runCount": "Suorituskerrat", + "failCount": "Epäonnistumiskerrat", + "lastRunAt": "Viimeisin suoritus", + "lockedAt": "Lukittu", + "lastFinishedAt": "Viimeisin valmistuminen", + "lastRunTook": "Viimeisin suoritus kesti", + "lastFailedAt": "Viimeisin epäonnistuminen", + "failReason": "Epäonnistumisen syy" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Luo huoltoikkuna!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Huoltoikkunalla voit:" + }, + "table": { + "headers": { + "nextWindow": "", + "repeat": "" + } + }, + "form": { + "general": { + "title": "Yleiset asetukset", + "description": "Aseta nimi ja toistovalinta huoltoikkunallesi.", + "option": { + "name": { + "label": "Nimi", + "placeholder": "esim. Viikoittainen huolto" + }, + "repeat": { + "label": "Toista" + } + } + }, + "startDate": { + "title": "Alkamispäivä", + "description": "Valitse huoltoikkunan alkamispäivä.", + "option": { + "startDate": { + "label": "Alkamispäivä" + } + } + }, + "startTime": { + "title": "Alkamisaika", + "description": "Aseta huoltoikkunan alkamisaika ja kesto. Kaikki arvot ovat UTC-aikaa.", + "option": { + "duration": { + "label": "Kesto" + }, + "startTime": { + "label": "Alkamisaika" + } + }, + "monitors": { + "title": "Monitorit", + "description": "Monitorit, joihin huoltoikkuna kohdistuu", + "option": { + "addMonitors": { + "label": "Lisää monitorit" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "Luo kanava", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Ilmoituskanavia käytetään:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Pääsytunniste", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "Osoite, johon ilmoitukset lähetetään.", + "optionAddress": "Osoite", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Osoite" + }, + "homeServer": { + "optionHomeServer": "Kotipalvelin", + "placeholder": "example.com" + }, + "matrix": { + "description": "Määritä Matrix-kotipalvelimen yhteys ilmoituksia varten.", + "title": "Matrix-määritykset" + }, + "notificationName": { + "description": "Kuvaava nimi ilmoituskanavalle", + "optionName": "Kanavan nimi", + "placeholder": "esim. Tuotantohälytykset", + "title": "Kanavan nimi" + }, + "pagerDuty": { + "description": "PagerDuty-integrointiavaimesi hälytysten vastaanottamiseen.", + "optionIntegrationKey": "Integrointiavain", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Integrointiavain" + }, + "roomId": { + "optionRoomId": "Huoneen tunnus", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Valitse luotavan ilmoituskanavan tyyppi.", + "optionType": "Tyyppi", + "title": "Kanavan tyyppi" + }, + "telegram": { + "title": "Telegram-määritykset", + "description": "", + "optionBotToken": "Bottitunniste", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "Chat-tunnus", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Kohde" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "Kumulatiivinen asettelun muutos (CLS)", + "fcp": "Ensimmäinen sisältöpiirto (FCP)", + "lcp": "Suurin sisältöpiirto (LCP)", + "si": "Nopeusindeksi (SI)", + "tbt": "Kokonaisestymisaika (TBT)" + }, + "legend": { + "title": "PageSpeed-raportti", + "weight": "Painoarvo" + }, + "pie": { + "title": "Suorituskykyraportti" + } + }, + "table": { + "headers": { + "pageSpeedScore": "PageSpeed-pisteet" + } + }, + "fallback": { + "actionButton": "Luo monitori!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "PageSpeed-monitorilla voit:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Näytettävä aikavyöhyke", + "description": "Valitse aikavyöhyke, jota käytetään päivämäärien ja aikojen näyttämiseen sovelluksessa.", + "option": { + "timezone": { + "label": "Näytettävä aikavyöhyke" + } + } + }, + "ui": { + "title": "Ulkoasu", + "description": "Vaihda vaalean ja tumman tilan välillä, muuta kieltä tai muokkaa kaavion näyttötyyppiä.", + "option": { + "theme": { + "label": "Teematila", + "light": "Vaalea", + "dark": "Tumma" + }, + "language": { + "label": "Kieli" + }, + "chartType": { + "label": "Kaaviotyyppi", + "histogram": "Histogrammi", + "heatmap": "Lämpökartta" + } + } + }, + "pagespeed": { + "title": "Google PageSpeed API -avain", + "description": "Syötä Google PageSpeed API -avaimesi ottaaksesi käyttöön Google PageSpeed -monitoroinnin. Napsauta Palauta päivittääksesi avaimen.", + "option": { + "apiKey": { + "label": "PageSpeed API -avain", + "labelSet": "API-avain on asetettu. Napsauta Palauta muuttaaksesi sitä.", + "placeholder": "Syötä Google PageSpeed API -avaimesi" + } + } + }, + "url": { + "title": "Monitorin IP/URL tilasivulla", + "description": "Näytä monitorin IP-osoite tai URL julkisella tilasivulla. Jos se on poistettu käytöstä, vain monitorin nimi näytetään arkaluonteisten tietojen suojaamiseksi.", + "option": { + "showURL": { + "label": "Näytä IP/URL tilasivulla", + "enabled": "Käytössä", + "disabled": "Pois käytöstä" + } + } + }, + "stats": { + "title": "Monitorointihistoria", + "description": "Tyhjennä kaikki tiimisi monitorointihistoria ja tilastot. Tätä toimintoa ei voi kumota.", + "option": { + "clear": { + "label": "Tyhjennä kaikki tilastot. Tätä ei voi kumota." + } + }, + "dialog": { + "title": "Tyhjennetäänkö kaikki monitorointihistoria?", + "description": "Tätä ei voi kumota" + } + }, + "retention": { + "title": "Tarkistusten säilytys", + "description": "Aseta kuinka kauan tarkistustietoja säilytetään ennen automaattista poistoa.", + "option": { + "days": { + "label": "Säilytysaika (päiviä)", + "unlimited": "Rajoittamaton" + } + } + }, + "thresholds": { + "title": "Yleiset kynnysarvot", + "description": "Aseta yleiset CPU:n, muistin, levyn ja lämpötilan kynnysarvot infrastruktuurin monitorointiin. Nämä koskevat kaikkia laitemonitoreja, ellei niitä ohiteta.", + "option": { + "cpu": { + "label": "CPU-kynnysarvo (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Muistin kynnysarvo (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Levyn kynnysarvo (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Lämpötilan kynnysarvo (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Sähköpostiasetukset", + "description": "Määritä järjestelmäsi sähköpostiasetukset. Niitä käytetään ilmoitusten ja hälytysten lähettämiseen.", + "descriptionTransport": "", + "descriptionTransportLink": "Katso tekniset tiedot täältä", + "option": { + "host": { + "label": "Sähköpostin isäntä - yhdistettävä isäntänimi tai IP-osoite", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Sähköpostin portti - yhdistettävä portti", + "placeholder": "587" + }, + "address": { + "label": "Sähköpostiosoite - käytetään tunnistautumiseen", + "placeholder": "you@example.com" + }, + "user": { + "label": "Sähköpostin käyttäjä - tunnistautumisen käyttäjänimi, ohittaa sähköpostiosoitteen jos määritelty", + "placeholder": "Jätä tyhjäksi, jos ei tarvita" + }, + "password": { + "label": "Sähköpostin salasana - tunnistautumisen salasana", + "labelSet": "Salasana on asetettu. Napsauta Palauta muuttaaksesi sitä.", + "placeholder": "Syötä salasanasi" + }, + "tlsServername": { + "label": "TLS-palvelinnimi - valinnainen isäntänimi TLS-vahvistukseen, kun isäntä on IP-osoite", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "Sähköpostiyhteyden isäntä - HELO/EHLO-tervehdyksessä käytettävä isäntänimi", + "placeholder": "localhost" + }, + "secure": { + "label": "Käytä SSL:ää (suositus): Salaa yhteys SSL/TLS:n avulla" + }, + "pool": { + "label": "Ota yhteyspoolaus käyttöön: Käytä olemassa olevia yhteyksiä suorituskyvyn parantamiseksi" + }, + "ignoreTLS": { + "label": "Poista STARTTLS käytöstä: Älä käytä TLS:ää, vaikka palvelin tukisi sitä" + }, + "requireTLS": { + "label": "Pakota STARTTLS: Vaadi TLS-päivitys, epäonnistu jos ei tueta" + }, + "rejectUnauthorized": { + "label": "Hylkää virheelliset varmenteet: Hylkää yhteydet itseallekirjoitetuilla tai ei-luotetuilla varmenteilla" + } + } + }, + "demoMonitors": { + "title": "Demomonitorit", + "description": "Lisää esimerkkimonitoreja esittelytarkoituksiin." + }, + "removeMonitors": { + "title": "Järjestelmän nollaus", + "description": "Poista kaikki monitorit järjestelmästäsi.", + "dialog": { + "title": "Poistetaanko kaikki monitorit?", + "description": "Tätä ei voi kumota." + } + }, + "exportMonitors": { + "title": "Vie monitorit" + }, + "importExportMonitors": { + "title": "Monitorien tuonti / vienti", + "description": "Tuo tai vie monitorien tiedot JSON-tiedostona varmuuskopiointia tai siirtoa varten." + }, + "about": { + "title": "Tietoja", + "developedBy": "Kehittäjä: Bluewave Labs" + }, + "validation": { + "errorMessage": "Korjaa seuraavat vahvistusvirheet:" + } + } + }, + "statusPages": { + "deleteSuccess": "Tilasivu poistettu onnistuneesti", + "fallback": { + "title": "Tilasivulla voit:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "Luo tilasivu!" + }, + "monitorsList": { + "chartTypeHeatmap": "Lämpökartta", + "chartTypeHistogram": "Histogrammi", + "noData": "Tietoja ei saatavilla", + "infrastructure": { + "title": "Infrastruktuuri", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Muisti", + "disk": "Levy", + "usage": "Käyttö", + "used": "Käytetty", + "total": "Yhteensä" + }, + "uptime": { + "title": "Käytettävyys", + "responseTime": "Vasteaika" + } + }, + "statusBar": { + "allDown": "Kaikki järjestelmät ovat alhaalla", + "allUp": "Kaikki järjestelmät toiminnassa", + "degraded": "Joissakin järjestelmissä on ongelmia", + "unknown": "Järjestelmän tilaa ei voida määrittää" + }, + "table": { + "headers": { + "name": "Tilasivun nimi", + "url": "Julkinen URL" + }, + "published": "Julkaistu", + "unpublished": "Julkaisematon" + }, + "title": "Tilasivut", + "details": { + "empty": { + "title": "Täällä ei ole vielä mitään", + "addMonitor": "Lisää monitori aloittaaksesi" + } + }, + "form": { + "access": { + "title": "Käyttöoikeus", + "description": "Jos tilasivusi on valmis, voit merkitä sen julkaistuksi.", + "option": { + "published": { + "name": "Julkaistu ja julkisesti näkyvissä" + } + } + }, + "basicInfo": { + "title": "Perustiedot", + "description": "Määritä yrityksen nimi ja aliverkkotunnus, johon tilasivusi osoittaa.", + "option": { + "name": { + "label": "Yrityksen nimi", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Tilasivusi osoite", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "Monitorit", + "description": "Valitse tilasivullasi näytettävät monitorit.", + "noMonitors": "Monitoreja ei valittu", + "option": { + "monitors": { + "label": "Valitse monitorit", + "placeholder": "Hae ja valitse monitorit..." + } + } + }, + "timezone": { + "title": "Aikavyöhyke", + "description": "Valitse aikavyöhyke, jossa tilasivusi näytetään.", + "option": { + "timezone": { + "label": "Aikavyöhyke", + "placeholder": "Valitse aikavyöhyke..." + } + } + }, + "appearance": { + "title": "Ulkoasu", + "description": "Määritä julkisen tilasivusi oletusilme.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Brändiväri" + } + } + }, + "features": { + "title": "Ominaisuudet", + "description": "Määritä mitä tietoja tilasivullasi näytetään.", + "option": { + "showCharts": { + "label": "Näytä vasteaikakaaviot" + }, + "showUptimePercentage": { + "label": "Näytä käytettävyysprosentti" + }, + "showAdminLoginLink": { + "label": "Näytä ylläpitäjän kirjautumislinkki" + }, + "showInfrastructure": { + "label": "Näytä infrastruktuurin mittarit" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Hae monitoreja..." + } + }, + "table": { + "headers": { + "responseTime": "Vasteaika" + } + }, + "fallback": { + "actionButton": "Luo monitori!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Käytettävyysmonitorilla voit:" } } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" - }, - "alertBox": "", - "description": "", - "retryButton": { - "default": "", - "processing": "" - } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" - }, - "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" - }, - "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "testNotification": "", - "dialogDeleteTitle": "", - "dialogDeleteConfirm": "" - }, - "notificationConfig": { - "title": "", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "", - "sendTestNotifications": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "", - "diagnostics": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" - } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "", - "intervalHeader": "" - }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" - }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "", - "deleteSuccess": "", - "deleteFailed": "", - "details": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" - }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "", - "toastEmailRequiredFieldsError": "" - }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" - }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" - }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "" - }, - "title": "", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" - }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" - }, - "globalThresholds": { - "title": "", - "description": "" - } - }, - "statusPageCreate": { - "buttonSave": "" - }, - "incidentsOptionsHeaderFilterResolved": "", - "settingsSave": "", - "statusPageCreateAppearanceTitle": "", - "confirmPassword": "", - "monitorHooks": { - "failureAddDemoMonitors": "", - "successAddDemoMonitors": "" - }, - "settingsAppearance": "", - "settingsDisplayTimezone": "", - "settingsGeneralSettings": "", - "incidentsOptionsHeaderTotalIncidents": "", - "statusPage": { - "deleteSuccess": "", - "deleteFailed": "", - "createSuccess": "", - "updateSuccess": "", - "generalSettings": "", - "contents": "", - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "testNotificationsDisabled": "", - "incidentsTableResolvedAt": "", - "incidentsTableActionResolve": "", - "checkHooks": { - "failureResolveOne": "", - "failureResolveAll": "", - "failureResolveMonitor": "" - }, - "checkFormError": "", - "diagnosticsPage": { - "diagnosticDescription": "", - "statsDescription": "", - "gauges": { - "heapAllocationTitle": "", - "heapAllocationSubtitle": "", - "heapUsageTitle": "", - "heapUsageSubtitle": "", - "heapUtilizationTitle": "", - "heapUtilizationSubtitle": "", - "instantCpuUsageTitle": "", - "instantCpuUsageSubtitle": "" - }, - "stats": { - "eventLoopDelayTitle": "", - "uptimeTitle": "", - "usedHeapSizeTitle": "", - "totalHeapSizeTitle": "", - "osMemoryLimitTitle": "" - } - }, - "pageSpeedLighthouseAPI": "", - "time": { - "threeMinutes": "", - "fiveMinutes": "", - "tenMinutes": "", - "twentyMinutes": "", - "oneHour": "", - "oneDay": "", - "oneWeek": "", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" - }, - "general": { - "noOptionsFound": "" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "pageSpeed": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "editUserPage": { - "form": { - "email": "", - "firstName": "", - "lastName": "", - "role": "", - "save": "" - }, - "table": { - "actionHeader": "", - "roleHeader": "" - }, - "title": "", - "toast": { - "successUserUpdate": "", - "validationErrors": "" - } - }, - "incidentsPageActionResolveMonitor": "", - "incidentsPageActionResolveAll": "", - "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + } } diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index 9e76440f0..9b7ec03f9 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -1,1140 +1,1305 @@ { - "submit": "Envoyer", - "title": "Titre", - "distributedStatusHeaderText": "Couverture en temps réel, sur des appareils en temps réel", - "distributedStatusSubHeaderText": "Alimenté par des millions d'appareils à travers le monde, consultez les performances du système par région, pays ou ville", - "settingsDisabled": "Désactivé", - "settingsSuccessSaved": "Les paramètres ont bien été sauvegardés", - "settingsFailedToSave": "Les paramètres n'ont pas pu être sauvegardés", - "settingsStatsCleared": "Les statistiques ont été réinitialisées avec succès", - "settingsFailedToClearStats": "Les statistiques n'ont pas pu être réinitialisées", - "settingsMonitorsDeleted": "Tous les moniteurs ont bien été supprimés", - "settingsFailedToDeleteMonitors": "La suppression de tous les moniteurs a échoué", - "starPromptTitle": "Ajouter Checkmate aux favoris", - "starPromptDescription": "Voir les dernières versions et aider la communauté à grandir sur Github", - "https": "HTTPS", - "http": "HTTP", - "monitor": "Moniteur", - "aboutus": "A propos de nous", - "now": "Maintenant", - "delete": "Supprimer", - "configure": "Configurer", - "responseTime": "Temps de réponse", - "ms": "ms", - "bar": "Graphique", - "area": "Zone", - "country": "PAYS", - "city": "VILLE", - "response": "Réponse", - "monitorStatusUp": "Moniteur {name} ({url}) est désormais EN LIGNE et répond aux requêtes", - "monitorStatusDown": "Moniteur {name} ({url}) est désormais HORS LIGNE et ne répond plus", - "webhookSendSuccess": "La notification webhook a bien été envoyée", - "webhookSendError": "Une erreur s'est produite lors de l'envoi de la notification webhook sur {platform}", - "webhookUnsupportedPlatform": "La plateforme {platform} n'est pas supportée actuellement", - "distributedRightCategoryTitle": "Moniteur", - "distributedStatusServerMonitors": "Monitoring des serveurs", - "distributedStatusServerMonitorsDescription": "Statut des moniteurs relatifs aux serveurs", - "distributedUptimeCreateSelectURL": "Ici vous pouvez sélectionner l'URL de l'hôte et le type de moniteur", - "distributedUptimeCreateChecks": "Vérifications à effectuer", - "distributedUptimeCreateChecksDescription": "Vous pourrez ajouter ou supprimer des vérifications après l'ajout de votre site", - "distributedUptimeCreateIncidentNotification": "Notifications d'incidents", - "distributedUptimeCreateIncidentDescription": "Quand il y a un incident, notifier les utilisateurs", - "distributedUptimeCreateAdvancedSettings": "Paramètres avancés", - "distributedUptimeDetailsNoMonitorHistory": "Il n'y a pas encore d'historique de vérification pour ce moniteur", - "distributedUptimeDetailsStatusHeaderUptime": "Temps en ligne :", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Dernière mise à jour", - "notifications": { - "enableNotifications": "Activer les notifications sur {{platform}}", - "testNotification": "Notification de test", - "addOrEditNotifications": "Ajouter ou éditer des notifications", - "slack": { - "label": "Slack", - "description": "Pour activer les notifications Slack, créez une application Slack et activez les webhooks entrants. Ensuite, indiquer l'URL du webhook ici.", - "webhookLabel": "URL du webhook", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "L'URL du webhook Slack est requise" - }, - "discord": { - "label": "Discord", - "description": "Pour envoyer des données sur un canal Discord depuis Checkmate, utilisez l'intégration Webhook Discord.", - "webhookLabel": "URL du webhook Discord", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "L'URL du webhook Discord est requise" - }, - "telegram": { - "label": "Telegram", - "description": "Pour activer les notifications Telegram, créez un bot Telegram en utilisant BotFather, le robot officiel pour créer et gérer ses propres robots. Indiquez ensuite votre clé API et l'ID du chat ici.", - "tokenLabel": "Token du robot", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "Votre Chat ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "Le token Telegram et l'ID du chat sont requis." - }, - "webhook": { - "label": "Webhooks", - "description": "Vous pouvez indiquer un webhook personnalisé pour recevoir des notifications lorsqu'un incident se produit.", - "urlLabel": "URL du webhook", - "urlPlaceholder": "https://votre-serveur.fr/webhook", - "urlRequired": "L'URL du webhook est nécessaire" - }, - "testNotificationDevelop": "Notification de test 2", - "integrationButton": "Intégration de notification", - "testSuccess": "La notification de test a été envoyée avec succès !", - "testFailed": "Échec de l'envoi de la notification de test", - "unsupportedType": "Type de notification non supporté", - "networkError": "Une erreur réseau s'est produite", - "fallback": { - "title": "canal de notification", - "checks": [ - "Alertez les équipes en cas de temps d'arrêt ou de problèmes de performances", - "Informez les ingénieurs lorsque des incidents se produisent", - "Tenez les administrateurs informés des changements apportés au système" - ], - "actionButton": "Créez votre premier canal de notification !" - }, - "createButton": "Créer un canal de notification", - "createTitle": "Canal de notification", - "create": { - "success": "La notification a été créée avec succès", - "failed": "Une erreur s'est produite lors de la création de la notification" - }, - "fetch": { - "success": "Les notifications ont été récupérées avec succès", - "failed": "Une erreur s'est produite lors de la récupération des notifications" - }, - "delete": { - "success": "Notification supprimée avec succès", - "failed": "Une erreur s'est produite lors de la suppression de la notification" - }, - "edit": { - "success": "Notification modifiée avec succès", - "failed": "Une erreur s'est produite lors de la modification de la notification" - }, - "test": { - "success": "Notification de test envoyée avec succès", - "failed": "Une erreur s'est produite lors de l'envoi de la notification de test" - } - }, - "testLocale": "testLocale", - "add": "Ajouter", - "monitors": "moniteurs", - "distributedUptimeStatusCreateStatusPage": "page de statut", - "distributedUptimeStatusCreateStatusPageAccess": "Accès", - "distributedUptimeStatusCreateStatusPageReady": "Si votre page de statut est prête, vous pouvez l'indiquer en \"Publiée\".", - "distributedUptimeStatusBasicInfoHeader": "Informations de base", - "distributedUptimeStatusBasicInfoDescription": "Définir le nom de la société et le sous domaine sur lequel votre page de statut pointe.", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "Ajoutez un logo pour votre page de statut", - "distributedUptimeStatusLogoUploadButton": "Téléverser un logo", - "distributedUptimeStatusStandardMonitorsHeader": "Moniteurs standards", - "distributedUptimeStatusStandardMonitorsDescription": "Ajouter un moniteur standard à votre page de statut", - "distributedUptimeStatusCreateYour": "Créer votre", - "distributedUptimeStatusEditYour": "Editer votre", - "distributedUptimeStatusPublishedLabel": "Publiée et visible publiquement", - "distributedUptimeStatusCompanyNameLabel": "Nom de l'entreprise", - "distributedUptimeStatusPageAddressLabel": "Adresse de votre page de statut", - "distributedUptimeStatus30Days": "30 jours", - "distributedUptimeStatus60Days": "60 jours", - "distributedUptimeStatus90Days": "90 jours", - "distributedUptimeStatusPageNotSetUp": "Aucune page de statut n'est mise en place.", - "distributedUptimeStatusContactAdmin": "Veuillez contacter votre administrateur", - "distributedUptimeStatusPageNotPublic": "Cette page de statut n'est pas publique.", - "distributedUptimeStatusPageDeleteDialog": "Voulez-vous supprimer cette page de statut ?", - "distributedUptimeStatusPageDeleteConfirm": "Oui, supprimer la page de statut", - "distributedUptimeStatusPageDeleteDescription": "Supprimer votre page de statut est irréversible.", - "distributedUptimeStatusDevices": "Équipements", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "Logo Upt", - "incidentsTableNoIncidents": "Aucun incident enregistrée", - "incidentsTablePaginationLabel": "incidents", - "incidentsTableMonitorName": "Nom du moniteur", - "incidentsTableStatus": "Statut", - "incidentsTableDateTime": "Date et heure", - "incidentsTableStatusCode": "Code de statut", - "incidentsTableMessage": "Message", - "incidentsOptionsHeader": "Incidents pour :", - "incidentsOptionsHeaderFilterBy": "Filtrer par :", - "incidentsOptionsHeaderFilterAll": "Tout", - "incidentsOptionsHeaderFilterDown": "Hors ligne", - "incidentsOptionsHeaderFilterCannotResolve": "Impossible de résoudre", - "incidentsOptionsHeaderShow": "Voir:", - "incidentsOptionsHeaderLastHour": "Dernière heure", - "incidentsOptionsHeaderLastDay": "Dernier jour", - "incidentsOptionsHeaderLastWeek": "Dernière semaine", - "incidentsOptionsPlaceholderAllServers": "Tous les serveurs", - "infrastructureCreateYour": "Créer votre", - "infrastructureCreateGeneralSettingsDescription": "Ici, vous pouvez sélectionner l'URL de l'hôte, ainsi que le nom familier et le secret d'autorisation pour vous connecter à l'agent sur le serveur.", - "infrastructureServerRequirement": "Le serveur que vous surveillez doit exécuter le", - "infrastructureCustomizeAlerts": "Personnaliser les alertes", - "infrastructureAlertNotificationDescription": "Envoyer une notification aux utilisateurs lorsque le seuil dépasse un pourcentage spécifié.", - "infrastructureCreateMonitor": "Créer un moniteur d'infrastructure", - "infrastructureProtocol": "Protocole", - "infrastructureServerUrlLabel": "URL du serveur", - "infrastructureDisplayNameLabel": "Nom d'affichage", - "infrastructureAuthorizationSecretLabel": "Secret d'autorisation", - "gb": "GB", - "mb": "MB", - "mem": "Mém", - "memoryUsage": "Utilisation de la mémoire", - "cpu": "CPU", - "cpuUsage": "Utilisation CPU", - "cpuTemperature": "Température CPU", - "diskUsage": "Utilisation du disque", - "used": "Utilisé", - "total": "Total", - "cores": "Coeurs", - "frequency": "Fréquence", - "status": "Statut", - "cpuPhysical": "CPU (Physique)", - "cpuLogical": "CPU (Logique)", - "cpuFrequency": "Fréquence CPU", - "avgCpuTemperature": "Température moyenne du CPU", - "memory": "Mémoire", - "disk": "Disque", - "uptime": "Temps en ligne", - "os": "OS", - "host": "Hôte", - "actions": "Actions", - "integrations": "Intégrations", - "integrationsPrism": "Connecter Prism à votre service préféré.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Connecter à Slack et voir les incidents dans un canal", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Connecter à Discord et voir les incidents dans un canal", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Envoyer tous les incidents à Zapier", - "commonSave": "Sauvegarder", - "createYour": "Créez votre", - "createMonitor": "Ajouter un moniteur", - "pause": "Mettre en pause", - "resume": "Reprendre", - "editing": "Edition...", - "url": "URL", - "access": "Accès", - "timezone": "Fuseau horaire", - "features": "Fonctionnalités", - "administrator": "Administrateur ?", - "loginHere": "Connexion", - "displayName": "Nom d'affichage", - "urlMonitor": "URL à suivre", - "portToMonitor": "Port du moniteur", - "websiteMonitoring": "Monitoring d'un site internet", - "websiteMonitoringDescription": "Utiliser HTTP(s) pour vérifier votre site ou votre API.", - "pingMonitoring": "Monitoring par ping", - "pingMonitoringDescription": "Vérifier si le serveur est disponible ou pas", - "dockerContainerMonitoring": "Monitoring d'un container Docker", - "dockerContainerMonitoringDescription": "Vérifier si votre container Docker fonctionne ou pas.", - "portMonitoring": "Monitoring d'un port", - "portMonitoringDescription": "Vérifier si votre port est ouvert ou non.", - "createMaintenanceWindow": "Créer une fenêtre de maintenance", - "createMaintenance": "Créer une maintenance", - "editMaintenance": "Modifier la maintenance", - "maintenanceWindowName": "Nom de la fenêtre de maintenance", - "friendlyNameInput": "Nom", - "friendlyNamePlaceholder": "Maintenance à __ : __ pendant ___ minutes", - "maintenanceRepeat": "Répéter la maintenance", - "maintenance": "maintenance", - "duration": "Durée", - "addMonitors": "Ajouter des moniteurs", - "window": "fenêtre", - "cancel": "Annuler", - "message": "Message", - "low": "bas", - "high": "haut", - "statusCode": "Code de statut", - "date&Time": "Date et heure", - "type": "Type", - "statusPageName": "Nom de la page de statut", - "publicURL": "URL publique", - "repeat": "Répéter", - "edit": "Modifier", - "createA": "Créer un", - "remove": "Supprimer", - "maintenanceWindowDescription": "Vos pings ne seront pas envoyés pendant cette fenêtre de temps.", - "startTime": "Date de démarrage", - "timeZoneInfo": "Toutes les dates et heures sont dans le fuseau GMT+0", - "monitorsToApply": "Moniteurs sur lesquels appliquer la fenêtre de maintenance", - "nextWindow": "Prochaine fenêtre de maintenance", - "notFoundButton": "Aller au tableau de bord principal", - "pageSpeedConfigureSettingsDescription": "Vous pouvez sélectionner l'URL de hôte et le type de moniteur.", - "monitorDisplayName": "Nom du moniteur", - "whenNewIncident": "Lors d'un nouvel incident,", - "notifySMS": "Notification par SMS (bientôt)", - "notifyEmails": "Envoyer également une notification par email à plusieurs adresses (bientôt).", - "seperateEmails": "Vous pouvez ajouter plusieurs adresses emails en les séparant par une virgule", - "checkFrequency": "Vérifier la fréquence", - "matchMethod": "Méthode de rapprochement", - "expectedValue": "Valeur attendue", - "deleteDialogTitle": "Voulez-vous vraiment supprimer ce moniteur ?", - "deleteDialogDescription": "Une fois supprimé, le moniteur ne peut pas être récupéré.", - "pageSpeedMonitor": "Moniteur PageSpeed", - "shown": "Visibles", - "ago": "depuis", - "companyName": "Nom de la société", - "pageSpeedDetailsPerformanceReport": "Les valeurs sont estimatives et peuvent varier.", - "pageSpeedDetailsPerformanceReportCalculator": "Voir le calculateur", - "checkingEvery": "Vérification tous les", - "statusPageCreateSettings": "Si votre page de statut est prête, vous pouvez la rendre publique.", - "basicInformation": "Informations de base", - "statusPageCreateBasicInfoDescription": "Définir le nom de la société et le sous-domaine vers lequel pointe votre page de statut.", - "statusPageCreateSelectTimeZoneDescription": "Sélectionnez le fuseau horaire qui sera utilisé pour votre page de statut", - "statusPageCreateAppearanceDescription": "Définir le style par défaut de votre page de statut.", - "statusPageCreateSettingsCheckboxLabel": "Publiée et visible au public", - "statusPageCreateBasicInfoStatusPageAddress": "L'adresse de votre page de statut", - "statusPageCreateTabsContent": "Serveurs de votre page de statut", - "statusPageCreateTabsContentDescription": "Vous pouvez ajouter autant de moniteurs à votre page de statut que vous le souhaitez. Vous pouvez également les réordonner pour améliorer l'expérience utilisateur.", - "statusPageCreateTabsContentFeaturesDescription": "Voir plus de détails sur la page de statut", - "showCharts": "Voir les graphiques", - "showUptimePercentage": "Voir le pourcentage de temps en ligne", - "removeLogo": "Supprimer le logo", - "statusPageStatus": "Aucune page de statut publique n'est déployée", - "statusPageStatusContactAdmin": "Merci de contacter votre administrateur", - "statusPageStatusNotPublic": "Cette page de statut n'est pas piblique", - "statusPageStatusNoPage": "Il n'y a aucune page de statut ici", - "statusPageStatusServiceStatus": "Statut des services", - "deleteStatusPage": "Voulez-vous supprimer cette page de statut ?", - "deleteStatusPageConfirm": "Oui, supprimer la page de statut", - "deleteStatusPageDescription": "Une fois supprimée, votre page de statut ne pourra plus être récupérée.", - "uptimeCreate": "La valeur attendue est utilisée pour comparer le résultat de la réponse et la correspondance détermine le statut du moniteur.", - "uptimeCreateJsonPath": "Cette expression sera évaluée par rapport aux données JSON de la réponse et le résultat sera utilisé pour la comparaison avec la valeur attendue. Voir", - "uptimeCreateJsonPathQuery": "pour la documentation sur le langage de requête.", - "maintenanceTableActionMenuDialogTitle": "Voulez-vous vraiment supprimer cette fenêtre de maintenance ?", - "infrastructureEditYour": "Modifier votre", - "infrastructureEditMonitor": "Sauvegarder le moniteur d'infrastructure", - "infrastructureMonitorCreated": "Le moniteur d'infrastructure a été créé avec succès !", - "infrastructureMonitorUpdated": "Le moniteur d'infrastructure a été modifié avec succès !", - "errorInvalidTypeId": "Le type de notification fourni est invalide", - "errorInvalidFieldId": "Le champ ID fourni est invalide", - "inviteNoTokenFound": "Aucun jeton d'invitation fourni", - "pageSpeedWarning": "Attention : vous n'avez pas encore ajouté de clé API Google PageSpeed. Sans celle-ci, le moniteur PageSpeed ne fonctionnera pas.", - "pageSpeedLearnMoreLink": "Cliquer ici", - "pageSpeedAddApiKey": "pour ajouter votre clé API.", - "update": "Mettre à jour", - "invalidFileFormat": "Format de fichier non supporté !", - "invalidFileSize": "Le fichier est trop volumineux !", - "ClickUpload": "Cliquez pour téléverser", - "DragandDrop": "glisser et déposer", - "MaxSize": "Taille maximum", - "SupportedFormats": "Formats supportés", - "FirstName": "Prénom", - "LastName": "Nom", - "EmailDescriptionText": "Il s'agit de votre adresse email actuelle : elle ne peut pas être changée.", - "YourPhoto": "Photo de profil", - "PhotoDescriptionText": "La photo sera affichée sur votre page de profil.", - "save": "Enregistrer", - "DeleteDescriptionText": "Cela supprimera le compte et toutes les données associées du serveur. Cette opération est irréversible.", - "DeleteAccountWarning": "Supprimer votre compte signifie que vous ne pourrez pas vous reconnecter et que toutes vos données seront supprimés. Ceci est irréversible.", - "DeleteWarningTitle": "Vous supprimez vraiment ce compte ?", - "bulkImport": { - "title": "Import en masse", - "selectFileTips": "Sélectionnez un fichier CSV pour l'upload", - "selectFileDescription": "Vous pouvez télécharger notre ou exemple.", - "selectFile": "Sélectionner un fichier", - "parsingFailed": "L'analyse du fichier a rencontré un problème", - "uploadSuccess": "Moniteurs créés avec succès !", - "validationFailed": "Echec de la validation", - "noFileSelected": "Aucun fichier sélectionné", - "fallbackPage": "Importer un fichier pour ajouter une liste de serveurs en masse", - "invalidFileType": "Type de fichier invalide", - "uploadFailed": "Le téléchargement a échoué" - }, - "DeleteAccountTitle": "Supprimer le compte", - "DeleteAccountButton": "Supprimer le compte", - "publicLink": "Lien public", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "Réinitialiser", - "ignoreTLSError": "Ignorer les erreurs TLS/SSL", - "tlsErrorIgnored": "Erreurs TLS/SSL ignorées", - "ignoreTLSErrorDescription": "Ignorer les erreurs TLS/SSL et continuer à vérifier l'accessibilité du site web", - "createNew": "Créer un nouveau", - "greeting": { - "prepend": "Salut !", - "append": "L'après-midi est votre libre, rendons-le épique !", - "overview": "Voici un aperçu de vos moniteurs {{type}}." - }, - "roles": { - "superAdmin": "Super admin", - "admin": "Admin", - "teamMember": "Membre de l'équipe", - "demoUser": "Utilisateur de démonstration" - }, - "teamPanel": { - "teamMembers": "Membres de l'équipe", - "filter": { - "all": "Tous", - "member": "Membre" - }, - "inviteTeamMember": "Inviter un membre de l'équipe", - "inviteNewTeamMember": "Inviter un nouveau membre d'équipe", - "inviteDescription": "Lorsque vous ajoutez un nouveau membre à l'équipe, il aura accès à tous les moniteurs.", - "email": "Email", - "selectRole": "Sélectionnez un rôle", - "inviteLink": "Lien d'invitation", - "cancel": "Annuler", - "noMembers": "Il n'y a aucun membre d'équipe avec ce rôle", - "getToken": "Récupérer un token", - "emailToken": "Token de l'email", - "table": { - "name": "Nom", - "email": "Email", - "role": "Rôle", - "created": "Créée" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "Pause", - "resumed": "Reprendre", - "active": "Actif" - }, - "menu": { - "uptime": "Temps en ligne", - "pagespeed": "Vitesse", - "infrastructure": "Infrastructure", - "incidents": "Incidents", - "statusPages": "Pages de statut", - "maintenance": "Maintenance", - "integrations": "Intégrations", - "settings": "Paramètres", - "support": "Support", - "discussions": "Discussions", - "docs": "Documentation", - "changelog": "Changelog", - "profile": "Profil", - "password": "Mot de passe", - "team": "Équipe", - "logOut": "Déconnexion", - "notifications": "Notifications", - "logs": "Journaux" - }, - "settingsEmailUser": "Utilisateur SMTP - Utilisateur pour l'authentification, écrase l'adresse email si spécifiée", - "state": "État", - "statusBreadCrumbsStatusPages": "Pages de statut", - "statusBreadCrumbsDetails": "Détails", - "commonSaving": "Sauvegarde...", - "navControls": "Contrôle", - "incidentsPageTitle": "Incidents", - "passwordPanel": { - "passwordChangedSuccess": "Votre mot de passe a été modifié avec succès.", - "passwordInputIncorrect": "La saisie de votre mot de passe est incorrecte.", - "currentPassword": "Mot de passe actuel", - "enterCurrentPassword": "Entrez votre mot de passe actuel", - "newPassword": "Nouveau mot de passe", - "enterNewPassword": "Entrez votre nouveau mot de passe", - "confirmNewPassword": "Confirmer le nouveau mot de passe", - "passwordRequirements": "Le nouveau mot de passe doit contenir au moins 8 caractères et au moins une lettre majuscule, une lettre minuscule, un chiffre et un caractère spécial.", - "saving": "Sauvegarde..." - }, - "emailSent": "Email envoyé avec succès", - "failedToSendEmail": "Une erreur s'est produite lors de l'envoi de l'email", - "settingsTestEmailSuccess": "Email de test envoyé avec succès", - "settingsTestEmailFailed": "Une erreur s'est produite lors de l'envoi de l'email de test", - "settingsTestEmailFailedWithReason": "Une erreur s'est produite lors de l'envoi de l'email de test : {{reason}}", - "settingsTestEmailUnknownError": "Erreur inconnue", - "statusMsg": { - "paused": "Le moniteur est en pause.", - "up": "Votre site est en ligne.", - "down": "Votre site est hors-ligne.", - "pending": "En attente..." - }, - "uptimeGeneralInstructions": { - "http": "Entrez l'URL ou l'IP du moniteur (par exemple https://exemple.fr ou 192.168.1.100) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.", - "ping": "Entrez l'adresse IP ou le nom d'hôte à tester (par exemple, 192.168.1.100 ou exemple.fr) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.", - "docker": "Entrer l'ID Docker du container. Les identifiants Docker doivent être les 64 caractères de l'ID Docker. Vous pouvez utiliser la commande docker inspect pour avoir l'ID complet.", - "port": "Entrez l'URL ou l'adresse IP du serveur, le numéro de port et un nom d'affichage familier qui apparaîtra sur le tableau de bord.", - "game": "", - "https": "" - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "Afficher le mode clair & sombre" + "auth": { + "roles": { + "admin": "Admin", + "demo": "Démo", + "superadmin": "Super admin", + "user": "Utilisateur" + } }, - "toasts": { - "networkError": "Erreur de réseau", - "checkConnection": "Merci de vérifier votre connexion", - "unknownError": "Erreur inconnue" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "Continuer", - "back": "Retour" + "alerts": { + "pageSpeedApiKey": { + "content": "Attention : vous n'avez pas encore ajouté de clé API Google PageSpeed. Rendez-vous dans Paramètres pour en ajouter une. Sans elle, le moniteur PageSpeed ne fonctionnera pas." + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "Détails", + "home": "Accueil" + }, + "buttons": { + "addMember": "Ajouter un membre", + "cancel": "Annuler", + "close": "Fermer", + "configure": "Configurer", + "confirm": "Confirmer", + "create": "Créer", + "delete": "Supprimer", + "generateToken": "Générer un jeton", + "incidents": "Incidents", + "inviteMember": "Inviter un membre", + "pause": "Pause", + "resume": "Reprendre", + "save": "Enregistrer", + "sendInvite": "Envoyer l'invitation", + "test": "Tester", + "testNotifications": "Tester les notifications", + "toggleTheme": "Afficher le mode clair & sombre", + "flushQueue": "Vider la file d'attente", + "notFound": "Aller au tableau de bord", + "resetPassword": "Réinitialiser le mot de passe", + "reset": "Réinitialiser", + "clear": "Effacer", + "addDemo": "Ajouter des moniteurs de démo", + "removeMonitors": "Supprimer les moniteurs", + "sendTestEmail": "Envoyer un email de test", + "exportToJSON": "Exporter en JSON", + "importFromJSON": "Importer depuis JSON", + "clearFilters": "Effacer les filtres", + "removeUser": "Supprimer l'utilisateur" + }, + "charts": { + "labels": { + "averageResponseTime": "Temps de réponse moyen", + "downtime": "Temps d'arrêt", + "high": "haut", + "low": "bas", + "uptime": "Disponibilité" }, - "inputs": { - "email": { - "label": "Email", - "placeholder": "jean.dupont@domaine.fr", - "errors": { - "empty": "Pour continuer, merci d'indiquer votre adresse email", - "invalid": "Merci de vérifier la validité de l'adresse email saisie" - } - }, - "password": { - "label": "Mot de passe", - "rules": { - "length": { - "beginning": "Doit contenir au moins", - "highlighted": "Longueur de 8 caractères" - }, - "special": { - "beginning": "Doit contenir au moins", - "highlighted": "un caractère spécial" - }, - "number": { - "beginning": "Doit contenir au moins", - "highlighted": "un nombre" - }, - "uppercase": { - "beginning": "Doit contenir au moins", - "highlighted": "un caractère majuscule" - }, - "lowercase": { - "beginning": "Doit contenir au moins", - "highlighted": "un caractère miniscule" - }, - "match": { - "beginning": "Confirmer le mot de passe et le mot de passe", - "highlighted": "doit valoir" - } - }, - "errors": { - "empty": "Merci d'entrer votre mot de passe", - "length": "Le mot de passe doit faire 8 caractères minimum", - "uppercase": "Le mot de passe doit contenir au minimum une lettre majuscule", - "lowercase": "Le mot de passe doit contenir au minimum une lettre minuscule", - "number": "Le mot de passe doit contenir au minimum un chiffre", - "special": "Le mot de passe doit contenir au minimum un caractère spécial", - "incorrect": "Le mot de passe fourni est erroné" - } - }, - "passwordConfirm": { - "label": "Confirmer le mot de passe", - "placeholder": "Entrez à nouveau le mot de passe pour confirmer", - "errors": { - "empty": "Merci d'entrer votre mot de passe à nouveau pour le confirmer (aide avec la typo)", - "different": "Les mots de passe indiqués ne sont pas identiques, l'un d'entre eux est probablement mal écrit" - } - }, - "firstName": { - "label": "Prénom", - "placeholder": "Jean", - "errors": { - "empty": "Merci d'entrer votre prénom", - "length": "Le prénom doit être inférieur à 50 caractères", - "pattern": "Le prénom ne peut contenir que des lettres, des espaces, des apostrophes ou des tirets" - } - }, - "lastName": { - "label": "Nom", - "placeholder": "Dupont", - "errors": { - "empty": "Merci d'entrer votre nom", - "length": "Le nom doit être inférieur à 50 caractères", - "pattern": "Le nom ne peut contenir que des lettres, des espaces, des apostrophes ou des tirets" + "histogram": { + "avg": "Moy : {{value}} ms", + "max": "Max : {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Cette action est irréversible.", + "title": "Êtes-vous sûr de vouloir supprimer ceci ?" + } + }, + "labels": { + "active": "Actif", + "paused": "en pause", + "na": "N/A", + "resolved": "Résolu", + "responseTime": "Temps de réponse" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Rôles" } } }, - "errors": { - "validation": "Une erreur s'est produite lors de la validation des données." - }, - "fields": { - "password": { - "errors": { - "incorrect": "Le mot de passe fourni est erroné" + "name": { + "option": { + "firstName": { + "label": "Prénom", + "placeholder": "Jean" + }, + "lastName": { + "label": "Nom", + "placeholder": "Dupont" } - }, - "role": { - "errors": { - "min": "Au moins un rôle est requis" + } + }, + "email": { + "option": { + "email": { + "label": "Email", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "Connexion", - "subheadings": { - "stepOne": "Entrez votre adresse email", - "stepTwo": "Entrez votre mot de passe" - }, - "links": { - "forgotPassword": "Mot de passe oublié ?", - "register": "Pas encore de compte ?", - "forgotPasswordLink": "Réinitialiser le mot de passe", - "registerLink": "Inscrivez vous ici" - }, - "toasts": { - "success": "Bon retour ! Vous êtes connecté avec succès.", - "incorrectPassword": "Mot de passe incorrect" - }, + "table": { + "empty": "Rien à afficher", + "headers": { + "actions": "Actions", + "dateTime": "Date et heure", + "message": "Message", + "monitor": "Moniteur", + "monitorId": "ID du moniteur", + "name": "Nom", + "status": "Statut", + "type": "Type", + "url": "URL", + "interval": "Intervalle", + "active": "Actif", + "responseTime": "Temps de réponse" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "Cliquer pour téléverser", + "dragAndDrop": "glisser et déposer", + "supportedFormats": "Formats supportés", + "maxSize": "Taille maximum", + "orDragAndDrop": "ou glisser et déposer", "errors": { - "password": { - "incorrect": "Le mot de passe fourni est erroné" - } - }, - "welcome": "Bon retour sur Checkmate !" + "invalidFileSize": "Le fichier est trop volumineux !", + "invalidFileFormat": "Format de fichier non supporté !" + } }, - "registration": { - "heading": { - "superAdmin": "Créer un super admin", - "user": "Inscription" - }, - "subheadings": { - "stepOne": "Entrez votre informations personnelles", - "stepTwo": "Entrez votre adresse email", - "stepThree": "Créez votre mot de passe" + "headerStatusPageControls": { + "publicLink": "Lien public" + }, + "headerTimeRange": { + "labels": { + "day": "Jour", + "month": "Mois", + "recent": "Récent", + "week": "Semaine" }, "description": { - "superAdmin": "Créer le compte \"SuperAdmin\" pour commencer", - "user": "inscription comme utilisateur et demander à l'administrateur principal l'accès aux moniteurs" - }, - "gettingStartedButton": { - "superAdmin": "Créer un compte super admin", - "user": "Inscription comme utilisateur normal" - }, - "termsAndPolicies": "En créant un compte, vous comprenez et acceptez les Conditions d'Utilisation et la Politique de Confidentialité.", - "links": { - "login": "Déjà inscrit ? Connexion" - }, - "toasts": { - "success": "Bienvenue ! Votre compte a été créé avec succès." - }, - "welcome": "Bienvenue sur Checkmate !" - }, - "forgotPassword": { - "heading": "Mot de passe oublié ?", - "subheadings": { - "stepOne": "Pas d'inquiétude, nous allons vous envoyer des informations pour réinitialiser votre mot de passe.", - "stepTwo": "Nous avons envoyer un lien de réinitialisation du mot de passe à l'email ", - "stepThree": "Votre nouveau mot de passe doit être différent des précédents.", - "stepFour": "Votre mot de passe a été réinitialisé avec succès. Cliquez ci-dessous pour vous connecter." - }, - "buttons": { - "openEmail": "Ouvrir votre application email", - "resetPassword": "Réinitialiser le mot de passe" - }, - "imageAlts": { - "passwordKey": "Icône du mot de passe", - "email": "Icône de l'email", - "lock": "Icône de verrouillage", - "passwordConfirm": "Icône de confirmation du mot de passe" - }, - "links": { - "login": "Retourner à la page de connexion", - "resend": "Vous n'avez pas reçu l'email ? Cliquez ici pour le renvoyer" - }, - "toasts": { - "sent": "Instructions envoyées sur l'adresse email .", - "emailNotFound": "L'email n'a pas été trouvé.", - "redirect": "Redirection dans ...", - "success": "Votre mot de passe", - "error": "Une erreur s'est produite lors de la réinitialisation du mot de passe. Réessayez ultérieurement ou contactez le support." + "recent": "Statistiques des 2 dernières heures.", + "day": "Statistiques des dernières 24 heures.", + "week": "Statistiques des 7 derniers jours.", + "month": "Statistiques des 30 derniers jours." } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "Connexion rétablie avec le serveur.", - "stillUnreachable": "Le serveur est toujours inaccessible. Réessayez ultérieurement." + }, + "offlineBanner": { + "serverUnreachable": "Impossible de joindre le serveur", + "retry": "Réessayer", + "retrying": "Nouvelle tentative...", + "reconnected": "Connexion rétablie avec le serveur." + }, + "sidebar": { + "menu": { + "uptime": "Temps en ligne", + "pagespeed": "Vitesse", + "infrastructure": "Infrastructure", + "notifications": "Notifications", + "checks": "Vérifications", + "incidents": "Incidents", + "statusPages": "Pages de statut", + "maintenance": "Maintenance", + "logs": "Journaux", + "settings": "Paramètres" }, - "alertBox": "Erreur de connexion au serveur", - "description": "Nous ne parvenons pas à nous connecter au serveur. Veuillez vérifier votre connexion Internet ou vérifier votre configuration de déploiement si le problème persiste.", - "retryButton": { - "default": "Réessayer la connexion", - "processing": "Connexion..." + "bottomMenu": { + "support": "Support", + "discussions": "Discussions", + "docs": "Documentation", + "changelog": "Journal des modifications" + }, + "accountMenu": { + "profile": "Profil", + "password": "Mot de passe", + "team": "Équipe" + }, + "starPrompt": { + "title": "Star Checkmate", + "description": "Consultez les dernières versions et aidez la communauté à grandir sur GitHub" + }, + "authFooter": { + "navControls": "Contrôle", + "logOut": "Déconnexion", + "roles": { + "superAdmin": "Super admin", + "admin": "Admin", + "user": "Utilisateur", + "demoUser": "Utilisateur de démonstration" + } } + }, + "starPrompt": { + "title": "Star Checkmate", + "description": "Consultez les dernières versions et aidez la communauté à grandir sur GitHub" } }, - "createNotifications": { - "title": "Créer un canal de notification", - "nameSettings": { - "title": "Nom", - "description": "Une description pour votre intégration.", - "nameLabel": "Nom", - "namePlaceholder": "e.g. notifications Slack" + "pages": { + "notFound": { + "title": "Oh non ! Vous avez fait tomber vos sushis !", + "subtitle": "Soit l'URL n'existe pas, soit vous n'y avez pas accès." }, - "typeSettings": { - "title": "Type", - "description": "Sélectionnez le type de canal de notification que vous souhaitez créer.", - "typeLabel": "Type" - }, - "emailSettings": { - "title": "Email", - "description": "Adresse email du destinataire", - "emailLabel": "Adresse email", - "emailPlaceholder": "e.g. jean@exemple.fr" - }, - "slackSettings": { - "title": "Slack", - "description": "Configurez votre webhook Slack ici", - "webhookLabel": "URL du webhook Slack", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "Configurez l'intégration PagerDuty ici", - "integrationKeyLabel": "Clé d'intégration", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discord", - "description": "Configurez votre webhook Discord ici", - "webhookLabel": "URL du webhook Discord", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "webhookSettings": { - "title": "Webhook", - "description": "Configurez votre webhook ici", - "webhookLabel": "URL du webhook", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "testNotification": "Notification de test", - "dialogDeleteTitle": "Voulez-vous vraiment supprimer cette notification ?", - "dialogDeleteConfirm": "Supprimer" - }, - "notificationConfig": { - "title": "Notifications", - "description": "Sélectionnez les canaux de notification que vous souhaitez utiliser" - }, - "monitorStatus": { - "checkingEvery": "Vérifier toutes les {{interval}}", - "withCaptureAgent": "avec l'agent Capture {{version}}", - "up": "en ligne", - "down": "hors ligne", - "paused": "en pause" - }, - "advancedMatching": "Filtrage avancé", - "sendTestNotifications": "Envoyer une notification de test", - "selectAll": "Sélectionner tout", - "showAdminLoginLink": "Voir le lien \"Administrateur ? Connectez-vous ici\" sur la page de statut", - "logsPage": { - "title": "Journaux", - "description": "Journaux systèmes - 1000 dernières lignes", - "tabs": { - "queue": "File d'attente des tâches", - "logs": "Journaux des serveurs", - "diagnostics": "Diagnostic" - }, - "toast": { - "fetchLogsSuccess": "Journaux récupérés avec succès" - }, - "logLevelSelect": { - "title": "Niveau de journalisation", - "values": { - "all": "Tout", - "info": "Info", - "warn": "Attention", - "error": "Erreur", - "debug": "Debug" + "account": { + "tabs": { + "profile": "Profil", + "password": "Mot de passe", + "team": "Équipe" + }, + "form": { + "name": { + "title": "Nom", + "description": "Mettez à jour vos informations personnelles" + }, + "photo": { + "title": "Photo de profil", + "description": "Téléversez une photo de profil" + }, + "currentPassword": { + "title": "Mot de passe actuel", + "description": "Saisissez votre mot de passe actuel pour vérifier votre identité", + "option": { + "label": "Mot de passe actuel", + "placeholder": "Saisissez le mot de passe actuel" + } + }, + "newPassword": { + "title": "Nouveau mot de passe", + "description": "Choisissez un mot de passe fort d'au moins 8 caractères", + "option": { + "newPassword": { + "label": "Nouveau mot de passe", + "placeholder": "Saisissez le nouveau mot de passe" + }, + "confirm": { + "label": "Confirmer le mot de passe", + "placeholder": "Confirmez le nouveau mot de passe" + } + } + }, + "deleteAccount": { + "title": "Supprimer le compte", + "description": "Cette action est permanente et irréversible" + } + }, + "team": { + "filter": { + "placeholder": "Filtrer par rôle", + "all": "Tous", + "admin": "Admin", + "member": "Membre" + }, + "table": { + "headers": { + "email": "Email", + "role": "Rôle", + "created": "Créée" + } + }, + "addMember": { + "title": "Inscrire un nouveau membre", + "description": "Créez un nouveau compte utilisateur. Partagez les identifiants de manière sécurisée après la création." + }, + "invite": { + "title": "Inviter un membre", + "description": "Envoyez une invitation à rejoindre votre équipe", + "email": { + "label": "Adresse email", + "placeholder": "Saisissez l'adresse email" + }, + "role": { + "label": "Rôle", + "placeholder": "Sélectionnez un rôle" + }, + "linkLabel": "Lien d'invitation" + } } - } - }, - "queuePage": { - "title": "File d'attente", - "refreshButton": "Rafraîchir", - "flushButton": "Vider la file d'attente", - "jobTable": { - "title": "Tâches actuellement dans la file d'attente", - "idHeader": "ID du moniteur", - "urlHeader": "URL", - "typeHeader": "Type", - "activeHeader": "Actif", - "lockedAtHeader": "Verrouillé le", - "runCountHeader": "Nombre de tentatives", - "failCountHeader": "Nombre d'échecs", - "lastRunHeader": "Dernière tentative le", - "lastFinishedAtHeader": "Dernière finalisation à", - "lastRunTookHeader": "Dernière exécution effectuée", - "intervalHeader": "" }, - "metricsTable": { - "title": "Métriques des files", - "metricHeader": "Métrique", - "valueHeader": "Valeur" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "Doit contenir au moins", + "highlighted": "Longueur de 8 caractères" + }, + "lowercase": { + "beginning": "Doit contenir au moins", + "highlighted": "un caractère miniscule" + }, + "match": { + "beginning": "Confirmer le mot de passe et le mot de passe", + "highlighted": "doit valoir" + }, + "number": { + "beginning": "Doit contenir au moins", + "highlighted": "un nombre" + }, + "special": { + "beginning": "Doit contenir au moins", + "highlighted": "un caractère spécial" + }, + "uppercase": { + "beginning": "Doit contenir au moins", + "highlighted": "un caractère majuscule" + } + }, + "form": { + "option": { + "email": { + "label": "Email", + "placeholder": "me@example.com" + }, + "password": { + "label": "Mot de passe", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Confirmer le mot de passe" + } + } + } + }, + "login": { + "title": "Bienvenue sur Checkmate !", + "subtitle": "Connectez-vous pour continuer", + "submit": "Connexion", + "links": { + "forgotPassword": { + "text": "Mot de passe oublié ?", + "linkText": "Réinitialiser le mot de passe" + }, + "register": { + "text": "Vous n'avez pas de compte ?", + "linkText": "Inscrivez-vous ici" + } + } + }, + "register": { + "title": "Bienvenue sur Checkmate !", + "subtitle": "Inscrivez-vous pour commencer", + "submit": "S'inscrire" + }, + "forgotPassword": { + "title": "Mot de passe oublié ?", + "subtitle": "Pas de souci, nous vous enverrons les instructions de réinitialisation.", + "submit": "Demander la récupération", + "links": { + "login": { + "text": "Retour à la", + "linkText": "connexion" + } + } + }, + "setNewPassword": { + "title": "Réinitialisez votre mot de passe", + "subtitle": "Votre nouveau mot de passe doit être différent des mots de passe utilisés précédemment." + } }, - "failedJobTable": { - "title": "Tâches en échec", - "monitorIdHeader": "ID du moniteur", - "monitorUrlHeader": "URL du moniteur", - "failCountHeader": "Nombre de tentatives échouées", - "failedAtHeader": "Dernier échec à", - "failReasonHeader": "Raison de l'échec" - } - }, - "export": { - "title": "Export des moniteurs", - "success": "Les moniteurs ont été exportés avec succès !", - "failed": "Une erreur s'est produite lors de l'export des moniteurs" - }, - "monitorActions": { - "title": "Export/Import", - "import": "Importer des moniteurs", - "export": "Exporter des moniteurs", - "deleteSuccess": "Le moniteur a été supprimé avec succès", - "deleteFailed": "Une erreur s'est produite lors de la suppression du moniteur", - "details": "Détails" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "Développé par Bluewave Labs", - "labelVersion": "Version", - "title": "A propos" + "checks": { + "selects": { + "monitor": { + "all": "Tous les moniteurs" + }, + "status": { + "all": "Tous", + "down": "Hors ligne", + "up": "En ligne" + } + }, + "table": { + "empty": "Aucune vérification en échec sur cette période", + "headers": { + "statusCode": "Code de statut", + "location": "Emplacement" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "Ajouter un moniteur de démonstration", - "description": "Ajouter un moniteur à des fins de démonstration.", - "title": "Moniteurs de démonstration" + "common": { + "monitors": { + "actions": { + "configure": "Configurer", + "delete": "Supprimer", + "generateToken": "Générer un jeton", + "details": "Détails", + "incidents": "Incidents", + "inviteMember": "Inviter un membre", + "openSite": "Ouvrir le site", + "pause": "Pause", + "resume": "Reprendre" + }, + "statBoxes": { + "activeFor": "Actif depuis", + "certificateExpiry": "Expiration du certificat", + "lastCheck": "Dernière vérification", + "lastResponseTime": "Dernier temps de réponse" + }, + "status": { + "down": "hors ligne", + "breached": "seuil dépassé", + "initializing": "initialisation", + "maintenance": "maintenance", + "paused": "en pause", + "total": "total", + "up": "en ligne" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Jeu", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Port", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infrastructure", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "emailSettings": { - "buttonSendTestEmail": "Envoyer un mail de test", - "description": "Configurez les paramètres emails pour le système. Il s'agit du système utilisé pour les notifications & alertes.", - "descriptionTransport": "Ceci créé un transport SMTP pour NodeMailer", - "labelAddress": "Adresse email - utilisée pour la connexion", - "labelConnectionHost": "Hôte SMTP - Nom d'hôte à utiliser dans l'introduction HELO/EHLO", - "labelHost": "Hôte SMTP - Nom d'hôte ou IP sur lequel se connecter", - "labelIgnoreTLS": "Désactiver STARTTLS : ne pas utiliser TLS même si le serveur le supporte", - "labelPassword": "Mot de passe SMTP - Mot de passe requis pour l'authentification", - "labelPasswordSet": "Le mot de passe est renseigné. Réinitialisez le pour le changer.", - "labelPool": "Activer le pool de connexion SMTP afin de réutiliser les connexions existantes pour améliorer la performance", - "labelPort": "Port SMTP - Port auquel se connecter", - "labelRejectUnauthorized": "Rejeter les certificats non valides : rejeter les connexions avec des certificats auto-signés ou non fiables", - "labelRequireTLS": "Forcer STARTTLS : nécessite la mise à niveau TLS, échouer si ce n'est pas supporté", - "labelSecure": "Utiliser le SSL (recommandé) : chiffrer la connexion SSL/TLS", - "labelTLSServername": "Nom du serveur TLS - Nom d'hôte facultatif pour la validation TLS lorsque l'hôte est une adresse IP", - "labelUser": "Utilisateur SMTP - Nom d'utilisateur pour l'authentification, remplace l'adresse email si spécifié", - "linkTransport": "Voir les spécifications ici", - "placeholderUser": "Laisser vide si non requis", - "title": "Email", - "toastEmailRequiredFieldsError": "L'adresse email, l'hôte, le port et le mot de passe sont obligatoires." + "createMonitor": { + "form": { + "advanced": { + "description": "Paramètres optionnels pour les cas d'utilisation avancés", + "option": { + "advancedMatching": { + "label": "Utiliser la correspondance avancée" + }, + "expectedValue": { + "label": "Valeur attendue" + }, + "jsonPath": { + "description": "Cette expression sera évaluée sur les données JSON de la réponse et le résultat sera utilisé pour la comparaison avec la valeur attendue. Consultez jmespath.org pour la documentation du langage de requête.", + "label": "Expression JSONPath" + }, + "matchMethod": { + "label": "Méthode de correspondance", + "equal": "Égal", + "include": "Inclure", + "regex": "Regex" + } + }, + "title": "Paramètres avancés" + }, + "frequency": { + "description": "À quelle fréquence souhaitez-vous vérifier le statut de ce moniteur ?", + "option": { + "frequency": { + "label": "Fréquence de vérification", + "value": { + "fifteenMinutes": "15 minutes", + "fifteenSeconds": "15 secondes", + "fiveMinutes": "5 minutes", + "fourMinutes": "4 minutes", + "oneMinute": "1 minute", + "tenMinutes": "10 minutes", + "thirtyMinutes": "30 minutes", + "thirtySeconds": "30 secondes", + "threeMinutes": "3 minutes", + "twoMinutes": "2 minutes" + } + } + }, + "title": "Fréquence de vérification" + }, + "general": { + "option": { + "container": { + "label": "Nom/ID du conteneur", + "placeholder": "my-app ou abcd1234" + }, + "host": { + "label": "Hôte", + "placeholder": "192.168.1.100 ou example.com" + }, + "name": { + "label": "Nom d'affichage", + "placeholder": "ex. Mon site web" + }, + "secret": { + "label": "Secret d'autorisation", + "placeholder": "Saisissez votre clé secrète" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "", + "placeholder": "" + }, + "port": { + "label": "Port à surveiller", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Nom du service", + "placeholder": "ex. my.service.v1 (laisser vide pour la santé globale)" + }, + "wsUrl": { + "label": "URL WebSocket", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Paramètres généraux", + "description": { + "http": "Saisissez l'URL ou l'IP à surveiller (ex. https://example.com/ ou 192.168.1.100) et ajoutez un nom d'affichage clair visible sur le tableau de bord.", + "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.", + "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.", + "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.", + "game": "", + "pagespeed": "Suivez les performances de chargement, les Core Web Vitals et les scores d'optimisation de votre site web.", + "grpc": "Saisissez le nom d'hôte et le port du serveur gRPC, spécifiez éventuellement un nom de service pour le bilan de santé, et ajoutez un nom d'affichage.", + "websocket": "Saisissez l'URL WebSocket à surveiller (ex. wss://example.com/socket) et ajoutez un nom d'affichage.", + "hardware": "Surveillez l'utilisation du CPU, de la mémoire, du disque et la température de votre infrastructure." + } + }, + "ignoreTls": { + "description": "Configurez la validation des certificats TLS/SSL pour les connexions HTTPS.", + "option": { + "tls": { + "label": "Ignorer les erreurs TLS/SSL" + } + }, + "title": "Paramètres TLS/SSL" + }, + "incidents": { + "description": "Une fenêtre glissante est utilisée pour déterminer quand un moniteur tombe en panne. Le statut d'un moniteur ne changera que lorsque le pourcentage de vérifications dans la fenêtre glissante atteindra la valeur spécifiée.", + "option": { + "checks": { + "label": "Nombre de vérifications dans la fenêtre glissante" + }, + "percentage": { + "label": "Quel pourcentage de vérifications dans la fenêtre glissante doit échouer/réussir avant que le statut du moniteur ne change ?" + } + }, + "title": "Incidents" + }, + "notifications": { + "description": "Sélectionnez les canaux de notification que vous souhaitez utiliser", + "title": "Notifications" + }, + "type": { + "description": "Sélectionnez le type de vérification à effectuer", + "optionDockerDescription": "Utilisez Docker pour surveiller si un conteneur est en cours d'exécution.", + "optionGameDescription": "Surveillez si un serveur de jeu spécifique est en ligne.", + "optionGrpcDescription": "Surveillez les services gRPC en utilisant le protocole standard de vérification de santé.", + "optionWebSocketDescription": "Surveillez les points de terminaison WebSocket pour la santé de connexion et le temps de réponse.", + "optionHttpDescription": "Utilisez HTTP(S) pour surveiller votre site web ou point de terminaison API.", + "optionPingDescription": "Utilisez le Ping ICMP pour surveiller si un serveur est en ligne.", + "optionPortDescription": "Surveillez si un port spécifique sur un serveur est ouvert.", + "title": "Type" + }, + "thresholds": { + "title": "Seuils d'alerte", + "description": "Définissez les seuils auxquels les alertes doivent être déclenchées pour ce moniteur d'infrastructure.", + "option": { + "cpuThreshold": { + "label": "Seuil d'alerte CPU (%)" + }, + "memoryThreshold": { + "label": "Seuil d'alerte mémoire (%)" + }, + "diskThreshold": { + "label": "Seuil d'alerte disque (%)" + }, + "tempThreshold": { + "label": "Seuil d'alerte température (°C)" + } + } + }, + "geoChecks": { + "title": "Vérifications géo-distribuées", + "description": "Exécutez des vérifications depuis plusieurs emplacements géographiques pour surveiller la disponibilité et les performances globales.", + "option": { + "enabled": { + "label": "Activer les vérifications géo-distribuées" + }, + "locations": { + "label": "Emplacements", + "placeholder": "Sélectionner les emplacements", + "options": { + "EU": "Europe", + "NA": "Amérique du Nord", + "AS": "Asie", + "SA": "Amérique du Sud", + "AF": "Afrique", + "OC": "Océanie" + } + }, + "interval": { + "label": "Intervalle de vérification", + "value": { + "fiveMinutes": "5 minutes", + "tenMinutes": "10 minutes", + "fifteenMinutes": "15 minutes", + "thirtyMinutes": "30 minutes" + } + } + } + }, + "url": { + "title": "IP/URL du moniteur sur la page de statut", + "description": "Affichez l'adresse IP ou l'URL du moniteur sur la page de statut publique. Si désactivé, seul le nom du moniteur sera affiché pour protéger les informations sensibles.", + "option": { + "showURL": { + "label": "Afficher l'IP/URL sur la page de statut", + "enabled": "Activé", + "disabled": "Désactivé" + } + } + }, + "stats": { + "title": "Historique du moniteur", + "description": "Effacez tout l'historique de surveillance et les statistiques de votre équipe. Cette action est irréversible.", + "buttonText": "Effacer toutes les statistiques", + "dialog": { + "title": "Effacer tout l'historique de surveillance ?", + "description": "Ceci supprimera définitivement tout l'historique de surveillance, les statistiques et les données de vérification de votre équipe. Cette action est irréversible.", + "confirm": "Oui, tout effacer" + } + } + } }, - "pageSpeedSettings": { - "description": "Entrez votre clé API Google PageSpeed pour activer la surveillance Google PageSpeed. Cliquez sur Réinitialiser pour mettre à jour la clé.", - "labelApiKeySet": "La clé API est générée. Cliquez sur \"Réinitialiser\" pour la modifier.", - "labelApiKey": "Clé API PageSpeed", - "title": "Clé API Google PageSpeed" + "editUser": { + "form": { + "roles": { + "title": "Rôles", + "description": "Attribuez des rôles à l'utilisateur. Plusieurs rôles peuvent être sélectionnés." + } + }, + "dialog": { + "removeUser": { + "title": "Supprimer l'utilisateur", + "content": "Êtes-vous sûr de vouloir supprimer {{name}} de votre équipe ? Cette action est irréversible." + } + } }, - "saveButtonLabel": "Sauvegarder", - "statsSettings": { - "clearAllStatsButton": "Supprimer toutes les statistiques", - "clearAllStatsDescription": "Supprimer toutes les statistiques. Cette action est irréversible.", - "clearAllStatsDialogConfirm": "Oui, supprimer toutes les statistiques", - "clearAllStatsDialogDescription": "Une fois supprimés, l'historique et les statistiques du moniteur ne peuvent plus être récupérés.", - "clearAllStatsDialogTitle": "Voulez-vous supprimer toutes les statistiques ?", - "description": "Définissez la durée pendant laquelle vous souhaitez conserver les données historiques. Vous pouvez également effacer toutes les données existantes.", - "labelTTL": "Le nombre de jours dont vous souhaitez garder l'historique.", - "labelTTLOptional": "0 pour une durée infinie", - "title": "Historique du moniteur" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Résoudre l'incident", + "option": { + "comment": { + "label": "Commentaire (optionnel)", + "placeholder": "Ajoutez un commentaire sur la résolution..." + } + } + }, + "details": { + "analysis": "Analyse de l'incident", + "comment": "Commentaire :", + "detailsLabel": "Détails", + "downtime": "Temps d'arrêt :", + "message": "Message :", + "monitor": "Moniteur :", + "overview": "Aperçu", + "resolutionDetails": "Détails de la résolution", + "resolutionType": "Type :", + "resolutionTypes": { + "automatic": "Automatique", + "manual": "Manuelle" + }, + "resolve": "Résoudre l'incident", + "resolvedAt": "Résolu le :", + "resolvedBy": "Résolu par :", + "startedAt": "Démarré le :", + "status": "Statut :", + "statusCode": "Code de statut :", + "timeline": "Chronologie", + "title": "Détails de l'incident", + "url": "URL :" + } + }, + "filters": { + "allMonitors": "Tous les moniteurs", + "monitor": "Moniteur", + "resolutionType": "Type de résolution", + "resolutionTypes": { + "manual": "Manuelle", + "automatic": "Automatique", + "all": "Tous" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Incidents actifs", + "active_zero": "Aucun incident actif", + "active_one": "{{count}} incident actif", + "active_other": "{{count}} incidents actifs" + }, + "incidentStats": { + "avgResolutionTime": "Temps de résolution moyen", + "mostAffectedMonitor": "Moniteur le plus affecté", + "title": "Statistiques des incidents", + "totalIncidents": "Total des incidents" + }, + "latestIncidents": { + "title": "Derniers incidents" + } + }, + "table": { + "actions": { + "details": "Détails", + "goToMonitor": "Aller au moniteur", + "resolveManually": "Résoudre manuellement" + }, + "activeIncidents": "Incidents actifs", + "headers": { + "endTime": "Heure de fin", + "resolutionType": "Type de résolution", + "startTime": "Date de démarrage", + "statusCode": "Code de statut" + }, + "resolvedIncidents": "Incidents résolus", + "status": { + "active": "Actif", + "resolved": "Résolu" + } + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "Supprimer tous les moniteurs", - "description": "Supprimer toutes les moniteurs du système.", - "dialogConfirm": "Oui, supprimer tous les moniteurs", - "dialogDescription": "Une fois supprimés, les moniteurs ne peuvent plus être récupérés.", - "dialogTitle": "Voulez-vous supprimer tous les moniteurs ?", - "title": "Réinitialisation du système" - }, - "timezoneSettings": { - "description": "Sélectionnez le fuseau horaire utilisé pour afficher les dates et les heures dans l'application.", - "label": "Fuseau horaire d'affichage", - "title": "Fuseau horaire d'affichage" - }, - "title": "Paramètres", - "uiSettings": { - "description": "Changer entre le mode clair et sombre, ou changer le langage de l'interface utilisateur.", - "labelLanguage": "Langage", - "labelTheme": "Thème", - "title": "Apparence" - }, - "urlSettings": { - "description": "Affichez l'adresse IP ou l'URL du moniteur sur la page de statut publique. Si cette option est désactivée, seul le nom du moniteur sera affiché afin de protéger les informations sensibles.", - "label": "Afficher l'IP/URL sur la page de statut", - "selectDisabled": "Désactivé", - "selectEnabled": "Activé", - "title": "IP/URL du moniteur sur la page de statut" - }, - "globalThresholds": { - "title": "Seuils globaux", - "description": "Configurez des seuils globaux pour le CPU, la mémoire, le disque et la température. Si une valeur est fournie, elle sera automatiquement activée pour la surveillance." - } - }, - "statusPageCreate": { - "buttonSave": "Sauvegarder" - }, - "incidentsOptionsHeaderFilterResolved": "Résolu", - "settingsSave": "Sauvegarder", - "statusPageCreateAppearanceTitle": "Apparence", - "confirmPassword": "Confirmer le mot de passe", - "monitorHooks": { - "failureAddDemoMonitors": "Une erreur s'est produite lors de l'ajout des moniteurs de démonstration", - "successAddDemoMonitors": "Les moniteurs de démonstration ont bien été ajoutés" - }, - "settingsAppearance": "Apparance", - "settingsDisplayTimezone": "Fuseau horaire d'affichage", - "settingsGeneralSettings": "Paramètres généraux", - "incidentsOptionsHeaderTotalIncidents": "Nombre d'incidents", - "statusPage": { - "deleteSuccess": "La page de statut a été supprimée avec succès", - "deleteFailed": "Une erreur s'est produite lors de l'ajout de la page de statut", - "createSuccess": "La page de statut a été ajoutée avec succès", - "updateSuccess": "La page de statut a été modifiée avec succès", - "generalSettings": "Paramètres généraux", - "contents": "Contenus", - "fallback": { - "checks": [ - "Surveillez et affichez l’état de santé de vos services en temps réel", - "Suivez plusieurs services et partagez leur statut", - "Tenez les utilisateurs informés des pannes et des performances" - ], - "title": "Une page de status est utilisée pour :", - "actionButton": "Créer votre première page de status !" - } - }, - "testNotificationsDisabled": "Il n'y aucune notification paramétrée pour ce moniteur. Vous pouvez en ajouter une en cliquant sur le bouton 'Configurer'", - "incidentsTableResolvedAt": "Résolu le", - "incidentsTableActionResolve": "Résoudre", - "checkHooks": { - "failureResolveOne": "Une erreur s'est produite lors de la résolution de l'incident.", - "failureResolveAll": "Une erreur s'est produite lors de la résolution des incidents.", - "failureResolveMonitor": "" - }, - "checkFormError": "Merci de vérifier les erreurs du formulaire", - "diagnosticsPage": { - "diagnosticDescription": "Diagnostic du système", - "statsDescription": "Statistiques du système", - "gauges": { - "heapAllocationTitle": "Allocation du tas", - "heapAllocationSubtitle": "% de mémoire disponible", - "heapUsageTitle": "Utilisation du tas", - "heapUsageSubtitle": "% de mémoire disponible", - "heapUtilizationTitle": "Utilisation du tas", - "heapUtilizationSubtitle": "% d'allocation", - "instantCpuUsageTitle": "Utilisation instantanée du CPU", - "instantCpuUsageSubtitle": "% de CPU utilisé" - }, - "stats": { - "eventLoopDelayTitle": "Délai de la boucle d'évènements", - "uptimeTitle": "Temps en ligne", - "usedHeapSizeTitle": "Taille du tas utilisé", - "totalHeapSizeTitle": "Taille total du tas", - "osMemoryLimitTitle": "Limite de mémoire de l'OS" - } - }, - "pageSpeedLighthouseAPI": "Utilisez l'API Lighthouse PageSpeed pour suivre votre site web", - "time": { - "threeMinutes": "3 minutes", - "fiveMinutes": "5 minutes", - "tenMinutes": "10 minutes", - "twentyMinutes": "20 minutes", - "oneHour": "1 heure", - "oneDay": "1 jour", - "oneWeek": "1 semaine", - "fourMinutes": "4 minutes", - "oneMinute": "1 minute", - "twoMinutes": "2 minutes", - "fifteenSeconds": "15 secondes", - "thirtySeconds": "30 secondes" - }, - "general": { - "noOptionsFound": "Aucune {{unit}} trouvée" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [ - "Suivez les performances de vos serveurs", - "Identifiez les points de blocage et optimisez l’utilisation", - "Assurez la fiabilité grâce à la surveillance en temps réel" - ], - "title": "Une surveillance d'infrastructure est utilisé pour :", - "actionButton": "Créer votre premier moniteur d'infrastructure !" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [ - "Marquez vos périodes de maintenance", - "Éliminez tout malentendu", - "Arrêtez d’envoyer des alertes pendant les fenêtres de maintenance" - ], - "title": "Une fenêtre de maintenance est utilisé pour :", - "actionButton": "Créer votre première fenêtre de maintenance !" - } - }, - "pageSpeed": { - "fallback": { - "checks": [ - "Rapport sur l’expérience utilisateur d’une page", - "Aider à analyser la vitesse de la page web", - "Donner des suggestions sur la façon dont la page peut être améliorée" - ], - "title": "", - "actionButton": "" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [ - "Vérifiez si les sites web ou serveurs sont en ligne et réactifs", - "Alertez les équipes en cas de panne ou de problème de performance", - "Surveillez les points de terminaison HTTP, les pings, les conteneurs et les ports", - "Suivez l’historique de la disponibilité et les tendances de fiabilité" - ], - "title": "", - "actionButton": "" - } - }, - "editUserPage": { - "form": { - "email": "Email", - "firstName": "Prénom", - "lastName": "Nom", - "role": "Rôles", - "save": "Enregistrer" - }, - "table": { - "actionHeader": "Action", - "roleHeader": "Rôle" - }, - "title": "Modifier l'utilisateur", - "toast": { - "successUserUpdate": "L'utilisateur à bien été modifié", - "validationErrors": "" - } - }, - "incidentsPageActionResolveMonitor": "Résoudres les incidents de surveillance", - "incidentsPageActionResolveAll": "Résoudre tous les incidents", - "matchMethodOptions": { - "equal": "Égal", - "equalPlaceholder": "", - "include": "Inclure", - "includePlaceholder": "ok", - "regex": "Regex", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "ID du conteneur", - "namePlaceholder": "Mon conteneur", - "placeholder": "abcd1234" - }, - "http": { - "label": "URL à surveiller", - "namePlaceholder": "Google", - "placeholder": "google.com" - }, - "ping": { - "label": "Adresse IP à surveiller", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" - }, - "port": { - "label": "URL à surveiller", - "namePlaceholder": "", - "placeholder": "localhost" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "Chemin JSON" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "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": "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." + "charts": { + "labels": { + "cpu": "Utilisation CPU", + "disk": "Utilisation disque", + "memory": "Utilisation mémoire", + "netBytesRecv": "{{name}} - Octets reçus", + "netBytesSent": "{{name}} - Octets envoyés", + "temp": "Temp." + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Fréquence max.", + "title": "Utilisation CPU", + "upperLabel": "Fréquence actuelle" + }, + "disk": { + "lowerLabel": "Libre", + "title": "Utilisation disque {{idx}}", + "upperLabel": "Utilisé" + }, + "memory": { + "lowerLabel": "Libre", + "title": "Utilisation mémoire", + "upperLabel": "Utilisé" + } + }, + "statBoxes": { + "avgCpuTemperature": "Température moyenne du CPU", + "cpuFrequency": "Fréquence CPU", + "cpuLogical": "CPU (Logique)", + "cpuPhysical": "CPU (Physique)", + "disk": "Disque", + "memory": "Mémoire", + "os": "OS" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Disque", + "memory": "Mémoire" + } + }, + "tabs": { + "labels": { + "network": "", + "overview": "Aperçu" + } + }, + "fallback": { + "actionButton": "Créer un moniteur !", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Un moniteur d'infrastructure sert à :" + } + }, + "logs": { + "tabs": { + "diagnostics": "Diagnostic", + "logs": "Journaux des serveurs", + "queue": "File d'attente des tâches" + }, + "logLevelSelect": { + "label": "Niveau de log" + }, + "jobQueue": "File d'attente des tâches", + "failedJobs": "Tâches échouées", + "noLogs": "Aucun log trouvé", + "metrics": { + "jobs": "Tâches", + "activeJobs": "Tâches actives", + "failingJobs": "Tâches en échec", + "totalRuns": "Exécutions totales", + "totalFailures": "Échecs totaux" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Délai de boucle d'événements", + "uptime": "Temps de fonctionnement", + "usedHeapSize": "Taille du tas utilisée", + "totalHeapSize": "Taille totale du tas", + "osMemoryLimit": "Limite mémoire de l'OS" + }, + "gauges": { + "heapAllocation": "Allocation du tas", + "heapUsage": "Utilisation du tas", + "heapUtilization": "Utilisation du tas", + "instantCpuUsage": "Utilisation CPU instantanée", + "availableMemoryPercentage": "% de mémoire disponible", + "allocatedPercentage": "% alloué", + "usedSPercentage": "% de 1s utilisé par le CPU", + "total": "Total", + "used": "Utilisé" + } + }, + "table": { + "headers": { + "timestamp": "Horodatage", + "level": "Niveau", + "service": "Service", + "method": "Méthode", + "monitorId": "ID du moniteur", + "runCount": "Nombre d'exécutions", + "failCount": "Nombre d'échecs", + "lastRunAt": "Dernière exécution", + "lockedAt": "Verrouillé le", + "lastFinishedAt": "Dernière fin", + "lastRunTook": "Durée dernière exécution", + "lastFailedAt": "Dernier échec", + "failReason": "Raison de l'échec" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Créer une fenêtre de maintenance !", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Une fenêtre de maintenance sert à :" + }, + "table": { + "headers": { + "nextWindow": "Prochaine fenêtre de maintenance", + "repeat": "Répéter" + } + }, + "form": { + "general": { + "title": "Paramètres généraux", + "description": "Définissez un nom et une option de répétition pour votre fenêtre de maintenance.", + "option": { + "name": { + "label": "Nom", + "placeholder": "ex. Maintenance hebdomadaire" + }, + "repeat": { + "label": "Répéter" + } + } + }, + "startDate": { + "title": "Date de début", + "description": "Sélectionnez la date de début de votre fenêtre de maintenance.", + "option": { + "startDate": { + "label": "Date de début" + } + } + }, + "startTime": { + "title": "Heure de début", + "description": "Définissez l'heure de début et la durée de votre fenêtre de maintenance. Toutes les valeurs sont en UTC", + "option": { + "duration": { + "label": "Durée" + }, + "startTime": { + "label": "Heure de début" + } + }, + "monitors": { + "title": "Moniteurs", + "description": "Moniteurs auxquels la fenêtre de maintenance doit s'appliquer", + "option": { + "addMonitors": { + "label": "Ajouter des moniteurs" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "Créer un canal", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Les canaux de notification servent à :" + }, + "form": { + "accessToken": { + "optionAccessToken": "Jeton d'accès", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "L'adresse où les notifications seront envoyées.", + "optionAddress": "Adresse", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Adresse" + }, + "homeServer": { + "optionHomeServer": "Serveur d'accueil", + "placeholder": "example.com" + }, + "matrix": { + "description": "Configurez la connexion de votre serveur Matrix pour les notifications.", + "title": "Configuration Matrix" + }, + "notificationName": { + "description": "Un nom descriptif pour le canal de notification", + "optionName": "Nom du canal", + "placeholder": "ex. Alertes de production", + "title": "Nom du canal" + }, + "pagerDuty": { + "description": "Votre clé d'intégration PagerDuty pour recevoir les alertes.", + "optionIntegrationKey": "Clé d'intégration", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Clé d'intégration" + }, + "roomId": { + "optionRoomId": "ID de la salle", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Sélectionnez le type de canal de notification à créer.", + "optionType": "Type", + "title": "Type de canal" + }, + "telegram": { + "title": "Configuration Telegram", + "description": "Pour activer les notifications Telegram, créez un bot Telegram en utilisant BotFather, le robot officiel pour créer et gérer ses propres robots. Indiquez ensuite votre clé API et l'ID du chat ici.", + "optionBotToken": "Jeton du bot", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "ID du chat", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Destination" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "Décalage cumulatif de mise en page (CLS)", + "fcp": "Premier affichage de contenu (FCP)", + "lcp": "Plus grand affichage de contenu (LCP)", + "si": "Indice de vitesse (SI)", + "tbt": "Temps de blocage total (TBT)" + }, + "legend": { + "title": "Rapport PageSpeed", + "weight": "Poids" + }, + "pie": { + "title": "Rapport de performance" + } + }, + "table": { + "headers": { + "pageSpeedScore": "Score PageSpeed" + } + }, + "fallback": { + "actionButton": "Créer un moniteur !", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "Un moniteur PageSpeed sert à :" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Fuseau horaire d'affichage", + "description": "Sélectionnez le fuseau horaire utilisé pour afficher les dates et heures dans l'application.", + "option": { + "timezone": { + "label": "Fuseau horaire d'affichage" + } + } + }, + "ui": { + "title": "Apparence", + "description": "Basculez entre le mode clair et sombre, changez la langue ou personnalisez le type de graphique.", + "option": { + "theme": { + "label": "Mode du thème", + "light": "Clair", + "dark": "Sombre" + }, + "language": { + "label": "Langue" + }, + "chartType": { + "label": "Type de graphique", + "histogram": "Histogramme", + "heatmap": "Carte thermique" + } + } + }, + "pagespeed": { + "title": "Clé API Google PageSpeed", + "description": "Saisissez votre clé API Google PageSpeed pour activer la surveillance PageSpeed. Cliquez sur Réinitialiser pour mettre à jour la clé.", + "option": { + "apiKey": { + "label": "Clé API PageSpeed", + "labelSet": "La clé API est définie. Cliquez sur Réinitialiser pour la modifier.", + "placeholder": "Saisissez votre clé API Google PageSpeed" + } + } + }, + "url": { + "title": "IP/URL du moniteur sur la page de statut", + "description": "Affichez l'adresse IP ou l'URL du moniteur sur la page de statut publique. Si désactivé, seul le nom du moniteur sera affiché pour protéger les informations sensibles.", + "option": { + "showURL": { + "label": "Afficher l'IP/URL sur la page de statut", + "enabled": "Activé", + "disabled": "Désactivé" + } + } + }, + "stats": { + "title": "Historique du moniteur", + "description": "Effacez tout l'historique de surveillance et les statistiques de votre équipe. Cette action est irréversible.", + "option": { + "clear": { + "label": "Effacer toutes les statistiques. Cette action est irréversible." + } + }, + "dialog": { + "title": "Effacer tout l'historique de surveillance ?", + "description": "Cette action est irréversible" + } + }, + "retention": { + "title": "Rétention des vérifications", + "description": "Définissez la durée de conservation des données de vérification avant leur nettoyage automatique.", + "option": { + "days": { + "label": "Période de rétention (jours)", + "unlimited": "Illimité" + } + } + }, + "thresholds": { + "title": "Seuils globaux", + "description": "Définissez les seuils globaux de CPU, mémoire, disque et température pour la surveillance d'infrastructure. Ils s'appliquent à tous les moniteurs d'infrastructure sauf remplacement.", + "option": { + "cpu": { + "label": "Seuil CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Seuil mémoire (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Seuil disque (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Seuil température (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Paramètres email", + "description": "Configurez les paramètres email de votre système. Ils sont utilisés pour envoyer des notifications et des alertes.", + "descriptionTransport": "Ceci créé un transport SMTP pour NodeMailer", + "descriptionTransportLink": "Voir les spécifications ici", + "option": { + "host": { + "label": "Hôte email - Nom d'hôte ou adresse IP de connexion", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Port email - Port de connexion", + "placeholder": "587" + }, + "address": { + "label": "Adresse email - Utilisée pour l'authentification", + "placeholder": "you@example.com" + }, + "user": { + "label": "Utilisateur email - Nom d'utilisateur pour l'authentification, remplace l'adresse email si spécifié", + "placeholder": "Laisser vide si non requis" + }, + "password": { + "label": "Mot de passe email - Mot de passe pour l'authentification", + "labelSet": "Le mot de passe est défini. Cliquez sur Réinitialiser pour le modifier.", + "placeholder": "Saisissez votre mot de passe" + }, + "tlsServername": { + "label": "Nom de serveur TLS - Nom d'hôte optionnel pour la validation TLS quand l'hôte est une IP", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "Hôte de connexion email - Nom d'hôte à utiliser dans le message HELO/EHLO", + "placeholder": "localhost" + }, + "secure": { + "label": "Utiliser SSL (recommandé) : chiffrer la connexion avec SSL/TLS" + }, + "pool": { + "label": "Activer le pool de connexions : réutiliser les connexions existantes pour améliorer les performances" + }, + "ignoreTLS": { + "label": "Désactiver STARTTLS : ne pas utiliser TLS même si le serveur le supporte" + }, + "requireTLS": { + "label": "Forcer STARTTLS : exiger la mise à niveau TLS, échouer si non supporté" + }, + "rejectUnauthorized": { + "label": "Rejeter les certificats invalides : refuser les connexions avec des certificats auto-signés ou non fiables" + } + } + }, + "demoMonitors": { + "title": "Moniteurs de démonstration", + "description": "Ajoutez des moniteurs d'exemple à des fins de démonstration." + }, + "removeMonitors": { + "title": "Réinitialisation du système", + "description": "Supprimez tous les moniteurs de votre système.", + "dialog": { + "title": "Supprimer tous les moniteurs ?", + "description": "Cette action est irréversible." + } + }, + "exportMonitors": { + "title": "Exporter les moniteurs" + }, + "importExportMonitors": { + "title": "Importer / Exporter les moniteurs", + "description": "Importez ou exportez vos données de moniteurs sous forme de fichier JSON pour sauvegarde ou transfert." + }, + "about": { + "title": "À propos", + "developedBy": "Développé par Bluewave Labs" + }, + "validation": { + "errorMessage": "Veuillez corriger les erreurs de validation suivantes :" + } + } + }, + "statusPages": { + "deleteSuccess": "Page de statut supprimée avec succès", + "fallback": { + "title": "Une page de statut sert à :", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "Créer une page de statut !" + }, + "monitorsList": { + "chartTypeHeatmap": "Carte thermique", + "chartTypeHistogram": "Histogramme", + "noData": "Aucune donnée disponible", + "infrastructure": { + "title": "Infrastructure", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Mémoire", + "disk": "Disque", + "usage": "Utilisation", + "used": "Utilisé", + "total": "Total" + }, + "uptime": { + "title": "Disponibilité", + "responseTime": "Temps de réponse" + } + }, + "statusBar": { + "allDown": "Tous les systèmes sont hors service", + "allUp": "Tous les systèmes sont opérationnels", + "degraded": "Certains systèmes rencontrent des problèmes", + "unknown": "Impossible de déterminer le statut du système" + }, + "table": { + "headers": { + "name": "Nom de la page de statut", + "url": "URL publique" + }, + "published": "Publiée", + "unpublished": "Non publiée" + }, + "title": "Pages de statut", + "details": { + "empty": { + "title": "Il n'y a rien ici pour le moment", + "addMonitor": "Ajoutez un moniteur pour commencer" + } + }, + "form": { + "access": { + "title": "Accès", + "description": "Si votre page de statut est prête, vous pouvez la marquer comme publiée.", + "option": { + "published": { + "name": "Publiée et visible par le public" + } + } + }, + "basicInfo": { + "title": "Informations de base", + "description": "Définissez le nom de l'entreprise et le sous-domaine vers lequel pointe votre page de statut.", + "option": { + "name": { + "label": "Nom de l'entreprise", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Adresse de votre page de statut", + "placeholder": "ma-page-de-statut" + } + } + }, + "monitors": { + "title": "Moniteurs", + "description": "Sélectionnez les moniteurs à afficher sur votre page de statut.", + "noMonitors": "Aucun moniteur sélectionné", + "option": { + "monitors": { + "label": "Sélectionner les moniteurs", + "placeholder": "Rechercher et sélectionner des moniteurs..." + } + } + }, + "timezone": { + "title": "Fuseau horaire", + "description": "Sélectionnez le fuseau horaire d'affichage de votre page de statut.", + "option": { + "timezone": { + "label": "Fuseau horaire", + "placeholder": "Sélectionner un fuseau horaire..." + } + } + }, + "appearance": { + "title": "Apparence", + "description": "Définissez l'apparence par défaut de votre page de statut publique.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Couleur de marque" + } + } + }, + "features": { + "title": "Fonctionnalités", + "description": "Configurez les informations affichées sur votre page de statut.", + "option": { + "showCharts": { + "label": "Afficher les graphiques de temps de réponse" + }, + "showUptimePercentage": { + "label": "Afficher le pourcentage de disponibilité" + }, + "showAdminLoginLink": { + "label": "Afficher le lien de connexion administrateur" + }, + "showInfrastructure": { + "label": "Afficher les métriques d'infrastructure" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Rechercher des moniteurs..." + } + }, + "table": { + "headers": { + "responseTime": "Temps de réponse" + } + }, + "fallback": { + "actionButton": "Créer un moniteur !", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Un moniteur de disponibilité sert à :" + } } } } diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json index 68d94139f..7dae6c9e0 100644 --- a/client/src/locales/ja.json +++ b/client/src/locales/ja.json @@ -1,1119 +1,1305 @@ { - "submit": "送信", - "title": "タイトル", - "distributedStatusHeaderText": "リアルタイム、実デバイス対応", - "distributedStatusSubHeaderText": "世界中の数百万台のデバイスを活用し、グローバル地域、国、都市別にシステムパフォーマンスを表示", - "settingsDisabled": "無効", - "settingsSuccessSaved": "設定が正常に保存されました", - "settingsFailedToSave": "設定の保存に失敗しました", - "settingsStatsCleared": "統計が正常にクリアされました", - "settingsFailedToClearStats": "統計のクリアに失敗しました", - "settingsMonitorsDeleted": "すべてのモニターが正常に削除されました", - "settingsFailedToDeleteMonitors": "すべてのモニターの削除に失敗しました", - "starPromptTitle": "Checkmateにスター", - "starPromptDescription": "GitHubで最新リリースを確認し、コミュニティの成長を支援", - "https": "HTTPS", - "http": "HTTP", - "monitor": "モニター", - "aboutus": "私たちについて", - "now": "今", - "delete": "削除", - "configure": "設定", - "responseTime": "レスポンス時間", - "ms": "ミリ秒", - "bar": "バー", - "area": "エリア", - "country": "国", - "city": "都市", - "response": "レスポンス", - "monitorStatusUp": "モニター {name} ({url}) がアップして応答中です", - "monitorStatusDown": "モニター {name} ({url}) がダウンして応答していません", - "webhookSendSuccess": "Webhook通知が正常に送信されました", - "webhookSendError": "{platform}へのWebhook通知送信エラー", - "webhookUnsupportedPlatform": "サポートされていないプラットフォーム: {platform}", - "distributedRightCategoryTitle": "モニター", - "distributedStatusServerMonitors": "サーバーモニター", - "distributedStatusServerMonitorsDescription": "関連サーバーのステータスを監視", - "distributedUptimeCreateSelectURL": "ここでホストのURL、モニタータイプを選択できます。", - "distributedUptimeCreateChecks": "実行するチェック", - "distributedUptimeCreateChecksDescription": "サイト追加後はいつでもチェックを追加・削除できます。", - "distributedUptimeCreateIncidentNotification": "インシデント通知", - "distributedUptimeCreateIncidentDescription": "インシデント発生時、ユーザーに通知。", - "distributedUptimeCreateAdvancedSettings": "詳細設定", - "distributedUptimeDetailsNoMonitorHistory": "このモニターのチェック履歴はまだありません。", - "distributedUptimeDetailsStatusHeaderUptime": "稼働時間:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "最終更新", - "notifications": { - "enableNotifications": "{{platform}}通知を有効化", - "testNotification": "テスト通知", - "addOrEditNotifications": "通知を追加または編集", - "slack": { - "label": "Slack", - "description": "Slack通知を有効にするには、Slackアプリを作成し、incoming webhookを有効にします。その後、ここでWebhook URLを提供するだけです。", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "Slack Webhook URLが必要です" - }, - "discord": { - "label": "Discord", - "description": "Webhookを使用してDiscord通知経由でCheckmateからDiscordチャンネルにデータを送信するには、DiscordのIncoming Webhook機能を使用できます。", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "Discord Webhook URLが必要です" - }, - "telegram": { - "label": "Telegram", - "description": "Telegram通知を有効にするには、Telegramボットを作成・管理するための公式ボットであるBotFatherを使用してTelegramボットを作成します。次に、APIトークンとチャットIDを取得してここに記入します。", - "tokenLabel": "ボットトークン", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "チャットID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "TelegramトークンとチャットIDが必要です" - }, - "webhook": { - "label": "Webhook", - "description": "インシデント発生時に通知を受信するためのカスタムWebhookを設定できます。", - "urlLabel": "Webhook URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "Webhook URLが必要です" - }, - "testNotificationDevelop": "テスト通知2", - "integrationButton": "通知統合", - "testSuccess": "テスト通知が正常に送信されました!", - "testFailed": "テスト通知の送信に失敗しました", - "unsupportedType": "サポートされていない通知タイプ", - "networkError": "ネットワークエラーが発生しました", - "fallback": { - "title": "通知チャンネル", - "checks": [ - "ダウンタイムやパフォーマンス問題についてチームにアラート", - "インシデント発生時にエンジニアに通知", - "システム変更について管理者に情報提供" - ], - "actionButton": "" - }, - "createButton": "通知チャンネルを作成", - "createTitle": "通知チャンネル", - "create": { - "success": "通知が正常に作成されました", - "failed": "通知の作成に失敗しました" - }, - "fetch": { - "success": "通知が正常に取得されました", - "failed": "通知の取得に失敗しました" - }, - "delete": { - "success": "通知が正常に削除されました", - "failed": "通知の削除に失敗しました" - }, - "edit": { - "success": "通知が正常に更新されました", - "failed": "通知の更新に失敗しました" - }, - "test": { - "success": "テスト通知が正常に送信されました", - "failed": "テスト通知の送信に失敗しました" - } - }, - "testLocale": "テストロケール", - "add": "追加", - "monitors": "モニター", - "distributedUptimeStatusCreateStatusPage": "ステータスページ", - "distributedUptimeStatusCreateStatusPageAccess": "アクセス", - "distributedUptimeStatusCreateStatusPageReady": "ステータスページの準備ができたら、公開済みとしてマークできます。", - "distributedUptimeStatusBasicInfoHeader": "基本情報", - "distributedUptimeStatusBasicInfoDescription": "会社名とステータスページが指すサブドメインを定義。", - "distributedUptimeStatusLogoHeader": "ロゴ", - "distributedUptimeStatusLogoDescription": "ステータスページ用のロゴをアップロード", - "distributedUptimeStatusLogoUploadButton": "ロゴをアップロード", - "distributedUptimeStatusStandardMonitorsHeader": "標準モニター", - "distributedUptimeStatusStandardMonitorsDescription": "標準モニターをステータスページに添付。", - "distributedUptimeStatusCreateYour": "作成する", - "distributedUptimeStatusEditYour": "編集する", - "distributedUptimeStatusPublishedLabel": "公開済みで一般に表示", - "distributedUptimeStatusCompanyNameLabel": "会社名", - "distributedUptimeStatusPageAddressLabel": "ステータスページアドレス", - "distributedUptimeStatus30Days": "30日", - "distributedUptimeStatus60Days": "60日", - "distributedUptimeStatus90Days": "90日", - "distributedUptimeStatusPageNotSetUp": "ステータスページが設定されていません。", - "distributedUptimeStatusContactAdmin": "管理者にお問い合わせください", - "distributedUptimeStatusPageNotPublic": "このステータスページは非公開です。", - "distributedUptimeStatusPageDeleteDialog": "このステータスページを削除しますか?", - "distributedUptimeStatusPageDeleteConfirm": "はい、ステータスページを削除", - "distributedUptimeStatusPageDeleteDescription": "削除されると、ステータスページは復元できません。", - "distributedUptimeStatusDevices": "デバイス", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "UPT消費", - "distributedUptimeStatusUptLogo": "Uptロゴ", - "incidentsTableNoIncidents": "記録されたインシデントはありません", - "incidentsTablePaginationLabel": "インシデント", - "incidentsTableMonitorName": "モニター名", - "incidentsTableStatus": "ステータス", - "incidentsTableDateTime": "日付と時刻", - "incidentsTableStatusCode": "ステータスコード", - "incidentsTableMessage": "メッセージ", - "incidentsOptionsHeader": "インシデント対象:", - "incidentsOptionsHeaderFilterBy": "フィルター:", - "incidentsOptionsHeaderFilterAll": "すべて", - "incidentsOptionsHeaderFilterDown": "ダウン", - "incidentsOptionsHeaderFilterCannotResolve": "解決不可", - "incidentsOptionsHeaderShow": "表示:", - "incidentsOptionsHeaderLastHour": "最終時間", - "incidentsOptionsHeaderLastDay": "最終日", - "incidentsOptionsHeaderLastWeek": "最終週", - "incidentsOptionsPlaceholderAllServers": "すべてのサーバー", - "infrastructureCreateYour": "作成する", - "infrastructureCreateGeneralSettingsDescription": "ここでホストのURL、フレンドリーネーム、サーバーエージェントに接続するための認証シークレットを選択できます。", - "infrastructureServerRequirement": "監視中のサーバーで実行されている必要があります", - "infrastructureCustomizeAlerts": "アラートをカスタマイズ", - "infrastructureAlertNotificationDescription": "しきい値が指定された割合を超えた場合にユーザーに通知を送信。", - "infrastructureCreateMonitor": "インフラストラクチャモニターを作成", - "infrastructureProtocol": "プロトコル", - "infrastructureServerUrlLabel": "サーバーURL", - "infrastructureDisplayNameLabel": "表示名", - "infrastructureAuthorizationSecretLabel": "認証シークレット", - "gb": "GB", - "mb": "MB", - "mem": "メモリ", - "memoryUsage": "メモリ使用率", - "cpu": "CPU", - "cpuUsage": "CPU使用率", - "cpuTemperature": "CPU温度", - "diskUsage": "ディスク使用率", - "used": "使用済み", - "total": "合計", - "cores": "コア", - "frequency": "頻度", - "status": "ステータス", - "cpuPhysical": "CPU(物理)", - "cpuLogical": "CPU(論理)", - "cpuFrequency": "CPU周波数", - "avgCpuTemperature": "平均CPU温度", - "memory": "メモリ", - "disk": "ディスク", - "uptime": "稼働時間", - "os": "OS", - "host": "ホスト", - "actions": "アクション", - "integrations": "統合", - "integrationsPrism": "Prismをお気に入りのサービスに接続。", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Slackと接続してチャンネルでインシデントを表示", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Discordと接続してチャンネルでインシデントを直接表示", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "すべてのインシデントをZapierに送信し、どこでも表示", - "commonSave": "保存", - "createYour": "作成する", - "createMonitor": "モニターを作成", - "pause": "一時停止", - "resume": "再開", - "editing": "編集中...", - "url": "URL", - "access": "アクセス", - "timezone": "タイムゾーン", - "features": "機能", - "administrator": "管理者?", - "loginHere": "こちらでログイン", - "displayName": "表示名", - "urlMonitor": "監視するURL", - "portToMonitor": "監視するポート", - "websiteMonitoring": "ウェブサイト監視", - "websiteMonitoringDescription": "HTTP(s)を使用してウェブサイトやAPIエンドポイントを監視。", - "pingMonitoring": "Ping監視", - "pingMonitoringDescription": "サーバーが利用可能かどうかをチェック。", - "dockerContainerMonitoring": "Dockerコンテナ監視", - "dockerContainerMonitoringDescription": "Dockerコンテナが実行中かどうかをチェック。", - "portMonitoring": "ポート監視", - "portMonitoringDescription": "ポートが開いているかどうかをチェック。", - "createMaintenanceWindow": "メンテナンスウィンドウを作成", - "createMaintenance": "メンテナンスを作成", - "editMaintenance": "メンテナンスを編集", - "maintenanceWindowName": "メンテナンスウィンドウ名", - "friendlyNameInput": "フレンドリーネーム", - "friendlyNamePlaceholder": "__:__で___分間のメンテナンス", - "maintenanceRepeat": "メンテナンス繰り返し", - "maintenance": "メンテナンス", - "duration": "期間", - "addMonitors": "モニターを追加", - "window": "ウィンドウ", - "cancel": "キャンセル", - "message": "メッセージ", - "low": "低", - "high": "高", - "statusCode": "ステータスコード", - "date&Time": "日付と時刻", - "type": "タイプ", - "statusPageName": "ステータスページ名", - "publicURL": "公開URL", - "repeat": "繰り返し", - "edit": "編集", - "createA": "作成する", - "remove": "削除", - "maintenanceWindowDescription": "この時間帯はpingが送信されません", - "startTime": "開始時刻", - "timeZoneInfo": "すべての日付と時刻はGMT+0タイムゾーンです。", - "monitorsToApply": "メンテナンスウィンドウを適用するモニター", - "nextWindow": "次のウィンドウ", - "notFoundButton": "メインダッシュボードに移動", - "pageSpeedConfigureSettingsDescription": "ここでホストのURL、モニタータイプを選択できます。", - "monitorDisplayName": "モニター表示名", - "whenNewIncident": "新しいインシデントが発生した場合、", - "notifySMS": "SMS経由で通知(近日公開)", - "notifyEmails": "複数のアドレスにメールでも通知(近日公開)", - "seperateEmails": "複数のメールはカンマで区切ることができます", - "checkFrequency": "チェック頻度", - "matchMethod": "マッチ方法", - "expectedValue": "期待値", - "deleteDialogTitle": "本当にこのモニターを削除しますか?", - "deleteDialogDescription": "削除されると、このモニターは復元できません。", - "pageSpeedMonitor": "PageSpeedモニター", - "shown": "表示", - "ago": "前", - "companyName": "会社名", - "pageSpeedDetailsPerformanceReport": "値は推定値で変動する可能性があります。", - "pageSpeedDetailsPerformanceReportCalculator": "計算機を見る", - "checkingEvery": "チェック間隔", - "statusPageCreateSettings": "ステータスページの準備ができたら、公開済みとしてマークできます。", - "basicInformation": "基本情報", - "statusPageCreateBasicInfoDescription": "会社名とステータスページが指すサブドメインを定義。", - "statusPageCreateSelectTimeZoneDescription": "ステータスページが表示されるタイムゾーンを選択。", - "statusPageCreateAppearanceDescription": "公開ステータスページのデフォルトの外観を定義。", - "statusPageCreateSettingsCheckboxLabel": "公開済みで一般に表示", - "statusPageCreateBasicInfoStatusPageAddress": "ステータスページアドレス", - "statusPageCreateTabsContent": "ステータスページサーバー", - "statusPageCreateTabsContentDescription": "監視している任意の数のサーバーをステータスページに追加できます。最適な表示体験のために並び替えることもできます。", - "statusPageCreateTabsContentFeaturesDescription": "ステータスページでより多くの詳細を表示", - "showCharts": "チャートを表示", - "showUptimePercentage": "稼働率を表示", - "removeLogo": "ロゴを削除", - "statusPageStatus": "公開ステータスページが設定されていません。", - "statusPageStatusContactAdmin": "管理者にお問い合わせください", - "statusPageStatusNotPublic": "このステータスページは非公開です。", - "statusPageStatusNoPage": "ここにはステータスページがありません。", - "statusPageStatusServiceStatus": "サービスステータス", - "deleteStatusPage": "このステータスページを削除しますか?", - "deleteStatusPageConfirm": "はい、ステータスページを削除", - "deleteStatusPageDescription": "削除されると、ステータスページは復元できません。", - "uptimeCreate": "期待値はレスポンス結果と照合するために使用され、マッチがステータスを決定します。", - "uptimeCreateJsonPath": "この式はレスポンスJSONデータに対して評価され、結果は期待値との照合に使用されます。参照", - "uptimeCreateJsonPathQuery": "クエリ言語ドキュメント。", - "maintenanceTableActionMenuDialogTitle": "本当にこのメンテナンスウィンドウを削除しますか?", - "infrastructureEditYour": "編集する", - "infrastructureEditMonitor": "インフラストラクチャモニターを保存", - "infrastructureMonitorCreated": "インフラストラクチャモニターが正常に作成されました!", - "infrastructureMonitorUpdated": "インフラストラクチャモニターが正常に更新されました!", - "errorInvalidTypeId": "無効な通知タイプが提供されました", - "errorInvalidFieldId": "無効なフィールドIDが提供されました", - "inviteNoTokenFound": "招待トークンが見つかりません", - "pageSpeedWarning": "警告: Google PageSpeed APIキーがまだ追加されていません。これがないとPageSpeedモニターは機能しません。", - "pageSpeedLearnMoreLink": "こちらをクリック", - "pageSpeedAddApiKey": "APIキーを追加する。", - "update": "更新", - "invalidFileFormat": "サポートされていないファイル形式!", - "invalidFileSize": "ファイルサイズが大きすぎます!", - "ClickUpload": "クリックしてアップロード", - "DragandDrop": "ドラッグアンドドロップ", - "MaxSize": "最大サイズ", - "SupportedFormats": "対応フォーマット", - "FirstName": "名", - "LastName": "姓", - "EmailDescriptionText": "こちらは現在ご使用のメールアドレスです — 変更することはできません。", - "YourPhoto": "プロフィール写真", - "PhotoDescriptionText": "この写真はプロフィールページに表示されます。", - "save": "保存", - "DeleteDescriptionText": "これにより、アカウントと関連するすべてのデータがサーバーから削除されます。この操作は元に戻せません。", - "DeleteAccountWarning": "アカウントを削除すると、再度サインインできなくなり、すべてのデータが削除されます。この操作は元に戻せません。", - "DeleteWarningTitle": "本当にこのアカウントを削除しますか?", - "bulkImport": { - "title": "一括インポート", - "selectFileTips": "アップロードするCSVファイルを選択", - "selectFileDescription": "またはサンプルをダウンロードできます", - "selectFile": "ファイルを選択", - "parsingFailed": "解析に失敗しました", - "uploadSuccess": "モニターが正常に作成されました!", - "validationFailed": "検証に失敗しました", - "noFileSelected": "ファイルが選択されていません", - "fallbackPage": "サーバーのリストを一括アップロードするためのファイルをインポート", - "invalidFileType": "無効なファイルタイプ", - "uploadFailed": "アップロードに失敗しました" - }, - "DeleteAccountTitle": "アカウントを削除", - "DeleteAccountButton": "アカウントを削除", - "publicLink": "公開リンク", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "リセット", - "ignoreTLSError": "TLS/SSLエラーを無視", - "tlsErrorIgnored": "TLS/SSLエラーが無視されました", - "ignoreTLSErrorDescription": "TLS/SSLエラーを無視してウェブサイトの可用性をチェック続行", - "createNew": "新規作成", - "greeting": { - "prepend": "こんにちは", - "append": "午後はあなたの遊び場です—壮大なものにしましょう!", - "overview": "{{type}}モニターの概要です。" - }, - "roles": { - "superAdmin": "スーパー管理者", - "admin": "管理者", - "teamMember": "チームメンバー", - "demoUser": "デモユーザー" - }, - "teamPanel": { - "teamMembers": "チームメンバー", - "filter": { - "all": "すべて", - "member": "メンバー" - }, - "inviteTeamMember": "チームメンバーを招待", - "inviteNewTeamMember": "新しいチームメンバーを招待", - "inviteDescription": "新しいチームメンバーを追加すると、すべてのモニターへのアクセス権が付与されます。", - "email": "メール", - "selectRole": "役割を選択", - "inviteLink": "招待リンク", - "cancel": "キャンセル", - "noMembers": "この役割のチームメンバーはいません", - "getToken": "トークンを取得", - "emailToken": "メールトークン", - "table": { - "name": "名前", - "email": "メール", - "role": "役割", - "created": "作成日" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "一時停止", - "resumed": "再開", - "active": "アクティブ" - }, - "menu": { - "uptime": "稼働時間", - "pagespeed": "ページ速度", - "infrastructure": "インフラストラクチャ", - "incidents": "インシデント", - "statusPages": "ステータスページ", - "maintenance": "メンテナンス", - "integrations": "統合", - "settings": "設定", - "support": "サポート", - "discussions": "ディスカッション", - "docs": "ドキュメント", - "changelog": "変更履歴", - "profile": "プロフィール", - "password": "パスワード", - "team": "チーム", - "logOut": "ログアウト", - "notifications": "通知", - "logs": "ログ" - }, - "settingsEmailUser": "メールユーザー - 認証用ユーザー名、指定されている場合はメールアドレスを上書き", - "state": "状態", - "statusBreadCrumbsStatusPages": "ステータスページ", - "statusBreadCrumbsDetails": "詳細", - "commonSaving": "保存中...", - "navControls": "コントロール", - "incidentsPageTitle": "インシデント", - "passwordPanel": { - "passwordChangedSuccess": "パスワードが正常に変更されました。", - "passwordInputIncorrect": "パスワード入力が間違っています。", - "currentPassword": "現在のパスワード", - "enterCurrentPassword": "現在のパスワードを入力", - "newPassword": "新しいパスワード", - "enterNewPassword": "新しいパスワードを入力", - "confirmNewPassword": "新しいパスワードの確認", - "passwordRequirements": "新しいパスワードは少なくとも8文字で、少なくとも1つの大文字、1つの小文字、1つの数字、1つの特殊文字を含む必要があります。", - "saving": "保存中..." - }, - "emailSent": "メールが正常に送信されました", - "failedToSendEmail": "メールの送信に失敗しました", - "settingsTestEmailSuccess": "テストメールが正常に送信されました", - "settingsTestEmailFailed": "テストメールの送信に失敗しました", - "settingsTestEmailFailedWithReason": "テストメールの送信に失敗しました: {{reason}}", - "settingsTestEmailUnknownError": "不明なエラー", - "statusMsg": { - "paused": "監視が一時停止されています。", - "up": "サイトが稼働中です。", - "down": "サイトがダウンしています。", - "pending": "保留中..." - }, - "uptimeGeneralInstructions": { - "http": "監視するURLまたはIPを入力(例: https://example.com/ または 192.168.1.100)し、ダッシュボードに表示される明確な表示名を追加。", - "ping": "pingするIPアドレスまたはホスト名を入力(例: 192.168.1.100 または example.com)し、ダッシュボードに表示される明確な表示名を追加。", - "docker": "コンテナのDocker IDを入力。Docker IDは64文字のフルDocker IDである必要があります。docker inspect を実行してフルコンテナIDを取得できます。", - "port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。", - "game": "", - "https": "" - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "ライト&ダークを切り替え" + "auth": { + "roles": { + "admin": "管理者", + "demo": "デモ", + "superadmin": "スーパー管理者", + "user": "ユーザー" + } }, - "toasts": { - "networkError": "ネットワークエラー", - "checkConnection": "接続を確認してください", - "unknownError": "不明なエラー" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "続行", - "back": "戻る" + "alerts": { + "pageSpeedApiKey": { + "content": "警告: Google PageSpeed APIキーがまだ追加されていません。設定からキーを追加してください。キーがないとPageSpeedモニターは機能しません。" + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "詳細", + "home": "ホーム" + }, + "buttons": { + "addMember": "メンバーを追加", + "cancel": "キャンセル", + "close": "閉じる", + "configure": "設定", + "confirm": "確認", + "create": "作成", + "delete": "削除", + "generateToken": "トークンを生成", + "incidents": "インシデント", + "inviteMember": "メンバーを招待", + "pause": "一時停止", + "resume": "再開", + "save": "保存", + "sendInvite": "招待を送信", + "test": "テスト", + "testNotifications": "通知テスト", + "toggleTheme": "ライト&ダークを切り替え", + "flushQueue": "キューをフラッシュ", + "notFound": "メインダッシュボードへ移動", + "resetPassword": "パスワードをリセット", + "reset": "リセット", + "clear": "クリア", + "addDemo": "デモモニターを追加", + "removeMonitors": "モニターを削除", + "sendTestEmail": "テストメールを送信", + "exportToJSON": "JSONにエクスポート", + "importFromJSON": "JSONからインポート", + "clearFilters": "フィルターをクリア", + "removeUser": "ユーザーを削除" + }, + "charts": { + "labels": { + "averageResponseTime": "平均応答時間", + "downtime": "ダウンタイム", + "high": "高", + "low": "低", + "uptime": "稼働時間" }, - "inputs": { - "email": { - "label": "メール", - "placeholder": "jordan.ellis@domain.com", - "errors": { - "empty": "続行するには、メールアドレスを入力してください", - "invalid": "入力されたメールアドレスの有効性を再確認してください" - } - }, - "password": { - "label": "パスワード", - "rules": { - "length": { - "beginning": "少なくとも", - "highlighted": "8文字である必要があります" - }, - "special": { - "beginning": "少なくとも", - "highlighted": "1つの特殊文字を含む必要があります" - }, - "number": { - "beginning": "少なくとも", - "highlighted": "1つの数字を含む必要があります" - }, - "uppercase": { - "beginning": "少なくとも", - "highlighted": "1つの大文字を含む必要があります" - }, - "lowercase": { - "beginning": "少なくとも", - "highlighted": "1つの小文字を含む必要があります" - }, - "match": { - "beginning": "パスワードの確認とパスワードは", - "highlighted": "一致する必要があります" - } - }, - "errors": { - "empty": "パスワードを入力してください", - "length": "パスワードは少なくとも8文字である必要があります", - "uppercase": "パスワードは少なくとも1つの大文字を含む必要があります", - "lowercase": "パスワードは少なくとも1つの小文字を含む必要があります", - "number": "パスワードは少なくとも1つの数字を含む必要があります", - "special": "パスワードは少なくとも1つの特殊文字を含む必要があります", - "incorrect": "入力されたパスワードが記録と一致しません" - } - }, - "passwordConfirm": { - "label": "パスワードの確認", - "placeholder": "確認のためにパスワードを再入力", - "errors": { - "empty": "確認のためにパスワードを再度入力してください(タイプミスを防ぐため)", - "different": "入力されたパスワードが一致しないため、どちらかが間違っています" - } - }, - "firstName": { - "label": "名前", - "placeholder": "太郎", - "errors": { - "empty": "名前を入力してください", - "length": "名前は50文字未満である必要があります", - "pattern": "名前は文字、スペース、アポストロフィ、またはハイフンのみを含む必要があります" - } - }, - "lastName": { - "label": "姓", - "placeholder": "田中", - "errors": { - "empty": "姓を入力してください", - "length": "姓は50文字未満である必要があります", - "pattern": "姓は文字、スペース、アポストロフィ、またはハイフンのみを含む必要があります" + "histogram": { + "avg": "平均: {{value}} ms", + "max": "最大: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "この操作は元に戻せません。", + "title": "本当に削除しますか?" + } + }, + "labels": { + "active": "アクティブ", + "paused": "一時停止中", + "na": "N/A", + "resolved": "解決済み", + "responseTime": "応答時間" + }, + "form": { + "role": { + "option": { + "role": { + "label": "ロール" } } }, - "errors": { - "validation": "データの検証エラー。" - }, - "fields": { - "password": { - "errors": { - "incorrect": "入力されたパスワードが記録と一致しません" + "name": { + "option": { + "firstName": { + "label": "名前", + "placeholder": "太郎" + }, + "lastName": { + "label": "姓", + "placeholder": "田中" } - }, - "role": { - "errors": { - "min": "" + } + }, + "email": { + "option": { + "email": { + "label": "メールアドレス", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "ログイン", - "subheadings": { - "stepOne": "メールアドレスを入力", - "stepTwo": "パスワードを入力" - }, - "links": { - "forgotPassword": "パスワードを忘れましたか?", - "register": "アカウントをお持ちでないですか?", - "forgotPasswordLink": "パスワードをリセット", - "registerLink": "こちらで登録" - }, - "toasts": { - "success": "おかえりなさい!正常にログインしました。", - "incorrectPassword": "パスワードが間違っています" - }, + "table": { + "empty": "データがありません", + "headers": { + "actions": "アクション", + "dateTime": "日時", + "message": "メッセージ", + "monitor": "モニター", + "monitorId": "モニターID", + "name": "名前", + "status": "ステータス", + "type": "タイプ", + "url": "URL", + "interval": "間隔", + "active": "アクティブ", + "responseTime": "応答時間" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "クリックしてアップロード", + "dragAndDrop": "ドラッグアンドドロップ", + "supportedFormats": "対応フォーマット", + "maxSize": "最大サイズ", + "orDragAndDrop": "またはドラッグアンドドロップ", "errors": { - "password": { - "incorrect": "入力されたパスワードが記録と一致しません" - } - }, - "welcome": "" + "invalidFileSize": "ファイルサイズが大きすぎます!", + "invalidFileFormat": "サポートされていないファイル形式!" + } }, - "registration": { - "heading": { - "superAdmin": "スーパー管理者を作成", - "user": "サインアップ" - }, - "subheadings": { - "stepOne": "個人情報を入力", - "stepTwo": "メールアドレスを入力", - "stepThree": "パスワードを作成" + "headerStatusPageControls": { + "publicLink": "公開リンク" + }, + "headerTimeRange": { + "labels": { + "day": "日", + "month": "月", + "recent": "最近", + "week": "週" }, "description": { - "superAdmin": "スーパー管理者アカウントを作成して開始", - "user": "ユーザーとしてサインアップし、スーパー管理者にモニターへのアクセスを求める" - }, - "gettingStartedButton": { - "superAdmin": "スーパー管理者アカウントを作成", - "user": "一般ユーザーでサインアップ" - }, - "termsAndPolicies": "アカウントを作成することで、利用規約とプライバシーポリシーに同意したことになります。", - "links": { - "login": "既にアカウントをお持ちですか? ログイン" - }, - "toasts": { - "success": "ようこそ!アカウントが正常に作成されました。" - }, - "welcome": "" - }, - "forgotPassword": { - "heading": "パスワードを忘れましたか?", - "subheadings": { - "stepOne": "ご心配なく、リセット手順をお送りします。", - "stepTwo": "パスワードリセットリンクを に送信しました", - "stepThree": "新しいパスワードは以前に使用したパスワードとは異なる必要があります。", - "stepFour": "パスワードが正常にリセットされました。下記をクリックして魔法のようにログインしてください。" - }, - "buttons": { - "openEmail": "メールアプリを開く", - "resetPassword": "パスワードをリセット" - }, - "imageAlts": { - "passwordKey": "パスワードキーアイコン", - "email": "メールアイコン", - "lock": "ロックアイコン", - "passwordConfirm": "パスワード確認アイコン" - }, - "links": { - "login": "ログインに戻る", - "resend": "メールが届きませんか? クリックして再送信" - }, - "toasts": { - "sent": "手順を に送信しました。", - "emailNotFound": "メールが見つかりません。", - "redirect": "秒後にリダイレクトします...", - "success": "パスワードが正常にリセットされました。", - "error": "パスワードをリセットできません。後でもう一度試すか、サポートにお問い合わせください。" + "recent": "過去2時間の統計を表示しています。", + "day": "過去24時間の統計を表示しています。", + "week": "過去7日間の統計を表示しています。", + "month": "過去30日間の統計を表示しています。" } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "サーバーに正常に再接続しました。", - "stillUnreachable": "サーバーにまだ到達できません。後でもう一度お試しください。" + }, + "offlineBanner": { + "serverUnreachable": "サーバーに接続できません", + "retry": "再試行", + "retrying": "再試行中...", + "reconnected": "サーバーに正常に再接続しました。" + }, + "sidebar": { + "menu": { + "uptime": "稼働時間", + "pagespeed": "ページ速度", + "infrastructure": "インフラストラクチャ", + "notifications": "通知", + "checks": "チェック", + "incidents": "インシデント", + "statusPages": "ステータスページ", + "maintenance": "メンテナンス", + "logs": "ログ", + "settings": "設定" }, - "alertBox": "サーバー接続エラー", - "description": "サーバーに接続できません。インターネット接続を確認するか、問題が続く場合はデプロイ設定を確認してください。", - "retryButton": { - "default": "接続を再試行", - "processing": "接続中..." + "bottomMenu": { + "support": "サポート", + "discussions": "ディスカッション", + "docs": "ドキュメント", + "changelog": "変更履歴" + }, + "accountMenu": { + "profile": "プロフィール", + "password": "パスワード", + "team": "チーム" + }, + "starPrompt": { + "title": "Checkmateにスターを付ける", + "description": "GitHubで最新リリースを確認し、コミュニティの成長に貢献しましょう" + }, + "authFooter": { + "navControls": "コントロール", + "logOut": "ログアウト", + "roles": { + "superAdmin": "スーパー管理者", + "admin": "管理者", + "user": "ユーザー", + "demoUser": "デモユーザー" + } } + }, + "starPrompt": { + "title": "Checkmateにスターを付ける", + "description": "GitHubで最新リリースを確認し、コミュニティの成長に貢献しましょう" } }, - "createNotifications": { - "title": "通知チャンネルを作成", - "nameSettings": { - "title": "名前", - "description": "統合の説明的な名前。", - "nameLabel": "名前", - "namePlaceholder": "例: Slack通知" + "pages": { + "notFound": { + "title": "おっと!お寿司を落としてしまいました!", + "subtitle": "URLが存在しないか、アクセス権限がありません。" }, - "typeSettings": { - "title": "タイプ", - "description": "作成したい通知チャンネルのタイプを選択。", - "typeLabel": "タイプ" - }, - "emailSettings": { - "title": "メール", - "description": "送信先メールアドレス。", - "emailLabel": "メールアドレス", - "emailPlaceholder": "例: john@example.com" - }, - "slackSettings": { - "title": "Slack", - "description": "Slack Webhookをここで設定", - "webhookLabel": "Slack Webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "PagerDuty統合をここで設定", - "integrationKeyLabel": "統合キー", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discord", - "description": "Discord Webhookをここで設定", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "webhookSettings": { - "title": "Webhook", - "description": "Webhookをここで設定", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "testNotification": "テスト通知", - "dialogDeleteTitle": "この通知を削除してもよろしいですか?", - "dialogDeleteConfirm": "削除" - }, - "notificationConfig": { - "title": "通知", - "description": "使用したい通知チャンネルを選択" - }, - "monitorStatus": { - "checkingEvery": "{{interval}}ごとにチェック", - "withCaptureAgent": "Captureエージェント{{version}}で", - "up": "アップ", - "down": "ダウン", - "paused": "一時停止" - }, - "advancedMatching": "高度なマッチング", - "sendTestNotifications": "テスト通知を送信", - "selectAll": "すべて選択", - "showAdminLoginLink": "ステータスページに「管理者?こちらでログイン」リンクを表示", - "logsPage": { - "title": "ログ", - "description": "システムログ - 最新1000行", - "tabs": { - "queue": "ジョブキュー", - "logs": "サーバーログ", - "diagnostics": "診断" - }, - "toast": { - "fetchLogsSuccess": "ログが正常に取得されました" - }, - "logLevelSelect": { - "title": "ログレベル", - "values": { - "all": "すべて", - "info": "情報", - "warn": "警告", - "error": "エラー", - "debug": "デバッグ" + "account": { + "tabs": { + "profile": "プロフィール", + "password": "パスワード", + "team": "チーム" + }, + "form": { + "name": { + "title": "名前", + "description": "個人情報を更新" + }, + "photo": { + "title": "プロフィール写真", + "description": "プロフィール画像をアップロード" + }, + "currentPassword": { + "title": "現在のパスワード", + "description": "本人確認のために現在のパスワードを入力してください", + "option": { + "label": "現在のパスワード", + "placeholder": "現在のパスワードを入力" + } + }, + "newPassword": { + "title": "新しいパスワード", + "description": "8文字以上の強力なパスワードを選択してください", + "option": { + "newPassword": { + "label": "新しいパスワード", + "placeholder": "新しいパスワードを入力" + }, + "confirm": { + "label": "パスワードを確認", + "placeholder": "新しいパスワードを確認" + } + } + }, + "deleteAccount": { + "title": "アカウントを削除", + "description": "この操作は永久的で元に戻せません" + } + }, + "team": { + "filter": { + "placeholder": "ロールでフィルター", + "all": "すべて", + "admin": "管理者", + "member": "メンバー" + }, + "table": { + "headers": { + "email": "メールアドレス", + "role": "ロール", + "created": "作成日" + } + }, + "addMember": { + "title": "新しいチームメンバーを登録", + "description": "新しいユーザーアカウントを作成します。作成後、認証情報を安全に共有してください。" + }, + "invite": { + "title": "チームメンバーを招待", + "description": "チームへの招待を送信", + "email": { + "label": "メールアドレス", + "placeholder": "メールアドレスを入力" + }, + "role": { + "label": "ロール", + "placeholder": "ロールを選択" + }, + "linkLabel": "招待リンク" + } } - } - }, - "queuePage": { - "title": "キュー", - "refreshButton": "更新", - "flushButton": "キューをフラッシュ", - "jobTable": { - "title": "現在キューにあるジョブ", - "idHeader": "モニターID", - "urlHeader": "URL", - "typeHeader": "タイプ", - "activeHeader": "アクティブ", - "lockedAtHeader": "ロック日時", - "runCountHeader": "実行回数", - "failCountHeader": "失敗回数", - "lastRunHeader": "最終実行日時", - "lastFinishedAtHeader": "最終完了日時", - "lastRunTookHeader": "最終実行時間", - "intervalHeader": "" }, - "metricsTable": { - "title": "キューメトリック", - "metricHeader": "メトリック", - "valueHeader": "値" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "少なくとも", + "highlighted": "8文字である必要があります" + }, + "lowercase": { + "beginning": "少なくとも", + "highlighted": "1つの小文字を含む必要があります" + }, + "match": { + "beginning": "パスワードの確認とパスワードは", + "highlighted": "一致する必要があります" + }, + "number": { + "beginning": "少なくとも", + "highlighted": "1つの数字を含む必要があります" + }, + "special": { + "beginning": "少なくとも", + "highlighted": "1つの特殊文字を含む必要があります" + }, + "uppercase": { + "beginning": "少なくとも", + "highlighted": "1つの大文字を含む必要があります" + } + }, + "form": { + "option": { + "email": { + "label": "メールアドレス", + "placeholder": "me@example.com" + }, + "password": { + "label": "パスワード", + "placeholder": "********" + }, + "confirmPassword": { + "label": "パスワードを確認" + } + } + } + }, + "login": { + "title": "Checkmateへようこそ!", + "subtitle": "ログインして続行", + "submit": "ログイン", + "links": { + "forgotPassword": { + "text": "パスワードをお忘れですか?", + "linkText": "パスワードをリセット" + }, + "register": { + "text": "アカウントをお持ちでないですか?", + "linkText": "こちらから登録" + } + } + }, + "register": { + "title": "Checkmateへようこそ!", + "subtitle": "サインアップして始めましょう", + "submit": "登録" + }, + "forgotPassword": { + "title": "パスワードをお忘れですか?", + "subtitle": "ご安心ください。リセット手順をお送りします。", + "submit": "リカバリーをリクエスト", + "links": { + "login": { + "text": "戻る:", + "linkText": "ログイン" + } + } + }, + "setNewPassword": { + "title": "パスワードをリセット", + "subtitle": "新しいパスワードは以前使用したパスワードと異なる必要があります。" + } }, - "failedJobTable": { - "title": "失敗したジョブ", - "monitorIdHeader": "モニターID", - "monitorUrlHeader": "モニターURL", - "failCountHeader": "失敗回数", - "failedAtHeader": "最終失敗日時", - "failReasonHeader": "失敗理由" - } - }, - "export": { - "title": "モニターをエクスポート", - "success": "モニターが正常にエクスポートされました!", - "failed": "モニターのエクスポートに失敗しました" - }, - "monitorActions": { - "title": "エクスポート/インポート", - "import": "モニターをインポート", - "export": "モニターをエクスポート", - "deleteSuccess": "モニターが正常に削除されました", - "deleteFailed": "モニターの削除に失敗しました", - "details": "詳細" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "Bluewave Labsによる開発", - "labelVersion": "バージョン", - "title": "について" + "checks": { + "selects": { + "monitor": { + "all": "すべてのモニター" + }, + "status": { + "all": "すべて", + "down": "ダウン", + "up": "アップ" + } + }, + "table": { + "empty": "この期間にダウンチェックはありません", + "headers": { + "statusCode": "ステータスコード", + "location": "ロケーション" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "デモモニターを追加", - "description": "デモンストレーション目的でサンプルモニターを追加。", - "title": "デモモニター" + "common": { + "monitors": { + "actions": { + "configure": "設定", + "delete": "削除", + "generateToken": "トークンを生成", + "details": "詳細", + "incidents": "インシデント", + "inviteMember": "メンバーを招待", + "openSite": "サイトを開く", + "pause": "一時停止", + "resume": "再開" + }, + "statBoxes": { + "activeFor": "稼働期間", + "certificateExpiry": "証明書の有効期限", + "lastCheck": "最終チェック", + "lastResponseTime": "最終応答時間" + }, + "status": { + "down": "ダウン", + "breached": "しきい値超過", + "initializing": "初期化中", + "maintenance": "メンテナンス中", + "paused": "一時停止中", + "total": "合計", + "up": "稼働中" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "ゲーム", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "ポート", + "optionPagespeed": "PageSpeed", + "optionHardware": "インフラストラクチャ", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "emailSettings": { - "buttonSendTestEmail": "テストメールを送信", - "description": "システムのメール設定を構成。これは通知とアラートの送信に使用されます。", - "descriptionTransport": "これはNodeMailer用のSMTPトランスポートを構築します", - "labelAddress": "メールアドレス - 認証に使用", - "labelConnectionHost": "メール接続ホスト - HELO/EHLO挨拶で使用するホスト名", - "labelHost": "メールホスト - 接続するホスト名またはIPアドレス", - "labelIgnoreTLS": "STARTTLSを無効化: サーバーがサポートしていてもTLSを使用しない", - "labelPassword": "メールパスワード - 認証用パスワード", - "labelPasswordSet": "パスワードが設定されています。変更するにはリセットをクリック。", - "labelPool": "接続プーリングを有効化: パフォーマンス向上のため既存接続を再利用", - "labelPort": "メールポート - 接続するポート", - "labelRejectUnauthorized": "無効な証明書を拒否: 自己署名または信頼されていない証明書での接続を拒否", - "labelRequireTLS": "STARTTLSを強制: TLSアップグレードを必須とし、サポートされていない場合は失敗", - "labelSecure": "SSL使用(推奨): SSL/TLSを使用して接続を暗号化", - "labelTLSServername": "TLS Servername - ホストがIPの場合のTLS検証用オプションホスト名", - "labelUser": "メールユーザー - 認証用ユーザー名、指定されている場合はメールアドレスを上書き", - "linkTransport": "仕様はこちらを参照", - "placeholderUser": "不要な場合は空白のまま", - "title": "メール", - "toastEmailRequiredFieldsError": "メールアドレス、ホスト、ポート、パスワードが必要です" + "createMonitor": { + "form": { + "advanced": { + "description": "高度なユースケース向けのオプション設定", + "option": { + "advancedMatching": { + "label": "高度なマッチングを使用" + }, + "expectedValue": { + "label": "期待値" + }, + "jsonPath": { + "description": "この式はレスポンスのJSONデータに対して評価され、結果は期待値とのマッチングに使用されます。クエリ言語のドキュメントはjmespath.orgをご参照ください。", + "label": "JSONPath式" + }, + "matchMethod": { + "label": "マッチ方法", + "equal": "", + "include": "", + "regex": "" + } + }, + "title": "詳細設定" + }, + "frequency": { + "description": "このモニターのステータスをどのくらいの頻度で確認しますか?", + "option": { + "frequency": { + "label": "チェック頻度", + "value": { + "fifteenMinutes": "15分", + "fifteenSeconds": "", + "fiveMinutes": "5分", + "fourMinutes": "", + "oneMinute": "", + "tenMinutes": "10分", + "thirtyMinutes": "30分", + "thirtySeconds": "", + "threeMinutes": "3分", + "twoMinutes": "" + } + } + }, + "title": "チェック頻度" + }, + "general": { + "option": { + "container": { + "label": "コンテナ名/ID", + "placeholder": "my-app または abcd1234" + }, + "host": { + "label": "ホスト", + "placeholder": "192.168.1.100 または example.com" + }, + "name": { + "label": "表示名", + "placeholder": "例: My Website" + }, + "secret": { + "label": "認証シークレット", + "placeholder": "シークレットキーを入力" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "", + "placeholder": "" + }, + "port": { + "label": "監視するポート", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "サービス名", + "placeholder": "例: my.service.v1(全体のヘルスチェックの場合は空欄)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "一般設定", + "description": { + "http": "監視するURLまたはIPを入力し(例: https://example.com/ または 192.168.1.100)、ダッシュボードに表示される表示名を追加してください。", + "ping": "pingするIPアドレスまたはホスト名を入力(例: 192.168.1.100 または example.com)し、ダッシュボードに表示される明確な表示名を追加。", + "port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。", + "docker": "コンテナのDocker IDを入力。Docker IDは64文字のフルDocker IDである必要があります。docker inspect を実行してフルコンテナIDを取得できます。", + "game": "", + "pagespeed": "Webサイトのページ読み込みパフォーマンス、Core Web Vitals、最適化スコアを追跡します。", + "grpc": "gRPCサーバーのホスト名とポートを入力し、必要に応じてヘルスチェック用のサービス名を指定し、表示名を追加してください。", + "websocket": "監視するWebSocket URLを入力し(例: wss://example.com/socket)、表示名を追加してください。", + "hardware": "インフラストラクチャのCPU、メモリ、ディスク使用量、温度を監視します。" + } + }, + "ignoreTls": { + "description": "HTTPS接続のTLS/SSL証明書検証を設定します。", + "option": { + "tls": { + "label": "TLS/SSLエラーを無視" + } + }, + "title": "TLS/SSL設定" + }, + "incidents": { + "description": "スライディングウィンドウを使用してモニターのダウンを判定します。スライディングウィンドウ内のチェックの割合が指定値を満たした場合にのみ、モニターのステータスが変更されます。", + "option": { + "checks": { + "label": "スライディングウィンドウ内のチェック数" + }, + "percentage": { + "label": "モニターのステータスが変更される前に、スライディングウィンドウ内のチェックの何パーセントが失敗/成功する必要がありますか?" + } + }, + "title": "インシデント" + }, + "notifications": { + "description": "使用する通知チャンネルを選択してください", + "title": "通知" + }, + "type": { + "description": "実行するチェックの種類を選択", + "optionDockerDescription": "Dockerを使用してコンテナが稼働しているか監視します。", + "optionGameDescription": "特定のゲームサーバーがオンラインかどうかを監視します。", + "optionGrpcDescription": "標準のHealth Checking Protocolを使用してgRPCサービスを監視します。", + "optionWebSocketDescription": "WebSocketエンドポイントの接続状態と応答時間を監視します。", + "optionHttpDescription": "HTTP(S)を使用してWebサイトまたはAPIエンドポイントを監視します。", + "optionPingDescription": "ICMP Pingを使用してサーバーがオンラインかどうかを監視します。", + "optionPortDescription": "サーバーの特定のポートが開いているかどうかを監視します。", + "title": "タイプ" + }, + "thresholds": { + "title": "アラートしきい値", + "description": "このハードウェアモニターでアラートがトリガーされるしきい値を定義します。", + "option": { + "cpuThreshold": { + "label": "CPUアラートしきい値(%)" + }, + "memoryThreshold": { + "label": "メモリアラートしきい値(%)" + }, + "diskThreshold": { + "label": "ディスクアラートしきい値(%)" + }, + "tempThreshold": { + "label": "温度アラートしきい値(°C)" + } + } + }, + "geoChecks": { + "title": "地理分散チェック", + "description": "複数の地理的ロケーションからチェックを実行し、グローバルな可用性とパフォーマンスを監視します。", + "option": { + "enabled": { + "label": "地理分散チェックを有効化" + }, + "locations": { + "label": "ロケーション", + "placeholder": "ロケーションを選択", + "options": { + "EU": "ヨーロッパ", + "NA": "北米", + "AS": "アジア", + "SA": "南米", + "AF": "アフリカ", + "OC": "オセアニア" + } + }, + "interval": { + "label": "チェック間隔", + "value": { + "fiveMinutes": "5分", + "tenMinutes": "10分", + "fifteenMinutes": "15分", + "thirtyMinutes": "30分" + } + } + } + }, + "url": { + "title": "ステータスページにモニターのIP/URLを表示", + "description": "公開ステータスページにモニターのIPアドレスまたはURLを表示します。無効の場合、機密情報を保護するためにモニター名のみが表示されます。", + "option": { + "showURL": { + "label": "ステータスページにIP/URLを表示", + "enabled": "有効", + "disabled": "無効" + } + } + }, + "stats": { + "title": "モニター履歴", + "description": "チームのすべての監視履歴と統計をクリアします。この操作は元に戻せません。", + "buttonText": "すべての統計をクリア", + "dialog": { + "title": "すべての監視履歴をクリアしますか?", + "description": "チームのすべての監視履歴、統計、チェックデータが完全に削除されます。この操作は元に戻せません。", + "confirm": "はい、すべての統計をクリア" + } + } + } }, - "pageSpeedSettings": { - "description": "Google PageSpeed APIキーを入力してGoogle PageSpeed監視を有効化。キーを更新するにはリセットをクリック。", - "labelApiKeySet": "APIキーが設定されています。変更するにはリセットをクリック。", - "labelApiKey": "PageSpeed APIキー", - "title": "Google PageSpeed APIキー" + "editUser": { + "form": { + "roles": { + "title": "ロール", + "description": "ユーザーにロールを割り当てます。複数のロールを選択できます。" + } + }, + "dialog": { + "removeUser": { + "title": "ユーザーを削除", + "content": "{{name}}をチームから削除してもよろしいですか?この操作は元に戻せません。" + } + } }, - "saveButtonLabel": "保存", - "statsSettings": { - "clearAllStatsButton": "すべての統計をクリア", - "clearAllStatsDescription": "すべての統計をクリア。この操作は元に戻せません。", - "clearAllStatsDialogConfirm": "はい、すべての統計をクリア", - "clearAllStatsDialogDescription": "削除されると、監視履歴と統計は復元できません。", - "clearAllStatsDialogTitle": "すべての統計をクリアしますか?", - "description": "履歴データを保持する期間を定義。既存のデータをすべてクリアすることもできます。", - "labelTTL": "監視履歴を保持したい日数。", - "labelTTLOptional": "0で無限", - "title": "モニター履歴" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "インシデントを解決", + "option": { + "comment": { + "label": "コメント(任意)", + "placeholder": "解決に関するコメントを追加..." + } + } + }, + "details": { + "analysis": "インシデント分析", + "comment": "コメント:", + "detailsLabel": "詳細", + "downtime": "ダウンタイム:", + "message": "メッセージ:", + "monitor": "モニター:", + "overview": "概要", + "resolutionDetails": "解決の詳細", + "resolutionType": "タイプ:", + "resolutionTypes": { + "automatic": "自動", + "manual": "手動" + }, + "resolve": "インシデントを解決", + "resolvedAt": "解決日時:", + "resolvedBy": "解決者:", + "startedAt": "開始日時:", + "status": "ステータス:", + "statusCode": "ステータスコード:", + "timeline": "タイムライン", + "title": "インシデント詳細", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "すべてのモニター", + "monitor": "モニター", + "resolutionType": "解決タイプ", + "resolutionTypes": { + "manual": "手動", + "automatic": "自動", + "all": "すべて" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "アクティブなインシデント", + "active_zero": "アクティブなインシデントはありません", + "active_one": "{{count}}件のアクティブなインシデント", + "active_other": "{{count}}件のアクティブなインシデント" + }, + "incidentStats": { + "avgResolutionTime": "平均解決時間", + "mostAffectedMonitor": "最も影響を受けたモニター", + "title": "インシデント統計", + "totalIncidents": "インシデント合計" + }, + "latestIncidents": { + "title": "最新のインシデント" + } + }, + "table": { + "actions": { + "details": "詳細", + "goToMonitor": "モニターへ移動", + "resolveManually": "手動で解決" + }, + "activeIncidents": "アクティブなインシデント", + "headers": { + "endTime": "終了時刻", + "resolutionType": "解決タイプ", + "startTime": "開始時刻", + "statusCode": "ステータスコード" + }, + "resolvedIncidents": "解決済みインシデント", + "status": { + "active": "アクティブ", + "resolved": "解決済み" + } + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "すべてのモニターを削除", - "description": "システムからすべてのモニターを削除。", - "dialogConfirm": "はい、すべてのモニターを削除", - "dialogDescription": "削除されると、モニターは復元できません。", - "dialogTitle": "すべてのモニターを削除しますか?", - "title": "システムリセット" - }, - "timezoneSettings": { - "description": "アプリケーション全体で日付と時刻を表示するために使用するタイムゾーンを選択。", - "label": "表示タイムゾーン", - "title": "表示タイムゾーン" - }, - "title": "設定", - "uiSettings": { - "description": "ライトモードとダークモードを切り替えるか、ユーザーインターフェース言語を変更。", - "labelLanguage": "言語", - "labelTheme": "テーマモード", - "title": "外観" - }, - "urlSettings": { - "description": "公開ステータスページでモニターのIPアドレスまたはURLを表示。無効にすると、機密情報を保護するためモニター名のみが表示されます。", - "label": "ステータスページでIP/URLを表示", - "selectDisabled": "無効", - "selectEnabled": "有効", - "title": "ステータスページのモニターIP/URL" - }, - "globalThresholds": { - "title": "", - "description": "" - } - }, - "statusPageCreate": { - "buttonSave": "保存" - }, - "incidentsOptionsHeaderFilterResolved": "解決済み", - "settingsSave": "保存", - "statusPageCreateAppearanceTitle": "外観", - "confirmPassword": "パスワードの確認", - "monitorHooks": { - "failureAddDemoMonitors": "デモモニターの追加に失敗しました", - "successAddDemoMonitors": "デモモニターが正常に追加されました" - }, - "settingsAppearance": "外観", - "settingsDisplayTimezone": "表示タイムゾーン", - "settingsGeneralSettings": "一般設定", - "incidentsOptionsHeaderTotalIncidents": "総インシデント", - "statusPage": { - "deleteSuccess": "ステータスページが正常に削除されました", - "deleteFailed": "ステータスページの削除に失敗しました", - "createSuccess": "ステータスページが正常に作成されました", - "updateSuccess": "ステータスページが正常に更新されました", - "generalSettings": "一般設定", - "contents": "コンテンツ", - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "testNotificationsDisabled": "このモニターには通知が設定されていません。「設定」ボタンをクリックして追加する必要があります", - "incidentsTableResolvedAt": "解決日時", - "incidentsTableActionResolve": "解決", - "checkHooks": { - "failureResolveOne": "インシデントの解決に失敗しました。", - "failureResolveAll": "すべてのインシデントの解決に失敗しました。", - "failureResolveMonitor": "" - }, - "checkFormError": "フォームでエラーを確認してください。", - "diagnosticsPage": { - "diagnosticDescription": "システム診断", - "statsDescription": "システム統計", - "gauges": { - "heapAllocationTitle": "ヒープ割り当て", - "heapAllocationSubtitle": "利用可能メモリの%", - "heapUsageTitle": "ヒープ使用量", - "heapUsageSubtitle": "利用可能メモリの%", - "heapUtilizationTitle": "ヒープ利用率", - "heapUtilizationSubtitle": "割り当て済みの%", - "instantCpuUsageTitle": "瞬間CPU使用率", - "instantCpuUsageSubtitle": "CPUが使用した1秒の%" - }, - "stats": { - "eventLoopDelayTitle": "イベントループ遅延", - "uptimeTitle": "稼働時間", - "usedHeapSizeTitle": "使用ヒープサイズ", - "totalHeapSizeTitle": "総ヒープサイズ", - "osMemoryLimitTitle": "OSメモリ制限" - } - }, - "pageSpeedLighthouseAPI": "Lighthouse PageSpeed APIを使用してウェブサイトを監視", - "time": { - "threeMinutes": "3分", - "fiveMinutes": "5分", - "tenMinutes": "10分", - "twentyMinutes": "20分", - "oneHour": "1時間", - "oneDay": "1日", - "oneWeek": "1週間", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" - }, - "general": { - "noOptionsFound": "" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "pageSpeed": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "editUserPage": { - "form": { - "email": "", - "firstName": "", - "lastName": "", - "role": "", - "save": "" - }, - "table": { - "actionHeader": "", - "roleHeader": "" - }, - "title": "", - "toast": { - "successUserUpdate": "", - "validationErrors": "" - } - }, - "incidentsPageActionResolveMonitor": "", - "incidentsPageActionResolveAll": "", - "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "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": "監視したい特定のディスクを選択してください。" + "charts": { + "labels": { + "cpu": "CPU使用率", + "disk": "ディスク使用率", + "memory": "メモリ使用率", + "netBytesRecv": "{{name}} - 受信バイト数", + "netBytesSent": "{{name}} - 送信バイト数", + "temp": "温度" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "最大周波数", + "title": "CPU使用率", + "upperLabel": "現在の周波数" + }, + "disk": { + "lowerLabel": "空き", + "title": "ディスク{{idx}}使用率", + "upperLabel": "使用済み" + }, + "memory": { + "lowerLabel": "空き", + "title": "メモリ使用率", + "upperLabel": "使用済み" + } + }, + "statBoxes": { + "avgCpuTemperature": "平均CPU温度", + "cpuFrequency": "CPU周波数", + "cpuLogical": "CPU(論理)", + "cpuPhysical": "CPU(物理)", + "disk": "ディスク", + "memory": "メモリ", + "os": "OS" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "ディスク", + "memory": "メモリ" + } + }, + "tabs": { + "labels": { + "network": "", + "overview": "概要" + } + }, + "fallback": { + "actionButton": "モニターを作成!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "インフラストラクチャモニターの用途:" + } + }, + "logs": { + "tabs": { + "diagnostics": "診断", + "logs": "サーバーログ", + "queue": "ジョブキュー" + }, + "logLevelSelect": { + "label": "ログレベル" + }, + "jobQueue": "ジョブキュー", + "failedJobs": "失敗したジョブ", + "noLogs": "ログが見つかりません", + "metrics": { + "jobs": "ジョブ", + "activeJobs": "アクティブなジョブ", + "failingJobs": "失敗中のジョブ", + "totalRuns": "合計実行数", + "totalFailures": "合計失敗数" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "イベントループ遅延", + "uptime": "稼働時間", + "usedHeapSize": "使用済みヒープサイズ", + "totalHeapSize": "合計ヒープサイズ", + "osMemoryLimit": "OSメモリ制限" + }, + "gauges": { + "heapAllocation": "ヒープ割り当て", + "heapUsage": "ヒープ使用率", + "heapUtilization": "ヒープ使用効率", + "instantCpuUsage": "瞬時CPU使用率", + "availableMemoryPercentage": "利用可能メモリの%", + "allocatedPercentage": "割り当て済み%", + "usedSPercentage": "CPUによる1秒あたりの使用%", + "total": "合計", + "used": "使用済み" + } + }, + "table": { + "headers": { + "timestamp": "タイムスタンプ", + "level": "レベル", + "service": "サービス", + "method": "メソッド", + "monitorId": "モニターID", + "runCount": "実行回数", + "failCount": "失敗回数", + "lastRunAt": "最終実行日時", + "lockedAt": "ロック日時", + "lastFinishedAt": "最終完了日時", + "lastRunTook": "最終実行時間", + "lastFailedAt": "最終失敗日時", + "failReason": "失敗理由" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "メンテナンスウィンドウを作成!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "メンテナンスウィンドウの用途:" + }, + "table": { + "headers": { + "nextWindow": "次のウィンドウ", + "repeat": "繰り返し" + } + }, + "form": { + "general": { + "title": "一般設定", + "description": "メンテナンスウィンドウの名前と繰り返しオプションを設定します。", + "option": { + "name": { + "label": "名前", + "placeholder": "例: 週次メンテナンス" + }, + "repeat": { + "label": "繰り返し" + } + } + }, + "startDate": { + "title": "開始日", + "description": "メンテナンスウィンドウの開始日を選択してください。", + "option": { + "startDate": { + "label": "開始日" + } + } + }, + "startTime": { + "title": "開始時刻", + "description": "メンテナンスウィンドウの開始時刻と期間を設定します。すべての値はUTCです", + "option": { + "duration": { + "label": "期間" + }, + "startTime": { + "label": "開始時刻" + } + }, + "monitors": { + "title": "モニター", + "description": "メンテナンスウィンドウを適用するモニター", + "option": { + "addMonitors": { + "label": "モニターを追加" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "チャンネルを作成", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "通知チャンネルの用途:" + }, + "form": { + "accessToken": { + "optionAccessToken": "アクセストークン", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "通知の送信先アドレスです。", + "optionAddress": "アドレス", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "アドレス" + }, + "homeServer": { + "optionHomeServer": "ホームサーバー", + "placeholder": "example.com" + }, + "matrix": { + "description": "通知用のMatrixホームサーバー接続を設定します。", + "title": "Matrix設定" + }, + "notificationName": { + "description": "通知チャンネルの説明的な名前", + "optionName": "チャンネル名", + "placeholder": "例: 本番アラート", + "title": "チャンネル名" + }, + "pagerDuty": { + "description": "アラート受信用のPagerDutyインテグレーションキーです。", + "optionIntegrationKey": "インテグレーションキー", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "インテグレーションキー" + }, + "roomId": { + "optionRoomId": "ルームID", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "作成する通知チャンネルの種類を選択してください。", + "optionType": "タイプ", + "title": "チャンネルタイプ" + }, + "telegram": { + "title": "Telegram設定", + "description": "Telegram通知を有効にするには、Telegramボットを作成・管理するための公式ボットであるBotFatherを使用してTelegramボットを作成します。次に、APIトークンとチャットIDを取得してここに記入します。", + "optionBotToken": "ボットトークン", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "チャットID", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "送信先" + } + } + }, + "pageSpeed": { + "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)" + }, + "legend": { + "title": "PageSpeedレポート", + "weight": "重み" + }, + "pie": { + "title": "パフォーマンスレポート" + } + }, + "table": { + "headers": { + "pageSpeedScore": "PageSpeedスコア" + } + }, + "fallback": { + "actionButton": "モニターを作成!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "PageSpeedモニターの用途:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "表示タイムゾーン", + "description": "アプリケーション全体で日時の表示に使用するタイムゾーンを選択してください。", + "option": { + "timezone": { + "label": "表示タイムゾーン" + } + } + }, + "ui": { + "title": "外観", + "description": "ライトモードとダークモードの切り替え、言語の変更、またはチャート表示タイプのカスタマイズができます。", + "option": { + "theme": { + "label": "テーマモード", + "light": "ライト", + "dark": "ダーク" + }, + "language": { + "label": "言語" + }, + "chartType": { + "label": "チャートタイプ", + "histogram": "ヒストグラム", + "heatmap": "ヒートマップ" + } + } + }, + "pagespeed": { + "title": "Google PageSpeed APIキー", + "description": "Google PageSpeed監視を有効にするにはGoogle PageSpeed APIキーを入力してください。キーを更新するにはリセットをクリックしてください。", + "option": { + "apiKey": { + "label": "PageSpeed APIキー", + "labelSet": "APIキーが設定されています。変更するにはリセットをクリックしてください。", + "placeholder": "Google PageSpeed APIキーを入力" + } + } + }, + "url": { + "title": "ステータスページにモニターのIP/URLを表示", + "description": "公開ステータスページにモニターのIPアドレスまたはURLを表示します。無効の場合、機密情報を保護するためにモニター名のみが表示されます。", + "option": { + "showURL": { + "label": "ステータスページにIP/URLを表示", + "enabled": "有効", + "disabled": "無効" + } + } + }, + "stats": { + "title": "モニター履歴", + "description": "チームのすべての監視履歴と統計をクリアします。この操作は元に戻せません。", + "option": { + "clear": { + "label": "すべての統計をクリアします。この操作は元に戻せません。" + } + }, + "dialog": { + "title": "すべての監視履歴をクリアしますか?", + "description": "この操作は元に戻せません" + } + }, + "retention": { + "title": "チェックデータの保持", + "description": "チェックデータが自動クリーンアップされるまでの保持期間を設定します。", + "option": { + "days": { + "label": "保持期間(日数)", + "unlimited": "無制限" + } + } + }, + "thresholds": { + "title": "グローバルしきい値", + "description": "インフラストラクチャ監視のグローバルなCPU、メモリ、ディスク、温度しきい値を設定します。オーバーライドしない限り、すべてのハードウェアモニターに適用されます。", + "option": { + "cpu": { + "label": "CPUしきい値(%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "メモリしきい値(%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "ディスクしきい値(%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "温度しきい値(°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "メール設定", + "description": "システムのメール設定を構成します。通知とアラートの送信に使用されます。", + "descriptionTransport": "これはNodeMailer用のSMTPトランスポートを構築します", + "descriptionTransportLink": "仕様はこちらを参照", + "option": { + "host": { + "label": "メールホスト - 接続先のホスト名またはIPアドレス", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "メールポート - 接続先のポート", + "placeholder": "587" + }, + "address": { + "label": "メールアドレス - 認証に使用", + "placeholder": "you@example.com" + }, + "user": { + "label": "メールユーザー - 認証用のユーザー名、指定時はメールアドレスを上書き", + "placeholder": "不要な場合は空欄のまま" + }, + "password": { + "label": "メールパスワード - 認証用のパスワード", + "labelSet": "パスワードが設定されています。変更するにはリセットをクリックしてください。", + "placeholder": "パスワードを入力" + }, + "tlsServername": { + "label": "TLSサーバー名 - ホストがIPの場合のTLS検証用オプションホスト名", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "メール接続ホスト - HELO/EHLOグリーティングで使用するホスト名", + "placeholder": "localhost" + }, + "secure": { + "label": "SSLを使用(推奨): SSL/TLSで接続を暗号化" + }, + "pool": { + "label": "接続プーリングを有効化: 既存の接続を再利用してパフォーマンスを向上" + }, + "ignoreTLS": { + "label": "STARTTLSを無効化: サーバーがサポートしていてもTLSを使用しない" + }, + "requireTLS": { + "label": "STARTTLSを強制: TLSアップグレードを要求、未サポート時は失敗" + }, + "rejectUnauthorized": { + "label": "無効な証明書を拒否: 自己署名または信頼されていない証明書の接続を拒否" + } + } + }, + "demoMonitors": { + "title": "デモモニター", + "description": "デモ目的でサンプルモニターを追加します。" + }, + "removeMonitors": { + "title": "システムリセット", + "description": "システムからすべてのモニターを削除します。", + "dialog": { + "title": "すべてのモニターを削除しますか?", + "description": "この操作は元に戻せません。" + } + }, + "exportMonitors": { + "title": "モニターをエクスポート" + }, + "importExportMonitors": { + "title": "モニターのインポート/エクスポート", + "description": "バックアップまたは転送のために、モニターデータをJSONファイルとしてインポートまたはエクスポートします。" + }, + "about": { + "title": "概要", + "developedBy": "Bluewave Labsが開発" + }, + "validation": { + "errorMessage": "以下のバリデーションエラーを修正してください:" + } + } + }, + "statusPages": { + "deleteSuccess": "ステータスページが正常に削除されました", + "fallback": { + "title": "ステータスページの用途:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "ステータスページを作成!" + }, + "monitorsList": { + "chartTypeHeatmap": "ヒートマップ", + "chartTypeHistogram": "ヒストグラム", + "noData": "データがありません", + "infrastructure": { + "title": "インフラストラクチャ", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "メモリ", + "disk": "ディスク", + "usage": "使用量", + "used": "使用済み", + "total": "合計" + }, + "uptime": { + "title": "稼働時間", + "responseTime": "応答時間" + } + }, + "statusBar": { + "allDown": "すべてのシステムがダウンしています", + "allUp": "すべてのシステムが正常に稼働しています", + "degraded": "一部のシステムに問題が発生しています", + "unknown": "システムステータスを判定できません" + }, + "table": { + "headers": { + "name": "ステータスページ名", + "url": "公開URL" + }, + "published": "公開済み", + "unpublished": "未公開" + }, + "title": "ステータスページ", + "details": { + "empty": { + "title": "まだ何もありません", + "addMonitor": "開始するにはモニターを追加してください" + } + }, + "form": { + "access": { + "title": "アクセス", + "description": "ステータスページの準備ができたら、公開済みとしてマークできます。", + "option": { + "published": { + "name": "公開済みで一般に表示" + } + } + }, + "basicInfo": { + "title": "基本情報", + "description": "会社名とステータスページが指すサブドメインを定義します。", + "option": { + "name": { + "label": "会社名", + "placeholder": "Acme Inc." + }, + "url": { + "label": "ステータスページのアドレス", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "モニター", + "description": "ステータスページに表示するモニターを選択してください。", + "noMonitors": "モニターが選択されていません", + "option": { + "monitors": { + "label": "モニターを選択", + "placeholder": "モニターを検索して選択..." + } + } + }, + "timezone": { + "title": "タイムゾーン", + "description": "ステータスページの表示に使用するタイムゾーンを選択してください。", + "option": { + "timezone": { + "label": "タイムゾーン", + "placeholder": "タイムゾーンを選択..." + } + } + }, + "appearance": { + "title": "外観", + "description": "公開ステータスページのデフォルトの外観を定義します。", + "option": { + "logo": { + "label": "ロゴ" + }, + "color": { + "label": "ブランドカラー" + } + } + }, + "features": { + "title": "機能", + "description": "ステータスページに表示する情報を設定します。", + "option": { + "showCharts": { + "label": "応答時間チャートを表示" + }, + "showUptimePercentage": { + "label": "稼働率を表示" + }, + "showAdminLoginLink": { + "label": "管理者ログインリンクを表示" + }, + "showInfrastructure": { + "label": "インフラストラクチャメトリクスを表示" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "モニターを検索..." + } + }, + "table": { + "headers": { + "responseTime": "応答時間" + } + }, + "fallback": { + "actionButton": "モニターを作成!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "稼働時間モニターの用途:" + } } } } diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json index 193c53788..9b292dd68 100644 --- a/client/src/locales/pt-BR.json +++ b/client/src/locales/pt-BR.json @@ -1,1140 +1,1305 @@ { - "submit": "Enviar", - "title": "Título", - "distributedStatusHeaderText": "Cobertura em tempo real e em dispositivos reais", - "distributedStatusSubHeaderText": "Impulsionado por milhões de dispositivos em todo o mundo, visualize o desempenho do sistema por região global, país ou cidade", - "settingsDisabled": "Desabilitado", - "settingsSuccessSaved": "Configurações salvas com sucesso", - "settingsFailedToSave": "Falha ao salvar as configurações", - "settingsStatsCleared": "Estatísticas limpas com sucesso", - "settingsFailedToClearStats": "Falha ao limpar estatísticas", - "settingsMonitorsDeleted": "Todos os monitores foram excluídos com sucesso", - "settingsFailedToDeleteMonitors": "Falha ao excluir todos os monitores", - "starPromptTitle": "Star Checkmate", - "starPromptDescription": "Veja os últimos lançamentos e ajude a expandir a comunidade no GitHub", - "https": "HTTPS", - "http": "HTTP", - "monitor": "monitor", - "aboutus": "Sobre nós", - "now": "Agora", - "delete": "Deletar", - "configure": "Configurar", - "responseTime": "Tempo de resposta", - "ms": "ms", - "bar": "Bar", - "area": "Área", - "country": "PAÍS", - "city": "CIDADE", - "response": "RESPOSTA", - "monitorStatusUp": "O monitor {name} ({url}) agora está ATIVO e respondendo", - "monitorStatusDown": "O monitor {name} ({url}) está INATIVO e não está respondendo", - "webhookSendSuccess": "Notificação de webhook enviada com sucesso", - "webhookSendError": "Erro ao enviar notificação de webhook para {platform}", - "webhookUnsupportedPlatform": "Plataforma não suportada: {platform}", - "distributedRightCategoryTitle": "Monitor", - "distributedStatusServerMonitors": "Monitores de servidor", - "distributedStatusServerMonitorsDescription": "Monitorar o status dos servidores relacionados", - "distributedUptimeCreateSelectURL": "Aqui você pode selecionar a URL do host, juntamente com o tipo de monitor.", - "distributedUptimeCreateChecks": "Verificações para realizar", - "distributedUptimeCreateChecksDescription": "Você sempre pode adicionar ou remover verificações depois de adicionar seu site.", - "distributedUptimeCreateIncidentNotification": "Notificações de incidentes", - "distributedUptimeCreateIncidentDescription": "Quando ocorrer um incidente, notifique os usuários.", - "distributedUptimeCreateAdvancedSettings": "Configurações avançadas", - "distributedUptimeDetailsNoMonitorHistory": "Ainda não há histórico de verificações para este monitor.", - "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Última atualização", - "notifications": { - "enableNotifications": "Habilitar notificações da {{platform}}", - "testNotification": "Notificação de teste", - "addOrEditNotifications": "Adicionar ou editar notificações", - "slack": { - "label": "Slack", - "description": "Para habilitar as notificações do Slack, crie um aplicativo Slack e habilite os webhooks de entrada. Depois disso, basta fornecer a URL do webhook aqui.", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "A URL do webhook do Slack é necessária" - }, - "discord": { - "label": "Discord", - "description": "Para enviar dados para um canal do Discord a partir do Checkmate por meio de notificações do Discord usando webhooks, você pode usar o recurso de Webhooks de entrada do Discord.", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "A URL do webhook do Discord é necessária" - }, - "telegram": { - "label": "Telegram", - "description": "Para habilitar as notificações do Telegram, crie um bot do Telegram usando o BotFather, um bot oficial para criar e gerenciar bots do Telegram. Em seguida, obtenha o token da API e o ID do chat e os coloque aqui.", - "tokenLabel": "Seu token de bot", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "Seu ID de bate-papo", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "O token do Telegram e o ID do chat são necessários" - }, - "webhook": { - "label": "Webhooks", - "description": "Você pode configurar um webhook personalizado para receber notificações quando ocorrerem incidentes.", - "urlLabel": "Webhook URL", - "urlPlaceholder": "https://seu-servidor.com/webhook", - "urlRequired": "URL do webhook é necessária" - }, - "testNotificationDevelop": "Notificação de teste 2", - "integrationButton": "Integração de notificação", - "testSuccess": "Notificação de teste enviada com sucesso!", - "testFailed": "Falha ao enviar notificação de teste", - "unsupportedType": "Tipo de notificação não suportado", - "networkError": "Ocorreu um erro de rede", - "fallback": { - "title": "Um canal de notificação é usado para:", - "checks": [ - "Alerte as equipes sobre tempo de inatividade ou problemas de desempenho", - "Informe os engenheiros sobre incidentes", - "Mantenha os administradores informados sobre as mudanças no sistema" - ], - "actionButton": "Vamos criar seu primeiro canal de notificação!" - }, - "createButton": "Criar canal de notificação", - "createTitle": "Canal de notificação", - "create": { - "success": "Notificação criada com sucesso", - "failed": "Falha ao criar notificação" - }, - "fetch": { - "success": "Notificações obtidas com sucesso", - "failed": "Falha ao buscar notificações" - }, - "delete": { - "success": "Notificação excluída com sucesso", - "failed": "Falha ao excluir a notificação" - }, - "edit": { - "success": "Notificação atualizada com sucesso", - "failed": "Falha ao atualizar a notificação" - }, - "test": { - "success": "Notificação de teste enviada com sucesso", - "failed": "Falha ao enviar notificação de teste" - } - }, - "testLocale": "testLocale", - "add": "Adicionar", - "monitors": "monitores", - "distributedUptimeStatusCreateStatusPage": "Pagina de status", - "distributedUptimeStatusCreateStatusPageAccess": "Acesso", - "distributedUptimeStatusCreateStatusPageReady": "Se sua página de status estiver pronta, você pode marcá-la como publicada.", - "distributedUptimeStatusBasicInfoHeader": "Informação básica", - "distributedUptimeStatusBasicInfoDescription": "Defina o nome da empresa e o subdomínio para o qual sua página de status aponta.", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "Carregue um logo para sua página de status", - "distributedUptimeStatusLogoUploadButton": "Upload logo", - "distributedUptimeStatusStandardMonitorsHeader": "Monitores padrão", - "distributedUptimeStatusStandardMonitorsDescription": "Anexe monitores padrão à sua página de status.", - "distributedUptimeStatusCreateYour": "Crie o seu", - "distributedUptimeStatusEditYour": "Edite seu", - "distributedUptimeStatusPublishedLabel": "Publicado e visível ao público", - "distributedUptimeStatusCompanyNameLabel": "Nome da empresa", - "distributedUptimeStatusPageAddressLabel": "Endereço da sua página de status", - "distributedUptimeStatus30Days": "30 dias", - "distributedUptimeStatus60Days": "60 dias", - "distributedUptimeStatus90Days": "90 dias", - "distributedUptimeStatusPageNotSetUp": "Uma página de status não está configurada.", - "distributedUptimeStatusContactAdmin": "Entre em contato com seu administrador", - "distributedUptimeStatusPageNotPublic": "Esta página de status não é pública.", - "distributedUptimeStatusPageDeleteDialog": "Você quer excluir esta página de status?", - "distributedUptimeStatusPageDeleteConfirm": "Sim, excluir página de status", - "distributedUptimeStatusPageDeleteDescription": "Uma vez excluída, sua página de status não poderá ser recuperada.", - "distributedUptimeStatusDevices": "Dispositivos", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "UPT queimado", - "distributedUptimeStatusUptLogo": "Logo UPT", - "incidentsTableNoIncidents": "Nenhum incidente registrado", - "incidentsTablePaginationLabel": "incidentes", - "incidentsTableMonitorName": "Nome do monitor", - "incidentsTableStatus": "Status", - "incidentsTableDateTime": "Data & Hora", - "incidentsTableStatusCode": "Código de status", - "incidentsTableMessage": "Mensagem", - "incidentsOptionsHeader": "Incidentes para:", - "incidentsOptionsHeaderFilterBy": "Filtrar por:", - "incidentsOptionsHeaderFilterAll": "Todos", - "incidentsOptionsHeaderFilterDown": "Inativos", - "incidentsOptionsHeaderFilterCannotResolve": "Não é possível resolver", - "incidentsOptionsHeaderShow": "Mostrar:", - "incidentsOptionsHeaderLastHour": "Última hora", - "incidentsOptionsHeaderLastDay": "Último dia", - "incidentsOptionsHeaderLastWeek": "Ultima semana", - "incidentsOptionsPlaceholderAllServers": "Todos os servidores", - "infrastructureCreateYour": "Crie o seu", - "infrastructureCreateGeneralSettingsDescription": "Aqui você pode inserir a URL do host, juntamente com o nome de exibição e o API secret de autorização para se conectar ao agente do servidor.", - "infrastructureServerRequirement": "O servidor que você quer monitorar deve estar executando o", - "infrastructureCustomizeAlerts": "Personalize alertas", - "infrastructureAlertNotificationDescription": "Envie uma notificação ao(s) usuário(s) quando os limites excederem uma porcentagem especificada.", - "infrastructureCreateMonitor": "Criar monitor de infraestrutura", - "infrastructureProtocol": "Protocolo", - "infrastructureServerUrlLabel": "URL do servidor", - "infrastructureDisplayNameLabel": "Nome de exibição", - "infrastructureAuthorizationSecretLabel": "API Secret", - "gb": "GB", - "mb": "MB", - "mem": "Mem", - "memoryUsage": "Uso de memória", - "cpu": "CPU", - "cpuUsage": "Uso da CPU", - "cpuTemperature": "Temperatura da CPU", - "diskUsage": "Uso de disco", - "used": "Usado", - "total": "Total", - "cores": "Núcleos", - "frequency": "Frequência", - "status": "Status", - "cpuPhysical": "CPU (Física)", - "cpuLogical": "CPU (Lógica)", - "cpuFrequency": "Frequência da CPU", - "avgCpuTemperature": "Temperatura média da CPU", - "memory": "Memória", - "disk": "Disco", - "uptime": "Uptime", - "os": "OS", - "host": "Host", - "actions": "Ações", - "integrations": "Integrações", - "integrationsPrism": "Conecte o Prism ao seu serviço favorito.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Conecte-se com o Slack e veja incidentes em um canal", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Conecte-se com o Discord e visualize incidentes diretamente em um canal", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Envie todos os incidentes para o Zapier e veja-os em todos os lugares", - "commonSave": "Salvar", - "createYour": "Crie o seu", - "createMonitor": "Criar monitor", - "pause": "Pausar", - "resume": "Resumir", - "editing": "Editando...", - "url": "URL", - "access": "Acesso", - "timezone": "Fuso horário", - "features": "Recursos", - "administrator": "Administrador?", - "loginHere": "Login aqui", - "displayName": "Nome de exibição", - "urlMonitor": "URL do monitor", - "portToMonitor": "Porta do monitor", - "websiteMonitoring": "Monitoramento de sites", - "websiteMonitoringDescription": "Use HTTP(s) para monitorar seu site ou API endpoint.", - "pingMonitoring": "Monitoramento de ping", - "pingMonitoringDescription": "Verifique se o seu servidor está disponível ou não.", - "dockerContainerMonitoring": "Monitoramento de contêiner Docker", - "dockerContainerMonitoringDescription": "Verifique se o seu contêiner Docker está em execução ou não.", - "portMonitoring": "Monitoramento de porta", - "portMonitoringDescription": "Verifique se sua porta está aberta ou não.", - "createMaintenanceWindow": "Criar janela de manutenção", - "createMaintenance": "Criar manutenção", - "editMaintenance": "Editar manutenção", - "maintenanceWindowName": "Nome da janela de manutenção", - "friendlyNameInput": "Nome amigável", - "friendlyNamePlaceholder": "Manutenção em __ : __ por ___ minutos", - "maintenanceRepeat": "Repetição de manutenção", - "maintenance": "manutenção", - "duration": "Duração", - "addMonitors": "Adicionar monitores", - "window": "janela", - "cancel": "Cancelar", - "message": "Mensagem", - "low": "baixo", - "high": "alto", - "statusCode": "Código de status", - "date&Time": "Data & Hora", - "type": "Tipo", - "statusPageName": "Nome da página de status", - "publicURL": "URL publica", - "repeat": "Repetir", - "edit": "Editar", - "createA": "Crie um", - "remove": "Remover", - "maintenanceWindowDescription": "Seus pings não serão enviados durante esse período", - "startTime": "Hora de início", - "timeZoneInfo": "Todas as datas e horários estão no fuso horário GMT+0.", - "monitorsToApply": "Monitores para aplicar a janela de manutenção", - "nextWindow": "Próxima janela", - "notFoundButton": "Vá para o painel principal", - "pageSpeedConfigureSettingsDescription": "Aqui você pode selecionar a URL do host, juntamente com o tipo de monitor.", - "monitorDisplayName": "Nome de exibição do monitor", - "whenNewIncident": "Quando há um novo incidente,", - "notifySMS": "Notificar via SMS (em breve)", - "notifyEmails": "Também notificar por e-mail para vários endereços (em breve)", - "seperateEmails": "Você pode separar vários e-mails com uma vírgula", - "checkFrequency": "Verifique a frequência", - "matchMethod": "Método de correspondência", - "expectedValue": "Valor esperado", - "deleteDialogTitle": "Você realmente deseja excluir este monitor?", - "deleteDialogDescription": "Uma vez excluído, este monitor não poderá ser recuperado.", - "pageSpeedMonitor": "Monitor PageSpeed", - "shown": "Mostrado", - "ago": "atrás", - "companyName": "Nome da empresa", - "pageSpeedDetailsPerformanceReport": "Os valores são estimados e podem variar.", - "pageSpeedDetailsPerformanceReportCalculator": "Veja calculadora", - "checkingEvery": "Verificando a cada", - "statusPageCreateSettings": "Se sua página de status estiver pronta, você pode marcá-la como publicada.", - "basicInformation": "Informações Básicas", - "statusPageCreateBasicInfoDescription": "Defina o nome da empresa e o subdomínio para o qual sua página de status aponta.", - "statusPageCreateSelectTimeZoneDescription": "Selecione o fuso horário em que será exibido na sua página de status.", - "statusPageCreateAppearanceDescription": "Defina a aparência padrão da sua página de status pública.", - "statusPageCreateSettingsCheckboxLabel": "Publicado e visível ao público", - "statusPageCreateBasicInfoStatusPageAddress": "Endereço da sua página de status", - "statusPageCreateTabsContent": "Servidores de páginas de status", - "statusPageCreateTabsContentDescription": "Você pode adicionar quantos monitores quiser à sua página de status. Você também pode reordená-los para uma melhor experiência de visualização.", - "statusPageCreateTabsContentFeaturesDescription": "Mostrar mais detalhes na página de status", - "showCharts": "Mostrar gráficos", - "showUptimePercentage": "Mostrar porcentagem de Uptime", - "removeLogo": "Remover logo", - "statusPageStatus": "Uma página de status pública não está configurada.", - "statusPageStatusContactAdmin": "Entre em contato com seu administrador", - "statusPageStatusNotPublic": "Esta página de status não é pública.", - "statusPageStatusNoPage": "Não há página de status aqui.", - "statusPageStatusServiceStatus": "Status do serviço", - "deleteStatusPage": "Você quer excluir esta página de status?", - "deleteStatusPageConfirm": "Sim, excluir página de status", - "deleteStatusPageDescription": "Uma vez excluída, sua página de status não poderá ser recuperada.", - "uptimeCreate": "O valor esperado é usado para corresponder ao resultado da resposta, e a correspondência determina o status.", - "uptimeCreateJsonPath": "Esta expressão será avaliada em relação aos dados JSON de resposta e o resultado será usado para corresponder ao valor esperado. Veja", - "uptimeCreateJsonPathQuery": "para documentação de linguagem de consulta.", - "maintenanceTableActionMenuDialogTitle": "Você realmente deseja remover esta janela de manutenção?", - "infrastructureEditYour": "Edite seu", - "infrastructureEditMonitor": "Salvar Monitor de Infraestrutura", - "infrastructureMonitorCreated": "Monitor de infraestrutura criado com sucesso!", - "infrastructureMonitorUpdated": "Monitor de infraestrutura atualizado com sucesso!", - "errorInvalidTypeId": "Tipo de notificação fornecido inválido", - "errorInvalidFieldId": "ID do campo fornecido inválido", - "inviteNoTokenFound": "Nenhum token de convite encontrado", - "pageSpeedWarning": "Aviso: você ainda não adicionou uma chave de API do Google PageSpeed. Sem ela, o monitor PageSpeed ​​não funcionará.", - "pageSpeedLearnMoreLink": "Clique aqui", - "pageSpeedAddApiKey": "para adicionar sua chave de API.", - "update": "Atualizar", - "invalidFileFormat": "Formato de arquivo não suportado!", - "invalidFileSize": "O tamanho do arquivo é muito grande!", - "ClickUpload": "Clique para enviar", - "DragandDrop": "arraste e solte", - "MaxSize": "Tamanho Máximo", - "SupportedFormats": "Formatos suportados", - "FirstName": "Nome", - "LastName": "Sobrenome", - "EmailDescriptionText": "Este é seu endereço de e-mail atual — ele não pode ser alterado.", - "YourPhoto": "Foto de perfil", - "PhotoDescriptionText": "Esta foto será exibida na sua página de perfil.", - "save": "Salvar", - "DeleteDescriptionText": "Isso removerá a conta e todos os dados associados do servidor. Essa ação não é reversível.", - "DeleteAccountWarning": "Remover sua conta significa que você não poderá fazer login novamente e todos os seus dados serão removidos. Isso não é reversível.", - "DeleteWarningTitle": "Deseja realmente remover esta conta?", - "bulkImport": { - "title": "Bulk Import", - "selectFileTips": "Selecione o arquivo CSV para enviar", - "selectFileDescription": "Você pode baixar nosso ou amostra", - "selectFile": "Selecione o arquivo", - "parsingFailed": "Falha na análise", - "uploadSuccess": "Monitores criados com sucesso!", - "validationFailed": "Falha na validação", - "noFileSelected": "Nenhum arquivo selecionado", - "fallbackPage": "Importe um arquivo para enviar uma lista de servidores em massa", - "invalidFileType": "Tipo de arquivo inválido", - "uploadFailed": "Falha no upload" - }, - "DeleteAccountTitle": "Remover conta", - "DeleteAccountButton": "Remover conta", - "publicLink": "Link publico", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "Resetar", - "ignoreTLSError": "Ignorar erro TLS/SSL", - "tlsErrorIgnored": "Erros TLS/SSL ignorados", - "ignoreTLSErrorDescription": "Ignore os erros de TLS/SSL e continue verificando a disponibilidade do site", - "createNew": "Criar novo", - "greeting": { - "prepend": "Ei", - "append": "A tarde é seu playground — vamos torná-la épica!", - "overview": "Aqui está uma visão geral dos seus monitores {{type}}." - }, - "roles": { - "superAdmin": "Super Admin", - "admin": "Admin", - "teamMember": "Membro da equipe", - "demoUser": "Usuário demo" - }, - "teamPanel": { - "teamMembers": "Membros da equipe", - "filter": { - "all": "Todos", - "member": "Membro" - }, - "inviteTeamMember": "Convidar um membro da equipe", - "inviteNewTeamMember": "Convidar novo membro da equipe", - "inviteDescription": "Ao adicionar um novo membro da equipe, ele terá acesso a todos os monitores.", - "email": "E-mail", - "selectRole": "Selecione a cargo", - "inviteLink": "Link de convite", - "cancel": "Cancelar", - "noMembers": "Não há membros da equipe com este cargo", - "getToken": "Obter token", - "emailToken": "Token de e-mail", - "table": { - "name": "Nome", - "email": "E-mail", - "role": "Cargo", - "created": "Criado" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "Pausado", - "resumed": "Retomado", - "active": "Ativo" - }, - "menu": { - "uptime": "Uptime", - "pagespeed": "Pagespeed", - "infrastructure": "Infraestrutura", - "incidents": "Incidentes", - "statusPages": "Pagina de status", - "maintenance": "Manutenção", - "integrations": "Integrações", - "settings": "Configurações", - "support": "Suporte", - "discussions": "Discussões", - "docs": "Docs", - "changelog": "Changelog", - "profile": "Perfil", - "password": "Senha", - "team": "Equipe", - "logOut": "Sair", - "notifications": "Notificações", - "logs": "Registros" - }, - "settingsEmailUser": "Usuário de e-mail - Nome de usuário para autenticação, substitui o endereço de e-mail, se especificado", - "state": "Estado", - "statusBreadCrumbsStatusPages": "Pagina de status", - "statusBreadCrumbsDetails": "Detalhes", - "commonSaving": "Salvando...", - "navControls": "Controles", - "incidentsPageTitle": "Incidentes", - "passwordPanel": { - "passwordChangedSuccess": "Sua senha foi alterada com sucesso.", - "passwordInputIncorrect": "Sua senha digitada está incorreta.", - "currentPassword": "Senha atual", - "enterCurrentPassword": "Digite sua senha atual", - "newPassword": "Nova Senha", - "enterNewPassword": "Digite sua nova senha", - "confirmNewPassword": "Confirme a nova senha", - "passwordRequirements": "A nova senha deve conter pelo menos 8 caracteres e deve ter pelo menos uma letra maiúscula, uma letra minúscula, um número e um caractere especial.", - "saving": "Salvando..." - }, - "emailSent": "E-mail enviado com sucesso", - "failedToSendEmail": "Falha ao enviar e-mail", - "settingsTestEmailSuccess": "E-mail de teste enviado com sucesso", - "settingsTestEmailFailed": "Falha ao enviar e-mail de teste", - "settingsTestEmailFailedWithReason": "Falha ao enviar e-mail de teste: {{reason}}", - "settingsTestEmailUnknownError": "Erro desconhecido", - "statusMsg": { - "paused": "O monitoramento está pausado.", - "up": "Seu site está no ar.", - "down": "Seu site está fora do ar.", - "pending": "Pendente..." - }, - "uptimeGeneralInstructions": { - "http": "Insira a URL ou IP para monitorar (ex. https://exemplo.com.br/ ou 192.168.1.100) e adicione uma descrição que aparecerá na dashboard.", - "ping": "Insira o endereço IP ou nome de domínio para ping (ex. 192.168.1.100 ou exemplo.com.br) e adicione uma descrição que aparecerá na dashboard.", - "docker": "Insira o Docker Id do seu container. Docker Ids devem ser todos os 64 caracteres. Você pode executar docker inspect para descobrir o Id completo.", - "port": "Insira a URL ou o IP do servidor, aporta e uma descrição que aparecerá na dashboard.", - "game": "", - "https": "" - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "Trocar claro e escuro" + "auth": { + "roles": { + "admin": "Admin", + "demo": "Demo", + "superadmin": "Super admin", + "user": "Usuário" + } }, - "toasts": { - "networkError": "Erro de rede", - "checkConnection": "Por favor, verifique a conexão", - "unknownError": "Erro desconhecido" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "Continuar", - "back": "Voltar" + "alerts": { + "pageSpeedApiKey": { + "content": "Aviso: Você ainda não adicionou uma chave de API do Google PageSpeed. Acesse Configurações para adicionar uma. Sem ela, o monitor PageSpeed não funcionará." + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "Detalhes", + "home": "Início" + }, + "buttons": { + "addMember": "Adicionar membro", + "cancel": "Cancelar", + "close": "Fechar", + "configure": "Configurar", + "confirm": "Confirmar", + "create": "Criar", + "delete": "Excluir", + "generateToken": "Gerar token", + "incidents": "Incidentes", + "inviteMember": "Convidar membro", + "pause": "Pausar", + "resume": "Retomar", + "save": "Salvar", + "sendInvite": "Enviar convite", + "test": "Testar", + "testNotifications": "Testar notificações", + "toggleTheme": "Trocar claro e escuro", + "flushQueue": "Limpar fila", + "notFound": "Ir para o painel principal", + "resetPassword": "Redefinir senha", + "reset": "Resetar", + "clear": "Limpar", + "addDemo": "Adicionar monitores demo", + "removeMonitors": "Remover monitores", + "sendTestEmail": "Enviar email de teste", + "exportToJSON": "Exportar para JSON", + "importFromJSON": "Importar de JSON", + "clearFilters": "Limpar filtros", + "removeUser": "Remover usuário" + }, + "charts": { + "labels": { + "averageResponseTime": "Tempo médio de resposta", + "downtime": "Tempo inativo", + "high": "alto", + "low": "baixo", + "uptime": "Uptime" }, - "inputs": { - "email": { - "label": "Email", - "placeholder": "joao.silva@dominio.com.br", - "errors": { - "empty": "Para continuar, insira seu endereço de email", - "invalid": "Por favor, confirme o endereço de email informado" - } - }, - "password": { - "label": "Senha", - "rules": { - "length": { - "beginning": "Deve ter no mínimo", - "highlighted": "8 caracteres de comprimento" - }, - "special": { - "beginning": "Deve conter pelo menos", - "highlighted": "um caractere especial" - }, - "number": { - "beginning": "Deve ter pelo menos", - "highlighted": "um número" - }, - "uppercase": { - "beginning": "Deve ter pelo menos", - "highlighted": "uma letra maiúscula" - }, - "lowercase": { - "beginning": "Deve ter pelo menos", - "highlighted": "uma letra minúscula" - }, - "match": { - "beginning": "Senha e confirmação de senha", - "highlighted": "devem combinar" - } - }, - "errors": { - "empty": "Por favor, digite sua senha", - "length": "A senha deve ter pelo menos 8 caracteres", - "uppercase": "A senha deve conter pelo menos 1 letra maiúscula", - "lowercase": "Senha deve ter pelo menos uma letra minúscula", - "number": "Senha deve ter pelo menos um número", - "special": "Senha deve ter pelo menos um caractere especial", - "incorrect": "A senha que você forneceu não está registrada" - } - }, - "passwordConfirm": { - "label": "Confirme a senha", - "placeholder": "Informe a senha novamente para confirmar", - "errors": { - "empty": "Por favor, informe a senha novamente para confirmação (cuidado com erros de digitação)", - "different": "As senhas informadas não combinam, então uma deve estar errada" - } - }, - "firstName": { - "label": "Nome", - "placeholder": "João", - "errors": { - "empty": "Por favor, insira seu nome", - "length": "Nome deve ter menos de 50 caracteres", - "pattern": "Nome deve conter apenas letras, espaços, apóstrofos ou hífens" - } - }, - "lastName": { - "label": "Sobrenome", - "placeholder": "Silva", - "errors": { - "empty": "Por favor, insira seu sobrenome", - "length": "Sobrenome deve ter menos de 50 caracteres", - "pattern": "Sobrenome deve conter apenas letras, espaços, apóstrofos ou hífens" + "histogram": { + "avg": "Média: {{value}} ms", + "max": "Máx: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Esta ação não pode ser desfeita.", + "title": "Tem certeza de que deseja excluir isto?" + } + }, + "labels": { + "active": "Ativo", + "paused": "pausado", + "na": "N/D", + "resolved": "Resolvido", + "responseTime": "Tempo de resposta" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Funções" } } }, - "errors": { - "validation": "Erro de validação de dados." - }, - "fields": { - "password": { - "errors": { - "incorrect": "A senha que você forneceu não corresponde aos nossos registros" + "name": { + "option": { + "firstName": { + "label": "Nome", + "placeholder": "João" + }, + "lastName": { + "label": "Sobrenome", + "placeholder": "Silva" } - }, - "role": { - "errors": { - "min": "Pelo menos uma função é necessária" + } + }, + "email": { + "option": { + "email": { + "label": "Email", + "placeholder": "voce@exemplo.com" } } } }, - "login": { - "heading": "Entrar", - "subheadings": { - "stepOne": "Insira seu email", - "stepTwo": "Insira sua senha" - }, - "links": { - "forgotPassword": "Esqueceu sua senha?", - "register": "Não tem uma conta?", - "forgotPasswordLink": "Redefinir senha", - "registerLink": "Registre-se aqui" - }, - "toasts": { - "success": "Bem-vindo de volta! Você está autenticado.", - "incorrectPassword": "Senha incorreta" - }, + "table": { + "empty": "Nada aqui", + "headers": { + "actions": "Ações", + "dateTime": "Data e hora", + "message": "Mensagem", + "monitor": "Monitor", + "monitorId": "ID do monitor", + "name": "Nome", + "status": "Status", + "type": "Tipo", + "url": "URL", + "interval": "Intervalo", + "active": "Ativo", + "responseTime": "Tempo de resposta" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "Clique para enviar", + "dragAndDrop": "arraste e solte", + "supportedFormats": "Formatos suportados", + "maxSize": "Tamanho Máximo", + "orDragAndDrop": "ou arraste e solte", "errors": { - "password": { - "incorrect": "A senha que você forneceu não corresponde aos nossos registros" - } - }, - "welcome": "" + "invalidFileSize": "O tamanho do arquivo é muito grande!", + "invalidFileFormat": "Formato de arquivo não suportado!" + } }, - "registration": { - "heading": { - "superAdmin": "Crie um usuário administrador", - "user": "Se inscrever" - }, - "subheadings": { - "stepOne": "Informe seus dados pessoais", - "stepTwo": "Informe seu email", - "stepThree": "Crie sua senha" + "headerStatusPageControls": { + "publicLink": "Link publico" + }, + "headerTimeRange": { + "labels": { + "day": "Dia", + "month": "Mês", + "recent": "Recente", + "week": "Semana" }, "description": { - "superAdmin": "Crie sua conta de superadministrador para começar", - "user": "Cadastre-se como usuário e peça ao superadministrador acesso aos seus monitores" - }, - "gettingStartedButton": { - "superAdmin": "Criar conta de superadministrador", - "user": "Cadastre-se como usuário normal" - }, - "termsAndPolicies": "Ao criar uma conta, você concorda com nossos Termos de Serviço e Política de Privacidade.", - "links": { - "login": "Já tem uma conta? Entrar" - }, - "toasts": { - "success": "Bem-vindo! Sua conta foi criada com sucesso." - }, - "welcome": "" - }, - "forgotPassword": { - "heading": "Esqueceu sua senha?", - "subheadings": { - "stepOne": "Não se preocupe, enviaremos instruções de redefinição.", - "stepTwo": "Enviamos um link de redefinição de senha para ", - "stepThree": "Sua nova senha deve ser diferente das senhas usadas anteriormente.", - "stepFour": "Sua senha foi redefinida com sucesso. Clique abaixo para fazer login magicamente." - }, - "buttons": { - "openEmail": "Abra o aplicativo de e-mail", - "resetPassword": "Redefinir senha" - }, - "imageAlts": { - "passwordKey": "Ícone de chave de senha", - "email": "Ícone de e-mail", - "lock": "Ícone de cadeado", - "passwordConfirm": "Ícone de confirmação de senha" - }, - "links": { - "login": "Voltar para o login", - "resend": "Não recebeu o email? Clique para reenviar" - }, - "toasts": { - "sent": "Instruções enviadas para .", - "emailNotFound": "E-mail não encontrado.", - "redirect": "Redirecionando em ...", - "success": "Sua senha foi redefinida com sucesso.", - "error": "Não foi possível redefinir a senha. Tente novamente mais tarde ou entre em contato com o suporte." + "recent": "Exibindo estatísticas das últimas 2 horas.", + "day": "Exibindo estatísticas das últimas 24 horas.", + "week": "Exibindo estatísticas dos últimos 7 dias.", + "month": "Exibindo estatísticas dos últimos 30 dias." } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "Reconectado ao servidor com sucesso.", - "stillUnreachable": "O servidor ainda está inacessível. Tente novamente mais tarde." + }, + "offlineBanner": { + "serverUnreachable": "Não foi possível conectar ao servidor", + "retry": "Tentar novamente", + "retrying": "Tentando novamente...", + "reconnected": "Reconectado ao servidor com sucesso." + }, + "sidebar": { + "menu": { + "uptime": "Uptime", + "pagespeed": "Pagespeed", + "infrastructure": "Infraestrutura", + "notifications": "Notificações", + "checks": "Verificações", + "incidents": "Incidentes", + "statusPages": "Pagina de status", + "maintenance": "Manutenção", + "logs": "Registros", + "settings": "Configurações" }, - "alertBox": "Erro de conexão do servidor", - "description": "Não conseguimos conectar ao servidor. Verifique sua conexão com a internet ou sua configuração de implantação se o problema persistir.", - "retryButton": { - "default": "Tentar conectar novamente", - "processing": "Conectando..." + "bottomMenu": { + "support": "Suporte", + "discussions": "Discussões", + "docs": "Docs", + "changelog": "Changelog" + }, + "accountMenu": { + "profile": "Perfil", + "password": "Senha", + "team": "Equipe" + }, + "starPrompt": { + "title": "Avalie o Checkmate", + "description": "Veja os últimos lançamentos e ajude a comunidade a crescer no GitHub" + }, + "authFooter": { + "navControls": "Controles", + "logOut": "Sair", + "roles": { + "superAdmin": "Super admin", + "admin": "Admin", + "user": "Usuário", + "demoUser": "Usuário demo" + } } + }, + "starPrompt": { + "title": "Avalie o Checkmate", + "description": "Veja os últimos lançamentos e ajude a comunidade a crescer no GitHub" } }, - "createNotifications": { - "title": "Criar canal de notificação", - "nameSettings": { - "title": "Nome", - "description": "Um nome descritivo para sua integração.", - "nameLabel": "Nome", - "namePlaceholder": "por exemplo, notificações do Slack" + "pages": { + "notFound": { + "title": "Oh não! Você derrubou seu sushi!", + "subtitle": "A URL não existe ou você não tem acesso a ela." }, - "typeSettings": { - "title": "Tipo", - "description": "Selecione o tipo de canal de notificação que você deseja criar.", - "typeLabel": "Tipo" - }, - "emailSettings": { - "title": "E-mail", - "description": "Endereços de e-mail de destino.", - "emailLabel": "Endereço de email", - "emailPlaceholder": "por exemplo john@example.com" - }, - "slackSettings": { - "title": "Folga", - "description": "Configure seu webhook do Slack aqui", - "webhookLabel": "URL do webhook do Slack", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "Configure sua integração com o PagerDuty aqui", - "integrationKeyLabel": "Chave de integração", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discórdia", - "description": "Configure seu webhook do Discord aqui", - "webhookLabel": "URL do webhook do Discord", - "webhookPlaceholder": "https://seu-servidor.com/webhook" - }, - "webhookSettings": { - "title": "Webhook", - "description": "Configure seu webhook aqui", - "webhookLabel": "URL do webhook", - "webhookPlaceholder": "https://seu-servidor.com/webhook" - }, - "testNotification": "Notificação de teste", - "dialogDeleteTitle": "Tem certeza de que deseja excluir esta notificação?", - "dialogDeleteConfirm": "Excluir" - }, - "notificationConfig": { - "title": "Notificações", - "description": "Selecione os canais de notificação que deseja usar" - }, - "monitorStatus": { - "checkingEvery": "Verificando a cada {{intervalo}}", - "withCaptureAgent": "com agente de captura {{versão}}", - "up": "acima", - "down": "abaixo", - "paused": "pausado" - }, - "advancedMatching": "Correspondência avançada", - "sendTestNotifications": "Enviar notificações de teste", - "selectAll": "Selecionar tudo", - "showAdminLoginLink": "Mostrar o link \"Administrador? Efetue login aqui\" na página de status", - "logsPage": { - "title": "Registros", - "description": "Logs do sistema - últimas 1000 linhas", - "tabs": { - "queue": "Fila de tarefas", - "logs": "Logs do servidor", - "diagnostics": "Diagnóstico" - }, - "toast": { - "fetchLogsSuccess": "Logs obtidos com sucesso" - }, - "logLevelSelect": { - "title": "Nível de log", - "values": { - "all": "Todos", - "info": "Informações", - "warn": "Avisar", - "error": "Erro", - "debug": "Depurar" + "account": { + "tabs": { + "profile": "Perfil", + "password": "Senha", + "team": "Equipe" + }, + "form": { + "name": { + "title": "Nome", + "description": "Atualize suas informações pessoais" + }, + "photo": { + "title": "Foto de perfil", + "description": "Envie uma foto de perfil" + }, + "currentPassword": { + "title": "Senha atual", + "description": "Digite sua senha atual para verificar sua identidade", + "option": { + "label": "Senha atual", + "placeholder": "Digite a senha atual" + } + }, + "newPassword": { + "title": "Nova senha", + "description": "Escolha uma senha forte com pelo menos 8 caracteres", + "option": { + "newPassword": { + "label": "Nova senha", + "placeholder": "Digite a nova senha" + }, + "confirm": { + "label": "Confirmar senha", + "placeholder": "Confirme a nova senha" + } + } + }, + "deleteAccount": { + "title": "Excluir conta", + "description": "Esta ação é permanente e não pode ser desfeita" + } + }, + "team": { + "filter": { + "placeholder": "Filtrar por função", + "all": "Todos", + "admin": "Admin", + "member": "Membro" + }, + "table": { + "headers": { + "email": "Email", + "role": "Função", + "created": "Criado" + } + }, + "addMember": { + "title": "Registrar novo membro da equipe", + "description": "Crie uma nova conta de usuário. Compartilhe as credenciais de forma segura após a criação." + }, + "invite": { + "title": "Convidar membro da equipe", + "description": "Envie um convite para se juntar à sua equipe", + "email": { + "label": "Endereço de email", + "placeholder": "Digite o endereço de email" + }, + "role": { + "label": "Função", + "placeholder": "Selecione uma função" + }, + "linkLabel": "Link de convite" + } } - } - }, - "queuePage": { - "title": "Fila", - "refreshButton": "Atualizar", - "flushButton": "Fila de descarga", - "jobTable": { - "title": "Empregos atualmente na fila", - "idHeader": "ID do monitor", - "urlHeader": "URL", - "typeHeader": "Tipo", - "activeHeader": "Ativo", - "lockedAtHeader": "Trancado em", - "runCountHeader": "Contagem de corridas", - "failCountHeader": "Contagem de falhas", - "lastRunHeader": "Última corrida em", - "lastFinishedAtHeader": "Última conclusão em", - "lastRunTookHeader": "A última corrida aconteceu", - "intervalHeader": "" }, - "metricsTable": { - "title": "Métricas de fila", - "metricHeader": "Métrica", - "valueHeader": "Valor" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "Deve ter no mínimo", + "highlighted": "8 caracteres de comprimento" + }, + "lowercase": { + "beginning": "Deve ter pelo menos", + "highlighted": "uma letra minúscula" + }, + "match": { + "beginning": "Senha e confirmação de senha", + "highlighted": "devem combinar" + }, + "number": { + "beginning": "Deve ter pelo menos", + "highlighted": "um número" + }, + "special": { + "beginning": "Deve conter pelo menos", + "highlighted": "um caractere especial" + }, + "uppercase": { + "beginning": "Deve ter pelo menos", + "highlighted": "uma letra maiúscula" + } + }, + "form": { + "option": { + "email": { + "label": "Email", + "placeholder": "eu@exemplo.com" + }, + "password": { + "label": "Senha", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Confirmar senha" + } + } + } + }, + "login": { + "title": "Bem-vindo de volta ao Checkmate!", + "subtitle": "Faça login para continuar", + "submit": "Entrar", + "links": { + "forgotPassword": { + "text": "Esqueceu a senha?", + "linkText": "Redefinir senha" + }, + "register": { + "text": "Não tem uma conta?", + "linkText": "Cadastre-se aqui" + } + } + }, + "register": { + "title": "Bem-vindo ao Checkmate!", + "subtitle": "Cadastre-se para começar", + "submit": "Cadastrar" + }, + "forgotPassword": { + "title": "Esqueceu sua senha?", + "subtitle": "Não se preocupe, enviaremos instruções de recuperação.", + "submit": "Solicitar recuperação", + "links": { + "login": { + "text": "Voltar para", + "linkText": "login" + } + } + }, + "setNewPassword": { + "title": "Redefinir sua senha", + "subtitle": "Sua nova senha deve ser diferente das senhas usadas anteriormente." + } }, - "failedJobTable": { - "title": "Trabalhos com falha", - "monitorIdHeader": "ID do monitor", - "monitorUrlHeader": "URL do monitor", - "failCountHeader": "Contagem de falhas", - "failedAtHeader": "Última falha em", - "failReasonHeader": "Motivo da falha" - } - }, - "export": { - "title": "Monitores de Exportação", - "success": "Monitores exportados com sucesso!", - "failed": "Falha ao exportar monitores" - }, - "monitorActions": { - "title": "Exportação/Importação", - "import": "Monitores de importação", - "export": "Monitores de Exportação", - "deleteSuccess": "Monitor excluído com sucesso", - "deleteFailed": "Falha ao excluir o monitor", - "details": "Detalhes" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "Desenvolvido pela Bluewave Labs", - "labelVersion": "Versão", - "title": "Sobre" + "checks": { + "selects": { + "monitor": { + "all": "Todos os monitores" + }, + "status": { + "all": "Todos", + "down": "Fora do ar", + "up": "Online" + } + }, + "table": { + "empty": "Nenhuma verificação com falha neste período", + "headers": { + "statusCode": "Código de status", + "location": "Localização" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "Adicionar monitores de demonstração", - "description": "Adicione monitores de amostra para fins de demonstração.", - "title": "Monitores de demonstração" + "common": { + "monitors": { + "actions": { + "configure": "Configurar", + "delete": "Excluir", + "generateToken": "Gerar token", + "details": "Detalhes", + "incidents": "Incidentes", + "inviteMember": "Convidar membro", + "openSite": "Abrir site", + "pause": "Pausar", + "resume": "Retomar" + }, + "statBoxes": { + "activeFor": "Ativo há", + "certificateExpiry": "Validade do certificado", + "lastCheck": "Última verificação", + "lastResponseTime": "Último tempo de resposta" + }, + "status": { + "down": "fora do ar", + "breached": "limite excedido", + "initializing": "inicializando", + "maintenance": "manutenção", + "paused": "pausado", + "total": "total", + "up": "online" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Jogo", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Porta", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infraestrutura", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "emailSettings": { - "buttonSendTestEmail": "Enviar e-mail de teste", - "description": "Configure as configurações de e-mail do seu sistema. Elas são usadas para enviar notificações e alertas.", - "descriptionTransport": "Isso cria um transporte SMTP para o NodeMailer", - "labelAddress": "Endereço de e-mail - Usado para autenticação", - "labelConnectionHost": "Host de conexão de e-mail - Nome do host a ser usado na saudação HELO/EHLO", - "labelHost": "Host de e-mail - Nome do host ou endereço IP para conectar", - "labelIgnoreTLS": "Desabilitar STARTTLS: Não use TLS mesmo que o servidor suporte", - "labelPassword": "Senha de e-mail - Senha para autenticação", - "labelPasswordSet": "A senha foi definida. Clique em Redefinir para alterá-la.", - "labelPool": "Habilitar pool de conexões: Reutilize conexões existentes para melhorar o desempenho", - "labelPort": "Porta de e-mail - Porta para conectar", - "labelRejectUnauthorized": "Rejeitar certificados inválidos: rejeitar conexões com certificados autoassinados ou não confiáveis", - "labelRequireTLS": "Forçar STARTTLS: requer atualização de TLS, falha se não for suportado", - "labelSecure": "Usar SSL (recomendado): criptografar a conexão usando SSL/TLS", - "labelTLSServername": "Nome do servidor TLS - Nome do host opcional para validação TLS quando o host é um IP", - "labelUser": "Usuário de e-mail - Nome de usuário para autenticação, substitui o endereço de e-mail, se especificado", - "linkTransport": "Veja as especificações aqui", - "placeholderUser": "Deixe em branco se não for necessário", - "title": "E-mail", - "toastEmailRequiredFieldsError": "Endereço de e-mail, host, porta e senha são obrigatórios" + "createMonitor": { + "form": { + "advanced": { + "description": "Configurações opcionais para casos de uso avançados", + "option": { + "advancedMatching": { + "label": "Usar correspondência avançada" + }, + "expectedValue": { + "label": "Valor esperado" + }, + "jsonPath": { + "description": "Esta expressão será avaliada contra os dados JSON da resposta e o resultado será usado para comparar com o valor esperado. Consulte jmespath.org para a documentação da linguagem de consulta.", + "label": "Expressão JSONPath" + }, + "matchMethod": { + "label": "Método de correspondência", + "equal": "", + "include": "", + "regex": "Regex" + } + }, + "title": "Configurações avançadas" + }, + "frequency": { + "description": "Com que frequência você deseja verificar o status deste monitor?", + "option": { + "frequency": { + "label": "Frequência de verificação", + "value": { + "fifteenMinutes": "15 minutos", + "fifteenSeconds": "", + "fiveMinutes": "5 minutos", + "fourMinutes": "4 minutos", + "oneMinute": "1 minuto", + "tenMinutes": "10 minutos", + "thirtyMinutes": "30 minutos", + "thirtySeconds": "", + "threeMinutes": "3 minutos", + "twoMinutes": "2 minutos" + } + } + }, + "title": "Frequência de verificação" + }, + "general": { + "option": { + "container": { + "label": "Nome/ID do container", + "placeholder": "meu-app ou abcd1234" + }, + "host": { + "label": "Host", + "placeholder": "192.168.1.100 ou exemplo.com" + }, + "name": { + "label": "Nome de exibição", + "placeholder": "ex. Meu Site" + }, + "secret": { + "label": "Chave de autorização", + "placeholder": "Digite sua chave secreta" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "", + "placeholder": "" + }, + "port": { + "label": "Porta para monitorar", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Nome do serviço", + "placeholder": "ex. meu.servico.v1 (deixe vazio para verificação geral)" + }, + "wsUrl": { + "label": "URL do WebSocket", + "placeholder": "wss://exemplo.com/socket" + } + }, + "title": "Configurações gerais", + "description": { + "http": "Digite a URL ou IP para monitorar (ex.: https://exemplo.com/ ou 192.168.1.100) e adicione um nome de exibição claro que aparecerá no painel.", + "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.", + "port": "Insira a URL ou o IP do servidor, aporta e 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.", + "game": "", + "pagespeed": "Acompanhe o desempenho de carregamento da página, Core Web Vitals e pontuações de otimização do seu site.", + "grpc": "Digite o hostname e a porta do servidor gRPC, opcionalmente especifique um nome de serviço para a verificação de saúde e adicione um nome de exibição.", + "websocket": "Digite a URL do WebSocket para monitorar (ex.: wss://exemplo.com/socket) e adicione um nome de exibição.", + "hardware": "Monitore o uso de CPU, memória, disco e temperatura da sua infraestrutura." + } + }, + "ignoreTls": { + "description": "Configure a validação de certificados TLS/SSL para conexões HTTPS.", + "option": { + "tls": { + "label": "Ignorar erros de TLS/SSL" + } + }, + "title": "Configurações de TLS/SSL" + }, + "incidents": { + "description": "Uma janela deslizante é usada para determinar quando um monitor fica fora do ar. O status de um monitor só mudará quando a porcentagem de verificações na janela deslizante atingir o valor especificado.", + "option": { + "checks": { + "label": "Número de verificações na janela deslizante" + }, + "percentage": { + "label": "Qual porcentagem de verificações na janela deslizante deve falhar/ter sucesso antes de mudar o status do monitor?" + } + }, + "title": "Incidentes" + }, + "notifications": { + "description": "Selecione os canais de notificação que deseja usar", + "title": "Notificações" + }, + "type": { + "description": "Selecione o tipo de verificação a ser realizada", + "optionDockerDescription": "Use Docker para monitorar se um container está em execução.", + "optionGameDescription": "Monitore se um servidor de jogo específico está online.", + "optionGrpcDescription": "Monitore serviços gRPC usando o protocolo padrão de verificação de saúde.", + "optionWebSocketDescription": "Monitore endpoints WebSocket para verificar a saúde da conexão e o tempo de resposta.", + "optionHttpDescription": "Use HTTP(S) para monitorar seu site ou endpoint de API.", + "optionPingDescription": "Use ICMP Ping para monitorar se um servidor está online.", + "optionPortDescription": "Monitore se uma porta específica em um servidor está aberta.", + "title": "Tipo" + }, + "thresholds": { + "title": "Limites de alerta", + "description": "Defina os limites em que os alertas devem ser disparados para este monitor de hardware.", + "option": { + "cpuThreshold": { + "label": "Limite de alerta de CPU (%)" + }, + "memoryThreshold": { + "label": "Limite de alerta de memória (%)" + }, + "diskThreshold": { + "label": "Limite de alerta de disco (%)" + }, + "tempThreshold": { + "label": "Limite de alerta de temperatura (°C)" + } + } + }, + "geoChecks": { + "title": "Verificações geodistribuídas", + "description": "Execute verificações de múltiplas localizações geográficas para monitorar a disponibilidade e o desempenho global.", + "option": { + "enabled": { + "label": "Ativar verificações geodistribuídas" + }, + "locations": { + "label": "Localizações", + "placeholder": "Selecionar localizações", + "options": { + "EU": "Europa", + "NA": "América do Norte", + "AS": "Ásia", + "SA": "América do Sul", + "AF": "África", + "OC": "Oceania" + } + }, + "interval": { + "label": "Intervalo de verificação", + "value": { + "fiveMinutes": "5 minutos", + "tenMinutes": "10 minutos", + "fifteenMinutes": "15 minutos", + "thirtyMinutes": "30 minutos" + } + } + } + }, + "url": { + "title": "IP/URL do monitor na página de status", + "description": "Exiba o endereço IP ou a URL do monitor na página de status pública. Se desativado, apenas o nome do monitor será exibido para proteger informações sensíveis.", + "option": { + "showURL": { + "label": "Exibir IP/URL na página de status", + "enabled": "Ativado", + "disabled": "Desativado" + } + } + }, + "stats": { + "title": "Histórico do monitor", + "description": "Limpe todo o histórico de monitoramento e estatísticas da sua equipe. Esta ação é irreversível.", + "buttonText": "Limpar todas as estatísticas", + "dialog": { + "title": "Limpar todo o histórico de monitoramento?", + "description": "Isso excluirá permanentemente todo o histórico de monitoramento, estatísticas e dados de verificação da sua equipe. Esta ação não pode ser desfeita.", + "confirm": "Sim, limpar todas as estatísticas" + } + } + } }, - "pageSpeedSettings": { - "description": "Insira sua chave da API do Google PageSpeed para ativar o monitoramento do Google PageSpeed. Clique em Redefinir para atualizar a chave.", - "labelApiKeySet": "A chave de API está definida. Clique em Redefinir para alterá-la.", - "labelApiKey": "Chave da API do PageSpeed", - "title": "Chave da API do Google PageSpeed" + "editUser": { + "form": { + "roles": { + "title": "Funções", + "description": "Atribua funções ao usuário. Múltiplas funções podem ser selecionadas." + } + }, + "dialog": { + "removeUser": { + "title": "Remover usuário", + "content": "Tem certeza de que deseja remover {{name}} da sua equipe? Esta ação não pode ser desfeita." + } + } }, - "saveButtonLabel": "Salvar", - "statsSettings": { - "clearAllStatsButton": "Limpar todas as estatísticas", - "clearAllStatsDescription": "Limpe todas as estatísticas. Isso é irreversível.", - "clearAllStatsDialogConfirm": "Sim, limpar todas as estatísticas", - "clearAllStatsDialogDescription": "Uma vez removido, o histórico de monitoramento e as estatísticas não podem ser recuperados.", - "clearAllStatsDialogTitle": "Você quer limpar todas as estatísticas?", - "description": "Defina por quanto tempo você deseja manter os dados históricos. Você também pode limpar todos os dados existentes.", - "labelTTL": "Os dias em que você deseja continuar monitorando o histórico.", - "labelTTLOptional": "0 para infinito", - "title": "Histórico do monitor" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Resolver incidente", + "option": { + "comment": { + "label": "Comentário (opcional)", + "placeholder": "Adicione um comentário sobre a resolução..." + } + } + }, + "details": { + "analysis": "Análise do incidente", + "comment": "Comentário:", + "detailsLabel": "Detalhes", + "downtime": "Tempo inativo:", + "message": "Mensagem:", + "monitor": "Monitor:", + "overview": "Visão geral", + "resolutionDetails": "Detalhes da resolução", + "resolutionType": "Tipo:", + "resolutionTypes": { + "automatic": "Automática", + "manual": "Manual" + }, + "resolve": "Resolver incidente", + "resolvedAt": "Resolvido em:", + "resolvedBy": "Resolvido por:", + "startedAt": "Iniciado em:", + "status": "Status:", + "statusCode": "Código de status:", + "timeline": "Linha do tempo", + "title": "Detalhes do incidente", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Todos os monitores", + "monitor": "Monitor", + "resolutionType": "Tipo de resolução", + "resolutionTypes": { + "manual": "Manual", + "automatic": "Automática", + "all": "Todos" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Incidentes ativos", + "active_zero": "Nenhum incidente ativo", + "active_one": "{{count}} incidente ativo", + "active_other": "{{count}} incidentes ativos" + }, + "incidentStats": { + "avgResolutionTime": "Tempo médio de resolução", + "mostAffectedMonitor": "Monitor mais afetado", + "title": "Estatísticas de incidentes", + "totalIncidents": "Total de incidentes" + }, + "latestIncidents": { + "title": "Últimos incidentes" + } + }, + "table": { + "actions": { + "details": "Detalhes", + "goToMonitor": "Ir para o monitor", + "resolveManually": "Resolver manualmente" + }, + "activeIncidents": "Incidentes ativos", + "headers": { + "endTime": "Hora de término", + "resolutionType": "Tipo de resolução", + "startTime": "Hora de início", + "statusCode": "Código de status" + }, + "resolvedIncidents": "Incidentes resolvidos", + "status": { + "active": "Ativo", + "resolved": "Resolvido" + } + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "Remover todos os monitores", - "description": "Remova todos os monitores do seu sistema.", - "dialogConfirm": "Sim, remova todos os monitores", - "dialogDescription": "Uma vez removidos, os monitores não podem ser recuperados.", - "dialogTitle": "Você deseja remover todos os monitores?", - "title": "Reinicialização do sistema" - }, - "timezoneSettings": { - "description": "Selecione o fuso horário usado para exibir datas e horas em todo o aplicativo.", - "label": "Exibir fuso horário", - "title": "Exibir fuso horário" - }, - "title": "Configurações", - "uiSettings": { - "description": "Alterne entre o modo claro e escuro ou altere o idioma da interface do usuário.", - "labelLanguage": "Linguagem", - "labelTheme": "Modo de tema", - "title": "Aparência" - }, - "urlSettings": { - "description": "Exiba o endereço IP ou URL do monitor na página de status pública. Se estiver desabilitado, apenas o nome do monitor será exibido para proteger informações confidenciais.", - "label": "Exibir IP/URL na página de status", - "selectDisabled": "Desabilitado", - "selectEnabled": "Habilitado", - "title": "Monitorar IP/URL na página de status" - }, - "globalThresholds": { - "title": "Limites Globais", - "description": "Configurar limites globais da CPU, Memória, Disco e Temperatura. Se um valor for fornecido, o monitoramento será ativado automaticamente" - } - }, - "statusPageCreate": { - "buttonSave": "Salvar" - }, - "incidentsOptionsHeaderFilterResolved": "Resolvido", - "settingsSave": "Salvar", - "statusPageCreateAppearanceTitle": "Aparência", - "confirmPassword": "Confirme sua senha", - "monitorHooks": { - "failureAddDemoMonitors": "Falha ao adicionar monitores de demonstração", - "successAddDemoMonitors": "Monitores de demonstração adicionados com sucesso" - }, - "settingsAppearance": "Aparência", - "settingsDisplayTimezone": "Exibir fuso horário", - "settingsGeneralSettings": "Configurações gerais", - "incidentsOptionsHeaderTotalIncidents": "Total de incidentes", - "statusPage": { - "deleteSuccess": "Página de status excluída com sucesso", - "deleteFailed": "Falha ao excluir a página de status", - "createSuccess": "Página de status criada com sucesso", - "updateSuccess": "Página de status atualizada com sucesso", - "generalSettings": "Configurações gerais", - "contents": "Conteúdo", - "fallback": { - "checks": [ - "Monitore e exiba a integridade dos seus serviços em tempo real", - "Acompanhe vários serviços e compartilhe seus status", - "Mantenha os usuários informados sobre interrupções e desempenho" - ], - "title": "Uma página de status é usada para:", - "actionButton": "Vamos criar sua primeira página de status!" - } - }, - "testNotificationsDisabled": "Não há notificações configuradas para este monitor. Você precisa adicionar uma clicando no botão \"Configurar\".", - "incidentsTableResolvedAt": "Resolvido em", - "incidentsTableActionResolve": "Resolver", - "checkHooks": { - "failureResolveOne": "Falha ao resolver o incidente.", - "failureResolveAll": "Falha ao resolver todos os incidentes.", - "failureResolveMonitor": "Falha ao resolver incidentes do monitor." - }, - "checkFormError": "Verifique se há erros no formulário.", - "diagnosticsPage": { - "diagnosticDescription": "Diagnóstico do sistema", - "statsDescription": "Estatísticas do sistema", - "gauges": { - "heapAllocationTitle": "Alocação de heap", - "heapAllocationSubtitle": "% de memória disponível", - "heapUsageTitle": "Uso de heap", - "heapUsageSubtitle": "% de memória disponível", - "heapUtilizationTitle": "Utilização de heap", - "heapUtilizationSubtitle": "% do alocado", - "instantCpuUsageTitle": "Uso instantâneo da CPU", - "instantCpuUsageSubtitle": "% de 1s usados pela CPU" - }, - "stats": { - "eventLoopDelayTitle": "Atraso do loop de eventos", - "uptimeTitle": "Tempo de atividade", - "usedHeapSizeTitle": "Tamanho de heap usado", - "totalHeapSizeTitle": "Tamanho total do heap", - "osMemoryLimitTitle": "Limite de memória do sistema operacional" - } - }, - "pageSpeedLighthouseAPI": "Use a API Lighthouse PageSpeed para monitorar seu site", - "time": { - "threeMinutes": "3 minutos", - "fiveMinutes": "5 minutos", - "tenMinutes": "10 minutos", - "twentyMinutes": "20 minutos", - "oneHour": "1 hora", - "oneDay": "1 dia", - "oneWeek": "1 semana", - "fourMinutes": "4 minutos", - "oneMinute": "1 minuto", - "twoMinutes": "2 minutos", - "fifteenSeconds": "", - "thirtySeconds": "" - }, - "general": { - "noOptionsFound": "Nenhuma {{unidade}} encontrada" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [ - "Acompanhe o desempenho dos seus servidores", - "Identifique gargalos e otimize o uso", - "Garanta a confiabilidade com monitoramento em tempo real" - ], - "title": "Um monitor de infraestrutura é usado para:", - "actionButton": "Vamos criar seu primeiro monitor de infraestrutura!" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [ - "Marque seus períodos de manutenção", - "Elimine qualquer mal-entendido", - "Pare de enviar alertas em janelas de manutenção" - ], - "title": "Uma janela de manutenção é usada para:", - "actionButton": "Vamos criar sua primeira janela de manutenção!" - } - }, - "pageSpeed": { - "fallback": { - "checks": [ - "Relatar a experiência do usuário em uma página", - "Ajudar a analisar a velocidade da página", - "Dar sugestões sobre como a página pode ser melhorada" - ], - "title": "Um monitor PageSpeed é usado para:", - "actionButton": "Vamos criar seu primeiro monitor PageSpeed!" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [ - "Verifique se sites ou servidores estão online e responsivos", - "Alerte as equipes sobre tempo de inatividade ou problemas de desempenho", - "Monitore endpoints HTTP, pings, contêineres e portas", - "Acompanhe o histórico de tempo de atividade e tendências de confiabilidade" - ], - "title": "Um monitor de tempo de atividade é usado para:", - "actionButton": "Vamos criar seu primeiro monitor de tempo de atividade!" - } - }, - "editUserPage": { - "form": { - "email": "E-mail", - "firstName": "Primeiro nome", - "lastName": "Sobrenome", - "role": "Funções", - "save": "Salvar" - }, - "table": { - "actionHeader": "Ação", - "roleHeader": "Papel" - }, - "title": "Editar usuário", - "toast": { - "successUserUpdate": "Usuário atualizado com sucesso", - "validationErrors": "Erros de validação" - } - }, - "incidentsPageActionResolveMonitor": "Resolver incidentes de monitoramento", - "incidentsPageActionResolveAll": "Resolver todos os incidentes", - "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "Regex", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "", - "namePlaceholder": "Meu Contêiner", - "placeholder": "abcd1234" - }, - "http": { - "label": "URL para monitorar", - "namePlaceholder": "Google", - "placeholder": "google.com" - }, - "ping": { - "label": "Endereço IP para monitorar", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" - }, - "port": { - "label": "URL para monitorar", - "namePlaceholder": "Localhost:5173", - "placeholder": "localhost" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "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": "Seleção de Discos", - "disk_selection_info": "Nenhum disco detectado no momento.", - "disk_selection_description": "Selecione os discos específicos que você deseja monitorar." + "charts": { + "labels": { + "cpu": "Uso de CPU", + "disk": "Uso de disco", + "memory": "Uso de memória", + "netBytesRecv": "{{name}} - Bytes recebidos", + "netBytesSent": "{{name}} - Bytes enviados", + "temp": "Temp" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Frequência máxima", + "title": "Uso de CPU", + "upperLabel": "Frequência atual" + }, + "disk": { + "lowerLabel": "Livre", + "title": "Uso do disco {{idx}}", + "upperLabel": "Usado" + }, + "memory": { + "lowerLabel": "Livre", + "title": "Uso de memória", + "upperLabel": "Usado" + } + }, + "statBoxes": { + "avgCpuTemperature": "Temperatura média da CPU", + "cpuFrequency": "Frequência da CPU", + "cpuLogical": "CPU (Lógica)", + "cpuPhysical": "CPU (Física)", + "disk": "Disco", + "memory": "Memória", + "os": "SO" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Disco", + "memory": "Memória" + } + }, + "tabs": { + "labels": { + "network": "", + "overview": "Visão geral" + } + }, + "fallback": { + "actionButton": "Criar um monitor!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Um monitor de infraestrutura é usado para:" + } + }, + "logs": { + "tabs": { + "diagnostics": "Diagnóstico", + "logs": "Logs do servidor", + "queue": "Fila de tarefas" + }, + "logLevelSelect": { + "label": "Nível de log" + }, + "jobQueue": "Fila de tarefas", + "failedJobs": "Tarefas com falha", + "noLogs": "Nenhum log encontrado", + "metrics": { + "jobs": "Tarefas", + "activeJobs": "Tarefas ativas", + "failingJobs": "Tarefas com falha", + "totalRuns": "Total de execuções", + "totalFailures": "Total de falhas" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Atraso do event loop", + "uptime": "Uptime", + "usedHeapSize": "Tamanho do heap usado", + "totalHeapSize": "Tamanho total do heap", + "osMemoryLimit": "Limite de memória do SO" + }, + "gauges": { + "heapAllocation": "Alocação de heap", + "heapUsage": "Uso do heap", + "heapUtilization": "Utilização do heap", + "instantCpuUsage": "Uso instantâneo de CPU", + "availableMemoryPercentage": "% de memória disponível", + "allocatedPercentage": "% alocado", + "usedSPercentage": "% de 1s usado pela CPU", + "total": "Total", + "used": "Usado" + } + }, + "table": { + "headers": { + "timestamp": "Data/hora", + "level": "Nível", + "service": "Serviço", + "method": "Método", + "monitorId": "ID do monitor", + "runCount": "Contagem de execuções", + "failCount": "Contagem de falhas", + "lastRunAt": "Última execução em", + "lockedAt": "Bloqueado em", + "lastFinishedAt": "Última conclusão em", + "lastRunTook": "Última execução durou", + "lastFailedAt": "Última falha em", + "failReason": "Motivo da falha" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Criar uma janela de manutenção!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Uma janela de manutenção é usada para:" + }, + "table": { + "headers": { + "nextWindow": "Próxima janela", + "repeat": "Repetir" + } + }, + "form": { + "general": { + "title": "Configurações gerais", + "description": "Defina um nome e a opção de repetição para sua janela de manutenção.", + "option": { + "name": { + "label": "Nome", + "placeholder": "ex. Manutenção semanal" + }, + "repeat": { + "label": "Repetir" + } + } + }, + "startDate": { + "title": "Data de início", + "description": "Selecione a data de início da sua janela de manutenção.", + "option": { + "startDate": { + "label": "Data de início" + } + } + }, + "startTime": { + "title": "Hora de início", + "description": "Defina a hora de início e a duração da sua janela de manutenção. Todos os valores estão em UTC", + "option": { + "duration": { + "label": "Duração" + }, + "startTime": { + "label": "Hora de início" + } + }, + "monitors": { + "title": "Monitores", + "description": "Monitores aos quais a janela de manutenção deve ser aplicada", + "option": { + "addMonitors": { + "label": "Adicionar monitores" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "Criar um canal", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Canais de notificação são usados para:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Token de acesso", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "O endereço para onde as notificações serão enviadas.", + "optionAddress": "Endereço", + "placeholderEmail": "exemplo@exemplo.com", + "placeholderWebhook": "https://seu-servidor.com/webhook", + "title": "Endereço" + }, + "homeServer": { + "optionHomeServer": "Servidor principal", + "placeholder": "exemplo.com" + }, + "matrix": { + "description": "Configure a conexão do seu servidor Matrix para notificações.", + "title": "Configuração do Matrix" + }, + "notificationName": { + "description": "Um nome descritivo para o canal de notificação", + "optionName": "Nome do canal", + "placeholder": "ex. Alertas de produção", + "title": "Nome do canal" + }, + "pagerDuty": { + "description": "Sua chave de integração do PagerDuty para receber alertas.", + "optionIntegrationKey": "Chave de integração", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Chave de integração" + }, + "roomId": { + "optionRoomId": "ID da sala", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Selecione o tipo de canal de notificação a ser criado.", + "optionType": "Tipo", + "title": "Tipo de canal" + }, + "telegram": { + "title": "Configuração do Telegram", + "description": "Para habilitar as notificações do Telegram, crie um bot do Telegram usando o BotFather, um bot oficial para criar e gerenciar bots do Telegram. Em seguida, obtenha o token da API e o ID do chat e os coloque aqui.", + "optionBotToken": "Token do bot", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "ID do chat", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Destino" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "Mudança cumulativa de layout (CLS)", + "fcp": "Primeira exibição de conteúdo (FCP)", + "lcp": "Maior exibição de conteúdo (LCP)", + "si": "Índice de velocidade (SI)", + "tbt": "Tempo total de bloqueio (TBT)" + }, + "legend": { + "title": "Relatório PageSpeed", + "weight": "Peso" + }, + "pie": { + "title": "Relatório de desempenho" + } + }, + "table": { + "headers": { + "pageSpeedScore": "Pontuação PageSpeed" + } + }, + "fallback": { + "actionButton": "Criar um monitor!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "Um monitor PageSpeed é usado para:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Fuso horário de exibição", + "description": "Selecione o fuso horário usado para exibir datas e horários em toda a aplicação.", + "option": { + "timezone": { + "label": "Fuso horário de exibição" + } + } + }, + "ui": { + "title": "Aparência", + "description": "Alterne entre modo claro e escuro, mude o idioma ou personalize o tipo de gráfico.", + "option": { + "theme": { + "label": "Modo do tema", + "light": "Claro", + "dark": "Escuro" + }, + "language": { + "label": "Idioma" + }, + "chartType": { + "label": "Tipo de gráfico", + "histogram": "Histograma", + "heatmap": "Mapa de calor" + } + } + }, + "pagespeed": { + "title": "Chave de API do Google PageSpeed", + "description": "Digite sua chave de API do Google PageSpeed para ativar o monitoramento PageSpeed. Clique em Resetar para atualizar a chave.", + "option": { + "apiKey": { + "label": "Chave de API PageSpeed", + "labelSet": "Chave de API definida. Clique em Resetar para alterá-la.", + "placeholder": "Digite sua chave de API do Google PageSpeed" + } + } + }, + "url": { + "title": "IP/URL do monitor na página de status", + "description": "Exiba o endereço IP ou a URL do monitor na página de status pública. Se desativado, apenas o nome do monitor será exibido para proteger informações sensíveis.", + "option": { + "showURL": { + "label": "Exibir IP/URL na página de status", + "enabled": "Ativado", + "disabled": "Desativado" + } + } + }, + "stats": { + "title": "Histórico do monitor", + "description": "Limpe todo o histórico de monitoramento e estatísticas da sua equipe. Esta ação é irreversível.", + "option": { + "clear": { + "label": "Limpar todas as estatísticas. Isso é irreversível." + } + }, + "dialog": { + "title": "Limpar todo o histórico de monitoramento?", + "description": "Isso não pode ser desfeito" + } + }, + "retention": { + "title": "Retenção de verificações", + "description": "Defina por quanto tempo os dados de verificação são retidos antes de serem limpos automaticamente.", + "option": { + "days": { + "label": "Período de retenção (dias)", + "unlimited": "Ilimitado" + } + } + }, + "thresholds": { + "title": "Limites globais", + "description": "Defina limites globais de CPU, memória, disco e temperatura para monitoramento de infraestrutura. Eles se aplicam a todos os monitores de hardware, a menos que sejam substituídos.", + "option": { + "cpu": { + "label": "Limite de CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Limite de memória (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Limite de disco (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Limite de temperatura (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Configurações de email", + "description": "Configure as configurações de email do seu sistema. Isso é usado para enviar notificações e alertas.", + "descriptionTransport": "Isso cria um transporte SMTP para o NodeMailer", + "descriptionTransportLink": "Veja as especificações aqui", + "option": { + "host": { + "label": "Host de email - Nome do host ou endereço IP para conectar", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Porta de email - Porta para conectar", + "placeholder": "587" + }, + "address": { + "label": "Endereço de email - Usado para autenticação", + "placeholder": "voce@exemplo.com" + }, + "user": { + "label": "Usuário de email - Nome de usuário para autenticação, substitui o endereço de email se especificado", + "placeholder": "Deixe vazio se não for necessário" + }, + "password": { + "label": "Senha de email - Senha para autenticação", + "labelSet": "Senha definida. Clique em Resetar para alterá-la.", + "placeholder": "Digite sua senha" + }, + "tlsServername": { + "label": "Nome do servidor TLS - Nome de host opcional para validação TLS quando o host é um IP", + "placeholder": "exemplo.com" + }, + "connectionHost": { + "label": "Host de conexão de email - Nome de host para usar na saudação HELO/EHLO", + "placeholder": "localhost" + }, + "secure": { + "label": "Usar SSL (recomendado): Criptografar a conexão usando SSL/TLS" + }, + "pool": { + "label": "Ativar pool de conexões: Reutilizar conexões existentes para melhorar o desempenho" + }, + "ignoreTLS": { + "label": "Desativar STARTTLS: Não usar TLS mesmo que o servidor suporte" + }, + "requireTLS": { + "label": "Forçar STARTTLS: Exigir atualização TLS, falhar se não suportado" + }, + "rejectUnauthorized": { + "label": "Rejeitar certificados inválidos: Rejeitar conexões com certificados autoassinados ou não confiáveis" + } + } + }, + "demoMonitors": { + "title": "Monitores demo", + "description": "Adicione monitores de exemplo para fins de demonstração." + }, + "removeMonitors": { + "title": "Redefinição do sistema", + "description": "Remova todos os monitores do seu sistema.", + "dialog": { + "title": "Remover todos os monitores?", + "description": "Isso não pode ser desfeito." + } + }, + "exportMonitors": { + "title": "Exportar monitores" + }, + "importExportMonitors": { + "title": "Importar / Exportar monitores", + "description": "Importe ou exporte os dados dos seus monitores como um arquivo JSON para backup ou transferência." + }, + "about": { + "title": "Sobre", + "developedBy": "Desenvolvido por Bluewave Labs" + }, + "validation": { + "errorMessage": "Por favor, corrija os seguintes erros de validação:" + } + } + }, + "statusPages": { + "deleteSuccess": "Página de status excluída com sucesso", + "fallback": { + "title": "Uma página de status é usada para:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "Criar uma página de status!" + }, + "monitorsList": { + "chartTypeHeatmap": "Mapa de calor", + "chartTypeHistogram": "Histograma", + "noData": "Nenhum dado disponível", + "infrastructure": { + "title": "Infraestrutura", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Memória", + "disk": "Disco", + "usage": "Uso", + "used": "Usado", + "total": "Total" + }, + "uptime": { + "title": "Uptime", + "responseTime": "Tempo de resposta" + } + }, + "statusBar": { + "allDown": "Todos os sistemas estão fora do ar", + "allUp": "Todos os sistemas operacionais", + "degraded": "Alguns sistemas estão com problemas", + "unknown": "Não foi possível determinar o status do sistema" + }, + "table": { + "headers": { + "name": "Nome da página de status", + "url": "URL pública" + }, + "published": "Publicada", + "unpublished": "Não publicada" + }, + "title": "Páginas de status", + "details": { + "empty": { + "title": "Ainda não há nada aqui", + "addMonitor": "Adicione um monitor para começar" + } + }, + "form": { + "access": { + "title": "Acesso", + "description": "Se sua página de status estiver pronta, você pode marcá-la como publicada.", + "option": { + "published": { + "name": "Publicada e visível ao público" + } + } + }, + "basicInfo": { + "title": "Informações básicas", + "description": "Defina o nome da empresa e o subdomínio para onde sua página de status aponta.", + "option": { + "name": { + "label": "Nome da empresa", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Endereço da sua página de status", + "placeholder": "minha-pagina-de-status" + } + } + }, + "monitors": { + "title": "Monitores", + "description": "Selecione os monitores para exibir na sua página de status.", + "noMonitors": "Nenhum monitor selecionado", + "option": { + "monitors": { + "label": "Selecionar monitores", + "placeholder": "Buscar e selecionar monitores..." + } + } + }, + "timezone": { + "title": "Fuso horário", + "description": "Selecione o fuso horário em que sua página de status será exibida.", + "option": { + "timezone": { + "label": "Fuso horário", + "placeholder": "Selecione o fuso horário..." + } + } + }, + "appearance": { + "title": "Aparência", + "description": "Defina a aparência padrão da sua página de status pública.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Cor da marca" + } + } + }, + "features": { + "title": "Recursos", + "description": "Configure quais informações são exibidas na sua página de status.", + "option": { + "showCharts": { + "label": "Mostrar gráficos de tempo de resposta" + }, + "showUptimePercentage": { + "label": "Mostrar porcentagem de uptime" + }, + "showAdminLoginLink": { + "label": "Mostrar link de login do admin" + }, + "showInfrastructure": { + "label": "Mostrar métricas de infraestrutura" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Buscar monitores..." + } + }, + "table": { + "headers": { + "responseTime": "Tempo de resposta" + } + }, + "fallback": { + "actionButton": "Criar um monitor!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Um monitor de uptime é usado para:" + } } } } diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index ab40ecb0d..461187454 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -1,1140 +1,1305 @@ { - "submit": "Подтвердить", - "title": "Название", - "distributedStatusHeaderText": "Охват реального времени и реального устройства", - "distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу", - "settingsDisabled": "Выключено", - "settingsSuccessSaved": "Настройки успешно сохранены", - "settingsFailedToSave": "Не удалось сохранить настройки", - "settingsStatsCleared": "Статистика успешно очищена", - "settingsFailedToClearStats": "Не удалось очистить статистику", - "settingsMonitorsDeleted": "Успешно удалены все мониторы", - "settingsFailedToDeleteMonitors": "Не удалось удалить все мониторы", - "starPromptTitle": "Запустить Checkmate.", - "starPromptDescription": "Ознакомьтесь с последними релизами и помогите развить сообщество на GitHub", - "https": "HTTPS", - "http": "HTTP", - "monitor": "монитор", - "aboutus": "О Нас", - "now": "Сейчас", - "delete": "Удалить", - "configure": "Настроить", - "responseTime": "Время ответа:", - "ms": "мс", - "bar": "Bar", - "area": "Area", - "country": "СТРАНА", - "city": "ГОРОД", - "response": "ОТВЕТ", - "monitorStatusUp": "Монитор {name} ({url}) теперь включен и отвечает", - "monitorStatusDown": "Монитор {name} ({url}) ОТКЛЮЧЕН и не отвечает", - "webhookSendSuccess": "Уведомление Webhook успешно отправлено", - "webhookSendError": "Ошибка отправки уведомления webhook на {platform}", - "webhookUnsupportedPlatform": "Неподдерживаемая платформа: {platform}", - "distributedRightCategoryTitle": "Монитор", - "distributedStatusServerMonitors": "Серверные мониторы", - "distributedStatusServerMonitorsDescription": "Мониторинг состояния связанных серверов", - "distributedUptimeCreateSelectURL": "Здесь вы можете выбрать URL-адрес хоста, а также тип монитора.", - "distributedUptimeCreateChecks": "Проверки, которые необходимо выполнить", - "distributedUptimeCreateChecksDescription": "Вы всегда можете добавить или удалить проверки после добавления своего сайта.", - "distributedUptimeCreateIncidentNotification": "Уведомления об инцидентах", - "distributedUptimeCreateIncidentDescription": "В случае возникновения инцидента сообщите об этом пользователям.", - "distributedUptimeCreateAdvancedSettings": "Расширенные настройки", - "distributedUptimeDetailsNoMonitorHistory": "Для этого монитора пока нет истории проверок.", - "distributedUptimeDetailsStatusHeaderUptime": "Аптайм:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Последнее обновление", - "notifications": { - "enableNotifications": "Включить уведомления {{platform}}", - "testNotification": "Тестовое уведомление", - "addOrEditNotifications": "Добавить или изменить уведомления", - "slack": { - "label": "Slack", - "description": "Чтобы включить уведомления Slack, создайте приложение Slack и включите входящие вебхуки. После этого просто укажите URL вебхука здесь.", - "webhookLabel": "URL вебхука", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "Требуется URL-адрес Slack webhook" - }, - "discord": { - "label": "Discord", - "description": "Чтобы отправить данные на канал Discord из Checkmate через уведомления Discord с использованием веб-хуков, вы можете использовать функцию входящих веб-хуков Discord.", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "Требуется URL-адрес веб-сайта Discord webhook" - }, - "telegram": { - "label": "Telegram", - "description": "Чтобы включить уведомления Telegram, создайте бота Telegram с помощью BotFather, официального бота для создания и управления ботами Telegram. Затем получите токен API и идентификатор чата и запишите их здесь.", - "tokenLabel": "Ваш токен бота", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "ID Вашего Чата", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "Требуется токен Telegram и ID чата" - }, - "webhook": { - "label": "Вебхуки", - "description": "Вы можете настроить пользовательский вебхук для получения уведомлений о возникновении инцидентов.", - "urlLabel": "URL вебхука", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "Требуется URL-адрес webhook" - }, - "testNotificationDevelop": "Тестовое уведомление 2", - "integrationButton": "Интеграция уведомлений", - "testSuccess": "Уведомление о тестировании отправлено успешно!", - "testFailed": "Не удалось отправить тестовое уведомление", - "unsupportedType": "Неподдерживаемый тип уведомления", - "networkError": "Произошла сетевая ошибка", - "fallback": { - "title": "Канал уведомлений используется для:", - "checks": [ - "Дайте знать команде, если сервер ложится или что-то виснет", - "Сообщите инженерам, когда появляется какая-то проблема", - "Уведомляйте админов, если в системе что-то поменялось" - ], - "actionButton": "Давайте создадим ваш первый канал уведомлений!" - }, - "createButton": "Создать канал уведомлений", - "createTitle": "Канал уведомлений", - "create": { - "success": "Уведомление успешно создано", - "failed": "Не удалось создать уведомление." - }, - "fetch": { - "success": "Уведомления успешно получены", - "failed": "Не удалось получить уведомления." - }, - "delete": { - "success": "Уведомление успешно удалено", - "failed": "Не удалось удалить уведомление." - }, - "edit": { - "success": "Уведомление успешно обновлено", - "failed": "Не удалось обновить уведомление." - }, - "test": { - "success": "Тестовое уведомление успешно отправлено", - "failed": "Не удалось отправить тестовое уведомление." - } - }, - "testLocale": "testLocale", - "add": "Добавить", - "monitors": "мониторы", - "distributedUptimeStatusCreateStatusPage": "страница статуса", - "distributedUptimeStatusCreateStatusPageAccess": "Доступ", - "distributedUptimeStatusCreateStatusPageReady": "Если ваша страница статуса готова, вы можете отметить ее как опубликованную.", - "distributedUptimeStatusBasicInfoHeader": "Основная информация", - "distributedUptimeStatusBasicInfoDescription": "Определите название компании и поддомен, на который ссылается ваша страница статуса.", - "distributedUptimeStatusLogoHeader": "Логотип", - "distributedUptimeStatusLogoDescription": "Загрузите логотип для своей страницы статуса", - "distributedUptimeStatusLogoUploadButton": "Загрузить логотип", - "distributedUptimeStatusStandardMonitorsHeader": "Стандартные Мониторы", - "distributedUptimeStatusStandardMonitorsDescription": "Прикрепите стандартные мониторы к своей странице статуса.", - "distributedUptimeStatusCreateYour": "Создайте свой", - "distributedUptimeStatusEditYour": "Отредактируйте свой", - "distributedUptimeStatusPublishedLabel": "Опубликовано и доступно для общественности", - "distributedUptimeStatusCompanyNameLabel": "Название компании", - "distributedUptimeStatusPageAddressLabel": "Адрес вашей страницы статуса", - "distributedUptimeStatus30Days": "30 дней", - "distributedUptimeStatus60Days": "60 дней", - "distributedUptimeStatus90Days": "90 дней", - "distributedUptimeStatusPageNotSetUp": "Страница статуса не настроена.", - "distributedUptimeStatusContactAdmin": "Пожалуйста, свяжитесь с вашим администратором", - "distributedUptimeStatusPageNotPublic": "Эта страница статуса не является публичной.", - "distributedUptimeStatusPageDeleteDialog": "Вы хотите удалить эту страницу статуса?", - "distributedUptimeStatusPageDeleteConfirm": "Да, удалить страницу статуса", - "distributedUptimeStatusPageDeleteDescription": "После удаления ваша страница статуса не может быть восстановлена.", - "distributedUptimeStatusDevices": "Устройства", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "UPT Burned", - "distributedUptimeStatusUptLogo": "Upt Logo", - "incidentsTableNoIncidents": "Инцидентов не зафиксировано", - "incidentsTablePaginationLabel": "инциденты", - "incidentsTableMonitorName": "Имя Монитора", - "incidentsTableStatus": "Статус", - "incidentsTableDateTime": "Дата и Время", - "incidentsTableStatusCode": "Код Статуса", - "incidentsTableMessage": "Сообщение", - "incidentsOptionsHeader": "Инцидент для:", - "incidentsOptionsHeaderFilterBy": "Фильтровать по:", - "incidentsOptionsHeaderFilterAll": "Все", - "incidentsOptionsHeaderFilterDown": "Недоступно", - "incidentsOptionsHeaderFilterCannotResolve": "Невозможно решить", - "incidentsOptionsHeaderShow": "Показать:", - "incidentsOptionsHeaderLastHour": "Последний час", - "incidentsOptionsHeaderLastDay": "Последний день", - "incidentsOptionsHeaderLastWeek": "Последняя неделя", - "incidentsOptionsPlaceholderAllServers": "Все сервера", - "infrastructureCreateYour": "Создайте свой", - "infrastructureCreateGeneralSettingsDescription": "Здесь вы можете выбрать URL-адрес хоста, а также понятное имя и секретный ключ авторизации для подключения к агенту сервера.", - "infrastructureServerRequirement": "Сервер, который вы отслеживаете, должен быть запущен", - "infrastructureCustomizeAlerts": "Настройте оповещения", - "infrastructureAlertNotificationDescription": "Отправлять уведомления пользователям, когда пороговые значения превышают указанный процент.", - "infrastructureCreateMonitor": "Создать Монитор Инфраструктуры", - "infrastructureProtocol": "Протокол", - "infrastructureServerUrlLabel": "URL Сервера", - "infrastructureDisplayNameLabel": "Отображаемое имя", - "infrastructureAuthorizationSecretLabel": "Секрет авторизации", - "gb": "ГБ", - "mb": "МБ", - "mem": "Mem", - "memoryUsage": "Использование памяти", - "cpu": "CPU", - "cpuUsage": "Использование CPU", - "cpuTemperature": "Температура CPU", - "diskUsage": "Использование диска", - "used": "Использовал", - "total": "Всего", - "cores": "Ядра", - "frequency": "Частота", - "status": "Статус", - "cpuPhysical": "CPU (физический)", - "cpuLogical": "CPU (логический)", - "cpuFrequency": "Частота CPU", - "avgCpuTemperature": "Средняя температура CPU", - "memory": "Память", - "disk": "Диск", - "uptime": "Аптайм", - "os": "ОС", - "host": "Хост", - "actions": "Действия", - "integrations": "Интеграции", - "integrationsPrism": "Подключите Prism к вашему любимому сервису.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Подключитесь к Slack и просматривайте инциденты в канале", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Подключитесь к Discord и просматривайте инциденты прямо в канале", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Отправляйте все инциденты в Zapier, и тогда вы сможете видеть их везде", - "commonSave": "Сохранить", - "createYour": "Создать свой", - "createMonitor": "Создать монитор", - "pause": "Пауза", - "resume": "Продолжить", - "editing": "Редактирование...", - "url": "URL", - "access": "Доступ", - "timezone": "Часовой пояс", - "features": "Функции", - "administrator": "Администратор?", - "loginHere": "Войти здесь", - "displayName": "Отображаемое имя", - "urlMonitor": "URL для монитора", - "portToMonitor": "Порт для монитора", - "websiteMonitoring": "Мониторинг сайта", - "websiteMonitoringDescription": "Используйте HTTP(s) для вашего веб-сайта или API-endpoint.", - "pingMonitoring": "Ping мониторинг", - "pingMonitoringDescription": "Проверьте, доступен ли ваш сервер.", - "dockerContainerMonitoring": "Мониторинг Docker-контейнера", - "dockerContainerMonitoringDescription": "Проверьте, запущен ли ваш Docker-контейнер.", - "portMonitoring": "Мониторинг портов", - "portMonitoringDescription": "Проверьте, открыт ли ваш порт.", - "createMaintenanceWindow": "Создать окно техобслуживания", - "createMaintenance": "Создание техобслуживания", - "editMaintenance": "Редактировать техобслуживание", - "maintenanceWindowName": "Название окна техобслуживания", - "friendlyNameInput": "Дружественное имя", - "friendlyNamePlaceholder": "Техническое обслуживание в __ : __ в течение ___ минут", - "maintenanceRepeat": "Повторное техническое обслуживание", - "maintenance": "техобслуживание", - "duration": "Продолжительность", - "addMonitors": "Добавить мониторы", - "window": "окно", - "cancel": "Отмена", - "message": "Сообщение", - "low": "низкий", - "high": "высокий", - "statusCode": "Код статуса", - "date&Time": "Дата и Время", - "type": "Тип", - "statusPageName": "Имя страница статуса", - "publicURL": "Публичный URL", - "repeat": "Повторить", - "edit": "Редактировать", - "createA": "Создать", - "remove": "Удалить", - "maintenanceWindowDescription": "Ваши запросы не будут отправляться в этот период времени.", - "startTime": "Время начала", - "timeZoneInfo": "Все даты и время указаны в часовом поясе GMT+0.", - "monitorsToApply": "Мониторы, к которым применяется окно техобслуживания", - "nextWindow": "Следующее окно", - "notFoundButton": "Перейти на главную панель управления", - "pageSpeedConfigureSettingsDescription": "Здесь вы можете выбрать URL-адрес хоста, а также тип монитора.", - "monitorDisplayName": "Отображаемое имя монитора", - "whenNewIncident": "Когда происходит новый инцидент,", - "notifySMS": "Уведомить по SMS (скоро)", - "notifyEmails": "Также уведомлять по электронной почте на несколько адресов (скоро)", - "seperateEmails": "Вы можете разделить несколько адресов электронной почты запятой.", - "checkFrequency": "Проверить частоту", - "matchMethod": "Метод сопоставления", - "expectedValue": "Ожидаемое значение", - "deleteDialogTitle": "Вы действительно хотите удалить этот монитор?", - "deleteDialogDescription": "После удаления этот монитор не может быть восстановлен.", - "pageSpeedMonitor": "PageSpeed ​​монитор", - "shown": "Показано", - "ago": "назад", - "companyName": "Название компании", - "pageSpeedDetailsPerformanceReport": "Значения являются приблизительными и могут отличаться.", - "pageSpeedDetailsPerformanceReportCalculator": "Посмотреть калькулятор", - "checkingEvery": "Проверка каждого", - "statusPageCreateSettings": "Если ваша страница статуса готова, вы можете отметить ее как опубликованную.", - "basicInformation": "Основная информация", - "statusPageCreateBasicInfoDescription": "Определите название компании и поддомен, на который ссылается ваша страница статуса.", - "statusPageCreateSelectTimeZoneDescription": "Выберите часовой пояс, в котором будет отображаться ваша страница статуса.", - "statusPageCreateAppearanceDescription": "Определите внешний вид и содержание вашей публичной страницы статуса по умолчанию.", - "statusPageCreateSettingsCheckboxLabel": "Опубликовано и доступно для общественности", - "statusPageCreateBasicInfoStatusPageAddress": "Адрес вашей страницы статуса", - "statusPageCreateTabsContent": "Серверы страниц состояния", - "statusPageCreateTabsContentDescription": "Вы можете добавить любое количество серверов, которые вы отслеживаете, на свою страницу статуса. Вы также можете изменить их порядок для лучшего просмотра.", - "statusPageCreateTabsContentFeaturesDescription": "Показать больше подробностей на странице статуса", - "showCharts": "Показать графики", - "showUptimePercentage": "Показать процент работоспособности", - "removeLogo": "Удалить логотип", - "statusPageStatus": "Публичная страница статуса не создана.", - "statusPageStatusContactAdmin": "Пожалуйста, свяжитесь с вашим администратором.", - "statusPageStatusNotPublic": "Эта страница статуса не является публичной.", - "statusPageStatusNoPage": "Здесь нет страницы статуса.", - "statusPageStatusServiceStatus": "Статус услуги", - "deleteStatusPage": "Вы хотите удалить эту страницу статуса?", - "deleteStatusPageConfirm": "Да, удалить страницу статуса", - "deleteStatusPageDescription": "После удаления ваша страница статуса не может быть восстановлена.", - "uptimeCreate": "Ожидаемое значение используется для сопоставления с результатом ответа, и совпадение определяет статус.", - "uptimeCreateJsonPath": "Это выражение будет оценено по данным ответа JSON, а результат будет использован для сопоставления с ожидаемым значением. См.", - "uptimeCreateJsonPathQuery": "для документации по языку запросов.", - "maintenanceTableActionMenuDialogTitle": "Вы действительно хотите удалить это окно обслуживания?", - "infrastructureEditYour": "Отредактируйте свой", - "infrastructureEditMonitor": "Сохранить Монитор Инфраструктуры", - "infrastructureMonitorCreated": "Монитор инфраструктуры успешно создан!", - "infrastructureMonitorUpdated": "Монитор инфраструктуры успешно обновлен!", - "errorInvalidTypeId": "Указанный неверный тип уведомления", - "errorInvalidFieldId": "Указан неверный идентификатор поля", - "inviteNoTokenFound": "Токен приглашения не найден", - "pageSpeedWarning": "Внимание: Вы еще не добавили API-ключ Google PageSpeed. Без него PageSpeed монитор не будет работать.", - "pageSpeedLearnMoreLink": "Нажми здесь", - "pageSpeedAddApiKey": "чтобы добавить свой API-ключ.", - "update": "Обновить", - "invalidFileFormat": "Неподдерживаемый формат файла!", - "invalidFileSize": "Размер файла слишком велик!", - "ClickUpload": "Нажмите, чтобы загрузить", - "DragandDrop": "drag and drop", - "MaxSize": "Максимальный размер", - "SupportedFormats": "Поддерживаемые форматы", - "FirstName": "Имя", - "LastName": "Фамилия", - "EmailDescriptionText": "Это ваш текущий адрес электронной почты — он не может быть изменен.", - "YourPhoto": "Фотография профиля", - "PhotoDescriptionText": "Эта фотография будет отображена на странице вашего профиля.", - "save": "Сохранить", - "DeleteDescriptionText": "Это приведет к удалению учетной записи и всех связанных с ней данных с сервера. Это необратимо.", - "DeleteAccountWarning": "Удаление вашей учетной записи означает, что вы не сможете снова войти в систему, и все ваши данные будут удалены. Это необратимо.", - "DeleteWarningTitle": "Действительно удаляете эту учетную запись?", - "bulkImport": { - "title": "Массовый импорт", - "selectFileTips": "Выберите CSV-файл для загрузки", - "selectFileDescription": "Вы можете скачать наш или образец", - "selectFile": "Выберите файл", - "parsingFailed": "Не удалось выполнить синтаксический анализ", - "uploadSuccess": "Мониторы успешно созданы!", - "validationFailed": "Не удалось выполнить проверку", - "noFileSelected": "Файл не выбран", - "fallbackPage": "Импортируйте файл для массовой загрузки списка серверов", - "invalidFileType": "Неверный тип файла", - "uploadFailed": "Загрузка не удалась" - }, - "DeleteAccountTitle": "Удалить аккаунт", - "DeleteAccountButton": "Удалить аккаунт", - "publicLink": "Публичная ссылка", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "Сброс", - "ignoreTLSError": "Игнорировать ошибку TLS/SSL", - "tlsErrorIgnored": "Ошибки TLS/SSL игнорируются", - "ignoreTLSErrorDescription": "Игнорируйте ошибки TLS/SSL и продолжайте проверять доступность веб-сайта", - "createNew": "Создавать новые", - "greeting": { - "prepend": "Эй там", - "append": "Вторая половина дня — это ваша игровая площадка - давайте сделаем ее эпичной!", - "overview": "Вот обзор ваших мониторов {{type}}." - }, - "roles": { - "superAdmin": "Главный админ", - "admin": "Админ", - "teamMember": "Член команды", - "demoUser": "Демо пользователь" - }, - "teamPanel": { - "teamMembers": "Члены команды", - "filter": { - "all": "Все", - "member": "Член" - }, - "inviteTeamMember": "Пригласить члена команды", - "inviteNewTeamMember": "Пригласите нового члена команды", - "inviteDescription": "Когда вы добавите нового члена команды, он получит доступ ко всем мониторам.", - "email": "Email", - "selectRole": "Выберите роль", - "inviteLink": "Ссылка для приглашения", - "cancel": "Отменить", - "noMembers": "В команде нет членов с такой ролью", - "getToken": "Получить токен", - "emailToken": "E-mail токен", - "table": { - "name": "Имя", - "email": "Email", - "role": "Роль", - "created": "Создано" - }, - "addTeamMember": { - "addMemberMenu": "Добавить члена команды", - "title": "Зарегистрируйте нового члена команды", - "description": "Создайте нового пользователя и передайте ему учётные данные. Этот метод даёт участнику мгновенный доступ ко всем мониторам.", - "addButton": "Добавить участника" - }, - "register": "Зарегистрируйте члена команды", - "registerToast": { - "success": "Созданный пользователем, безопасный обмен учетными данными с участником.", - "dbUserExists": "Пользователь уже существует.", - "unknownError": "Произошла неизвестная ошибка." - }, - "registerTeamMember": { - "title": "Зарегистрировать члена команды", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "Пожалуйста, введите имя", - "pattern": "Имя должно содержать только буквы, пробелы, апострофы или дефисы." - } - }, - "lastName": { - "errors": { - "empty": "Пожалуйста, введите фамилию", - "pattern": "Фамилия должна содержать только буквы, пробелы, апострофы или дефисы." - } - }, - "email": { - "errors": { - "empty": "Чтобы продолжить, введите адрес электронной почты.", - "invalid": "Пожалуйста, перепроверьте правильность введенного адреса электронной почты." - } - }, - "role": { - "errors": { - "empty": "Требуется роль" - } - } - } - } - } - }, - "role": "Роль", - "changeTeamPassword": { - "changePasswordMenu": "Сбросить пароль", - "title": "Сбросить пароль члена команды", - "description": "Создайте новый пароль для этого члена команды. Вам необходимо будет сообщить ему пароль по секретному адресу.", - "success": "Пароль успешно сброшен. Убедитесь, что вы предоставили учётные данные участнику безопасным способом." - } - }, - "monitorState": { - "paused": "Остановлено", - "resumed": "Продолжено", - "active": "Активно" - }, - "menu": { - "uptime": "Аптайм", - "pagespeed": "Pagespeed", - "infrastructure": "Инфраструктура", - "incidents": "Инциденты", - "statusPages": "Страницы статуса", - "maintenance": "Обслуживание", - "integrations": "Интеграции", - "settings": "Настройки", - "support": "Поддержка", - "discussions": "Обсуждения", - "docs": "Документация", - "changelog": "Журнал изменений", - "profile": "Профиль", - "password": "Пароль", - "team": "Команда", - "logOut": "Выйти", - "notifications": "Уведомления", - "logs": "Логи" - }, - "settingsEmailUser": "Email пользователь", - "state": "Состояние", - "statusBreadCrumbsStatusPages": "Страницы статуса", - "statusBreadCrumbsDetails": "Подробности", - "commonSaving": "Сохранение...", - "navControls": "Управления", - "incidentsPageTitle": "Инциденты", - "passwordPanel": { - "passwordChangedSuccess": "Ваш пароль был успешно изменен.", - "passwordInputIncorrect": "Ваш пароль был введен неверно.", - "currentPassword": "Current пароль", - "enterCurrentPassword": "Введите свой текущий пароль", - "newPassword": "Новый пароль", - "enterNewPassword": "Введите свой новый пароль", - "confirmNewPassword": "Подтвердите новый пароль", - "passwordRequirements": "Новый пароль должен содержать не менее 8 символов и содержать как минимум одну заглавную букву, одну строчную букву, одну цифру и один специальный символ.", - "saving": "Сохранение..." - }, - "emailSent": "Письмо успешно отправлено", - "failedToSendEmail": "Не удалось отправить письмо", - "settingsTestEmailSuccess": "Тестовое письмо успешно отправлено", - "settingsTestEmailFailed": "Не удалось отправить тестовое письмо", - "settingsTestEmailFailedWithReason": "Не удалось отправить тестовое письмо: {{reason}}", - "settingsTestEmailUnknownError": "Неизвестная ошибка", - "statusMsg": { - "paused": "Мониторинг приостановлен", - "up": "Ваш сайт запущен.", - "down": "Сайт оффлайн.", - "pending": "Ожидание..." - }, - "uptimeGeneralInstructions": { - "http": "Введите URL-адрес или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное имя, которое будет отображаться на панели инструментов.", - "ping": "Ведите IP-адрес или имя хоста для пинга (например, 192.168.1.100 или example.com) и добавьте понятное имя, которое будет отображаться на панели инструментов.", - "docker": "Введите Docker ID вашего контейнера. Docker ID должен быть полным 64-символьным Docker ID. Вы можете запустить docker inspect , чтобы получить полный ID контейнера.", - "port": "Введите URL-адрес или IP-адрес сервера, номер порта и понятное имя, которое будет отображаться на панели инструментов.", - "game": "Введите IP-адрес или имя хоста и номер порта для пинга (например, 192.168.1.100 или example.com) и выберите тип игры.", - "https": "Введите URL-адрес или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное отображаемое имя, которое будет отображаться на панели управления." - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Захватить", - "buttons": { - "toggleTheme": "Переключить тему" + "auth": { + "roles": { + "admin": "Администратор", + "demo": "Демо", + "superadmin": "Суперадминистратор", + "user": "Пользователь" + } }, - "toasts": { - "networkError": "Ошибка сети", - "checkConnection": "Пожалуйста, проверьте свое сетевое соединение", - "unknownError": "Неизвестная ошибка" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "Продолжить", - "back": "Назад" + "alerts": { + "pageSpeedApiKey": { + "content": "Внимание: Вы ещё не добавили ключ API Google PageSpeed. Перейдите в Настройки, чтобы добавить его. Без него монитор PageSpeed не будет работать." + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "Подробности", + "home": "Главная" + }, + "buttons": { + "addMember": "Добавить участника", + "cancel": "Отмена", + "close": "Закрыть", + "configure": "Настроить", + "confirm": "Подтвердить", + "create": "Создать", + "delete": "Удалить", + "generateToken": "Сгенерировать токен", + "incidents": "Инциденты", + "inviteMember": "Пригласить участника", + "pause": "Пауза", + "resume": "Продолжить", + "save": "Сохранить", + "sendInvite": "Отправить приглашение", + "test": "Тест", + "testNotifications": "Тестовые уведомления", + "toggleTheme": "Переключить тему", + "flushQueue": "Очистить очередь", + "notFound": "Перейти на главную панель", + "resetPassword": "Сбросить пароль", + "reset": "Сброс", + "clear": "Очистить", + "addDemo": "Добавить демо-мониторы", + "removeMonitors": "Удалить мониторы", + "sendTestEmail": "Отправить тестовое письмо", + "exportToJSON": "Экспорт в JSON", + "importFromJSON": "Импорт из JSON", + "clearFilters": "Очистить фильтры", + "removeUser": "Удалить пользователя" + }, + "charts": { + "labels": { + "averageResponseTime": "Среднее время ответа", + "downtime": "Простой", + "high": "высокий", + "low": "низкий", + "uptime": "Аптайм" }, - "inputs": { - "email": { - "label": "Электронная почта", - "placeholder": "jordan.ellis@domain.com", - "errors": { - "empty": "Для продолжения введите свой адрес электронной почты", - "invalid": "Пожалуйста, проверьте правильность введённого адреса электронной почты" - } - }, - "password": { - "label": "Пароль", - "rules": { - "length": { - "beginning": "Должен быть как минимум", - "highlighted": "8 символов в длину" - }, - "special": { - "beginning": "Должен содержать как минимум", - "highlighted": "один спецсимвол" - }, - "number": { - "beginning": "Должен содержать как минимум", - "highlighted": "одно число" - }, - "uppercase": { - "beginning": "Должен содержать как минимум", - "highlighted": "одну заглавную букву" - }, - "lowercase": { - "beginning": "Должен содержать как минимум", - "highlighted": "одну прописную букву" - }, - "match": { - "beginning": "Пароли", - "highlighted": "должны совпадать" - } - }, - "errors": { - "empty": "Пожалуйста, введите пароль", - "length": "Пароль должен быть длиной не менее 8 символов", - "uppercase": "Пароль должен содержать минимум 1 прописную букву", - "lowercase": "Пароль должен содержать минимум 1 строчную букву", - "number": "Пароль должен содержать минимум 1 цифру", - "special": "Пароль должен содержать минимум 1 спецсимвол", - "incorrect": "Предоставленный вами пароль не соответствует нашим записям" - } - }, - "passwordConfirm": { - "label": "Подтвердите пароль", - "placeholder": "Введите пароль еще раз для подтверждения", - "errors": { - "empty": "Пожалуйста, введите свой пароль еще раз для подтверждения (помогает избежать опечаток)", - "different": "Введённые пароли не совпадают, вероятно, один из них напечатан с ошибкой" - } - }, - "firstName": { - "label": "Имя", - "placeholder": "Jordan", - "errors": { - "empty": "Пожалуйста, введите свое имя", - "length": "Имя должно быть короче 50 символов", - "pattern": "Имя должно содержать только буквы, пробелы, апострофы или дефисы" - } - }, - "lastName": { - "label": "Фамилия", - "placeholder": "Ellis", - "errors": { - "empty": "Пожалуйста, введите свою фамилию", - "length": "Фамилия должна быть короче 50 символов", - "pattern": "Фамилия должна содержать только буквы, пробелы, апострофы или дефисы" + "histogram": { + "avg": "Сред: {{value}} мс", + "max": "Макс: {{value}} мс" + } + }, + "dialogs": { + "delete": { + "description": "Это действие нельзя отменить.", + "title": "Вы уверены, что хотите удалить это?" + } + }, + "labels": { + "active": "Активный", + "paused": "приостановлен", + "na": "Н/Д", + "resolved": "Решён", + "responseTime": "Время ответа" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Роли" } } }, - "errors": { - "validation": "Ошибка при проверке данных." - }, - "fields": { - "password": { - "errors": { - "incorrect": "Введённый вами пароль неверный" + "name": { + "option": { + "firstName": { + "label": "Имя", + "placeholder": "Jordan" + }, + "lastName": { + "label": "Фамилия", + "placeholder": "Ellis" } - }, - "role": { - "errors": { - "min": "Требуется хотя бы одна роль" + } + }, + "email": { + "option": { + "email": { + "label": "Эл. почта", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "Войдите, чтобы продолжить", - "subheadings": { - "stepOne": "Введите свою электронную почту", - "stepTwo": "Введите свой пароль" - }, - "links": { - "forgotPassword": "Забыли пароль?", - "register": "У вас нет учетной записи?", - "forgotPasswordLink": "Сбросить пароль", - "registerLink": "Зарегистрируйтесь здесь" - }, - "toasts": { - "success": "С возвращением! Вы успешно вошли в систему.", - "incorrectPassword": "Неверный пароль" - }, + "table": { + "empty": "Здесь пусто", + "headers": { + "actions": "Действия", + "dateTime": "Дата и время", + "message": "Сообщение", + "monitor": "Монитор", + "monitorId": "ID монитора", + "name": "Имя", + "status": "Статус", + "type": "Тип", + "url": "URL", + "interval": "Интервал", + "active": "Активный", + "responseTime": "Время ответа" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "Нажмите для загрузки", + "dragAndDrop": "drag and drop", + "supportedFormats": "Поддерживаемые форматы", + "maxSize": "Максимальный размер", + "orDragAndDrop": "или перетащите", "errors": { - "password": { - "incorrect": "Предоставленный вами пароль не соответствует нашим записям." - } - }, - "welcome": "Добро пожаловать обратно в Checkmate!" + "invalidFileSize": "Размер файла слишком велик!", + "invalidFileFormat": "Неподдерживаемый формат файла!" + } }, - "registration": { - "heading": { - "superAdmin": "Создать супер-администратора", - "user": "Зарегистрироваться" - }, - "subheadings": { - "stepOne": "Введите свои личные данные", - "stepTwo": "Введите вашу электронную почту", - "stepThree": "Создайте свой пароль" + "headerStatusPageControls": { + "publicLink": "Публичная ссылка" + }, + "headerTimeRange": { + "labels": { + "day": "День", + "month": "Месяц", + "recent": "Недавние", + "week": "Неделя" }, "description": { - "superAdmin": "Создайте свой аккаунт супер-администратора, чтобы начать", - "user": "Зарегистрируйтесь как пользователь и обратитесь к супер-администратору для доступа к вашим мониторам" - }, - "gettingStartedButton": { - "superAdmin": "Создать учетную запись супер-администратора", - "user": "Зарегистрироваться как обычный пользователь" - }, - "termsAndPolicies": "Создавая учетную запись, вы соглашаетесь с нашими Условиями предоставления услуг и Политикой конфиденциальности.", - "links": { - "login": "Уже есть учетная запись? Войти" - }, - "toasts": { - "success": "Добро пожаловать! Ваша учетная запись была создана успешно." - }, - "welcome": "Добро пожаловать в Checkmate!" - }, - "forgotPassword": { - "heading": "Забыли пароль?", - "subheadings": { - "stepOne": "Не волнуйтесь, мы вышлем вам инструкции для сброса.", - "stepTwo": "Мы выслали ссылку для сброса пароля на ", - "stepThree": "Ваш новый пароль должен отличаться от ваших предыдущих пароей.", - "stepFour": "Ваш пароль был успешно сброшен. Нажмите ниже, чтобы волшебным образом войти в систему." - }, - "buttons": { - "openEmail": "Открыть Gmail", - "resetPassword": "Сбросить пароль" - }, - "imageAlts": { - "passwordKey": "Password key icon", - "email": "Email icon", - "lock": "Lock icon", - "passwordConfirm": "Password confirm icon" - }, - "links": { - "login": "Вернуться к Входу", - "resend": "Не получили письмо? Нажмите, чтобы отправить повторно" - }, - "toasts": { - "sent": "Инструкция отправлена на .", - "emailNotFound": "Email не найден.", - "redirect": "Перенаправление через ...", - "success": "Ваш пароль успешно сброшен.", - "error": "Не удалось сбросить пароль. Повторите попытку позже или обратитесь в службу поддержки." + "recent": "Статистика за последние 2 часа.", + "day": "Статистика за последние 24 часа.", + "week": "Статистика за последние 7 дней.", + "month": "Статистика за последние 30 дней." } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "Успешное повторное подключение к серверу.", - "stillUnreachable": "Сервер по-прежнему недоступен. Повторите попытку позже." + }, + "offlineBanner": { + "serverUnreachable": "Не удаётся подключиться к серверу", + "retry": "Повторить", + "retrying": "Повторная попытка...", + "reconnected": "Успешное повторное подключение к серверу." + }, + "sidebar": { + "menu": { + "uptime": "Аптайм", + "pagespeed": "Pagespeed", + "infrastructure": "Инфраструктура", + "notifications": "Уведомления", + "checks": "Проверки", + "incidents": "Инциденты", + "statusPages": "Страницы статуса", + "maintenance": "Обслуживание", + "logs": "Логи", + "settings": "Настройки" }, - "alertBox": "Ошибка подключения к серверу", - "description": "Не удаётся подключиться к серверу. Проверьте подключение к Интернету или конфигурацию развёртывания, если проблема не устранена.", - "retryButton": { - "default": "Повторите попытку подключения", - "processing": "Подключение..." + "bottomMenu": { + "support": "Поддержка", + "discussions": "Обсуждения", + "docs": "Документация", + "changelog": "Журнал изменений" + }, + "accountMenu": { + "profile": "Профиль", + "password": "Пароль", + "team": "Команда" + }, + "starPrompt": { + "title": "Star Checkmate", + "description": "Следите за последними релизами и помогайте развивать сообщество на GitHub" + }, + "authFooter": { + "navControls": "Управления", + "logOut": "Выйти", + "roles": { + "superAdmin": "Суперадминистратор", + "admin": "Администратор", + "user": "Пользователь", + "demoUser": "Демо пользователь" + } } + }, + "starPrompt": { + "title": "Star Checkmate", + "description": "Следите за последними релизами и помогайте развивать сообщество на GitHub" } }, - "createNotifications": { - "title": "Создать канал уведомлений", - "nameSettings": { - "title": "Имя", - "description": "Описательное имя для вашей интеграции.", - "nameLabel": "Имя", - "namePlaceholder": "например, уведомления Slack" + "pages": { + "notFound": { + "title": "Ой! Вы уронили суши!", + "subtitle": "Либо URL не существует, либо у Вас нет к нему доступа." }, - "typeSettings": { - "title": "Тип", - "description": "Выберите тип канала уведомлений, который вы хотите создать.", - "typeLabel": "Тип" - }, - "emailSettings": { - "title": "Электронная почта", - "description": "Адреса электронной почты получателей.", - "emailLabel": "Адрес электронной почты", - "emailPlaceholder": "например, john@example.com" - }, - "slackSettings": { - "title": "Slack", - "description": "Настройте свой вебхук Slack здесь", - "webhookLabel": "Slack вебхук URL", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "Настройте интеграцию PagerDuty здесь", - "integrationKeyLabel": "Интеграционный ключ", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discord", - "description": "Настройте вебхук Discord здесь", - "webhookLabel": "Discord вебхук URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "webhookSettings": { - "title": "Вебхук", - "description": "Настройте свой вебхук здесь", - "webhookLabel": "URL-адрес веб-перехватчика", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "testNotification": "Тестовое уведомление", - "dialogDeleteTitle": "Вы уверены, что хотите удалить это уведомление?", - "dialogDeleteConfirm": "Удалить" - }, - "notificationConfig": { - "title": "Уведомления", - "description": "Выберите каналы уведомлений, которые вы хотите использовать" - }, - "monitorStatus": { - "checkingEvery": "Проверка каждый {{interval}}", - "withCaptureAgent": "с агентом захвата {{version}}", - "up": "вверх", - "down": "вниз", - "paused": "приостановлено" - }, - "advancedMatching": "Расширенное сопоставление", - "sendTestNotifications": "Отправить тестовое уведомление", - "selectAll": "Выбрать все", - "showAdminLoginLink": "Показывать ссылку «Администратор? Войти здесь» на странице статуса", - "logsPage": { - "title": "Логи", - "description": "Системные логи - последние 1000 строк", - "tabs": { - "queue": "Очередь заданий", - "logs": "Серверные логи", - "diagnostics": "Диагностика" - }, - "toast": { - "fetchLogsSuccess": "Логи успешно получены" - }, - "logLevelSelect": { - "title": "Уровень логов", - "values": { - "all": "Все", - "info": "Информация", - "warn": "Предупреждение", - "error": "Ошибка", - "debug": "Дебаг" + "account": { + "tabs": { + "profile": "Профиль", + "password": "Пароль", + "team": "Команда" + }, + "form": { + "name": { + "title": "Имя", + "description": "Обновите свою личную информацию" + }, + "photo": { + "title": "Фото профиля", + "description": "Загрузите фото профиля" + }, + "currentPassword": { + "title": "Текущий пароль", + "description": "Введите текущий пароль для подтверждения личности", + "option": { + "label": "Текущий пароль", + "placeholder": "Введите текущий пароль" + } + }, + "newPassword": { + "title": "Новый пароль", + "description": "Выберите надёжный пароль длиной не менее 8 символов", + "option": { + "newPassword": { + "label": "Новый пароль", + "placeholder": "Введите новый пароль" + }, + "confirm": { + "label": "Подтвердите пароль", + "placeholder": "Подтвердите новый пароль" + } + } + }, + "deleteAccount": { + "title": "Удалить аккаунт", + "description": "Это действие необратимо и не может быть отменено" + } + }, + "team": { + "filter": { + "placeholder": "Фильтр по роли", + "all": "Все", + "admin": "Администратор", + "member": "Член" + }, + "table": { + "headers": { + "email": "Эл. почта", + "role": "Роль", + "created": "Создано" + } + }, + "addMember": { + "title": "Зарегистрировать нового участника", + "description": "Создайте новый аккаунт. После создания безопасно передайте учётные данные." + }, + "invite": { + "title": "Пригласить участника команды", + "description": "Отправить приглашение в Вашу команду", + "email": { + "label": "Адрес эл. почты", + "placeholder": "Введите адрес эл. почты" + }, + "role": { + "label": "Роль", + "placeholder": "Выберите роль" + }, + "linkLabel": "Ссылка-приглашение" + } } - } - }, - "queuePage": { - "title": "Очередь", - "refreshButton": "Обновить", - "flushButton": "Очистить очередь", - "jobTable": { - "title": "Задачи в настоящее время в очереди", - "idHeader": "ID монитора", - "urlHeader": "URL", - "typeHeader": "Тип", - "activeHeader": "Активный", - "lockedAtHeader": "Заблокировано на", - "runCountHeader": "Количество запусков", - "failCountHeader": "Количество неудач", - "lastRunHeader": "Последний запуск в", - "lastFinishedAtHeader": "Последний раз закончил в", - "lastRunTookHeader": "Последний забег занял", - "intervalHeader": "Интервал" }, - "metricsTable": { - "title": "Метрики очереди", - "metricHeader": "Метрическая", - "valueHeader": "Ценить" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "Должен быть как минимум", + "highlighted": "8 символов в длину" + }, + "lowercase": { + "beginning": "Должен содержать как минимум", + "highlighted": "одну прописную букву" + }, + "match": { + "beginning": "Пароли", + "highlighted": "должны совпадать" + }, + "number": { + "beginning": "Должен содержать как минимум", + "highlighted": "одно число" + }, + "special": { + "beginning": "Должен содержать как минимум", + "highlighted": "один спецсимвол" + }, + "uppercase": { + "beginning": "Должен содержать как минимум", + "highlighted": "одну заглавную букву" + } + }, + "form": { + "option": { + "email": { + "label": "Эл. почта", + "placeholder": "me@example.com" + }, + "password": { + "label": "Пароль", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Подтвердите пароль" + } + } + } + }, + "login": { + "title": "С возвращением в Checkmate!", + "subtitle": "Войдите, чтобы продолжить", + "submit": "Войти", + "links": { + "forgotPassword": { + "text": "Забыли пароль?", + "linkText": "Сбросить пароль" + }, + "register": { + "text": "Нет аккаунта?", + "linkText": "Зарегистрируйтесь" + } + } + }, + "register": { + "title": "С возвращением в Checkmate!", + "subtitle": "Зарегистрируйтесь, чтобы начать", + "submit": "Регистрация" + }, + "forgotPassword": { + "title": "Забыли пароль?", + "subtitle": "Не беспокойтесь, мы отправим Вам инструкции по сбросу.", + "submit": "Запросить восстановление", + "links": { + "login": { + "text": "Вернуться к", + "linkText": "входу" + } + } + }, + "setNewPassword": { + "title": "Сбросить пароль", + "subtitle": "Новый пароль должен отличаться от ранее использованных паролей." + } }, - "failedJobTable": { - "title": "Неудачные задачи", - "monitorIdHeader": "ID монитора", - "monitorUrlHeader": "URL монитора", - "failCountHeader": "Количество падений", - "failedAtHeader": "Последнее падение в", - "failReasonHeader": "Причина падения" - } - }, - "export": { - "title": "Экспорт мониторов", - "success": "Мониторы успешно экспортированы!", - "failed": "Не удалось экспортировать мониторы" - }, - "monitorActions": { - "title": "Экспорт/Импорт", - "import": "Импортные мониторы", - "export": "Экспорт мониторов", - "deleteSuccess": "Монитор успешно удален", - "deleteFailed": "Не удалось удалить монитор", - "details": "Подробности" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "Разработано Bluewave Labs", - "labelVersion": "Версия", - "title": "Об" + "checks": { + "selects": { + "monitor": { + "all": "Все мониторы" + }, + "status": { + "all": "Все", + "down": "Недоступен", + "up": "Доступен" + } + }, + "table": { + "empty": "Нет проверок с ошибками за этот период", + "headers": { + "statusCode": "Код статуса", + "location": "Расположение" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "Добавить демо мониторы", - "description": "Добавьте образцы мониторов для демонстрации.", - "title": "Демо мониторы" + "common": { + "monitors": { + "actions": { + "configure": "Настроить", + "delete": "Удалить", + "generateToken": "Сгенерировать токен", + "details": "Подробности", + "incidents": "Инциденты", + "inviteMember": "Пригласить участника", + "openSite": "Открыть сайт", + "pause": "Пауза", + "resume": "Продолжить" + }, + "statBoxes": { + "activeFor": "Активен", + "certificateExpiry": "Истечение сертификата", + "lastCheck": "Последняя проверка", + "lastResponseTime": "Последнее время ответа" + }, + "status": { + "down": "недоступен", + "breached": "порог превышен", + "initializing": "инициализация", + "maintenance": "обслуживание", + "paused": "приостановлен", + "total": "всего", + "up": "доступен" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Игра", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Порт", + "optionPagespeed": "PageSpeed", + "optionHardware": "Инфраструктура", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "emailSettings": { - "buttonSendTestEmail": "Отправить тестовое письмо", - "description": "Настройте параметры электронной почты в вашей системе. Она используется для отправки уведомлений и оповещений.", - "descriptionTransport": "Это создает SMTP-транспорт для NodeMailer.", - "labelAddress": "Адрес электронной почты — используется для аутентификации.", - "labelConnectionHost": "Хост подключения электронной почты — имя хоста для использования в приветствии HELO/EHLO.", - "labelHost": "Хост электронной почты — имя хоста или IP-адрес для подключения.", - "labelIgnoreTLS": "Отключить STARTTLS: не использовать TLS, даже если сервер его поддерживает.", - "labelPassword": "Пароль электронной почты - Пароль для аутентификации", - "labelPasswordSet": "Пароль установлен. Нажмите «Сброс», чтобы изменить его.", - "labelPool": "Включить пул соединений: повторно использовать существующие соединения для повышения производительности.", - "labelPort": "Порт электронной почты — порт для подключения", - "labelRejectUnauthorized": "Отклонять недействительные сертификаты: отклонять соединения с самоподписанными или ненадежными сертификатами.", - "labelRequireTLS": "Принудительный STARTTLS: требуется обновление TLS, выдается ошибка, если не поддерживается", - "labelSecure": "Использовать SSL (рекомендуется): зашифровать соединение с помощью SSL/TLS.", - "labelTLSServername": "Имя сервера TLS — необязательное имя хоста для проверки TLS, если хост — это IP-адрес.", - "labelUser": "Пользователь электронной почты — имя пользователя для аутентификации, переопределяет адрес электронной почты, если указан", - "linkTransport": "Технические характеристики смотрите здесь.", - "placeholderUser": "Оставьте пустым, если не требуется", - "title": "Электронная почта", - "toastEmailRequiredFieldsError": "Требуется адрес электронной почты, хост, порт и пароль" + "createMonitor": { + "form": { + "advanced": { + "description": "Дополнительные настройки для расширенных сценариев", + "option": { + "advancedMatching": { + "label": "Использовать расширенное сопоставление" + }, + "expectedValue": { + "label": "Ожидаемое значение" + }, + "jsonPath": { + "description": "Это выражение будет вычислено для JSON-данных ответа, и результат будет сопоставлен с ожидаемым значением. См. документацию по языку запросов на jmespath.org.", + "label": "Выражение JSONPath" + }, + "matchMethod": { + "label": "Метод сопоставления", + "equal": "Равный", + "include": "Включить", + "regex": "Regex" + } + }, + "title": "Расширенные настройки" + }, + "frequency": { + "description": "Как часто Вы хотите проверять статус этого монитора?", + "option": { + "frequency": { + "label": "Частота проверки", + "value": { + "fifteenMinutes": "15 минут", + "fifteenSeconds": "15 секунд", + "fiveMinutes": "5 минут", + "fourMinutes": "4 минуты", + "oneMinute": "1 минута", + "tenMinutes": "10 минут", + "thirtyMinutes": "30 минут", + "thirtySeconds": "30 секунд", + "threeMinutes": "3 минуты", + "twoMinutes": "2 минуты" + } + } + }, + "title": "Частота проверки" + }, + "general": { + "option": { + "container": { + "label": "Имя/ID контейнера", + "placeholder": "my-app или abcd1234" + }, + "host": { + "label": "Хост", + "placeholder": "192.168.1.100 или example.com" + }, + "name": { + "label": "Отображаемое имя", + "placeholder": "напр. Мой сайт" + }, + "secret": { + "label": "Секрет авторизации", + "placeholder": "Введите Ваш секретный ключ" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "URL для мониторинга", + "placeholder": "localhost" + }, + "port": { + "label": "Порт для мониторинга", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Имя сервиса", + "placeholder": "напр. my.service.v1 (оставьте пустым для общей проверки)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Основные настройки", + "description": { + "http": "Введите URL или IP для мониторинга (напр., https://example.com/ или 192.168.1.100) и добавьте понятное имя для отображения на панели.", + "ping": "Ведите IP-адрес или имя хоста для пинга (например, 192.168.1.100 или example.com) и добавьте понятное имя, которое будет отображаться на панели инструментов.", + "port": "Введите URL-адрес или IP-адрес сервера, номер порта и понятное имя, которое будет отображаться на панели инструментов.", + "docker": "Введите Docker ID вашего контейнера. Docker ID должен быть полным 64-символьным Docker ID. Вы можете запустить docker inspect , чтобы получить полный ID контейнера.", + "game": "Введите IP-адрес или имя хоста и номер порта для пинга (например, 192.168.1.100 или example.com) и выберите тип игры.", + "pagespeed": "Отслеживайте производительность загрузки, Core Web Vitals и показатели оптимизации для Вашего сайта.", + "grpc": "Введите имя хоста и порт gRPC-сервера, при необходимости укажите имя сервиса для проверки состояния и добавьте отображаемое имя.", + "websocket": "Введите WebSocket URL для мониторинга (напр., wss://example.com/socket) и добавьте отображаемое имя.", + "hardware": "Мониторинг CPU, памяти, дисков и температуры Вашей инфраструктуры." + } + }, + "ignoreTls": { + "description": "Настройте проверку сертификатов TLS/SSL для HTTPS-подключений.", + "option": { + "tls": { + "label": "Игнорировать ошибки TLS/SSL" + } + }, + "title": "Настройки TLS/SSL" + }, + "incidents": { + "description": "Скользящее окно используется для определения момента, когда монитор становится недоступным. Статус монитора изменится только когда процент проверок в скользящем окне достигнет указанного значения.", + "option": { + "checks": { + "label": "Количество проверок в скользящем окне" + }, + "percentage": { + "label": "Какой процент проверок в скользящем окне должен завершиться неудачей/успехом, прежде чем статус монитора изменится?" + } + }, + "title": "Инциденты" + }, + "notifications": { + "description": "Выберите каналы уведомлений, которые хотите использовать", + "title": "Уведомления" + }, + "type": { + "description": "Выберите тип проверки", + "optionDockerDescription": "Используйте Docker для мониторинга работы контейнера.", + "optionGameDescription": "Мониторинг доступности игрового сервера.", + "optionGrpcDescription": "Мониторинг gRPC-сервисов с использованием стандартного протокола проверки состояния.", + "optionWebSocketDescription": "Мониторинг WebSocket-эндпоинтов: состояние подключения и время ответа.", + "optionHttpDescription": "Используйте HTTP(S) для мониторинга Вашего сайта или API-эндпоинта.", + "optionPingDescription": "Используйте ICMP Ping для проверки доступности сервера.", + "optionPortDescription": "Мониторинг доступности определённого порта на сервере.", + "title": "Тип" + }, + "thresholds": { + "title": "Пороги оповещений", + "description": "Определите пороговые значения, при которых должны срабатывать оповещения для этого монитора оборудования.", + "option": { + "cpuThreshold": { + "label": "Порог оповещения CPU (%)" + }, + "memoryThreshold": { + "label": "Порог оповещения памяти (%)" + }, + "diskThreshold": { + "label": "Порог оповещения диска (%)" + }, + "tempThreshold": { + "label": "Порог оповещения температуры (°C)" + } + } + }, + "geoChecks": { + "title": "Гео-распределённые проверки", + "description": "Запускайте проверки из нескольких географических точек для мониторинга глобальной доступности и производительности.", + "option": { + "enabled": { + "label": "Включить гео-распределённые проверки" + }, + "locations": { + "label": "Расположения", + "placeholder": "Выберите расположения", + "options": { + "EU": "Европа", + "NA": "Северная Америка", + "AS": "Азия", + "SA": "Южная Америка", + "AF": "Африка", + "OC": "Океания" + } + }, + "interval": { + "label": "Интервал проверки", + "value": { + "fiveMinutes": "5 минут", + "tenMinutes": "10 минут", + "fifteenMinutes": "15 минут", + "thirtyMinutes": "30 минут" + } + } + } + }, + "url": { + "title": "IP/URL монитора на странице статуса", + "description": "Отображать IP-адрес или URL монитора на публичной странице статуса. Если отключено, будет показано только имя монитора для защиты конфиденциальной информации.", + "option": { + "showURL": { + "label": "Отображать IP/URL на странице статуса", + "enabled": "Включено", + "disabled": "Отключено" + } + } + }, + "stats": { + "title": "История мониторинга", + "description": "Очистить всю историю мониторинга и статистику Вашей команды. Это действие необратимо.", + "buttonText": "Очистить всю статистику", + "dialog": { + "title": "Очистить всю историю мониторинга?", + "description": "Это навсегда удалит всю историю мониторинга, статистику и данные проверок Вашей команды. Это действие нельзя отменить.", + "confirm": "Да, очистить всю статистику" + } + } + } }, - "pageSpeedSettings": { - "description": "Введите ключ API Google PageSpeed, чтобы включить мониторинг Google PageSpeed. Нажмите «Сбросить», чтобы обновить ключ.", - "labelApiKeySet": "API-ключ установлен. Нажмите «Сброс», чтобы изменить его.", - "labelApiKey": "API-ключ PageSpeed", - "title": "API-ключ Google PageSpeed" + "editUser": { + "form": { + "roles": { + "title": "Роли", + "description": "Назначьте роли пользователю. Можно выбрать несколько ролей." + } + }, + "dialog": { + "removeUser": { + "title": "Удалить пользователя", + "content": "Вы уверены, что хотите удалить {{name}} из Вашей команды? Это действие нельзя отменить." + } + } }, - "saveButtonLabel": "Сохранить", - "statsSettings": { - "clearAllStatsButton": "Очистить всю статистику", - "clearAllStatsDescription": "Очистить всю статистику. Это необратимо.", - "clearAllStatsDialogConfirm": "Да, очистить всю статистику", - "clearAllStatsDialogDescription": "После удаления история и статистика мониторинга не могут быть восстановлены.", - "clearAllStatsDialogTitle": "Вы хотите очистить всю статистику?", - "description": "Укажите, как долго вы хотите хранить данные. Вы также можете удалить все существующие данные.", - "labelTTL": "Дни, за которые вы хотите посмотреть историю.", - "labelTTLOptional": "0 для бесконечности", - "title": "История монитора" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Решить инцидент", + "option": { + "comment": { + "label": "Комментарий (необязательно)", + "placeholder": "Добавьте комментарий о решении..." + } + } + }, + "details": { + "analysis": "Анализ инцидента", + "comment": "Комментарий:", + "detailsLabel": "Подробности", + "downtime": "Простой:", + "message": "Сообщение:", + "monitor": "Монитор:", + "overview": "Обзор", + "resolutionDetails": "Подробности решения", + "resolutionType": "Тип:", + "resolutionTypes": { + "automatic": "Автоматически", + "manual": "Вручную" + }, + "resolve": "Решить инцидент", + "resolvedAt": "Решён в:", + "resolvedBy": "Решён:", + "startedAt": "Начался в:", + "status": "Статус:", + "statusCode": "Код статуса:", + "timeline": "Хронология", + "title": "Подробности инцидента", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Все мониторы", + "monitor": "Монитор", + "resolutionType": "Тип решения", + "resolutionTypes": { + "manual": "Вручную", + "automatic": "Автоматически", + "all": "Все" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Активные инциденты", + "active_zero": "Нет активных инцидентов", + "active_one": "{{count}} активный инцидент", + "active_other": "{{count}} активных инцидентов" + }, + "incidentStats": { + "avgResolutionTime": "Среднее время решения", + "mostAffectedMonitor": "Наиболее затронутый монитор", + "title": "Статистика инцидентов", + "totalIncidents": "Всего инцидентов" + }, + "latestIncidents": { + "title": "Последние инциденты" + } + }, + "table": { + "actions": { + "details": "Подробности", + "goToMonitor": "Перейти к монитору", + "resolveManually": "Решить вручную" + }, + "activeIncidents": "Активные инциденты", + "headers": { + "endTime": "Время окончания", + "resolutionType": "Тип решения", + "startTime": "Время начала", + "statusCode": "Код статуса" + }, + "resolvedIncidents": "Решённые инциденты", + "status": { + "active": "Активный", + "resolved": "Решён" + } + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "Удалить все мониторы", - "description": "Удалите все мониторы из вашей системы.", - "dialogConfirm": "Да, удалить все мониторы", - "dialogDescription": "После удаления, мониторы нельзя будет воостановить.", - "dialogTitle": "Вы хотите удалить все мониторы?", - "title": "Сброс системы" - }, - "timezoneSettings": { - "description": "Выберите часовой пояс, используемый для отображения даты и времени в приложении.", - "label": "Отображение часового пояса", - "title": "Отображение часового пояса" - }, - "title": "Настройки", - "uiSettings": { - "description": "Переключайтесь между светлым и темным режимами или меняйте язык пользовательского интерфейса.", - "labelLanguage": "Язык", - "labelTheme": "Режим темы", - "title": "Появление" - }, - "urlSettings": { - "description": "Отображение IP-адреса или URL-адреса монитора на общедоступной странице состояния. Если эта опция отключена, для защиты конфиденциальной информации будет отображаться только имя монитора.", - "label": "Отображать IP/URL на странице статуса", - "selectDisabled": "Выключено", - "selectEnabled": "Включено", - "title": "Мониторинг IP/URL на странице статуса" - }, - "globalThresholds": { - "title": "Глобальные пороги", - "description": "Настройте глобальные пороговые значения для процессора, памяти, диска и температуры. Если значение указано, оно будет автоматически включено для мониторинга." - } - }, - "statusPageCreate": { - "buttonSave": "Сохранить" - }, - "incidentsOptionsHeaderFilterResolved": "Решено", - "settingsSave": "Сохранить", - "statusPageCreateAppearanceTitle": "Внешний вид", - "confirmPassword": "Подтвердите пароль", - "monitorHooks": { - "failureAddDemoMonitors": "Не удалось добавить демонстрационные мониторы.", - "successAddDemoMonitors": "Демонстрационные мониторы успешно добавлены" - }, - "settingsAppearance": "Внешний вид", - "settingsDisplayTimezone": "Отображение часового пояса", - "settingsGeneralSettings": "Общие настройки", - "incidentsOptionsHeaderTotalIncidents": "Всего инцидентов", - "statusPage": { - "deleteSuccess": "Страница статуса успешно удалена", - "deleteFailed": "Не удалось удалить страницу статуса", - "createSuccess": "Страница статуса успешно создана", - "updateSuccess": "Страница статуса успешно обновлена", - "generalSettings": "Общие настройки", - "contents": "Содержание", - "fallback": { - "checks": [ - "Мониторинг и отображение состояния ваших сервисов в режиме реального времени", - "Отслеживайте состояние нескольких сервисов и делитесь их статусом", - "Дайте пользователям информацию о сбоях и производительности" - ], - "title": "Страница статуса используется для:", - "actionButton": "Давайте создадим вашу первую страницу статуса!" - } - }, - "testNotificationsDisabled": "Для этого монитора не настроены уведомления. Вам нужно добавить их, нажав кнопку «Настроить».", - "incidentsTableResolvedAt": "Решено в", - "incidentsTableActionResolve": "Решено", - "checkHooks": { - "failureResolveOne": "Не удалось разрешить инцидент.", - "failureResolveAll": "Не удалось разрешить все инциденты.", - "failureResolveMonitor": "Не удалось устранить инциденты с монитором." - }, - "checkFormError": "Пожалуйста, проверьте форму на наличие ошибок.", - "diagnosticsPage": { - "diagnosticDescription": "Системная диагностика", - "statsDescription": "Системная статистика", - "gauges": { - "heapAllocationTitle": "Использовано кучи", - "heapAllocationSubtitle": "% доступной памяти", - "heapUsageTitle": "Выделено памяти в куче", - "heapUsageSubtitle": "% доступно памяти", - "heapUtilizationTitle": "Использовано кучи", - "heapUtilizationSubtitle": "% от доступного", - "instantCpuUsageTitle": "Загруженность CPU", - "instantCpuUsageSubtitle": "% загрузка CPU за 1s" - }, - "stats": { - "eventLoopDelayTitle": "Задержка цикла событий", - "uptimeTitle": "Аптайм", - "usedHeapSizeTitle": "Размер используемой кучи", - "totalHeapSizeTitle": "Общий размер кучи", - "osMemoryLimitTitle": "Ограничение памяти ОS" - } - }, - "pageSpeedLighthouseAPI": "Используйте API Lighthouse PageSpeed для мониторинга вашего сайта", - "time": { - "threeMinutes": "3 минуты", - "fiveMinutes": "5 минут", - "tenMinutes": "10 минут", - "twentyMinutes": "20 минут", - "oneHour": "1 час", - "oneDay": "1 день", - "oneWeek": "1 неделя", - "fourMinutes": "4 минуты", - "oneMinute": "1 минута", - "twoMinutes": "2 минуты", - "fifteenSeconds": "15 секунд", - "thirtySeconds": "30 секунд" - }, - "general": { - "noOptionsFound": "{{unit}} не найдено" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [ - "Отслеживайте производительность серверов", - "Выявляйте узкие места и оптимизируйте использование", - "Обеспечивайте надежность с помощью мониторинга в режиме реального времени" - ], - "title": "Монитор инфраструктуры используется для:", - "actionButton": "Давайте создадим ваш первый монитор инфраструктуры!" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [ - "Отметьте периоды технического обслуживания.", - "Устраните любые недопонимания.", - "Прекратите отправку оповещений в периоды технического обслуживания." - ], - "title": "Период технического обслуживания используется для:", - "actionButton": "Давайте создадим ваше первое окно технического обслуживания!" - } - }, - "pageSpeed": { - "fallback": { - "checks": [ - "Отчет об удобстве использования страницы", - "Помощь в анализе скорости загрузки веб-страницы", - "Предложение о том, как можно улучшить страницу" - ], - "title": "Монитор PageSpeed используется для:", - "actionButton": "Давайте создадим ваш первый монитор PageSpeed!" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [ - "Проверьте, доступны ли веб-сайты или серверы и отвечают ли они", - "Оповещайте команды о сбоях или проблемах с производительностью", - "Мониторинг конечных точек HTTP, пингов, контейнеров и портов", - "Отслеживайте динамику времени безотказной работы и надежности" - ], - "title": "Монитор работоспособности используется для:", - "actionButton": "Давайте создадим ваш первый монитор времени безотказной работы!" - } - }, - "editUserPage": { - "form": { - "email": "Электронная почта", - "firstName": "Имя", - "lastName": "Фамилия", - "role": "Роли", - "save": "Сохранить" - }, - "table": { - "actionHeader": "Действие", - "roleHeader": "Роль" - }, - "title": "Редактировать пользователя", - "toast": { - "successUserUpdate": "Пользователь успешно обновлен", - "validationErrors": "Ошибки проверки" - } - }, - "incidentsPageActionResolveMonitor": "Разрешение инцидентов мониторинга", - "incidentsPageActionResolveAll": "Решить все инциденты", - "matchMethodOptions": { - "equal": "Равный", - "equalPlaceholder": "успех", - "include": "Включить", - "includePlaceholder": "хорошо", - "regex": "Regex", - "regexPlaceholder": "^(success|ok)$", - "text": "Метод сопоставления" - }, - "monitorType": { - "docker": { - "label": "Имя/ID контейнера", - "namePlaceholder": "Мой контейнер", - "placeholder": "my-app или abcd1234" - }, - "http": { - "label": "URL для мониторинга", - "namePlaceholder": "Google", - "placeholder": "google.com" - }, - "ping": { - "label": "IP-адрес для мониторинга", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" - }, - "port": { - "label": "URL для мониторинга", - "namePlaceholder": "Локальный хост:5173", - "placeholder": "локальный хост" - }, - "game": { - "label": "URL для мониторинга", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "путь к JSON" - }, - "bytesPerSecond": "Байты в секунду", - "bytesReceived": "Получено байтов", - "bytesSent": "Отправлено байтов", - "chooseGame": "Выбрать игру", - "createMonitorPage": { - "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": "Выберите интерфейс", - "v1": { "infrastructure": { - "disk_selection_title": "Выбор дисков", - "disk_selection_info": "На данный момент диск не обнаружен.", - "disk_selection_description": "Выберите конкретные диски, которые вы хотите отслеживать." + "charts": { + "labels": { + "cpu": "Загрузка CPU", + "disk": "Использование диска", + "memory": "Использование памяти", + "netBytesRecv": "{{name}} - байт получено", + "netBytesSent": "{{name}} - байт отправлено", + "temp": "Температура" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Макс. частота", + "title": "Загрузка CPU", + "upperLabel": "Текущая частота" + }, + "disk": { + "lowerLabel": "Свободно", + "title": "Использование диска {{idx}}", + "upperLabel": "Использовано" + }, + "memory": { + "lowerLabel": "Свободно", + "title": "Использование памяти", + "upperLabel": "Использовано" + } + }, + "statBoxes": { + "avgCpuTemperature": "Средняя температура CPU", + "cpuFrequency": "Частота CPU", + "cpuLogical": "CPU (логический)", + "cpuPhysical": "CPU (физический)", + "disk": "Диск", + "memory": "Память", + "os": "ОС" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Диск", + "memory": "Память" + } + }, + "tabs": { + "labels": { + "network": "Сеть", + "overview": "Обзор" + } + }, + "fallback": { + "actionButton": "Создать монитор!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Монитор инфраструктуры используется для:" + } + }, + "logs": { + "tabs": { + "diagnostics": "Диагностика", + "logs": "Серверные логи", + "queue": "Очередь заданий" + }, + "logLevelSelect": { + "label": "Уровень логов" + }, + "jobQueue": "Очередь заданий", + "failedJobs": "Неудачные задания", + "noLogs": "Логи не найдены", + "metrics": { + "jobs": "Задания", + "activeJobs": "Активные задания", + "failingJobs": "Неудачные задания", + "totalRuns": "Всего запусков", + "totalFailures": "Всего ошибок" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Задержка цикла событий", + "uptime": "Аптайм", + "usedHeapSize": "Используемый размер кучи", + "totalHeapSize": "Общий размер кучи", + "osMemoryLimit": "Лимит памяти ОС" + }, + "gauges": { + "heapAllocation": "Выделение кучи", + "heapUsage": "Использование кучи", + "heapUtilization": "Утилизация кучи", + "instantCpuUsage": "Мгновенная загрузка CPU", + "availableMemoryPercentage": "% доступной памяти", + "allocatedPercentage": "% выделено", + "usedSPercentage": "% 1с использовано CPU", + "total": "Всего", + "used": "Использовано" + } + }, + "table": { + "headers": { + "timestamp": "Метка времени", + "level": "Уровень", + "service": "Сервис", + "method": "Метод", + "monitorId": "ID монитора", + "runCount": "Кол-во запусков", + "failCount": "Кол-во ошибок", + "lastRunAt": "Последний запуск", + "lockedAt": "Заблокировано в", + "lastFinishedAt": "Последнее завершение", + "lastRunTook": "Последний запуск занял", + "lastFailedAt": "Последняя ошибка", + "failReason": "Причина ошибки" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Создать окно обслуживания!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Окно обслуживания используется для:" + }, + "table": { + "headers": { + "nextWindow": "Следующее окно", + "repeat": "Повторить" + } + }, + "form": { + "general": { + "title": "Основные настройки", + "description": "Задайте имя и параметры повтора для окна обслуживания.", + "option": { + "name": { + "label": "Имя", + "placeholder": "напр. Еженедельное обслуживание" + }, + "repeat": { + "label": "Повтор" + } + } + }, + "startDate": { + "title": "Дата начала", + "description": "Выберите дату начала окна обслуживания.", + "option": { + "startDate": { + "label": "Дата начала" + } + } + }, + "startTime": { + "title": "Время начала", + "description": "Задайте время начала и продолжительность окна обслуживания. Все значения указаны в UTC", + "option": { + "duration": { + "label": "Продолжительность" + }, + "startTime": { + "label": "Время начала" + } + }, + "monitors": { + "title": "Мониторы", + "description": "Мониторы, к которым применяется окно обслуживания", + "option": { + "addMonitors": { + "label": "Добавить мониторы" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "Создать канал", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Каналы уведомлений используются для:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Токен доступа", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "Адрес, на который будут отправляться уведомления.", + "optionAddress": "Адрес", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Адрес" + }, + "homeServer": { + "optionHomeServer": "Домашний сервер", + "placeholder": "example.com" + }, + "matrix": { + "description": "Настройте подключение к Matrix-серверу для уведомлений.", + "title": "Настройка Matrix" + }, + "notificationName": { + "description": "Описательное имя канала уведомлений", + "optionName": "Имя канала", + "placeholder": "напр. Оповещения продакшена", + "title": "Имя канала" + }, + "pagerDuty": { + "description": "Ваш ключ интеграции PagerDuty для получения оповещений.", + "optionIntegrationKey": "Ключ интеграции", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Ключ интеграции" + }, + "roomId": { + "optionRoomId": "ID комнаты", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Выберите тип канала уведомлений для создания.", + "optionType": "Тип", + "title": "Тип канала" + }, + "telegram": { + "title": "Настройка Telegram", + "description": "Чтобы включить уведомления Telegram, создайте бота Telegram с помощью BotFather, официального бота для создания и управления ботами Telegram. Затем получите токен API и идентификатор чата и запишите их здесь.", + "optionBotToken": "Токен бота", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "ID чата", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Назначение" + } + } + }, + "pageSpeed": { + "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)" + }, + "legend": { + "title": "Отчёт PageSpeed", + "weight": "Вес" + }, + "pie": { + "title": "Отчёт о производительности" + } + }, + "table": { + "headers": { + "pageSpeedScore": "Оценка PageSpeed" + } + }, + "fallback": { + "actionButton": "Создать монитор!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "Монитор PageSpeed используется для:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Часовой пояс отображения", + "description": "Выберите часовой пояс для отображения дат и времени в приложении.", + "option": { + "timezone": { + "label": "Часовой пояс отображения" + } + } + }, + "ui": { + "title": "Внешний вид", + "description": "Переключайтесь между светлой и тёмной темой, меняйте язык или настраивайте тип отображения графиков.", + "option": { + "theme": { + "label": "Режим темы", + "light": "Светлая", + "dark": "Тёмная" + }, + "language": { + "label": "Язык" + }, + "chartType": { + "label": "Тип графика", + "histogram": "Гистограмма", + "heatmap": "Тепловая карта" + } + } + }, + "pagespeed": { + "title": "Ключ API Google PageSpeed", + "description": "Введите ключ API Google PageSpeed для включения мониторинга Google PageSpeed. Нажмите «Сброс», чтобы обновить ключ.", + "option": { + "apiKey": { + "label": "Ключ API PageSpeed", + "labelSet": "Ключ API установлен. Нажмите «Сброс», чтобы изменить его.", + "placeholder": "Введите Ваш ключ API Google PageSpeed" + } + } + }, + "url": { + "title": "IP/URL монитора на странице статуса", + "description": "Отображать IP-адрес или URL монитора на публичной странице статуса. Если отключено, будет показано только имя монитора для защиты конфиденциальной информации.", + "option": { + "showURL": { + "label": "Отображать IP/URL на странице статуса", + "enabled": "Включено", + "disabled": "Отключено" + } + } + }, + "stats": { + "title": "История мониторинга", + "description": "Очистить всю историю мониторинга и статистику Вашей команды. Это действие необратимо.", + "option": { + "clear": { + "label": "Очистить всю статистику. Это действие необратимо." + } + }, + "dialog": { + "title": "Очистить всю историю мониторинга?", + "description": "Это действие нельзя отменить" + } + }, + "retention": { + "title": "Хранение проверок", + "description": "Укажите, как долго данные проверок хранятся перед автоматической очисткой.", + "option": { + "days": { + "label": "Период хранения (дни)", + "unlimited": "Без ограничений" + } + } + }, + "thresholds": { + "title": "Глобальные пороги", + "description": "Задайте глобальные пороги CPU, памяти, дисков и температуры для мониторинга инфраструктуры. Они применяются ко всем мониторам оборудования, если не переопределены.", + "option": { + "cpu": { + "label": "Порог CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Порог памяти (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Порог диска (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Порог температуры (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Настройки эл. почты", + "description": "Настройте параметры эл. почты для Вашей системы. Используется для отправки уведомлений и оповещений.", + "descriptionTransport": "Это создает SMTP-транспорт для NodeMailer.", + "descriptionTransportLink": "Смотрите спецификации здесь", + "option": { + "host": { + "label": "Хост эл. почты — имя хоста или IP-адрес для подключения", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Порт эл. почты — порт для подключения", + "placeholder": "587" + }, + "address": { + "label": "Адрес эл. почты — используется для аутентификации", + "placeholder": "you@example.com" + }, + "user": { + "label": "Пользователь эл. почты — имя пользователя для аутентификации, если указано, заменяет адрес", + "placeholder": "Оставьте пустым, если не требуется" + }, + "password": { + "label": "Пароль эл. почты — пароль для аутентификации", + "labelSet": "Пароль установлен. Нажмите «Сброс», чтобы изменить его.", + "placeholder": "Введите пароль" + }, + "tlsServername": { + "label": "Имя TLS-сервера — необязательное имя хоста для проверки TLS, когда хост является IP", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "Хост подключения — имя хоста для приветствия HELO/EHLO", + "placeholder": "localhost" + }, + "secure": { + "label": "Использовать SSL (рекомендуется): шифрование подключения с помощью SSL/TLS" + }, + "pool": { + "label": "Включить пул соединений: повторное использование соединений для повышения производительности" + }, + "ignoreTLS": { + "label": "Отключить STARTTLS: не использовать TLS, даже если сервер его поддерживает" + }, + "requireTLS": { + "label": "Принудительный STARTTLS: требовать обновление до TLS, ошибка если не поддерживается" + }, + "rejectUnauthorized": { + "label": "Отклонять недействительные сертификаты: отклонять соединения с самоподписанными или ненадёжными сертификатами" + } + } + }, + "demoMonitors": { + "title": "Демо-мониторы", + "description": "Добавить примеры мониторов для демонстрации." + }, + "removeMonitors": { + "title": "Сброс системы", + "description": "Удалить все мониторы из Вашей системы.", + "dialog": { + "title": "Удалить все мониторы?", + "description": "Это действие нельзя отменить." + } + }, + "exportMonitors": { + "title": "Экспорт мониторов" + }, + "importExportMonitors": { + "title": "Импорт / Экспорт мониторов", + "description": "Импортируйте или экспортируйте данные мониторов в формате JSON для резервного копирования или переноса." + }, + "about": { + "title": "О программе", + "developedBy": "Разработано Bluewave Labs" + }, + "validation": { + "errorMessage": "Пожалуйста, исправьте следующие ошибки валидации:" + } + } + }, + "statusPages": { + "deleteSuccess": "Страница статуса успешно удалена", + "fallback": { + "title": "Страница статуса используется для:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "Создать страницу статуса!" + }, + "monitorsList": { + "chartTypeHeatmap": "Тепловая карта", + "chartTypeHistogram": "Гистограмма", + "noData": "Нет доступных данных", + "infrastructure": { + "title": "Инфраструктура", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Память", + "disk": "Диск", + "usage": "Использование", + "used": "Использовано", + "total": "Всего" + }, + "uptime": { + "title": "Аптайм", + "responseTime": "Время ответа" + } + }, + "statusBar": { + "allDown": "Все системы недоступны", + "allUp": "Все системы работают", + "degraded": "Некоторые системы испытывают проблемы", + "unknown": "Не удаётся определить статус системы" + }, + "table": { + "headers": { + "name": "Имя страницы статуса", + "url": "Публичный URL" + }, + "published": "Опубликовано", + "unpublished": "Не опубликовано" + }, + "title": "Страницы статуса", + "details": { + "empty": { + "title": "Здесь пока ничего нет", + "addMonitor": "Добавьте монитор, чтобы начать" + } + }, + "form": { + "access": { + "title": "Доступ", + "description": "Если Ваша страница статуса готова, Вы можете отметить её как опубликованную.", + "option": { + "published": { + "name": "Опубликовано и доступно публично" + } + } + }, + "basicInfo": { + "title": "Основная информация", + "description": "Укажите название компании и субдомен, на который указывает Ваша страница статуса.", + "option": { + "name": { + "label": "Название компании", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Адрес Вашей страницы статуса", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "Мониторы", + "description": "Выберите мониторы для отображения на Вашей странице статуса.", + "noMonitors": "Мониторы не выбраны", + "option": { + "monitors": { + "label": "Выберите мониторы", + "placeholder": "Поиск и выбор мониторов..." + } + } + }, + "timezone": { + "title": "Часовой пояс", + "description": "Выберите часовой пояс для отображения Вашей страницы статуса.", + "option": { + "timezone": { + "label": "Часовой пояс", + "placeholder": "Выберите часовой пояс..." + } + } + }, + "appearance": { + "title": "Внешний вид", + "description": "Определите внешний вид Вашей публичной страницы статуса.", + "option": { + "logo": { + "label": "Логотип" + }, + "color": { + "label": "Цвет бренда" + } + } + }, + "features": { + "title": "Функции", + "description": "Настройте, какая информация отображается на Вашей странице статуса.", + "option": { + "showCharts": { + "label": "Показывать графики времени ответа" + }, + "showUptimePercentage": { + "label": "Показывать процент аптайма" + }, + "showAdminLoginLink": { + "label": "Показывать ссылку входа администратора" + }, + "showInfrastructure": { + "label": "Показывать метрики инфраструктуры" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Поиск мониторов..." + } + }, + "table": { + "headers": { + "responseTime": "Время ответа" + } + }, + "fallback": { + "actionButton": "Создать монитор!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Монитор аптайма используется для:" + } } } } diff --git a/client/src/locales/th.json b/client/src/locales/th.json index d5f9ad137..cf92d999f 100644 --- a/client/src/locales/th.json +++ b/client/src/locales/th.json @@ -1,1140 +1,1305 @@ { - "submit": "ยอมรับ", - "title": "หัวข้อ", - "distributedStatusHeaderText": "ตรวจสอบแบบเรียลไทม์บนอุปกรณ์จริง", - "distributedStatusSubHeaderText": "ระบบทำงานด้วยอุปกรณ์นับล้านทั่วโลก ให้คุณตรวจสอบประสิทธิภาพระบบตามภูมิภาค ประเทศ หรือเมืองได้", - "settingsDisabled": "ปิดการใช้งาน", - "settingsSuccessSaved": "บันทึกการตั้งค่าเรียบร้อยแล้ว", - "settingsFailedToSave": "ไม่สามารถบันทึกการตั้งค่าได้", - "settingsStatsCleared": "สถิติถูกล้างเรียบร้อยแล้ว", - "settingsFailedToClearStats": "ไม่สามารถล้างสถิติได้", - "settingsMonitorsDeleted": "ลบมอนิเตอร์ออกทั้งหมดเรียบร้อยแล้ว", - "settingsFailedToDeleteMonitors": "ไม่สามารถลบมอนิเตอร์ออกทั้งหมดได้", - "starPromptTitle": "ให้ดาวกับฉัน", - "starPromptDescription": "ดูการอัปเดตล่าสุด และมีส่วนร่วมในการขยายชุมชนบน GitHub", - "https": "HTTPS", - "http": "HTTP", - "monitor": "monitor", - "aboutus": "เกี่ยวกับฉัน", - "now": "ตอนนี้", - "delete": "ลบ", - "configure": "ตั้งค่า", - "responseTime": "ระยะเวลาการตอบสนอง", - "ms": "ms", - "bar": "Bar", - "area": "พื้นที่", - "country": "ประเทศ", - "city": "จังหวัด", - "response": "การตอบสนอง", - "monitorStatusUp": "มอนิเตอร์ {name} ({url}) สถานะตอนนี้ออนไลน์และตอบสนองปกติ", - "monitorStatusDown": "มอนิเตอร์ {name} ({url}) สถานะตอนนี้ล่มและไม่ตอบสนอง", - "webhookSendSuccess": "การแจ้งเตือนผ่าน Webhook ถูกส่งเรียบร้อยแล้ว", - "webhookSendError": "ไม่สามารถส่งการแจ้งเตือนผ่าน Webhook ไปที่ {platform} ได้", - "webhookUnsupportedPlatform": "ระบบไม่รองรับแพลตฟอร์ม {platform} นี้", - "distributedRightCategoryTitle": "มอนิเตอร์", - "distributedStatusServerMonitors": "มอนิเตอร์เชิร์ฟเวอร์", - "distributedStatusServerMonitorsDescription": "ตรวจสอบสถานะของเซิร์ฟเวอร์ที่เกี่ยวข้อง", - "distributedUptimeCreateSelectURL": "คุณสามารถระบุ URL ของโฮสต์และเลือกประเภทของตัวตรวจสอบได้ที่นี่", - "distributedUptimeCreateChecks": "รายการตรวจสอบ", - "distributedUptimeCreateChecksDescription": "คุณสามารถเพิ่มหรือลบการตรวจสอบได้เสมอหลังจากเพิ่มเว็บไซต์ของคุณแล้ว", - "distributedUptimeCreateIncidentNotification": "การแจ้งเตือนปัญหา", - "distributedUptimeCreateIncidentDescription": "เมื่อเกิดเหตุการณ์ผิดปกติ ให้แจ้งเตือนผู้ใช้งาน", - "distributedUptimeCreateAdvancedSettings": "ตั้งค่าขั้นสูง", - "distributedUptimeDetailsNoMonitorHistory": "ยังไม่มีประวัติการตรวจสอบสำหรับมอนิเตอร์นี้", - "distributedUptimeDetailsStatusHeaderUptime": "เวลาระบบทำงาน:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "แก้ไขล่าสุด", - "notifications": { - "enableNotifications": "เปิดการแจ้งเตือนของแพลตฟอร์ม {{platform}} นี้", - "testNotification": "ทดสอบระบบการแจ้งเตือน", - "addOrEditNotifications": "เพิ่มและแก้ไขระบบการแจ้งเตือน", - "slack": { - "label": "Slack", - "description": "เพื่อเปิดใช้งานการแจ้งเตือนผ่าน Slack ให้สร้างแอป Slack และเปิดใช้งาน Incoming Webhooks หลังจากนั้น เพียงใส่ URL ของ Webhook ที่นี่", - "webhookLabel": "URL ของ Webhook/เว็บฮุค", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "ต้องระบุ URL ของ Slack webhook" - }, - "discord": { - "label": "Discord", - "description": "เพื่อส่งข้อมูลไปยังช่อง Discord ผ่าน Discord notifications โดยใช้ Webhooks ให้เปิดใช้งาน Incoming Webhooks ของ Discord", - "webhookLabel": "URL ของ Webhook/เว็บฮุค สำหรับ Discord", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "ต้องระบุ URL ของ Discord webhook" - }, - "telegram": { - "label": "Telegram", - "description": "เพื่อเปิดใช้งานการแจ้งเตือนผ่าน Telegram ให้สร้างบอท Telegram โดยใช้ BotFather ซึ่งเป็นบอททางการสำหรับสร้างและจัดการบอท Telegram จากนั้นนำ API token และ Chat ID มาใส่ที่นี่", - "tokenLabel": "Token/โทเคน ของบอท", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "Chat ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "ต้องระบุโทเค็นและรหัสแชทของ Telegram" - }, - "webhook": { - "label": "Webhooks", - "description": "คุณสามารถตั้งค่าเว็บฮุคแบบกำหนดเองเพื่อรับการแจ้งเตือนเมื่อเกิดเหตุการณ์ผิดปกติ", - "urlLabel": "URL ของ Webhook", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "ต้องระบุ URL ของ Webhook" - }, - "testNotificationDevelop": "ทดสอบการแจ้งเตือน 2", - "integrationButton": "การรวมการแจ้งเตือน", - "testSuccess": "ส่งการแจ้งเตือนทดสอบเรียบร้อยแล้ว!", - "testFailed": "ไม่สามารถส่งการแจ้งเตือนทดสอบได้", - "unsupportedType": "การแจ้งเตือนไม่ได้รับการสนับสนุนประเภทนี้", - "networkError": "เกิดข้อผิดพลาดของเครือข่าย", - "fallback": { - "title": "ช่องทางแจ้งเตือนใช้เพื่อ:", - "checks": [ - "แจ้งทีมเกี่ยวกับการหยุดทำงานหรือปัญหาประสิทธิภาพ", - "แจ้งวิศวกรเมื่อเกิดเหตุการณ์", - "ให้ผู้ดูแลระบบทราบเกี่ยวกับการเปลี่ยนแปลงของระบบ" - ], - "actionButton": "มาสร้างช่องทางการแจ้งเตือนแรกของคุณกันเถอะ!" - }, - "createButton": "สร้างช่องทางแจ้งเตือน", - "createTitle": "ช่องทางแจ้งเตือน", - "create": { - "success": "สร้างช่องทางแจ้งเตือนสำเร็จแล้ว", - "failed": "ไม่สามารถสร้างช่องทางแจ้งเตือนได้" - }, - "fetch": { - "success": "ดึงข้อมูลการแจ้งเตือนสำเร็จแล้ว", - "failed": "ไม่สามารถดึงข้อมูลการแจ้งเตือนได้" - }, - "delete": { - "success": "ลบช่องทางแจ้งเตือนสำเร็จแล้ว", - "failed": "ไม่สามารถลบช่องทางแจ้งเตือนได้" - }, - "edit": { - "success": "ปรับปรุงช่องทางแจ้งเตือนสำเร็จแล้ว", - "failed": "ไม่สามารถปรับปรุงการแจ้งเตือนได้" - }, - "test": { - "success": "ส่งการแจ้งเตือนทดสอบสำเร็จแล้ว", - "failed": "ไม่สามารถส่งการแจ้งเตือนทดสอบได้" - } - }, - "testLocale": "testLocale", - "add": "เพิ่ม", - "monitors": "monitors", - "distributedUptimeStatusCreateStatusPage": "หน้าตรวจสอบสถานะระบบ", - "distributedUptimeStatusCreateStatusPageAccess": "สิทธิ์เข้าถึง", - "distributedUptimeStatusCreateStatusPageReady": "เมื่อหน้าสถานะพร้อม คุณสามารถตั้งค่าเป็นเผยแพร่สู่สาธารณะได้", - "distributedUptimeStatusBasicInfoHeader": "ข้อมูลทั่วไป", - "distributedUptimeStatusBasicInfoDescription": "ระบุชื่อบริษัทและซับโดเมนสำหรับหน้าสถานะของคุณ", - "distributedUptimeStatusLogoHeader": "โลโก้", - "distributedUptimeStatusLogoDescription": "อัปโหลดโลโก้ให้ Status Page ของคุณ", - "distributedUptimeStatusLogoUploadButton": "อัพโหลดโลโก้", - "distributedUptimeStatusStandardMonitorsHeader": "มอนิเตอร์พื้นฐาน", - "distributedUptimeStatusStandardMonitorsDescription": "เพิ่มตัวตรวจสอบมาตรฐานไปยัง Status Page ของคุณ", - "distributedUptimeStatusCreateYour": "Create your", - "distributedUptimeStatusEditYour": "Edit your", - "distributedUptimeStatusPublishedLabel": "เผยแพร่และมองเห็นได้ต่อสาธารณะ", - "distributedUptimeStatusCompanyNameLabel": "ชื่อบริษัท", - "distributedUptimeStatusPageAddressLabel": "ที่อยู่หน้าสถานะของคุณ", - "distributedUptimeStatus30Days": "30 วัน", - "distributedUptimeStatus60Days": "60 วัน", - "distributedUptimeStatus90Days": "90 วัน", - "distributedUptimeStatusPageNotSetUp": "หน้าสถานะยังไม่ถูกสร้างขึ้น", - "distributedUptimeStatusContactAdmin": "กรุณาติดต่อผู้ดูแลระบบของคุณ เพื่อขอความช่วยเหลือ", - "distributedUptimeStatusPageNotPublic": "หน้าสถานะนี้ไม่เปิดเผยต่อสาธารณะ", - "distributedUptimeStatusPageDeleteDialog": "คุณต้องการลบหน้าสถานะนี้หรือไม่?", - "distributedUptimeStatusPageDeleteConfirm": "ใช่ ฉันต้องการลบหน้าสถานะนี้", - "distributedUptimeStatusPageDeleteDescription": "เมื่อถูกลบแล้ว หน้าสถานะของคุณจะไม่สามารถกู้คืนได้", - "distributedUptimeStatusDevices": "อุปกรณ์", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "UPT Burned", - "distributedUptimeStatusUptLogo": "Upt Logo", - "incidentsTableNoIncidents": "ยังไม่มีเหตุการณ์ผิดปกติบันทึกไว้", - "incidentsTablePaginationLabel": "เหตุการณ์ผิดปกติ", - "incidentsTableMonitorName": "ชื่อมอนิเตอร์", - "incidentsTableStatus": "สถานะ", - "incidentsTableDateTime": "วันและเวลา", - "incidentsTableStatusCode": "รหัสสถานะ", - "incidentsTableMessage": "ข้อความ", - "incidentsOptionsHeader": "เหตุการณ์ผิดปกติสำหรับ:", - "incidentsOptionsHeaderFilterBy": "กรองโดย:", - "incidentsOptionsHeaderFilterAll": "ท้ังหมด", - "incidentsOptionsHeaderFilterDown": "ล่ม", - "incidentsOptionsHeaderFilterCannotResolve": "ไม่แก้ไข/หาสาเหตุไม่ได้", - "incidentsOptionsHeaderShow": "แสดง:", - "incidentsOptionsHeaderLastHour": "ชั่วโมงที่แล้ว", - "incidentsOptionsHeaderLastDay": "วันที่แล้ว", - "incidentsOptionsHeaderLastWeek": "สัปดาห์ที่แล้ว", - "incidentsOptionsPlaceholderAllServers": "เชิร์ฟเวอร์ทั้งหมด", - "infrastructureCreateYour": "สร้าง", - "infrastructureCreateGeneralSettingsDescription": "คุณสามารถเลือก URL ของโฮสต์ พร้อมกับชื่อที่เป็นมิตร Friendly Name และรหัสลับการอนุญาตเพื่อเชื่อมต่อกับตัวแทนเซิร์ฟเวอร์ Server Agent", - "infrastructureServerRequirement": "เซิร์ฟเวอร์ที่คุณมอนิเตอร์ต้องกำลังทำงานด้วย", - "infrastructureCustomizeAlerts": "ปรับแต่งการแจ้งเตือน", - "infrastructureAlertNotificationDescription": "ส่งการแจ้งเตือนถึงผู้ใช้เมื่อค่าขีดจำกัดเกินเปอร์เซ็นต์ที่กำหนด", - "infrastructureCreateMonitor": "สร้าง Infrastructure Monitor", - "infrastructureProtocol": "โปรโตคอล", - "infrastructureServerUrlLabel": "URL ของเซิร์ฟเวอร์", - "infrastructureDisplayNameLabel": "ชื่อที่แสดง", - "infrastructureAuthorizationSecretLabel": "Authorization secret / คีย์ลับสำหรับการยืนยันตัวตน", - "gb": "กิกะไบต์ GB", - "mb": "เมกะไบต์ MB", - "mem": "แรม", - "memoryUsage": "การใช้งานแรม", - "cpu": "ซีพียู", - "cpuUsage": "การใช้งาน CPU", - "cpuTemperature": "อุณหภูมิ CPU", - "diskUsage": "การใช้งาน Disk", - "used": "ใช้งาน", - "total": "ทั้งหมด", - "cores": "คอร์", - "frequency": "ความถี่", - "status": "สถานะ", - "cpuPhysical": "ซีพียู (คอร์หลัก)", - "cpuLogical": "ซีพียู (คอร์เสมือน)", - "cpuFrequency": "ความถี่ของ CPU", - "avgCpuTemperature": "อุณหภูมิเฉลี่ยของซีพียู", - "memory": "แรม", - "disk": "ดิส/พื้นที่", - "uptime": "", - "os": "ระบบปฏิบัติการ", - "host": "โฮส", - "actions": "จัดการ", - "integrations": "การรวมระบบ", - "integrationsPrism": "เชื่อมต่อ Prism กับบริการที่คุณชื่นชอบ", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "เชื่อมต่อกับ Slack และดูเหตุการณ์ผิดปกติในช่องทาง", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "เชื่อมต่อกับ Discord และดูเหตุการณ์ผิดปกติได้โดยตรงในช่องทาง", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "ส่งเหตุการณ์ทั้งหมดไปยัง Zapier แล้วสามารถดูได้ทุกที่", - "commonSave": "บันทึก", - "createYour": "Create your", - "createMonitor": "", - "pause": "พักชั่วคราว", - "resume": "ดำเนินการต่อ", - "editing": "กำลังปรับปรุง…", - "url": "URL", - "access": "การเข้าถึง", - "timezone": "เขตเวลา", - "features": "ฟีเจอร์", - "administrator": "ผู้ดูแลระบบ", - "loginHere": "กรุณาเข้าสู่ระบบที่นี่", - "displayName": "ชื่อที่แสดง", - "urlMonitor": "ลิงก์ที่จะมอนิเตอร์", - "portToMonitor": "พอร์ตที่จะมอนิเตอร์", - "websiteMonitoring": "เว็บไชต์ที่จะมอนิเตอร์", - "websiteMonitoringDescription": "ใช้ HTTP(s) เพื่อตรวจสอบเว็บไซต์หรือ API endpoint ของคุณ", - "pingMonitoring": "มอนิเตอร์แบบ Ping", - "pingMonitoringDescription": "ตรวจสอบว่าเซิร์ฟเวอร์ของคุณพร้อมใช้งานหรือไม่", - "dockerContainerMonitoring": "มอนิเตอร์คอนเทนเนอร์ใน Docker", - "dockerContainerMonitoringDescription": "ตรวจสอบว่าคอนเทนเนอร์ Docker ของคุณกำลังทำงานอยู่หรือไม่", - "portMonitoring": "มอนิเตอร์แบบ Ping", - "portMonitoringDescription": "ตรวจสอบว่าพอร์ตของคุณเปิดอยู่หรือไม่", - "createMaintenanceWindow": "สร้างช่วงเวลาบำรุงรักษา", - "createMaintenance": "สร้างการบำรุงรักษา", - "editMaintenance": "แก้ไขการบำรุงรักษา", - "maintenanceWindowName": "ชื่อหน้าบำรุงรักษา", - "friendlyNameInput": "ชื่อแสดงผล", - "friendlyNamePlaceholder": "Maintenance เริ่มต้นเวลา __ : __ และใช้เวลา ___ นาที", - "maintenanceRepeat": "ทำซ้ำการบำรุงรักษา", - "maintenance": "การบำรุงรักษา", - "duration": "ระยะเวลา", - "addMonitors": "เพิ่มมอนิเตอร์", - "window": "ช่วงเวลา", - "cancel": "ยกเลิก", - "message": "ข้อความ", - "low": "Low", - "high": "High", - "statusCode": "รหัสสถานะ", - "date&Time": "วันที่และเวลา", - "type": "ประเภท", - "statusPageName": "ชื่อ Status Page", - "publicURL": "URL สาธารณะ", - "repeat": "ทำซ้ำ", - "edit": "แก้ไข", - "createA": "สร้าง A", - "remove": "ลบ", - "maintenanceWindowDescription": "ช่วงเวลาการบำรุงรักษา การตรวจสอบทั้งหมดของมอนิเตอร์ที่ถูกเลือกจะถูกระงับ ไม่ทำการตรวจสอบเครือข่ายใด ๆ ทำให้ไม่มีการอัปเดตสถานะหรือแจ้งเตือนเกิดขึ้น มอนิเตอร์ของคุณจะคงแสดงสถานะล่าสุดที่ทราบ และหน้าสถานะจะมีสัญลักษณ์บ่งบอกว่ากำลังอยู่ระหว่างการบำรุงรักษา เมื่อเวลาการบำรุงรักษาสิ้นสุด การตรวจสอบจะกลับมาทำงานโดยอัตโนมัติ และหากพบปัญหา การแจ้งเตือนจะถูกส่งออก ช่วงเวลาการบำรุงรักษาจะไม่ถูกนับรวมในการคำนวณความพร้อมใช้งาน", - "startTime": "เวลาเริ่มต้น", - "timeZoneInfo": "วันที่และเวลาทั้งหมดเป็นเวลาตามโซน GMT+0", - "monitorsToApply": "มอนิเตอร์ที่จะใช้ช่วงเวลาการบำรุงรักษา", - "nextWindow": "ช่วงเวลาถัดไป", - "notFoundButton": "ไปยังหน้าจอแดชบอร์ดหลัก", - "pageSpeedConfigureSettingsDescription": "คุณสามารถเลือก URL ของโฮสต์ พร้อมกับประเภทของมอนิเตอร์ได้", - "monitorDisplayName": "ชื่อที่แสดงของมอนิเตอร์", - "whenNewIncident": "เมื่อเกิดเหตุการณ์ใหม่", - "notifySMS": "แจ้งเตือนทางSMS (กำลังจะมา)", - "notifyEmails": "แจ้งเตือนทางอีเมลไปยังหลายที่อยู่ด้วย (กำลังจะมา)", - "seperateEmails": "คุณสามารถแยกอีเมลหลายอันด้วยเครื่องหมายจุลภาค , ได้", - "checkFrequency": "ความถี่ในการตรวจสอบ", - "matchMethod": "วิธีการจับคู่", - "expectedValue": "ค่าที่คาดหวัง", - "deleteDialogTitle": "คุณแน่ใจหรือว่าต้องการลบมอนิเตอร์นี้ไหม", - "deleteDialogDescription": "เมื่อลบแล้ว มอนิเตอร์นี้ไม่สามารถกู้คืนได้", - "pageSpeedMonitor": "มอนิเตอร์ความเร็วหน้าเว็บ", - "shown": "แสดง", - "ago": "ที่ผ่านมา", - "companyName": "ชื่อบริษัท", - "pageSpeedDetailsPerformanceReport": "เป็นค่าโดยประมาณและอาจแตกต่างกันไป", - "pageSpeedDetailsPerformanceReportCalculator": "ดูเครื่องคิดเลข", - "checkingEvery": "ตรวจสอบทุก", - "statusPageCreateSettings": "หากหน้าสถานะของคุณพร้อมแล้ว คุณสามารถทำเครื่องหมายว่าเผยแพร่แล้วได้", - "basicInformation": "ข้อมูลพื้นฐาน", - "statusPageCreateBasicInfoDescription": "กำหนดชื่อบริษัทและซับโดเมนที่หน้าสถานะของคุณชี้ไป", - "statusPageCreateSelectTimeZoneDescription": "เลือกเขตเวลาที่หน้าสถานะของคุณจะแสดง", - "statusPageCreateAppearanceDescription": "กําหนดรูปลักษณ์เริ่มต้นของหน้าสถานะสาธารณะของคุณ", - "statusPageCreateSettingsCheckboxLabel": "เผยแพร่และเปิดเผยต่อสาธารณะ", - "statusPageCreateBasicInfoStatusPageAddress": "ที่อยู่หน้าสถานะของคุณ", - "statusPageCreateTabsContent": "หน้าสถานะเซิร์ฟเวอร์", - "statusPageCreateTabsContentDescription": "คุณสามารถเพิ่มเซิร์ฟเวอร์ที่คุณตรวจสอบเข้าไปยังหน้าสถานะของคุณได้จำนวนเท่าใดก็ได้ และสามารถจัดลำดับใหม่เพื่อการแสดงผลที่ดีที่สุด", - "statusPageCreateTabsContentFeaturesDescription": "แสดงรายละเอียดเพิ่มเติมบนหน้าสถานะ", - "showCharts": "แสดงแผนภูมิ", - "showUptimePercentage": "แสดงเปอร์เซ็นต์ความพร้อมใช้งาน", - "removeLogo": "ลบโลโก้", - "statusPageStatus": "ยังไม่ได้ตั้งค่าหน้าสถานะสาธารณะ", - "statusPageStatusContactAdmin": "กรุณาติดต่อผู้ดูแลระบบของคุณ", - "statusPageStatusNotPublic": "หน้าสถานะนี้ไม่เป็นสาธารณะ", - "statusPageStatusNoPage": "ที่นี่ยังไม่มีหน้าสถานะ", - "statusPageStatusServiceStatus": "สถานะบริการ", - "deleteStatusPage": "คุณต้องการลบหน้าสถานะนี้หรือไม่ไหม?", - "deleteStatusPageConfirm": "ใช่ ลบหน้าสถานะ", - "deleteStatusPageDescription": "เมื่อลบแล้ว หน้าสถานะของคุณไม่สามารถกู้คืนได้", - "uptimeCreate": "ค่าที่คาดหวังถูกใช้เพื่อตรวจสอบผลลัพธ์การตอบกลับ และการจับคู่จะเป็นตัวกำหนดสถานะ", - "uptimeCreateJsonPath": "นิพจน์นี้จะถูกประเมินกับข้อมูล JSON ที่ตอบกลับ และผลลัพธ์จะถูกใช้เพื่อตรวจสอบกับค่าที่คาดหวัง ดู", - "uptimeCreateJsonPathQuery": "for query language documentation.", - "maintenanceTableActionMenuDialogTitle": "คุณแน่ใจหรือว่าต้องการลบช่วงเวลาการบำรุงรักษานี้?", - "infrastructureEditYour": "Edit your", - "infrastructureEditMonitor": "บันทึก Infrastructure มอนิเตอร์", - "infrastructureMonitorCreated": "สร้างมอนิเตอร์ Infrastructure เรียบร้อยแล้ว!", - "infrastructureMonitorUpdated": "อัพเดทมอนิเตอร์ Infrastructure เรียบร้อยแล้ว!", - "errorInvalidTypeId": "ประเภทการแจ้งเตือนที่ระบุไม่ถูกต้อง", - "errorInvalidFieldId": "รหัสฟิลด์ที่ระบุไม่ถูกต้อง", - "inviteNoTokenFound": "ไม่พบโทเค็นคำเชิญ", - "pageSpeedWarning": "คำเตือน: คุณยังไม่ได้เพิ่มคีย์ Google PageSpeed API หากไม่มีคีย์นี้ มอนิเตอร์ความเร็วหน้าเว็บจะไม่ทำงาน", - "pageSpeedLearnMoreLink": "คลิกที่นี่", - "pageSpeedAddApiKey": "เพื่อเพิ่มคีย์ API ของคุณ", - "update": "อัปเดต", - "invalidFileFormat": "รูปแบบไฟล์ไม่รองรับ!", - "invalidFileSize": "ขนาดไฟล์ใหญ่เกินไป!", - "ClickUpload": "คลิกเพื่ออัปโหลด", - "DragandDrop": "ลากและวางเลย", - "MaxSize": "ขนาดสูงสุด", - "SupportedFormats": "รูปแบบที่รองรับ", - "FirstName": "ชื่อจริง", - "LastName": "นามสกุล", - "EmailDescriptionText": "นี่คือที่อยู่อีเมลปัจจุบันของคุณ — ไม่สามารถแก้ไขได้", - "YourPhoto": "รูปโปรไฟล์", - "PhotoDescriptionText": "รูปนี้จะแสดงบนหน้าประวัติของคุณ", - "save": "บันทึก", - "DeleteDescriptionText": "การทำเช่นนี้จะลบบัญชีและข้อมูลทั้งหมดที่เกี่ยวข้องบนเซิร์ฟเวอร์ ไม่สามารถกู้คืนได้", - "DeleteAccountWarning": "การลบบัญชีหมายความว่าคุณจะไม่สามารถเข้าสู่ระบบอีก และข้อมูลทั้งหมดของคุณจะถูกลบ ไม่สามารถกู้คืนได้", - "DeleteWarningTitle": "คุณแน่ใจหรือว่าต้องการลบบัญชีนี้?", - "bulkImport": { - "title": "นำเข้าจำนวนมาก", - "selectFileTips": "เลือกไฟล์ CSV เพื่อนำเข้า", - "selectFileDescription": "คุณสามารถดาวน์โหลด หรือ sample ของเราได้", - "selectFile": "เลือกไฟล์", - "parsingFailed": "การแยกวิเคราะห์ล้มเหลว", - "uploadSuccess": "สร้างมอนิเตอร์เรียบร้อยแล้ว!", - "validationFailed": "การตรวจสอบล้มเหลว", - "noFileSelected": "ยังไม่ได้เลือกไฟล์", - "fallbackPage": "นำเข้าไฟล์เพื่ออัปโหลดรายการเซิร์ฟเวอร์เป็นกลุ่ม", - "invalidFileType": "ประเภทไฟล์ไม่ถูกต้อง", - "uploadFailed": "อัปโหลดล้มเหลว" - }, - "DeleteAccountTitle": "ลบบัญชี", - "DeleteAccountButton": "ลบบัญชี", - "publicLink": "ลิงก์สาธารณะ", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "รีเช็ต", - "ignoreTLSError": "ละเว้นข้อผิดพลาด TLS/SSL", - "tlsErrorIgnored": "ละเว้นข้อผิดพลาด TLS/SSL แล้ว", - "ignoreTLSErrorDescription": "ละเว้นข้อผิดพลาด TLS/SSL และดำเนินการตรวจสอบความพร้อมใช้งานของเว็บไซต์ต่อ", - "createNew": "สร้างใหม่", - "greeting": { - "prepend": "สวัสดี", - "append": "ช่วงบ่ายคือเวลาเล่นของคุณ—มาทำให้สุดยอดกันเถอะ!", - "overview": "นี่คือภาพรวมระบบมอนิเตอร์ประเภท {{type}} ของคุณ" - }, - "roles": { - "superAdmin": "Super admin", - "admin": "แอดมิน", - "teamMember": "สมาชิกในทีม", - "demoUser": "ผู้ใช้ Demo" - }, - "teamPanel": { - "teamMembers": "สมาชิกในทีม", - "filter": { - "all": "ทั้งหมด", - "member": "สมาชิก" - }, - "inviteTeamMember": "เชิญสมาชิกเข้าทีม", - "inviteNewTeamMember": "เชิญสมาชิกทีมใหม่", - "inviteDescription": "เมื่อคุณเพิ่มสมาชิกทีมใหม่ เขาจะสามารถเข้าถึงมอนิเตอร์ทั้งหมดได้", - "email": "อีเมล", - "selectRole": "เลือกบทบาท", - "inviteLink": "ลิงก์เชิญ", - "cancel": "ยกเลิก", - "noMembers": "ยังไม่มีสมาชิกทีมในบทบาทนี้", - "getToken": "รับโทเค็น", - "emailToken": "Token ทางอีเมล", - "table": { - "name": "ชื่อ", - "email": "อีเมล", - "role": "บทบาท", - "created": "สร้างเมื่อ" - }, - "addTeamMember": { - "addMemberMenu": "เพิ่มสมาชิกทีม", - "title": "ลงทะเบียนสมาชิกทีมใหม่", - "description": "สร้างผู้ใช้ใหม่และแชร์ข้อมูลการเข้าสู่ระบบให้กับพวกเขา วิธีนี้จะทำให้สมาชิกสามารถเข้าถึงตัวตรวจสอบทั้งหมดได้ทันที", - "addButton": "เพิ่มสมาชิก" - }, - "register": "ลงทะเบียนสมาชิกทีม", - "registerToast": { - "success": "สร้างผู้ใช้เรียบร้อยแล้ว กรุณาแชร์ข้อมูลการเข้าสู่ระบบให้สมาชิกอย่างปลอดภัย", - "dbUserExists": "ผู้ใช้นี้มีอยู่แล้ว", - "unknownError": "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ" - }, - "registerTeamMember": { - "title": "ลงทะเบียนสมาชิกทีม", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "กรุณากรอกชื่อ", - "pattern": "ชื่อต้องมีเฉพาะตัวอักษร เว้นวรรค เครื่องหมายอัญประกาศ หรือขีดกลาง" - } - }, - "lastName": { - "errors": { - "empty": "กรุณากรอกนามสกุล", - "pattern": "นามสกุลต้องมีเฉพาะตัวอักษร เว้นวรรค เครื่องหมายอัญประกาศ หรือขีดกลาง" - } - }, - "email": { - "errors": { - "empty": "กรุณากรอกอีเมลเพื่อดำเนินการต่อ", - "invalid": "กรุณาตรวจสอบความถูกต้องของอีเมลอีกครั้ง" - } - }, - "role": { - "errors": { - "empty": "ต้องระบุบทบาท" - } - } - } - } - } - }, - "role": "บทบาท", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "หยุดชั่วคราว", - "resumed": "ดำเนินการต่อ", - "active": "ใช้งานอยู่" - }, - "menu": { - "uptime": "ความพร้อมใช้งาน", - "pagespeed": "ความเร็วหน้าเว็บ", - "infrastructure": "โครงสร้างพื้นฐาน", - "incidents": "เหตุการณ์", - "statusPages": "หน้าสถานะ", - "maintenance": "การบำรุงรักษา", - "integrations": "การรวมระบบ", - "settings": "การตั้งค่า", - "support": "การสนับสนุน", - "discussions": "การสนทนา", - "docs": "เอกสาร", - "changelog": "บันทึกการเปลี่ยนแปลง", - "profile": "โปรไฟล์", - "password": "รหัสผ่าน", - "team": "ทีม", - "logOut": "ออกจากระบบ", - "notifications": "การแจ้งเตือน", - "logs": "Logs" - }, - "settingsEmailUser": "อีเมลผู้ใช้ - ชื่อผู้ใช้สำหรับการยืนยันตัวตน จะใช้แทนอีเมลหากระบุไว้", - "state": "สถานะ", - "statusBreadCrumbsStatusPages": "หน้าสถานะ", - "statusBreadCrumbsDetails": "รายละเอียด", - "commonSaving": "กำลังบันทึก...", - "navControls": "การควบคุม", - "incidentsPageTitle": "เหตุการณ์", - "passwordPanel": { - "passwordChangedSuccess": "รหัสผ่านของคุณถูกเปลี่ยนเรียบร้อยแล้ว", - "passwordInputIncorrect": "คุณป้อนรหัสผ่านที่ไม่ถูกต้อง", - "currentPassword": "รหัสผ่านปัจจุบัน", - "enterCurrentPassword": "กรอกรหัสผ่านปัจจุบันของคุณ", - "newPassword": "รหัสผ่านใหม่", - "enterNewPassword": "กรอกรหัสผ่านใหม่ของคุณ", - "confirmNewPassword": "ยืนยันรหัสผ่านใหม่อีกครั้ง", - "passwordRequirements": "รหัสผ่านใหม่ต้องมีอย่างน้อย 8 ตัวอักษร และต้องมีตัวอักษรใหญ่ ตัวอักษรเล็ก ตัวเลข และอักขระพิเศษอย่างละอย่าง 1", - "saving": "กำลังบันทึก..." - }, - "emailSent": "ส่งอีเมลเรียบร้อยแล้ว", - "failedToSendEmail": "ส่งอีเมลล้มเหลว", - "settingsTestEmailSuccess": "ส่งอีเมลทดสอบเรียบร้อยแล้ว", - "settingsTestEmailFailed": "ส่งอีเมลทดสอบล้มเหลว", - "settingsTestEmailFailedWithReason": "ส่งอีเมลทดสอบล้มเหลว: {{reason}}", - "settingsTestEmailUnknownError": "ข้อผิดพลาดไม่ทราบสาเหตุ", - "statusMsg": { - "paused": "การตรวจสอบถูกพักชั่วคราว", - "up": "เว็บไซต์ของคุณใช้งานได้", - "down": "เว็บไซต์ของคุณล่ม", - "pending": "รอดำเนินการ..." - }, - "uptimeGeneralInstructions": { - "http": "กรอก URL หรือ IP ที่ต้องการตรวจสอบ (เช่น https://example.com/\n หรือ 192.168.1.100) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", - "ping": "กรอกที่อยู่ IP หรือชื่อโฮสต์ที่ต้องการ ping (เช่น 192.168.1.100 หรือ example.com) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", - "docker": "กรอกชื่อหรือ ID ของคอนเทนเนอร์ Docker คุณสามารถใช้ชื่อคอนเทนเนอร์ (เช่น my-app) หรือ ID ของคอนเทนเนอร์ (ID เต็ม 64 ตัวอักษร หรือ ID สั้น)", - "port": "กรอก URL หรือ IP ของเซิร์ฟเวอร์ หมายเลขพอร์ต และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", - "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม", - "https": "" - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "สลับโหมดสว่าง & มืด" + "auth": { + "roles": { + "admin": "ผู้ดูแลระบบ", + "demo": "สาธิต", + "superadmin": "ผู้ดูแลระบบสูงสุด", + "user": "ผู้ใช้" + } }, - "toasts": { - "networkError": "ข้อผิดพลาดเครือข่าย", - "checkConnection": "กรุณาตรวจสอบการเชื่อมต่อของคุณ", - "unknownError": "ข้อผิดพลาดไม่ทราบสาเหตุ" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "ดำเนินการต่อ", - "back": "ย้อนกลับ" + "alerts": { + "pageSpeedApiKey": { + "content": "คำเตือน: คุณยังไม่ได้เพิ่ม API key ของ Google PageSpeed ไปที่การตั้งค่าเพื่อเพิ่ม หากไม่มี ระบบตรวจสอบ PageSpeed จะไม่ทำงาน" + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "รายละเอียด", + "home": "หน้าหลัก" + }, + "buttons": { + "addMember": "เพิ่มสมาชิก", + "cancel": "ยกเลิก", + "close": "ปิด", + "configure": "ตั้งค่า", + "confirm": "ยืนยัน", + "create": "สร้าง", + "delete": "ลบ", + "generateToken": "สร้างโทเค็น", + "incidents": "เหตุการณ์", + "inviteMember": "เชิญสมาชิก", + "pause": "หยุดชั่วคราว", + "resume": "ดำเนินการต่อ", + "save": "บันทึก", + "sendInvite": "ส่งคำเชิญ", + "test": "ทดสอบ", + "testNotifications": "ทดสอบการแจ้งเตือน", + "toggleTheme": "สลับโหมดสว่าง & มืด", + "flushQueue": "ล้างคิว", + "notFound": "ไปยังแดชบอร์ดหลัก", + "resetPassword": "รีเซ็ตรหัสผ่าน", + "reset": "รีเช็ต", + "clear": "ล้าง", + "addDemo": "เพิ่มมอนิเตอร์สาธิต", + "removeMonitors": "ลบมอนิเตอร์", + "sendTestEmail": "ส่งอีเมลทดสอบ", + "exportToJSON": "ส่งออกเป็น JSON", + "importFromJSON": "นำเข้าจาก JSON", + "clearFilters": "ล้างตัวกรอง", + "removeUser": "ลบผู้ใช้" + }, + "charts": { + "labels": { + "averageResponseTime": "เวลาตอบสนองเฉลี่ย", + "downtime": "ดาวน์ไทม์", + "high": "สูง", + "low": "ต่ำ", + "uptime": "อัปไทม์" }, - "inputs": { - "email": { - "label": "อีเมล", - "placeholder": "jordan.ellis@domain.com", - "errors": { - "empty": "เพื่อดำเนินการต่อ กรุณากรอกที่อยู่อีเมลของคุณ", - "invalid": "กรุณาตรวจสอบความถูกต้องของอีเมลที่กรอก" - } - }, - "password": { - "label": "รหัสผ่าน", - "rules": { - "length": { - "beginning": "ต้องมีความยาวอย่างน้อย", - "highlighted": "8 ตัวอักษร" - }, - "special": { - "beginning": "ต้องมีอย่างน้อย", - "highlighted": "อักขระพิเศษ 1 ตัว" - }, - "number": { - "beginning": "ต้องมีอย่างน้อย", - "highlighted": "ตัวเลข 1 ตัว" - }, - "uppercase": { - "beginning": "ต้องมีอย่างน้อย", - "highlighted": "ตัวอักษรใหญ่ 1 ตัว" - }, - "lowercase": { - "beginning": "ต้องมีอย่างน้อย", - "highlighted": "ตัวอักษรเล็ก 1 ตัว" - }, - "match": { - "beginning": "รหัสผ่าน", - "highlighted": "ต้องตรงกัน" - } - }, - "errors": { - "empty": "กรุณากรอกรหัสผ่านของคุณ", - "length": "รหัสผ่านต้องมีความยาวอย่างน้อย 8 ตัวอักษร", - "uppercase": "รหัสผ่านต้องมีตัวอักษรใหญ่อย่างน้อย 1 ตัว", - "lowercase": "รหัสผ่านต้องมีตัวอักษรเล็กอย่างน้อย 1 ตัว", - "number": "รหัสผ่านต้องมีตัวเลขอย่างน้อย 1 ตัว", - "special": "รหัสผ่านต้องมีอักขระพิเศษอย่างน้อย 1 ตัว", - "incorrect": "รหัสผ่านที่คุณกรอกไม่ตรงกับข้อมูลของเรา" - } - }, - "passwordConfirm": { - "label": "ยืนยันรหัสผ่าน", - "placeholder": "กรอกรหัสผ่านอีกครั้งเพื่อยืนยัน", - "errors": { - "empty": "กรุณากรอกรหัสผ่านอีกครั้งเพื่อยืนยัน (ช่วยลดข้อผิดพลาดจากการพิมพ์ผิด)", - "different": "รหัสผ่านที่กรอกไม่ตรงกัน อาจมีตัวใดตัวหนึ่งพิมพ์ผิด" - } - }, - "firstName": { - "label": "ชื่อ", - "placeholder": "Brownyroll", - "errors": { - "empty": "กรุณากรอกชื่อของคุณ", - "length": "ชื่อต้องมีความยาวไม่เกิน 50 ตัวอักษร", - "pattern": "ชื่อต้องประกอบด้วยตัวอักษร ช่องว่าง เครื่องหมาย ' หรือ - เท่านั้น" - } - }, - "lastName": { - "label": "นามสกุล", - "placeholder": ".", - "errors": { - "empty": "กรุณากรอกนามสกุลของคุณ", - "length": "นามสกุลต้องมีความยาวไม่เกิน 50 ตัวอักษร", - "pattern": "นามสกุลต้องประกอบด้วยตัวอักษร ช่องว่าง เครื่องหมาย ' หรือ - เท่านั้น" + "histogram": { + "avg": "เฉลี่ย: {{value}} ms", + "max": "สูงสุด: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "การกระทำนี้ไม่สามารถย้อนกลับได้", + "title": "คุณแน่ใจหรือไม่ว่าต้องการลบรายการนี้?" + } + }, + "labels": { + "active": "ใช้งานอยู่", + "paused": "หยุดชั่วคราว", + "na": "ไม่มีข้อมูล", + "resolved": "แก้ไขแล้ว", + "responseTime": "เวลาตอบสนอง" + }, + "form": { + "role": { + "option": { + "role": { + "label": "บทบาท" } } }, - "errors": { - "validation": "เกิดข้อผิดพลาดในการตรวจสอบข้อมูล" - }, - "fields": { - "password": { - "errors": { - "incorrect": "รหัสผ่านที่คุณป้อนไม่ตรงกับข้อมูลของเรา" + "name": { + "option": { + "firstName": { + "label": "ชื่อ", + "placeholder": "Brownyroll" + }, + "lastName": { + "label": "นามสกุล", + "placeholder": "." } - }, - "role": { - "errors": { - "min": "ต้องมีอย่างน้อยหนึ่งบทบาท" + } + }, + "email": { + "option": { + "email": { + "label": "อีเมล", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "เข้าสู่ระบบเพื่อดำเนินการต่อ", - "subheadings": { - "stepOne": "กรอกอีเมลของคุณ", - "stepTwo": "กรอกรหัสผ่านของคุณ" - }, - "links": { - "forgotPassword": "ลืมรหัสผ่าน?", - "register": "ยังไม่มีบัญชีใช่ไหม?", - "forgotPasswordLink": "รีเซ็ตรหัสผ่าน", - "registerLink": "สมัครสมาชิกที่นี่" - }, - "toasts": { - "success": "ยินดีต้อนรับกลับ! คุณเข้าสู่ระบบเรียบร้อยแล้ว", - "incorrectPassword": "รหัสผ่านไม่ถูกต้อง" - }, + "table": { + "empty": "ไม่มีข้อมูล", + "headers": { + "actions": "จัดการ", + "dateTime": "วันที่และเวลา", + "message": "ข้อความ", + "monitor": "มอนิเตอร์", + "monitorId": "รหัสมอนิเตอร์", + "name": "ชื่อ", + "status": "สถานะ", + "type": "ประเภท", + "url": "URL", + "interval": "ช่วงเวลา", + "active": "ใช้งานอยู่", + "responseTime": "เวลาตอบสนอง" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "คลิกเพื่ออัปโหลด", + "dragAndDrop": "ลากและวางเลย", + "supportedFormats": "รูปแบบที่รองรับ", + "maxSize": "ขนาดสูงสุด", + "orDragAndDrop": "หรือลากและวาง", "errors": { - "password": { - "incorrect": "รหัสผ่านที่คุณป้อนไม่ตรงกับข้อมูลของเรา" - } - }, - "welcome": "ยินดีต้อนรับกลับสู่ Checkmate!" + "invalidFileSize": "ขนาดไฟล์ใหญ่เกินไป!", + "invalidFileFormat": "รูปแบบไฟล์ไม่รองรับ!" + } }, - "registration": { - "heading": { - "superAdmin": "สร้าง Super Admin", - "user": "สมัครสมาชิก" - }, - "subheadings": { - "stepOne": "กรอกรายละเอียดส่วนตัวของคุณ", - "stepTwo": "กรอกอีเมลของคุณ", - "stepThree": "สร้างรหัสผ่านของคุณ" + "headerStatusPageControls": { + "publicLink": "ลิงก์สาธารณะ" + }, + "headerTimeRange": { + "labels": { + "day": "วัน", + "month": "เดือน", + "recent": "ล่าสุด", + "week": "สัปดาห์" }, "description": { - "superAdmin": "สร้างบัญชี Super Admin เพื่อเริ่มต้นใช้งาน", - "user": "สมัครสมาชิกผู้ใช้และขอสิทธิ์เข้าถึงมอนิเตอร์จาก Super Admin" - }, - "gettingStartedButton": { - "superAdmin": "สร้างบัญชี Super Admin", - "user": "สมัครสมาชิกผู้ใช้ทั่วไป" - }, - "termsAndPolicies": "การสร้างบัญชีถือว่าคุณยอมรับ ข้อกำหนดการให้บริการ และ นโยบายความเป็นส่วนตัว ของเรา", - "links": { - "login": "มีบัญชีแล้ว? เข้าสู่ระบบ" - }, - "toasts": { - "success": "ยินดีต้อนรับ! สร้างบัญชีเรียบร้อยแล้ว" - }, - "welcome": "ยินดีต้อนรับสู่ Checkmate!" - }, - "forgotPassword": { - "heading": "ลืมรหัสผ่าน?", - "subheadings": { - "stepOne": "ไม่ต้องกังวล เราจะส่งคำแนะนำเพื่อรีเซ็ตรหัสผ่านให้คุณ", - "stepTwo": "เราได้ส่งลิงก์รีเซ็ตรหัสผ่านไปยัง ", - "stepThree": "รหัสผ่านใหม่ต้องแตกต่างจากรหัสผ่านที่ใช้ก่อนหน้านี้", - "stepFour": "รีเซ็ตรหัสผ่านเรียบร้อยแล้ว คลิกด้านล่างเพื่อเข้าสู่ระบบทันที" - }, - "buttons": { - "openEmail": "เปิดแอปอีเมล", - "resetPassword": "รีเซ็ตรหัสผ่าน" - }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" - }, - "links": { - "login": "กลับไปที่ เข้าสู่ระบบ", - "resend": "ไม่ได้รับอีเมล? คลิกเพื่อส่งอีกครั้ง" - }, - "toasts": { - "sent": "คำแนะนำถูกส่งไปยัง .", - "emailNotFound": "ไม่พบอีเมลนี้", - "redirect": "กำลังเปลี่ยนหน้าใน ...", - "success": "รีเซ็ตรหัสผ่านสำเร็จแล้ว", - "error": "ไม่สามารถรีเซ็ตรหัสผ่านได้ กรุณาลองอีกครั้งภายหลังหรือติดต่อฝ่ายสนับสนุน" + "recent": "แสดงสถิติย้อนหลัง 2 ชั่วโมง", + "day": "แสดงสถิติย้อนหลัง 24 ชั่วโมง", + "week": "แสดงสถิติย้อนหลัง 7 วัน", + "month": "แสดงสถิติย้อนหลัง 30 วัน" } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "เชื่อมต่อกับเซิร์ฟเวอร์สำเร็จแล้ว", - "stillUnreachable": "เซิร์ฟเวอร์ยังไม่สามารถเข้าถึงได้ กรุณาลองอีกครั้งภายหลัง" + }, + "offlineBanner": { + "serverUnreachable": "ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้", + "retry": "ลองอีกครั้ง", + "retrying": "กำลังลองอีกครั้ง...", + "reconnected": "เชื่อมต่อกับเซิร์ฟเวอร์สำเร็จแล้ว" + }, + "sidebar": { + "menu": { + "uptime": "ความพร้อมใช้งาน", + "pagespeed": "ความเร็วหน้าเว็บ", + "infrastructure": "โครงสร้างพื้นฐาน", + "notifications": "การแจ้งเตือน", + "checks": "การตรวจสอบ", + "incidents": "เหตุการณ์", + "statusPages": "หน้าสถานะ", + "maintenance": "การบำรุงรักษา", + "logs": "บันทึก", + "settings": "การตั้งค่า" }, - "alertBox": "เกิดข้อผิดพลาดการเชื่อมต่อเซิร์ฟเวอร์", - "description": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้ กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณหรือยืนยันการตั้งค่า deployment หากปัญหายังคงอยู่", - "retryButton": { - "default": "ลองเชื่อมต่ออีกครั้ง", - "processing": "กำลังเชื่อมต่อ…" + "bottomMenu": { + "support": "การสนับสนุน", + "discussions": "การสนทนา", + "docs": "เอกสาร", + "changelog": "บันทึกการเปลี่ยนแปลง" + }, + "accountMenu": { + "profile": "โปรไฟล์", + "password": "รหัสผ่าน", + "team": "ทีม" + }, + "starPrompt": { + "title": "ให้ดาว Checkmate", + "description": "ดูรุ่นล่าสุดและช่วยเติบโตชุมชนบน GitHub" + }, + "authFooter": { + "navControls": "การควบคุม", + "logOut": "ออกจากระบบ", + "roles": { + "superAdmin": "ผู้ดูแลระบบสูงสุด", + "admin": "ผู้ดูแลระบบ", + "user": "ผู้ใช้", + "demoUser": "ผู้ใช้ Demo" + } } + }, + "starPrompt": { + "title": "ให้ดาว Checkmate", + "description": "ดูรุ่นล่าสุดและช่วยเติบโตชุมชนบน GitHub" } }, - "createNotifications": { - "title": "สร้างช่องทางแจ้งเตือน", - "nameSettings": { - "title": "ชื่อ", - "description": "ชื่อที่บ่งบอกถึงการตั้งค่าการรวมระบบของคุณ", - "nameLabel": "ชื่อ", - "namePlaceholder": "เช่น การแจ้งเตือนผ่าน Slack" + "pages": { + "notFound": { + "title": "โอ้ไม่! ซูชิของคุณหล่นแล้ว!", + "subtitle": "URL ไม่มีอยู่ หรือคุณไม่มีสิทธิ์เข้าถึง" }, - "typeSettings": { - "title": "ประเภท", - "description": "เลือกประเภทของช่องทางแจ้งเตือนที่คุณต้องการสร้าง", - "typeLabel": "ประเภท" - }, - "emailSettings": { - "title": "อีเมล", - "description": "ที่อยู่อีเมลปลายทาง", - "emailLabel": "อีเมล", - "emailPlaceholder": "e.g. john@example.com" - }, - "slackSettings": { - "title": "Slack", - "description": "ตั้งค่า Slack webhook ของคุณที่นี่", - "webhookLabel": "URL ของ Slack webhook", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "ตั้งค่าการรวมระบบ PagerDuty ของคุณที่นี่", - "integrationKeyLabel": "คีย์การรวมระบบ", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discord", - "description": "ตั้งค่า Discord webhook ของคุณที่นี่", - "webhookLabel": "URL ของ Discord Webhook", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "webhookSettings": { - "title": "Webhook", - "description": "ตั้งค่า webhook ของคุณที่นี่", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "testNotification": "ทดสอบการแจ้งเตือน", - "dialogDeleteTitle": "คุณแน่ใจหรือไม่ว่าต้องการลบการแจ้งเตือนนี้?", - "dialogDeleteConfirm": "ลบ" - }, - "notificationConfig": { - "title": "การแจ้งเตือน", - "description": "เลือกช่องทางแจ้งเตือนที่คุณต้องการใช้" - }, - "monitorStatus": { - "checkingEvery": "ตรวจสอบทุก {{interval}}", - "withCaptureAgent": "โดยใช้ Capture agent {{version}}", - "up": "ออนไลน์", - "down": "ออฟไลน์", - "paused": "หยุดชั่วคราว" - }, - "advancedMatching": "การจับคู่ขั้นสูง", - "sendTestNotifications": "ส่งการแจ้งเตือนทดสอบ", - "selectAll": "Select all", - "showAdminLoginLink": "แสดงลิงก์ \"ผู้ดูแลระบบ? เข้าสู่ระบบที่นี่\" บนหน้าสถานะ", - "logsPage": { - "title": "Logs", - "description": "ระบบ Logs 1000 บรรทัดล่าสุด", - "tabs": { - "queue": "คิวงาน", - "logs": "Logs เซิร์ฟเวอร์", - "diagnostics": "วินิจฉัย" - }, - "toast": { - "fetchLogsSuccess": "ดึง Logs สำเร็จแล้ว" - }, - "logLevelSelect": { - "title": "Logs level", - "values": { - "all": "All", - "info": "Info", - "warn": "Warn", - "error": "Error", - "debug": "Debug" + "account": { + "tabs": { + "profile": "โปรไฟล์", + "password": "รหัสผ่าน", + "team": "ทีม" + }, + "form": { + "name": { + "title": "ชื่อ", + "description": "อัปเดตข้อมูลส่วนตัวของคุณ" + }, + "photo": { + "title": "รูปโปรไฟล์", + "description": "อัปโหลดรูปโปรไฟล์" + }, + "currentPassword": { + "title": "รหัสผ่านปัจจุบัน", + "description": "กรอกรหัสผ่านปัจจุบันเพื่อยืนยันตัวตน", + "option": { + "label": "รหัสผ่านปัจจุบัน", + "placeholder": "กรอกรหัสผ่านปัจจุบัน" + } + }, + "newPassword": { + "title": "รหัสผ่านใหม่", + "description": "เลือกรหัสผ่านที่รัดกุมอย่างน้อย 8 ตัวอักษร", + "option": { + "newPassword": { + "label": "รหัสผ่านใหม่", + "placeholder": "กรอกรหัสผ่านใหม่" + }, + "confirm": { + "label": "ยืนยันรหัสผ่าน", + "placeholder": "ยืนยันรหัสผ่านใหม่" + } + } + }, + "deleteAccount": { + "title": "ลบบัญชี", + "description": "การกระทำนี้เป็นการถาวรและไม่สามารถย้อนกลับได้" + } + }, + "team": { + "filter": { + "placeholder": "กรองตามบทบาท", + "all": "ทั้งหมด", + "admin": "ผู้ดูแลระบบ", + "member": "สมาชิก" + }, + "table": { + "headers": { + "email": "อีเมล", + "role": "บทบาท", + "created": "สร้างเมื่อ" + } + }, + "addMember": { + "title": "ลงทะเบียนสมาชิกทีมใหม่", + "description": "สร้างบัญชีผู้ใช้ใหม่ แชร์ข้อมูลรับรองอย่างปลอดภัยหลังสร้าง" + }, + "invite": { + "title": "เชิญสมาชิกทีม", + "description": "ส่งคำเชิญเข้าร่วมทีม", + "email": { + "label": "ที่อยู่อีเมล", + "placeholder": "กรอกที่อยู่อีเมล" + }, + "role": { + "label": "บทบาท", + "placeholder": "เลือกบทบาท" + }, + "linkLabel": "ลิงก์เชิญ" + } } - } - }, - "queuePage": { - "title": "Queue", - "refreshButton": "Refresh", - "flushButton": "Flush queue", - "jobTable": { - "title": "งานที่อยู่ในคิวปัจจุบัน", - "idHeader": "รหัสมอนิเตอร์", - "urlHeader": "URL", - "typeHeader": "Type", - "activeHeader": "ใช้งานอยู่", - "lockedAtHeader": "ล็อกเมื่อ", - "runCountHeader": "จำนวนรัน", - "failCountHeader": "จำนวนล้มเหลว", - "lastRunHeader": "รันล่าสุดเมื่อ", - "lastFinishedAtHeader": "เสร็จสิ้นล่าสุดเมื่อ", - "lastRunTookHeader": "รันครั้งล่าสุดใช้เวลา", - "intervalHeader": "ช่วงเวลา" }, - "metricsTable": { - "title": "คิวเมตริก", - "metricHeader": "เมตริก", - "valueHeader": "Value" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "ต้องมีความยาวอย่างน้อย", + "highlighted": "8 ตัวอักษร" + }, + "lowercase": { + "beginning": "ต้องมีอย่างน้อย", + "highlighted": "ตัวอักษรเล็ก 1 ตัว" + }, + "match": { + "beginning": "รหัสผ่าน", + "highlighted": "ต้องตรงกัน" + }, + "number": { + "beginning": "ต้องมีอย่างน้อย", + "highlighted": "ตัวเลข 1 ตัว" + }, + "special": { + "beginning": "ต้องมีอย่างน้อย", + "highlighted": "อักขระพิเศษ 1 ตัว" + }, + "uppercase": { + "beginning": "ต้องมีอย่างน้อย", + "highlighted": "ตัวอักษรใหญ่ 1 ตัว" + } + }, + "form": { + "option": { + "email": { + "label": "อีเมล", + "placeholder": "me@example.com" + }, + "password": { + "label": "รหัสผ่าน", + "placeholder": "********" + }, + "confirmPassword": { + "label": "ยืนยันรหัสผ่าน" + } + } + } + }, + "login": { + "title": "ยินดีต้อนรับกลับสู่ Checkmate!", + "subtitle": "เข้าสู่ระบบเพื่อดำเนินการต่อ", + "submit": "เข้าสู่ระบบ", + "links": { + "forgotPassword": { + "text": "ลืมรหัสผ่าน?", + "linkText": "รีเซ็ตรหัสผ่าน" + }, + "register": { + "text": "ยังไม่มีบัญชี?", + "linkText": "ลงทะเบียนที่นี่" + } + } + }, + "register": { + "title": "ยินดีต้อนรับกลับสู่ Checkmate!", + "subtitle": "สมัครสมาชิกเพื่อเริ่มต้น", + "submit": "ลงทะเบียน" + }, + "forgotPassword": { + "title": "ลืมรหัสผ่านใช่ไหม?", + "subtitle": "ไม่ต้องกังวล เราจะส่งคำแนะนำการรีเซ็ตให้คุณ", + "submit": "ขอกู้คืนรหัสผ่าน", + "links": { + "login": { + "text": "กลับไปยัง", + "linkText": "เข้าสู่ระบบ" + } + } + }, + "setNewPassword": { + "title": "รีเซ็ตรหัสผ่านของคุณ", + "subtitle": "รหัสผ่านใหม่ต้องแตกต่างจากรหัสผ่านที่เคยใช้ก่อนหน้า" + } }, - "failedJobTable": { - "title": "งานที่ล้มเหลว", - "monitorIdHeader": "รหัสมอนิเตอร์", - "monitorUrlHeader": "URL มอนิเตอร์", - "failCountHeader": "จำนวนล้มเหลว", - "failedAtHeader": "ล้มเหลวล่าสุดเมื่อ", - "failReasonHeader": "สาเหตุที่ล้มเหลว" - } - }, - "export": { - "title": "ส่งออกมอนิเตอร์", - "success": "ส่งออกมอนิเตอร์สำเร็จแล้ว", - "failed": "ไม่สามารถส่งออกมอนิเตอร์ได้" - }, - "monitorActions": { - "title": "ส่งออก/นำเข้า", - "import": "นำเข้ามอนิเตอร์", - "export": "ส่งออกมอนิเตอร์", - "deleteSuccess": "ลบมอนิเตอร์สำเร็จ", - "deleteFailed": "ลบมอนิเตอร์ล้มเหลว", - "details": "รายละเอียด" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "พัฒนาโดย Bluewave Labs", - "labelVersion": "เวอร์ชัน", - "title": "เกี่ยวกับ" + "checks": { + "selects": { + "monitor": { + "all": "มอนิเตอร์ทั้งหมด" + }, + "status": { + "all": "ทั้งหมด", + "down": "ล่ม", + "up": "ปกติ" + } + }, + "table": { + "empty": "ไม่พบการตรวจสอบที่ล้มเหลวในช่วงเวลานี้", + "headers": { + "statusCode": "รหัสสถานะ", + "location": "ตำแหน่ง" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "เพิ่มมอนิเตอร์ตัวอย่าง", - "description": "เพิ่มมอนิเตอร์ตัวอย่างเพื่อการสาธิต", - "title": "มอนิเตอร์สาธิต" + "common": { + "monitors": { + "actions": { + "configure": "ตั้งค่า", + "delete": "ลบ", + "generateToken": "สร้างโทเค็น", + "details": "รายละเอียด", + "incidents": "เหตุการณ์", + "inviteMember": "เชิญสมาชิก", + "openSite": "เปิดเว็บไซต์", + "pause": "หยุดชั่วคราว", + "resume": "ดำเนินการต่อ" + }, + "statBoxes": { + "activeFor": "ทำงานมาแล้ว", + "certificateExpiry": "วันหมดอายุใบรับรอง", + "lastCheck": "ตรวจสอบล่าสุด", + "lastResponseTime": "เวลาตอบสนองล่าสุด" + }, + "status": { + "down": "ล่ม", + "breached": "เกินเกณฑ์", + "initializing": "กำลังเริ่มต้น", + "maintenance": "บำรุงรักษา", + "paused": "หยุดชั่วคราว", + "total": "ทั้งหมด", + "up": "ปกติ" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "เกม", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "พอร์ต", + "optionPagespeed": "PageSpeed", + "optionHardware": "โครงสร้างพื้นฐาน", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "emailSettings": { - "buttonSendTestEmail": "ส่งอีเมลทดสอบ", - "description": "ตั้งค่าอีเมลสำหรับระบบของคุณ ใช้สำหรับส่งการแจ้งเตือนและเตือนภัย", - "descriptionTransport": "สร้าง SMTP transport สำหรับ NodeMailer", - "labelAddress": "ที่อยู่อีเมล - ใช้สำหรับการยืนยันตัวตน", - "labelConnectionHost": "โฮสต์เชื่อมต่ออีเมล - ชื่อโฮสต์ที่ใช้ในคำทักทาย HELO/EHLO", - "labelHost": "โฮสต์อีเมล - ชื่อโฮสต์หรือ IP สำหรับเชื่อมต่อ", - "labelIgnoreTLS": "ปิดใช้งาน STARTTLS: ไม่ใช้ TLS แม้เซิร์ฟเวอร์รองรับ", - "labelPassword": "รหัสผ่านอีเมล - สำหรับยืนยันตัวตน", - "labelPasswordSet": "ตั้งรหัสผ่านเรียบร้อยแล้ว คลิก รีเซ็ต เพื่อเปลี่ยน", - "labelPool": "เปิดใช้ connection pooling: ใช้การเชื่อมต่อเดิมซ้ำเพื่อเพิ่มประสิทธิภาพ", - "labelPort": "พอร์ตอีเมล - พอร์ตสำหรับเชื่อมต่อ", - "labelRejectUnauthorized": "ปฏิเสธใบรับรองไม่ถูกต้อง: ปฏิเสธการเชื่อมต่อที่ใช้ใบรับรอง self-signed หรือไม่เชื่อถือ", - "labelRequireTLS": "บังคับ STARTTLS: ต้องการอัปเกรด TLS ล้มเหลวหากไม่รองรับ", - "labelSecure": "ใช้ SSL (แนะนำ): เข้ารหัสการเชื่อมต่อด้วย SSL/TLS", - "labelTLSServername": "ชื่อเซิร์ฟเวอร์ TLS - ชื่อโฮสต์เสริมสำหรับตรวจสอบ TLS เมื่อโฮสต์เป็น IP", - "labelUser": "ผู้ใช้อีเมล - ชื่อผู้ใช้สำหรับยืนยันตัวตน ใช้แทนที่อีเมลถ้ามีการระบุ", - "linkTransport": "ดูข้อกำหนดที่นี่", - "placeholderUser": "ปล่อยว่างหากไม่จำเป็น", - "title": "อีเมล", - "toastEmailRequiredFieldsError": "ต้องระบุอีเมล, โฮสต์, พอร์ต และรหัสผ่าน" + "createMonitor": { + "form": { + "advanced": { + "description": "ตั้งค่าเพิ่มเติมสำหรับกรณีใช้งานขั้นสูง", + "option": { + "advancedMatching": { + "label": "ใช้การจับคู่ขั้นสูง" + }, + "expectedValue": { + "label": "ค่าที่คาดหวัง" + }, + "jsonPath": { + "description": "นิพจน์นี้จะถูกประเมินกับข้อมูล JSON ที่ตอบกลับ และผลลัพธ์จะถูกใช้จับคู่กับค่าที่คาดหวัง ดูเอกสาร query language ที่ jmespath.org", + "label": "นิพจน์ JSONPath" + }, + "matchMethod": { + "label": "วิธีการจับคู่", + "equal": "เท่ากับ", + "include": "รวม", + "regex": "Regex" + } + }, + "title": "ตั้งค่าขั้นสูง" + }, + "frequency": { + "description": "คุณต้องการตรวจสอบสถานะของมอนิเตอร์นี้บ่อยแค่ไหน?", + "option": { + "frequency": { + "label": "ความถี่การตรวจสอบ", + "value": { + "fifteenMinutes": "15 นาที", + "fifteenSeconds": "15 วินาที", + "fiveMinutes": "5 นาที", + "fourMinutes": "4 นาที", + "oneMinute": "1 นาที", + "tenMinutes": "10 นาที", + "thirtyMinutes": "30 นาที", + "thirtySeconds": "30 วินาที", + "threeMinutes": "3 นาที", + "twoMinutes": "2 นาที" + } + } + }, + "title": "ความถี่การตรวจสอบ" + }, + "general": { + "option": { + "container": { + "label": "ชื่อ/ID คอนเทนเนอร์", + "placeholder": "my-app หรือ abcd1234" + }, + "host": { + "label": "โฮสต์", + "placeholder": "192.168.1.100 หรือ example.com" + }, + "name": { + "label": "ชื่อที่แสดง", + "placeholder": "เช่น เว็บไซต์ของฉัน" + }, + "secret": { + "label": "รหัสลับสำหรับยืนยันตัวตน", + "placeholder": "กรอกรหัสลับของคุณ" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "URL ที่ต้องการมอนิเตอร์", + "placeholder": "localhost" + }, + "port": { + "label": "พอร์ตที่ต้องการตรวจสอบ", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "ชื่อบริการ", + "placeholder": "เช่น my.service.v1 (เว้นว่างสำหรับสถานะสุขภาพโดยรวม)" + }, + "wsUrl": { + "label": "URL ของ WebSocket", + "placeholder": "wss://example.com/socket" + } + }, + "title": "ตั้งค่าทั่วไป", + "description": { + "http": "กรอก URL หรือ IP ที่ต้องการตรวจสอบ (เช่น https://example.com/ หรือ 192.168.1.100) และตั้งชื่อที่แสดงบนแดชบอร์ด", + "ping": "กรอกที่อยู่ IP หรือชื่อโฮสต์ที่ต้องการ ping (เช่น 192.168.1.100 หรือ example.com) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", + "port": "กรอก URL หรือ IP ของเซิร์ฟเวอร์ หมายเลขพอร์ต และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", + "docker": "กรอกชื่อหรือ ID ของคอนเทนเนอร์ Docker คุณสามารถใช้ชื่อคอนเทนเนอร์ (เช่น my-app) หรือ ID ของคอนเทนเนอร์ (ID เต็ม 64 ตัวอักษร หรือ ID สั้น)", + "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม", + "pagespeed": "ติดตามประสิทธิภาพการโหลดหน้า Core Web Vitals และคะแนนการเพิ่มประสิทธิภาพของเว็บไซต์", + "grpc": "กรอกชื่อโฮสต์และพอร์ตของเซิร์ฟเวอร์ gRPC ระบุชื่อบริการสำหรับ health check (ไม่บังคับ) และตั้งชื่อที่แสดง", + "websocket": "กรอก URL ของ WebSocket ที่ต้องการตรวจสอบ (เช่น wss://example.com/socket) และตั้งชื่อที่แสดง", + "hardware": "ตรวจสอบการใช้งาน CPU หน่วยความจำ ดิสก์ และอุณหภูมิของโครงสร้างพื้นฐาน" + } + }, + "ignoreTls": { + "description": "กำหนดค่าการตรวจสอบใบรับรอง TLS/SSL สำหรับการเชื่อมต่อ HTTPS", + "option": { + "tls": { + "label": "ข้ามข้อผิดพลาด TLS/SSL" + } + }, + "title": "ตั้งค่า TLS/SSL" + }, + "incidents": { + "description": "ใช้หน้าต่างเลื่อนเพื่อกำหนดว่ามอนิเตอร์ล่มเมื่อใด สถานะของมอนิเตอร์จะเปลี่ยนเมื่อเปอร์เซ็นต์การตรวจสอบในหน้าต่างเลื่อนตรงตามค่าที่กำหนด", + "option": { + "checks": { + "label": "จำนวนการตรวจสอบในหน้าต่างเลื่อน" + }, + "percentage": { + "label": "เปอร์เซ็นต์การตรวจสอบในหน้าต่างเลื่อนที่ล้มเหลว/สำเร็จก่อนเปลี่ยนสถานะมอนิเตอร์?" + } + }, + "title": "เหตุการณ์" + }, + "notifications": { + "description": "เลือกช่องทางการแจ้งเตือนที่คุณต้องการใช้", + "title": "การแจ้งเตือน" + }, + "type": { + "description": "เลือกประเภทการตรวจสอบที่ต้องการ", + "optionDockerDescription": "ใช้ Docker เพื่อตรวจสอบว่าคอนเทนเนอร์กำลังทำงานอยู่", + "optionGameDescription": "ตรวจสอบว่าเซิร์ฟเวอร์เกมเฉพาะออนไลน์อยู่หรือไม่", + "optionGrpcDescription": "ตรวจสอบบริการ gRPC โดยใช้ Health Checking Protocol มาตรฐาน", + "optionWebSocketDescription": "ตรวจสอบ WebSocket endpoint สำหรับสุขภาพการเชื่อมต่อและเวลาตอบสนอง", + "optionHttpDescription": "ใช้ HTTP(S) เพื่อตรวจสอบเว็บไซต์หรือ API endpoint", + "optionPingDescription": "ใช้ ICMP Ping เพื่อตรวจสอบว่าเซิร์ฟเวอร์ออนไลน์อยู่หรือไม่", + "optionPortDescription": "ตรวจสอบว่าพอร์ตเฉพาะบนเซิร์ฟเวอร์เปิดอยู่หรือไม่", + "title": "ประเภท" + }, + "thresholds": { + "title": "เกณฑ์การแจ้งเตือน", + "description": "กำหนดเกณฑ์ที่ควรทริกเกอร์การแจ้งเตือนสำหรับมอนิเตอร์ฮาร์ดแวร์นี้", + "option": { + "cpuThreshold": { + "label": "เกณฑ์แจ้งเตือน CPU (%)" + }, + "memoryThreshold": { + "label": "เกณฑ์แจ้งเตือนหน่วยความจำ (%)" + }, + "diskThreshold": { + "label": "เกณฑ์แจ้งเตือนดิสก์ (%)" + }, + "tempThreshold": { + "label": "เกณฑ์แจ้งเตือนอุณหภูมิ (°C)" + } + } + }, + "geoChecks": { + "title": "การตรวจสอบแบบกระจายทางภูมิศาสตร์", + "description": "เรียกใช้การตรวจสอบจากหลายตำแหน่งทางภูมิศาสตร์เพื่อตรวจสอบความพร้อมใช้งานและประสิทธิภาพทั่วโลก", + "option": { + "enabled": { + "label": "เปิดใช้งานการตรวจสอบแบบกระจายทางภูมิศาสตร์" + }, + "locations": { + "label": "ตำแหน่ง", + "placeholder": "เลือกตำแหน่ง", + "options": { + "EU": "ยุโรป", + "NA": "อเมริกาเหนือ", + "AS": "เอเชีย", + "SA": "อเมริกาใต้", + "AF": "แอฟริกา", + "OC": "โอเชียเนีย" + } + }, + "interval": { + "label": "ช่วงเวลาตรวจสอบ", + "value": { + "fiveMinutes": "5 นาที", + "tenMinutes": "10 นาที", + "fifteenMinutes": "15 นาที", + "thirtyMinutes": "30 นาที" + } + } + } + }, + "url": { + "title": "แสดง IP/URL ของมอนิเตอร์บนหน้าสถานะ", + "description": "แสดง IP หรือ URL ของมอนิเตอร์บนหน้าสถานะสาธารณะ หากปิดใช้งาน จะแสดงเฉพาะชื่อมอนิเตอร์เพื่อปกป้องข้อมูลที่ละเอียดอ่อน", + "option": { + "showURL": { + "label": "แสดง IP/URL บนหน้าสถานะ", + "enabled": "เปิดใช้งาน", + "disabled": "ปิดใช้งาน" + } + } + }, + "stats": { + "title": "ประวัติการตรวจสอบ", + "description": "ล้างประวัติการตรวจสอบและสถิติทั้งหมดของทีม การกระทำนี้ไม่สามารถย้อนกลับได้", + "buttonText": "ล้างสถิติทั้งหมด", + "dialog": { + "title": "ล้างประวัติการตรวจสอบทั้งหมด?", + "description": "การดำเนินการนี้จะลบประวัติการตรวจสอบ สถิติ และข้อมูลการตรวจสอบทั้งหมดของทีมอย่างถาวร ไม่สามารถย้อนกลับได้", + "confirm": "ใช่ ล้างสถิติทั้งหมด" + } + } + } }, - "pageSpeedSettings": { - "description": "ใส่ Google PageSpeed API Key ของคุณเพื่อเปิดใช้งานการตรวจสอบ PageSpeed ของ Google คลิก รีเซ็ต เพื่ออัปเดต Key", - "labelApiKeySet": "ตั้งค่า API key แล้ว คลิก รีเซ็ต เพื่อเปลี่ยน", - "labelApiKey": "PageSpeed API key", - "title": "Google PageSpeed API key" + "editUser": { + "form": { + "roles": { + "title": "บทบาท", + "description": "กำหนดบทบาทให้กับผู้ใช้ สามารถเลือกได้หลายบทบาท" + } + }, + "dialog": { + "removeUser": { + "title": "ลบผู้ใช้", + "content": "คุณแน่ใจหรือไม่ว่าต้องการลบ {{name}} ออกจากทีม? การกระทำนี้ไม่สามารถย้อนกลับได้" + } + } }, - "saveButtonLabel": "บันทึก", - "statsSettings": { - "clearAllStatsButton": "ล้างสถิติทั้งหมด", - "clearAllStatsDescription": "ล้างสถิติทั้งหมด การกระทำนี้ไม่สามารถย้อนกลับได้", - "clearAllStatsDialogConfirm": "ใช่ ล้างสถิติทั้งหมด", - "clearAllStatsDialogDescription": "เมื่อถูกลบแล้ว ประวัติการตรวจสอบและสถิติไม่สามารถกู้คืนได้", - "clearAllStatsDialogTitle": "คุณต้องการล้างสถิติทั้งหมดหรือไม่?", - "description": "กำหนดระยะเวลาที่ต้องการเก็บข้อมูลประวัติ คุณยังสามารถล้างข้อมูลทั้งหมดที่มีอยู่ได้", - "labelTTL": "จำนวนวันที่ต้องการเก็บประวัติการตรวจสอบ", - "labelTTLOptional": "0 หมายถึงไม่จำกัด", - "title": "ประวัติการตรวจสอบ" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "แก้ไขเหตุการณ์", + "option": { + "comment": { + "label": "ความคิดเห็น (ไม่บังคับ)", + "placeholder": "เพิ่มความคิดเห็นเกี่ยวกับการแก้ไข..." + } + } + }, + "details": { + "analysis": "การวิเคราะห์เหตุการณ์", + "comment": "ความคิดเห็น:", + "detailsLabel": "รายละเอียด", + "downtime": "ดาวน์ไทม์:", + "message": "ข้อความ:", + "monitor": "มอนิเตอร์:", + "overview": "ภาพรวม", + "resolutionDetails": "รายละเอียดการแก้ไข", + "resolutionType": "ประเภท:", + "resolutionTypes": { + "automatic": "อัตโนมัติ", + "manual": "ด้วยตนเอง" + }, + "resolve": "แก้ไขเหตุการณ์", + "resolvedAt": "แก้ไขเมื่อ:", + "resolvedBy": "แก้ไขโดย:", + "startedAt": "เริ่มเมื่อ:", + "status": "สถานะ:", + "statusCode": "รหัสสถานะ:", + "timeline": "ไทม์ไลน์", + "title": "รายละเอียดเหตุการณ์", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "มอนิเตอร์ทั้งหมด", + "monitor": "มอนิเตอร์", + "resolutionType": "ประเภทการแก้ไข", + "resolutionTypes": { + "manual": "ด้วยตนเอง", + "automatic": "อัตโนมัติ", + "all": "ทั้งหมด" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "เหตุการณ์ที่ยังดำเนินอยู่", + "active_zero": "ไม่มีเหตุการณ์ที่ยังดำเนินอยู่", + "active_one": "{{count}} เหตุการณ์ที่ยังดำเนินอยู่", + "active_other": "{{count}} เหตุการณ์ที่ยังดำเนินอยู่" + }, + "incidentStats": { + "avgResolutionTime": "เวลาแก้ไขเฉลี่ย", + "mostAffectedMonitor": "มอนิเตอร์ที่ได้รับผลกระทบมากที่สุด", + "title": "สถิติเหตุการณ์", + "totalIncidents": "เหตุการณ์ทั้งหมด" + }, + "latestIncidents": { + "title": "เหตุการณ์ล่าสุด" + } + }, + "table": { + "actions": { + "details": "รายละเอียด", + "goToMonitor": "ไปยังมอนิเตอร์", + "resolveManually": "แก้ไขด้วยตนเอง" + }, + "activeIncidents": "เหตุการณ์ที่ยังดำเนินอยู่", + "headers": { + "endTime": "เวลาสิ้นสุด", + "resolutionType": "ประเภทการแก้ไข", + "startTime": "เวลาเริ่มต้น", + "statusCode": "รหัสสถานะ" + }, + "resolvedIncidents": "เหตุการณ์ที่แก้ไขแล้ว", + "status": { + "active": "ใช้งานอยู่", + "resolved": "แก้ไขแล้ว" + } + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "ลบมอนิเตอร์ทั้งหมด", - "description": "ลบมอนิเตอร์ทั้งหมดออกจากระบบของคุณ", - "dialogConfirm": "ใช่ ลบมอนิเตอร์ทั้งหมด", - "dialogDescription": "เมื่อถูกลบแล้ว มอนิเตอร์ไม่สามารถกู้คืนได้", - "dialogTitle": "คุณต้องการลบมอนิเตอร์ทั้งหมดหรือไม่?", - "title": "รีเซ็ตระบบ" - }, - "timezoneSettings": { - "description": "เลือกโซนเวลาที่ใช้แสดงวันที่และเวลาในแอปทั้งหมด", - "label": "โซนเวลาที่แสดง", - "title": "โซนเวลาที่แสดง" - }, - "title": "การตั้งค่า", - "uiSettings": { - "description": "สลับโหมดสว่าง/มืด หรือเปลี่ยนภาษาอินเทอร์เฟซผู้ใช้", - "labelLanguage": "ภาษา", - "labelTheme": "โหมดธีม", - "title": "รูปลักษณ์" - }, - "urlSettings": { - "description": "แสดง IP/URL ของมอนิเตอร์บนหน้าสถานะสาธารณะ หากปิดใช้งาน จะแสดงเฉพาะชื่อมอนิเตอร์เพื่อป้องกันข้อมูลสำคัญ", - "label": "แสดง IP/URL บนหน้าสถานะ", - "selectDisabled": "ปิดใช้งาน", - "selectEnabled": "เปิดใช้งาน", - "title": "IP/URL ของมอนิเตอร์บนหน้าสถานะ" - }, - "globalThresholds": { - "title": "เกณฑ์การตรวจสอบแบบรวม", - "description": "กำหนดค่าเกณฑ์ของ CPU, หน่วยความจำ, ดิสก์ และอุณหภูมิ หากระบุค่าไว้ ระบบจะเปิดใช้งานการตรวจสอบโดยอัตโนมัติ" - } - }, - "statusPageCreate": { - "buttonSave": "บันทึก" - }, - "incidentsOptionsHeaderFilterResolved": "แก้ไขแล้ว", - "settingsSave": "บันทึก", - "statusPageCreateAppearanceTitle": "รูปลักษณ์", - "confirmPassword": "ยืนยันรหัสผ่าน", - "monitorHooks": { - "failureAddDemoMonitors": "ไม่สามารถเพิ่มมอนิเตอร์ตัวอย่างได้", - "successAddDemoMonitors": "เพิ่มมอนิเตอร์ตัวอย่างสำเร็จ" - }, - "settingsAppearance": "รูปลักษณ์", - "settingsDisplayTimezone": "โซนเวลาที่แสดง", - "settingsGeneralSettings": "การตั้งค่าทั่วไป", - "incidentsOptionsHeaderTotalIncidents": "จำนวนเหตุการณ์ทั้งหมด", - "statusPage": { - "deleteSuccess": "ลบหน้าสถานะสำเร็จ", - "deleteFailed": "ลบหน้าสถานะล้มเหลว", - "createSuccess": "สร้างหน้าสถานะสำเร็จ", - "updateSuccess": "อัปเดตหน้าสถานะสำเร็จ", - "generalSettings": "การตั้งค่าทั่วไป", - "contents": "เนื้อหา", - "fallback": { - "checks": [ - "ตรวจสอบและแสดงสุขภาพของบริการแบบเรียลไทม์", - "ติดตามหลายบริการและแชร์สถานะ", - "แจ้งผู้ใช้ให้ทราบเกี่ยวกับการหยุดทำงานและประสิทธิภาพ" - ], - "title": "หน้าสถานะใช้สำหรับ:", - "actionButton": "มาสร้างหน้าสถานะแรกกันเถอะ!" - } - }, - "testNotificationsDisabled": "ยังไม่มีการตั้งค่าการแจ้งเตือนสำหรับมอนิเตอร์นี้ คุณต้องเพิ่มโดยคลิกปุ่ม 'ตั้งค่า'", - "incidentsTableResolvedAt": "แก้ไขเมื่อ", - "incidentsTableActionResolve": "แก้ไข", - "checkHooks": { - "failureResolveOne": "แก้ไขเหตุการณ์ล้มเหลว", - "failureResolveAll": "แก้ไขเหตุการณ์ทั้งหมดล้มเหลว", - "failureResolveMonitor": "ไม่สามารถแก้ไขเหตุการณ์ของตัวตรวจสอบได้" - }, - "checkFormError": "กรุณาตรวจสอบแบบฟอร์มสำหรับข้อผิดพลาด", - "diagnosticsPage": { - "diagnosticDescription": "วินิจฉัยระบบ", - "statsDescription": "สถิติระบบ", - "gauges": { - "heapAllocationTitle": "การจัดสรร Heap", - "heapAllocationSubtitle": "% ของหน่วยความจำที่ใช้ได้", - "heapUsageTitle": "การใช้งาน Heap", - "heapUsageSubtitle": "% ของหน่วยความจำที่ใช้ได้", - "heapUtilizationTitle": "การใช้งาน Heap", - "heapUtilizationSubtitle": "% ของที่จัดสรรแล้ว", - "instantCpuUsageTitle": "การใช้งาน CPU ทันที", - "instantCpuUsageSubtitle": "% ของเวลา 1 วินาทีที่ CPU ใช้" - }, - "stats": { - "eventLoopDelayTitle": "ความล่าช้าวงจร Event Loop", - "uptimeTitle": "เวลาทำงาน", - "usedHeapSizeTitle": "ขนาดที่ใช้ Heap", - "totalHeapSizeTitle": "รวมขนาด Heap", - "osMemoryLimitTitle": "ขีดจำกัดหน่วยความจำ OS" - } - }, - "pageSpeedLighthouseAPI": "ใช้ Lighthouse PageSpeed API เพื่อตรวจสอบเว็บไซต์ของคุณ", - "time": { - "threeMinutes": "3 นาที", - "fiveMinutes": "5 นาที", - "tenMinutes": "10 นาที", - "twentyMinutes": "20 นาที", - "oneHour": "1 ชั่วโมง", - "oneDay": "1 วัน", - "oneWeek": "1 สัปดาห์", - "fourMinutes": "4 นาที", - "oneMinute": "1 นาที", - "twoMinutes": "2 นาที", - "fifteenSeconds": "15 วินาที", - "thirtySeconds": "30 วินาที" - }, - "general": { - "noOptionsFound": "ไม่พบ {{unit}}" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [ - "ติดตามประสิทธิภาพของเซิร์ฟเวอร์ของคุณ", - "ระบุจุดคอขวดและเพิ่มประสิทธิภาพการใช้งาน", - "มั่นใจในความเสถียรด้วยการตรวจสอบแบบเรียลไทม์" - ], - "title": "ตัวตรวจสอบโครงสร้างพื้นฐานใช้สำหรับ:", - "actionButton": "มาสร้างตัวตรวจสอบโครงสร้างพื้นฐานตัวแรกกันเถอะ!" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [ - "กำหนดช่วงเวลาบำรุงรักษา", - "ลดความเข้าใจผิด", - "หยุดส่งการแจ้งเตือนในช่วงเวลาบำรุงรักษา" - ], - "title": "ช่วงเวลาบำรุงรักษาใช้สำหรับ:", - "actionButton": "มาสร้างช่วงเวลาบำรุงรักษาแรกกันเถอะ!" - } - }, - "pageSpeed": { - "fallback": { - "checks": [ - "รายงานเกี่ยวกับประสบการณ์ผู้ใช้ของหน้าเว็บ", - "ช่วยวิเคราะห์ความเร็วของหน้าเว็บ", - "ให้คำแนะนำในการปรับปรุงหน้าเว็บ" - ], - "title": "ตัวตรวจสอบ PageSpeed ใช้สำหรับ:", - "actionButton": "มาสร้างตัวตรวจสอบ PageSpeed ตัวแรกกันเถอะ!" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [ - "ตรวจสอบว่าเว็บไซต์หรือเซิร์ฟเวอร์ออนไลน์และตอบสนองหรือไม่", - "แจ้งทีมเมื่อเกิดปัญหาการหยุดทำงานหรือประสิทธิภาพ", - "ตรวจสอบ HTTP endpoints, ping, คอนเทนเนอร์ และพอร์ต", - "ติดตามสถิติ uptime และความเสถียรย้อนหลัง" - ], - "title": "ตัวตรวจสอบ Uptime ใช้สำหรับ:", - "actionButton": "มาสร้างตัวตรวจสอบ Uptime ตัวแรกกันเถอะ!" - } - }, - "editUserPage": { - "form": { - "email": "อีเมล", - "firstName": "ชื่อจริง", - "lastName": "นามสกุล", - "role": "บทบาท", - "save": "บันทึก" - }, - "table": { - "actionHeader": "การดำเนินการ", - "roleHeader": "บทบาท" - }, - "title": "แก้ไขผู้ใช้", - "toast": { - "successUserUpdate": "อัปเดตผู้ใช้เรียบร้อยแล้ว", - "validationErrors": "ข้อผิดพลาดในการตรวจสอบข้อมูล" - } - }, - "incidentsPageActionResolveMonitor": "แก้ไขเหตุการณ์ของตัวตรวจสอบ", - "incidentsPageActionResolveAll": "แก้ไขเหตุการณ์ทั้งหมด", - "matchMethodOptions": { - "equal": "Equal", - "equalPlaceholder": "success", - "include": "Include", - "includePlaceholder": "ok", - "regex": "Regex", - "regexPlaceholder": "^(success|ok)$", - "text": "วิธีการจับคู่" - }, - "monitorType": { - "docker": { - "label": "ชื่อ/ID คอนเทนเนอร์", - "namePlaceholder": "คอนเทนเนอร์ของฉัน", - "placeholder": "my-app or abcd1234" - }, - "http": { - "label": "URL ที่ต้องการตรวจสอบ", - "namePlaceholder": "Google", - "placeholder": "google.com" - }, - "ping": { - "label": "ที่อยู่ IP ที่ต้องการตรวจสอบ", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" - }, - "port": { - "label": "ที่อยู่ IP ที่ต้องการตรวจสอบ", - "namePlaceholder": "Localhost:5173", - "placeholder": "localhost" - }, - "game": { - "label": "URL ที่ต้องการมอนิเตอร์", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "JSON Path" - }, - "bytesPerSecond": "ไบต์ต่อวินาที", - "bytesReceived": "ไบต์ที่รับ", - "bytesSent": "ไบต์ที่ส่ง", - "chooseGame": "เลือกเกม", - "createMonitorPage": { - "incidentConfigDescription": "การใช้หน้าต่างเลื่อนจะช่วยกำหนดเวลาที่ตัวตรวจสอบล้มเหลว สถานะของตัวตรวจสอบจะเปลี่ยนก็ต่อเมื่อเปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนตรงตามค่าที่กำหนด", - "incidentConfigStatusWindowLabel": "ต้องมีการตรวจสอบกี่ครั้งในหน้าต่างเลื่อน?", - "incidentConfigStatusWindowThresholdLabel": "ต้องการให้กี่เปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนล้มเหลว/สำเร็จ ก่อนที่จะเปลี่ยนสถานะของตัวตรวจสอบ?", - "incidentConfigTitle": "เหตุการณ์", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "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": "เลือกดิสก์ที่คุณต้องการตรวจสอบ" + "charts": { + "labels": { + "cpu": "การใช้งาน CPU", + "disk": "การใช้งานดิสก์", + "memory": "การใช้งานหน่วยความจำ", + "netBytesRecv": "{{name}} - ไบต์ที่รับ", + "netBytesSent": "{{name}} - ไบต์ที่ส่ง", + "temp": "อุณหภูมิ" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "ความถี่สูงสุด", + "title": "การใช้งาน CPU", + "upperLabel": "ความถี่ปัจจ��บัน" + }, + "disk": { + "lowerLabel": "ว่าง", + "title": "การใช้งานดิสก์ {{idx}}", + "upperLabel": "ใช้แล้ว" + }, + "memory": { + "lowerLabel": "ว่าง", + "title": "การใช้งานหน่วยความจำ", + "upperLabel": "ใช้แล้ว" + } + }, + "statBoxes": { + "avgCpuTemperature": "อุณหภูมิเฉลี่ยของซีพียู", + "cpuFrequency": "ความถี่ของ CPU", + "cpuLogical": "ซีพียู (คอร์เสมือน)", + "cpuPhysical": "ซีพียู (คอร์หลัก)", + "disk": "ดิสก์", + "memory": "หน่วยความจำ", + "os": "ระบบปฏิบัติการ" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "ดิสก์", + "memory": "หน่วยความจำ" + } + }, + "tabs": { + "labels": { + "network": "เน็ตเวอร์", + "overview": "ภาพรวม" + } + }, + "fallback": { + "actionButton": "สร้างมอนิเตอร์!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "มอนิเตอร์โครงสร้างพื้นฐานใช้เพื่อ:" + } + }, + "logs": { + "tabs": { + "diagnostics": "วินิจฉัย", + "logs": "Logs เซิร์ฟเวอร์", + "queue": "คิวงาน" + }, + "logLevelSelect": { + "label": "ระดับบันทึก" + }, + "jobQueue": "คิวงาน", + "failedJobs": "งานที่ล้มเหลว", + "noLogs": "ไม่พบบันทึก", + "metrics": { + "jobs": "งาน", + "activeJobs": "งานที่กำลังดำเนินการ", + "failingJobs": "งานที่ล้มเหลว", + "totalRuns": "จำนวนรันทั้งหมด", + "totalFailures": "จำนวนล้มเหลวทั้งหมด" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "ดีเลย์ของ Event loop", + "uptime": "อัปไทม์", + "usedHeapSize": "ขนาด heap ที่ใช้", + "totalHeapSize": "ขนาด heap ทั้งหมด", + "osMemoryLimit": "ขีดจำกัดหน่วยความจำ OS" + }, + "gauges": { + "heapAllocation": "การจัดสรร Heap", + "heapUsage": "การใช้งาน Heap", + "heapUtilization": "การใช้ประโยชน์ Heap", + "instantCpuUsage": "การใช้งาน CPU ขณะนี้", + "availableMemoryPercentage": "% ของหน่วยความจำที่มี", + "allocatedPercentage": "% ที่จัดสรร", + "usedSPercentage": "% ของ 1 วินาทีที่ CPU ใช้", + "total": "ทั้งหมด", + "used": "ใช้แล้ว" + } + }, + "table": { + "headers": { + "timestamp": "เวลา", + "level": "ระดับ", + "service": "บริการ", + "method": "เมธอด", + "monitorId": "รหัสมอนิเตอร์", + "runCount": "จำนวนรัน", + "failCount": "จำนวนล้มเหลว", + "lastRunAt": "รันล่าสุดเมื่อ", + "lockedAt": "ล็อกเมื่อ", + "lastFinishedAt": "เสร็จสิ้นล่าสุดเมื่อ", + "lastRunTook": "รันล่าสุดใช้เวลา", + "lastFailedAt": "ล้มเหลวล่าสุดเมื่อ", + "failReason": "สาเหตุที่ล้มเหลว" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "สร้างช่วงเวลาบำรุงรักษา!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "ช่วงเวลาบำรุงรักษาใช้เพื่อ:" + }, + "table": { + "headers": { + "nextWindow": "ช่วงเวลาถัดไป", + "repeat": "ทำซ้ำ" + } + }, + "form": { + "general": { + "title": "ตั้งค่าทั่วไป", + "description": "ตั้งชื่อและตัวเลือกการทำซ้ำสำหรับช่วงเวลาบำรุงรักษา", + "option": { + "name": { + "label": "ชื่อ", + "placeholder": "เช่น บำรุงรักษาประจำสัปดาห์" + }, + "repeat": { + "label": "ทำซ้ำ" + } + } + }, + "startDate": { + "title": "วันที่เริ่มต้น", + "description": "เลือกวันที่เริ่มต้นสำหรับช่วงเวลาบำรุงรักษา", + "option": { + "startDate": { + "label": "วันที่เริ่มต้น" + } + } + }, + "startTime": { + "title": "เวลาเริ่มต้น", + "description": "ตั้งเวลาเริ่มต้นและระยะเวลาสำหรับช่วงเวลาบำรุงรักษา ค่าทั้งหมดเป็น UTC", + "option": { + "duration": { + "label": "ระยะเวลา" + }, + "startTime": { + "label": "เวลาเริ่มต้น" + } + }, + "monitors": { + "title": "มอนิเตอร์", + "description": "มอนิเตอร์ที่ช่วงเวลาบำรุงรักษาควรใช้งาน", + "option": { + "addMonitors": { + "label": "เพิ่มมอนิเตอร์" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "สร้างช่องทาง", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "ช่องทางการแจ้งเตือนใช้เพื่อ:" + }, + "form": { + "accessToken": { + "optionAccessToken": "โทเค็นการเข้าถึง", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "ที่อยู่ที่จะส่งการแจ้งเตือน", + "optionAddress": "ที่อยู่", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "ที่อยู่" + }, + "homeServer": { + "optionHomeServer": "โฮมเซิร์ฟเวอร์", + "placeholder": "example.com" + }, + "matrix": { + "description": "กำหนดค่าการเชื่อมต่อ Matrix homeserver สำหรับการแจ้งเตือน", + "title": "ตั้งค่า Matrix" + }, + "notificationName": { + "description": "ชื่อที่สื่อความหมายสำหรับช่องทางการแจ้งเตือน", + "optionName": "ชื่อช่องทาง", + "placeholder": "เช่น การแจ้งเตือน Production", + "title": "ชื่อช่องทาง" + }, + "pagerDuty": { + "description": "คีย์การรวม PagerDuty สำหรับรับการแจ้งเตือน", + "optionIntegrationKey": "คีย์การรวม", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "คีย์การรวม" + }, + "roomId": { + "optionRoomId": "รหัสห้อง", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "เลือกประเภทช่องทางการแจ้งเตือนที่ต้องการสร้าง", + "optionType": "ประเภท", + "title": "ประเภทช่องทาง" + }, + "telegram": { + "title": "ตั้งค่า Telegram", + "description": "เพื่อเปิดใช้งานการแจ้งเตือนผ่าน Telegram ให้สร้างบอท Telegram โดยใช้ BotFather ซึ่งเป็นบอททางการสำหรับสร้างและจัดการบอท Telegram จากนั้นนำ API token และ Chat ID มาใส่ที่นี่", + "optionBotToken": "โทเค็นบอท", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "Chat ID", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "ปลายทาง" + } + } + }, + "pageSpeed": { + "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)" + }, + "legend": { + "title": "รายงาน PageSpeed", + "weight": "น้ำหนัก" + }, + "pie": { + "title": "รายงานประสิทธิภาพ" + } + }, + "table": { + "headers": { + "pageSpeedScore": "คะแนน PageSpeed" + } + }, + "fallback": { + "actionButton": "สร้างมอนิเตอร์!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "มอนิเตอร์ PageSpeed ใช้เพื่อ:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "เขตเวลาที่แสดง", + "description": "เลือกเขตเวลาที่ใช้แสดงวันที่และเวลาทั่วทั้งแอปพลิเคชัน", + "option": { + "timezone": { + "label": "เขตเวลาที่แสดง" + } + } + }, + "ui": { + "title": "รูปลักษณ์", + "description": "สลับระหว่างโหมดสว่างและมืด เปลี่ยนภาษา หรือปรับแต่งประเภทกราฟ", + "option": { + "theme": { + "label": "โหมดธีม", + "light": "สว่าง", + "dark": "มืด" + }, + "language": { + "label": "ภาษา" + }, + "chartType": { + "label": "ประเภทกราฟ", + "histogram": "ฮิสโตแกรม", + "heatmap": "แผนที่ความร้อน" + } + } + }, + "pagespeed": { + "title": "คีย์ API ของ Google PageSpeed", + "description": "กรอกคีย์ API ของ Google PageSpeed เพื่อเปิดใช้งานการตรวจสอบ Google PageSpeed คลิกรีเซ็ตเพื่ออัปเดตคีย์", + "option": { + "apiKey": { + "label": "คีย์ API ของ PageSpeed", + "labelSet": "ตั้งค่าคีย์ API แล้ว คลิกรีเซ็ตเพื่อเปลี่ยน", + "placeholder": "กรอกคีย์ API ของ Google PageSpeed" + } + } + }, + "url": { + "title": "แสดง IP/URL ของมอนิเตอร์บนหน้าสถานะ", + "description": "แสดง IP หรือ URL ของมอนิเตอร์บนหน้าสถานะสาธารณะ หากปิดใช้งาน จะแสดงเฉพาะชื่อมอนิเตอร์เพื่อปกป้องข้อมูลที่ละเอียดอ่อน", + "option": { + "showURL": { + "label": "แสดง IP/URL บนหน้าสถานะ", + "enabled": "เปิดใช้งาน", + "disabled": "ปิดใช้งาน" + } + } + }, + "stats": { + "title": "ประวัติการตรวจสอบ", + "description": "ล้างประวัติการตรวจสอบและสถิติทั้งหมดของทีม การกระทำนี้ไม่สามารถย้อนกลับได้", + "option": { + "clear": { + "label": "ล้างสถิติทั้งหมด การกระทำนี้ไม่สามารถย้อนกลับได้" + } + }, + "dialog": { + "title": "ล้างประวัติการตรวจสอบทั้งหมด?", + "description": "ไม่สามารถย้อนกลับได้" + } + }, + "retention": { + "title": "การเก็บรักษาข้อมูลตรวจสอบ", + "description": "กำหนดระยะเวลาเก็บรักษาข้อมูลตรวจสอบก่อนถูกลบอัตโนมัติ", + "option": { + "days": { + "label": "ระยะเวลาเก็บรักษา (วัน)", + "unlimited": "ไม่จำกัด" + } + } + }, + "thresholds": { + "title": "เกณฑ์ทั่วไป", + "description": "กำหนดเกณฑ์ CPU หน่วยความจำ ดิสก์ และอุณหภูมิทั่วไปสำหรับการตรวจสอบโครงสร้างพื้นฐาน ใช้กับมอนิเตอร์ฮาร์ดแวร์ทั้งหมด เว้นแต่ถูกแทนที่", + "option": { + "cpu": { + "label": "เกณฑ์ CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "เกณฑ์หน่วยความจำ (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "เกณฑ์ดิสก์ (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "เกณฑ์อุณหภูมิ (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "ตั้งค่าอีเมล", + "description": "กำหนดค่าอีเมลสำหรับระบบ ใช้สำหรับส่งการแจ้งเตือน", + "descriptionTransport": "สร้าง SMTP transport สำหรับ NodeMailer", + "descriptionTransportLink": "ดูข้อกำหนดที่นี่", + "option": { + "host": { + "label": "โฮสต์อีเมล - ชื่อโฮสต์หรือ IP ที่จะเชื่อมต่อ", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "พอร์ตอีเมล - พอร์ตที่จะเชื่อมต่อ", + "placeholder": "587" + }, + "address": { + "label": "ที่อยู่อีเมล - ใช้สำหรับยืนยันตัวตน", + "placeholder": "you@example.com" + }, + "user": { + "label": "ผู้ใช้อีเมล - ชื่อผู้ใช้สำหรับยืนยันตัวตน แทนที่อีเมลหากระบุ", + "placeholder": "เว้นว่างหากไม่จำเป็น" + }, + "password": { + "label": "รหัสผ่านอีเมล - รหัสผ่านสำหรับยืนยันตัวตน", + "labelSet": "ตั้งค่ารหัสผ่านแล้ว คลิกรีเซ็ตเพื่อเปลี่ยน", + "placeholder": "กรอกรหัสผ่านของคุณ" + }, + "tlsServername": { + "label": "ชื่อเซิร์ฟเวอร์ TLS - ชื่อโฮสต์สำหรับตรวจสอบ TLS เมื่อโฮสต์เป็น IP (ไม่บังคับ)", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "โฮสต์เชื่อมต่ออีเมล - ชื่อโฮสต์ที่ใช้ในการ HELO/EHLO", + "placeholder": "localhost" + }, + "secure": { + "label": "ใช้ SSL (แนะนำ): เข้ารหัสการเชื่อมต่อด้วย SSL/TLS" + }, + "pool": { + "label": "เปิดใช้งาน connection pooling: ใช้การเชื่อมต่อที่มีอยู่ซ้ำเพื่อเพิ่มประสิทธิภาพ" + }, + "ignoreTLS": { + "label": "ปิดใช้งาน STARTTLS: ไม่ใช้ TLS แม้เซิร์ฟเวอร์จะรองรับ" + }, + "requireTLS": { + "label": "บังคับใช้ STARTTLS: ต้องอัปเกรดเป็น TLS ล้มเหลวหากไม่รองรับ" + }, + "rejectUnauthorized": { + "label": "ปฏิเสธใบรับรองที่ไม่ถูกต้อง: ปฏิเสธการเชื่อมต่อด้วยใบรับรอง self-signed หรือไม่น่าเชื่อถือ" + } + } + }, + "demoMonitors": { + "title": "มอนิเตอร์สาธิต", + "description": "เพิ่มมอนิเตอร์ตัวอย่างเพื่อวัตถุประสงค์ในการ���าธิต" + }, + "removeMonitors": { + "title": "รีเซ็ตระบบ", + "description": "ลบมอนิเตอร์ทั้งหมดออกจากระบบ", + "dialog": { + "title": "ลบมอนิเตอร์ทั้งหมด?", + "description": "ไม่สามารถย้อนกลับได้" + } + }, + "exportMonitors": { + "title": "ส่งออกมอนิเตอร์" + }, + "importExportMonitors": { + "title": "นำเข้า / ส่งออกมอนิเตอร์", + "description": "นำเข้าหรือส่งออกข้อมูลมอนิเตอร์เป็นไฟล์ JSON สำหรับสำรองหรือโอนย้าย" + }, + "about": { + "title": "เกี่ยวกับ", + "developedBy": "พัฒนาโดย Bluewave Labs" + }, + "validation": { + "errorMessage": "กรุณาแก้ไขข้อผิดพลาดการตรวจสอบต่อไปนี้:" + } + } + }, + "statusPages": { + "deleteSuccess": "ลบหน้าสถานะสำเร็จ", + "fallback": { + "title": "หน้าสถานะใช้เพื่อ:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "สร้างหน้าสถานะ!" + }, + "monitorsList": { + "chartTypeHeatmap": "แผนที่ความร้อน", + "chartTypeHistogram": "ฮิสโตแกรม", + "noData": "ไม่มีข้อมูล", + "infrastructure": { + "title": "โครงสร้างพื้นฐาน", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "หน่วยความจำ", + "disk": "ดิสก์", + "usage": "การใช้งาน", + "used": "ใช้แล้ว", + "total": "ทั้งหมด" + }, + "uptime": { + "title": "อัปไทม์", + "responseTime": "เวลาตอบสนอง" + } + }, + "statusBar": { + "allDown": "ระบบทั้งหมดล่ม", + "allUp": "ระบบทั้งหมดทำงานปกติ", + "degraded": "บางระบบกำลังประสบปัญหา", + "unknown": "ไม่สามารถระบุสถานะระบบได้" + }, + "table": { + "headers": { + "name": "ชื่อหน้าสถานะ", + "url": "URL สาธารณะ" + }, + "published": "เผยแพร่แล้ว", + "unpublished": "ยังไม่เผยแพร่" + }, + "title": "หน้าสถานะ", + "details": { + "empty": { + "title": "ยังไม่มีข้อมูลที่นี่", + "addMonitor": "เพิ่มมอนิเตอร์เพื่อเริ่มต้น" + } + }, + "form": { + "access": { + "title": "การเข้าถึง", + "description": "หากหน้าสถานะพร้อมแล้ว คุณสามารถทำเครื่องหมายว่าเผยแพร่แล้ว", + "option": { + "published": { + "name": "เผยแพร่แล้วและสาธารณะสามารถเห็นได้" + } + } + }, + "basicInfo": { + "title": "ข้อมูลพื้นฐาน", + "description": "กำหนดชื่อบริษัทและซับโดเมนที่หน้าสถานะชี้ไป", + "option": { + "name": { + "label": "ชื่อบริษัท", + "placeholder": "Acme Inc." + }, + "url": { + "label": "ที่อยู่หน้าสถานะของคุณ", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "มอนิเตอร์", + "description": "เลือกมอนิเตอร์ที่จะแสดงบนหน้าสถานะ", + "noMonitors": "ไม่ได้เลือกมอนิเตอร์", + "option": { + "monitors": { + "label": "เลือกมอนิเตอร์", + "placeholder": "ค้นหาและเลือกมอนิเตอร์..." + } + } + }, + "timezone": { + "title": "เขตเวลา", + "description": "เลือกเขตเวลาที่หน้าสถานะจะแสดง", + "option": { + "timezone": { + "label": "เขตเวลา", + "placeholder": "เลือกเขตเวลา..." + } + } + }, + "appearance": { + "title": "รูปลักษณ์", + "description": "กำหนดรูปลักษณ์เริ่มต้นของหน้าสถานะสาธารณะ", + "option": { + "logo": { + "label": "โลโก้" + }, + "color": { + "label": "สีแบรนด์" + } + } + }, + "features": { + "title": "ฟีเจอร์", + "description": "กำหนดค่าข้อมูลที่แสดงบนหน้าสถานะ", + "option": { + "showCharts": { + "label": "แสดงกราฟเวลาตอบสนอง" + }, + "showUptimePercentage": { + "label": "แสดงเปอร์เซ็นต์อัปไทม์" + }, + "showAdminLoginLink": { + "label": "แสดงลิงก์เข้าสู่ระบบผู้ดูแล" + }, + "showInfrastructure": { + "label": "แสดงเมตริกโครงสร้างพื้นฐาน" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "ค้นหามอนิเตอร์..." + } + }, + "table": { + "headers": { + "responseTime": "เวลาตอบสนอง" + } + }, + "fallback": { + "actionButton": "สร้างมอนิเตอร์!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "มอนิเตอร์อัปไทม์ใช้เพื่อ:" + } } } } diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 59f294533..43f0a8041 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -1,1108 +1,1305 @@ { - "submit": "Gönder", - "title": "Başlık", - "distributedStatusHeaderText": "Gerçek zamanlı, Gerçek cihazlar kapsamı", - "distributedStatusSubHeaderText": "Dünya çapında milyonlarca cihaz tarafından desteklenen sistem performansını küresel bölgeye, ülkeye veya şehre göre görüntüleyin", - "settingsDisabled": "Devre dışı", - "settingsSuccessSaved": "Ayarlar başarıyla kaydedildi", - "settingsFailedToSave": "Ayarlar kaydedilemedi", - "settingsStatsCleared": "İstatistikler başarıyla temizlendi", - "settingsFailedToClearStats": "İstatistikler temizlenemedi", - "settingsMonitorsDeleted": "Tüm monitörler başarıyla silindi", - "settingsFailedToDeleteMonitors": "Monitörler silinemedi", - "starPromptTitle": "Checkmate yıldızla değerlendirin", - "starPromptDescription": "En son sürümleri görün ve GitHub'daki topluluğun büyümesine yardımcı olun", - "https": "HTTPS", - "http": "HTTP", - "monitor": "monitör", - "aboutus": "Hakkımızda", - "now": "Şimdi", - "delete": "Sil", - "configure": "Yapılandır", - "responseTime": "Yanıt süresi:", - "ms": "ms", - "bar": "Çubuk", - "area": "Alan", - "country": "ÜLKE", - "city": "ŞEHİR", - "response": "YANIT", - "monitorStatusUp": "Monitör {name} ({url}) ayakta ve yanıt veriyor", - "monitorStatusDown": "Monitör {name} ({url}) yanıt vermiyor", - "webhookSendSuccess": "Web kanca bildirimi başarıyla gönderildi", - "webhookSendError": "Web kanca bildirimi bu platforma gönderilemedi: {platform}", - "webhookUnsupportedPlatform": "Desteklenmeyen platform: {platform}", - "distributedRightCategoryTitle": "Monitör", - "distributedStatusServerMonitors": "Sunucu monitörleri", - "distributedStatusServerMonitorsDescription": "İlgili sunucuların monitör durumları", - "distributedUptimeCreateSelectURL": "Burada sunucu adresini (URL) ve monitör türünü girebilirsiniz.", - "distributedUptimeCreateChecks": "Yapılacak kontroller", - "distributedUptimeCreateChecksDescription": "Adresi girdikten sonra kontrol ekleyebilir ya da çıkartabilirsiniz.", - "distributedUptimeCreateIncidentNotification": "Olay bildirimleri", - "distributedUptimeCreateIncidentDescription": "Bir olay olduğunda kullanıcıları bilgilendir.", - "distributedUptimeCreateAdvancedSettings": "Gelişmiş ayarlar", - "distributedUptimeDetailsNoMonitorHistory": "Bu monitör için henüz bir denetim günlüğü oluşturulmadı.", - "distributedUptimeDetailsStatusHeaderUptime": "Erişilebilirlik", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Son güncelleme", - "notifications": { - "enableNotifications": "{{platform}} bildirimlerini etkinleştir", - "testNotification": "Bildirimi test et", - "addOrEditNotifications": "Bildirimleri ekle ya da düzenle", - "slack": { - "label": "Slack", - "description": "Slack bildirimlerini etkinleştirmek için bir Slack uygulaması oluşturun ve gelen web kancalarını etkinleştirin. Daha sonra, webhook URL’sini buraya girmeniz yeterlidir.\n\n\n\n\n\n", - "webhookLabel": "Web kanca URL:", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "Slack webhook URL'si gereklidir" - }, - "discord": { - "label": "Discord", - "description": "Checkmate’ten Discord bildirimleri aracılığıyla bir Discord kanalına veri göndermek için Discord’un gelen web kancaları özelliğini kullanabilirsiniz.\n\n\n\n\n\n\n\n", - "webhookLabel": "Discord web kanca URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "Discord webhook URL'si gerekli" - }, - "telegram": { - "label": "Telegram", - "description": "Telegram bildirimlerini etkinleştirmek için, Telegram botlarını oluşturup yönetmek için kullanılan resmi bot BotFather ile bir Telegram botu oluşturun. Daha sonra, API jetonunu (token) ve sohbet kimliğini (chat ID) alın ve buraya yazın.", - "tokenLabel": "Bot jetonu (token)", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "Sohbet ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "Telegram token ve sohbet kimliği gereklidir" - }, - "webhook": { - "label": "Web kancaları", - "description": "Olaylar meydana geldiğinde bildirim almak için özel bir web kancası ayarlayabilirsiniz.", - "urlLabel": "Web kanca URL:", - "urlPlaceholder": "https://sunucu-adresiniz.com/webhook", - "urlRequired": "Webhook URL'si gerekli" - }, - "testNotificationDevelop": "Test bildirimi 2", - "integrationButton": "Bildirim Entegrasyonu", - "testSuccess": "Test bildirimi başarıyla gönderildi", - "testFailed": "Test bildirimi gönderilemedi", - "unsupportedType": "Desteklenmeyen bildirim türü", - "networkError": "Ağ hatası oluştu", - "fallback": { - "title": "", - "checks": [""], - "actionButton": "" - }, - "createButton": "Bildirim kanalı oluştur", - "createTitle": "Bildirim kanalı", - "create": { - "success": "", - "failed": "Bildirim kanalı oluşturulamadı" - }, - "fetch": { - "success": "Bildirimler başarıyla alındı", - "failed": "Bildirimler alınamadı" - }, - "delete": { - "success": "Bildirim başarıyla silindi", - "failed": "Bildirim silinemedi" - }, - "edit": { - "success": "Bildirim başarıyla güncellendi", - "failed": "Bildirim güncellenemedi" - }, - "test": { - "success": "Test bildirimi başarıyla gönderildi", - "failed": "" - } - }, - "testLocale": "TEST13 UPLOAD", - "add": "Ekle", - "monitors": "Monitörler", - "distributedUptimeStatusCreateStatusPage": "durum sayfası", - "distributedUptimeStatusCreateStatusPageAccess": "Erişim", - "distributedUptimeStatusCreateStatusPageReady": "Durum sayfanız hazırsa yayınlayabilirsiniz.", - "distributedUptimeStatusBasicInfoHeader": "Temel bilgiler", - "distributedUptimeStatusBasicInfoDescription": "Şirket adınızı ve durum sayfanızın işaret ettiği alt alan adını tanımlayın.", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "Durum sayfanız için bir logo yükleyin", - "distributedUptimeStatusLogoUploadButton": "Logo yükle", - "distributedUptimeStatusStandardMonitorsHeader": "Standart monitörler", - "distributedUptimeStatusStandardMonitorsDescription": "Durum sayfanıza standart monitörleri ekleyin.", - "distributedUptimeStatusCreateYour": "Oluştur:", - "distributedUptimeStatusEditYour": "Düzenle:", - "distributedUptimeStatusPublishedLabel": "Yayında ve herkes tarafından görülebilir", - "distributedUptimeStatusCompanyNameLabel": "Şirket adı", - "distributedUptimeStatusPageAddressLabel": "Durum sayfası adresi", - "distributedUptimeStatus30Days": "30 gün", - "distributedUptimeStatus60Days": "60 gün", - "distributedUptimeStatus90Days": "90 gün", - "distributedUptimeStatusPageNotSetUp": "Henüz bir durum sayfası oluşturulmadı.", - "distributedUptimeStatusContactAdmin": "Lütfen yöneticiniz ile görüşün", - "distributedUptimeStatusPageNotPublic": "Bu durum sayfası herkese açık değil", - "distributedUptimeStatusPageDeleteDialog": "Bu durum sayfasını silmek istiyor musunuz? ", - "distributedUptimeStatusPageDeleteConfirm": "Evet, durum sayfasını sil", - "distributedUptimeStatusPageDeleteDescription": "Silindiği zaman durum sayfalarını geri getiremezsiniz.", - "distributedUptimeStatusDevices": "Cihazlar", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "Yakılan UPT", - "distributedUptimeStatusUptLogo": "Upt Logo", - "incidentsTableNoIncidents": "Hiç olay kaydedilmedi", - "incidentsTablePaginationLabel": "olaylar", - "incidentsTableMonitorName": "Monitör adı", - "incidentsTableStatus": "Durum", - "incidentsTableDateTime": "Tarih ve saat", - "incidentsTableStatusCode": "Durum kodu", - "incidentsTableMessage": "Mesaj", - "incidentsOptionsHeader": "Servisler:", - "incidentsOptionsHeaderFilterBy": "Filtrele:", - "incidentsOptionsHeaderFilterAll": "Tümü", - "incidentsOptionsHeaderFilterDown": "Erişilemiyor", - "incidentsOptionsHeaderFilterCannotResolve": "Çözümlenemiyor", - "incidentsOptionsHeaderShow": "Göster:", - "incidentsOptionsHeaderLastHour": "Son saat", - "incidentsOptionsHeaderLastDay": "Son gün", - "incidentsOptionsHeaderLastWeek": "Son hafta", - "incidentsOptionsPlaceholderAllServers": "Tüm sunucular", - "infrastructureCreateYour": "Oluştur:", - "infrastructureCreateGeneralSettingsDescription": "Burada, sunucu aracısına bağlanmak için kolay ad ve yetkilendirme sırrıyla birlikte ana bilgisayarın URL'sini seçebilirsiniz.", - "infrastructureServerRequirement": "İzlediğiniz sunucunun şu şekilde çalışması gerekir:", - "infrastructureCustomizeAlerts": "Alarmları özelleştir", - "infrastructureAlertNotificationDescription": "Eşikler belirtilen bir yüzdeyi aştığında kullanıcıya bildirim gönder.", - "infrastructureCreateMonitor": "Altyapı Monitörü oluştur", - "infrastructureProtocol": "Protokol", - "infrastructureServerUrlLabel": "Sunucu adresi", - "infrastructureDisplayNameLabel": "Görünen adı", - "infrastructureAuthorizationSecretLabel": "Kimlik denetimi parolası", - "gb": "GB", - "mb": "MB", - "mem": "Bellek", - "memoryUsage": "Bellek kullanımı", - "cpu": "CPU", - "cpuUsage": "CPU kullanımı", - "cpuTemperature": "CPU sıcaklığı", - "diskUsage": "Disk kullanımı", - "used": "Kullanılan", - "total": "Toplam", - "cores": "Çekirdek", - "frequency": "Frekans", - "status": "Durum", - "cpuPhysical": "CPU (fiziksel)", - "cpuLogical": "CPU (Mantıksal)", - "cpuFrequency": "CPU Frekansı", - "avgCpuTemperature": "Ortalama CPU Isısı", - "memory": "Bellek", - "disk": "Disk", - "uptime": "Çalışma süresi", - "os": "İşletim sistemi", - "host": "Makine", - "actions": "İşlemler", - "integrations": "Entegrasyonlar", - "integrationsPrism": "Prism'i favori servisinize bağlayın.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Slack'e bağlanın ve bir kanaldaki olayları görün", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Discord'a bağlanın ve olayları doğrudan bir kanalda görüntüleyin", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Tüm olayları Zapier'a gönderin ve sonra her yerde görün", - "commonSave": "Kaydet", - "createYour": "Senin yarat", - "createMonitor": "Monitör oluştur", - "pause": "Beklet", - "resume": "Yeniden başlat", - "editing": "Düzenleniyor...", - "url": "URL", - "access": "Erişim", - "timezone": "Zaman dilimi", - "features": "Özellikler", - "administrator": "Yönetici misin?", - "loginHere": "Buradan giriş yap", - "displayName": "Görüntülenecek isim", - "urlMonitor": "Monitör edilecek URL", - "portToMonitor": "Monitör edilecek port", - "websiteMonitoring": "Web sitesi monitörü", - "websiteMonitoringDescription": "Web sitenizi veya API uç noktanızı izlemek için HTTP(s) kullanın.", - "pingMonitoring": "Ping monitörü", - "pingMonitoringDescription": "Sitenizin ayakta olup olmadığını denetleyin.", - "dockerContainerMonitoring": "Docker konteyner monitörü", - "dockerContainerMonitoringDescription": "Docker konteynerinizin ayakta olup olmadığını denetleyin.", - "portMonitoring": "Port monitörü", - "portMonitoringDescription": "Portunuzun açık olup olmadığını denetleyin.", - "createMaintenanceWindow": "Bakım aralığı oluşturun", - "createMaintenance": "Bakım oluştur", - "editMaintenance": "Bakımı düzenle", - "maintenanceWindowName": "Bakım aralığı adı", - "friendlyNameInput": "Görünen ad", - "friendlyNamePlaceholder": "__:__ saatinde __ dakika boyunca bakım var", - "maintenanceRepeat": "Bakım Tekrarı", - "maintenance": "bakım", - "duration": "Süre", - "addMonitors": "Monitör ekle", - "window": "pencere", - "cancel": "İptal", - "message": "Mesaj", - "low": "düşük", - "high": "yüksek", - "statusCode": "Durum kodu", - "date&Time": "Tarih & saat", - "type": "Tür", - "statusPageName": "Durum sayfası adı", - "publicURL": "Genel URL", - "repeat": "Tekrarla", - "edit": "Düzenle", - "createA": "Bir tane oluştur", - "remove": "Kaldırmak", - "maintenanceWindowDescription": "Bu zaman diliminde pingleriniz gönderilmeyecek", - "startTime": "Başlangıç saati", - "timeZoneInfo": "Tüm tarihler ve saatler GMT+0 zaman dilimindedir.", - "monitorsToApply": "Bakım penceresini uygulamak için monitörler", - "nextWindow": "Sonraki pencere", - "notFoundButton": "Ana panoya git", - "pageSpeedConfigureSettingsDescription": "Burada, monitör tipiyle birlikte sunucunun URL'sini seçebilirsiniz.", - "monitorDisplayName": "Monitör görüntüleme adı", - "whenNewIncident": "Yeni bir olay oluştuğunda,", - "notifySMS": "SMS ile bildir (yakında)", - "notifyEmails": "Ayrıca birden fazla eposta adresine bildirim gönderebilirsiniz (yakında geliyor)", - "seperateEmails": "Birden fazla epostayı virgülle ayırabilirsiniz", - "checkFrequency": "Frekansı denetle", - "matchMethod": "Eşleşme yöntemi", - "expectedValue": "Beklenen değer", - "deleteDialogTitle": "Gerçekten bu monitörü silmek istiyor musunuz?", - "deleteDialogDescription": "Bir kez silindiği zaman tekrar getiremezsiniz.", - "pageSpeedMonitor": "Sayfa Hızı Monitörü", - "shown": "Gösteriliyor: ", - "ago": "önce", - "companyName": "Firma adı", - "pageSpeedDetailsPerformanceReport": "Değerler yaklaşıktır ve değişebilir", - "pageSpeedDetailsPerformanceReportCalculator": "Hesap makinesini gör", - "checkingEvery": "Denetleme sıklığı", - "statusPageCreateSettings": "Eğer durum sayfanız hazırsa, herkese açık yayınlayabilirsiniz", - "basicInformation": "Temel Bilgiler", - "statusPageCreateBasicInfoDescription": "Şirket adınızı ve durum sayfanızın işaret ettiği alt alan adını tanımlayın.", - "statusPageCreateSelectTimeZoneDescription": "Durum sayfanızın hangi zaman diliminde görüntüleneceğini seçin.", - "statusPageCreateAppearanceDescription": "Genel durum sayfanızın varsayılan görünümünü ve hissini tanımlayın.", - "statusPageCreateSettingsCheckboxLabel": "Yayınlandı ve herkese açık", - "statusPageCreateBasicInfoStatusPageAddress": "Durum sayfası adresiniz", - "statusPageCreateTabsContent": "Durum sayfası sunucuları", - "statusPageCreateTabsContentDescription": "İzlediğiniz herhangi bir sayıda sunucuyu durum sayfanıza ekleyebilirsiniz. Ayrıca en iyi görüntüleme deneyimi için bunları yeniden sıralayabilirsiniz.", - "statusPageCreateTabsContentFeaturesDescription": "Durum sayfasında daha fazla bilgi göster", - "showCharts": "Çizelgeleri göster", - "showUptimePercentage": "Çalışma süresi yüzdesini göster", - "removeLogo": "Logoyu sil", - "statusPageStatus": "Herkese açık bir durum sayfası henüz oluşturulmadı.", - "statusPageStatusContactAdmin": "Lütfen yöneticinizle iletişime geçin.", - "statusPageStatusNotPublic": "Durum sayfası herkese açık değil.", - "statusPageStatusNoPage": "Burada bir durum sayfası bulunmuyor.", - "statusPageStatusServiceStatus": "Servis durumu", - "deleteStatusPage": "Gerçekten bu durum sayfasını silmek istiyor musunuz?", - "deleteStatusPageConfirm": "Evet, durum sayfasını sil", - "deleteStatusPageDescription": "Silindikten sonra durum sayfasını geri getiremezsiniz.", - "uptimeCreate": "Beklenen değer, yanıt sonucuyla eşleştirilir ve bu eşleşme durumu belirler.", - "uptimeCreateJsonPath": "Bu ifade, yanıt JSON verisi üzerinde değerlendirilir, ortaya çıkan sonuç beklenen değerle karşılaştırılır. Bkz:", - "uptimeCreateJsonPathQuery": "Sorgu dili dokümantasyonu için.", - "maintenanceTableActionMenuDialogTitle": "Gerçekten bu bakım aralığını silmek istiyor musunuz?", - "infrastructureEditYour": "Düzenle", - "infrastructureEditMonitor": "Altyapı İzlemeyi Kaydet", - "infrastructureMonitorCreated": "Altyapı izleyicisi başarıyla oluşturuldu!", - "infrastructureMonitorUpdated": "Altyapı izleyicisi başarıyla güncellendi!", - "errorInvalidTypeId": "Geçersiz bildirim türü sağlandı", - "errorInvalidFieldId": "Geçersiz alan kimliği sağlandı", - "inviteNoTokenFound": "Davet belirteci bulunamadı", - "pageSpeedWarning": "Uyarı: Henüz bir Google PageSpeed API anahtarı eklemediniz. Bu olmadan PageSpeed izleyicisi çalışmayacaktır.", - "pageSpeedLearnMoreLink": "Buraya tıklayın", - "pageSpeedAddApiKey": "(API anahtarınızı eklemek için)", - "update": "Güncelleme", - "invalidFileFormat": "Desteklenmeyen dosya biçimi!", - "invalidFileSize": "Dosya boyutu çok büyük!", - "ClickUpload": "Yüklemek için tıklayın", - "DragandDrop": "sürükle ve bırak", - "MaxSize": "Azami Boyut", - "SupportedFormats": "Desteklenen formatlar", - "FirstName": "İsim", - "LastName": "Soyisim", - "EmailDescriptionText": "Geçerli epostanız - bu eposta değiştirilemez.", - "YourPhoto": "Profil fotoğrafı", - "PhotoDescriptionText": "Bu fotoğrafınız profil sayfasında görüntülenecektir.", - "save": "Kaydet", - "DeleteDescriptionText": "Bu, hesabı ve ilişkili tüm verileri sunucudan kaldıracaktır. Bu geri alınamaz.", - "DeleteAccountWarning": "Hesabınızı kaldırmanız, tekrar oturum açamayacağınız ve tüm verilerinizin silineceği anlamına gelir. Bu geri alınamaz.", - "DeleteWarningTitle": "Gerçekten bu hesabı silmek istiyor musunuz?", - "bulkImport": { - "title": "Toplu içeri alma", - "selectFileTips": "Yüklenecek CSV dosyasını seçin", - "selectFileDescription": " veya örneğimizi indirebilirsiniz", - "selectFile": "Dosyayı seçin", - "parsingFailed": "Ayrıştırma başarısız oldu", - "uploadSuccess": "Monitör başarıyla oluşturuldu!", - "validationFailed": "Doğrulama başarısız oldu", - "noFileSelected": "Hiçbir dosya seçilmedi", - "fallbackPage": "Toplu olarak sunucu listesini yüklemek için bir dosyayı içe aktarın", - "invalidFileType": "", - "uploadFailed": "" - }, - "DeleteAccountTitle": "Hesabı sil", - "DeleteAccountButton": "Hesabı sil", - "publicLink": "Herkese açık bağlantı", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "Sıfırla", - "ignoreTLSError": "TLS/SSL hatalarını gözardı et", - "tlsErrorIgnored": "TLS/SSL hataları gözardı ediliyor", - "ignoreTLSErrorDescription": "TLS/SSL hatalarını gözardı et ve websitesinin ayakta olduğunu denetle", - "createNew": "Yeni oluştur", - "greeting": { - "prepend": "Merhaba", - "append": "Öğleden sonra muhteşem geçsin!", - "overview": "" - }, - "roles": { - "superAdmin": "Süper yönetici", - "admin": "Yönetici", - "teamMember": "Ekip elemanı", - "demoUser": "Demo kullanıcısı" - }, - "teamPanel": { - "teamMembers": "Ekip elemanları", - "filter": { - "all": "Tümü", - "member": "Eleman" - }, - "inviteTeamMember": "Ekip elemanı davet et", - "inviteNewTeamMember": "Yeni ekip elemanı davet et", - "inviteDescription": "", - "email": "Eposta", - "selectRole": "Rolü seçin", - "inviteLink": "Davet linki", - "cancel": "İptal", - "noMembers": "Bu role sahip hiç üye yok", - "getToken": "", - "emailToken": "", - "table": { - "name": "İsim", - "email": "Eposta", - "role": "Rol", - "created": "Oluşturulma" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "Durduruldu", - "resumed": "Yeniden başlatıldı", - "active": "Aktif" - }, - "menu": { - "uptime": "", - "pagespeed": "Sayfa hızı", - "infrastructure": "", - "incidents": "Olaylar", - "statusPages": "Durum sayfaları", - "maintenance": "Bakım", - "integrations": "Entegrasyonlar", - "settings": "Ayarlar", - "support": "Destek", - "discussions": "Soru-cevap", - "docs": "Dokümanlar", - "changelog": "Değişiklik günlüğü", - "profile": "Profil", - "password": "Parola", - "team": "Ekip", - "logOut": "Çıkış yap", - "notifications": "Bildirimler", - "logs": "Günlükler" - }, - "settingsEmailUser": "E-posta kullanıcısı – Kimlik doğrulama için kullanıcı adı; belirtilirse e-posta adresinin yerine geçer.", - "state": "Durum", - "statusBreadCrumbsStatusPages": "Durum sayfası", - "statusBreadCrumbsDetails": "Detaylar", - "commonSaving": "Kaydediliyor.", - "navControls": "Kontroller", - "incidentsPageTitle": "Olaylar", - "passwordPanel": { - "passwordChangedSuccess": "Şifreniz başarı ile değiştirildi.", - "passwordInputIncorrect": "Şifreniz hatalı", - "currentPassword": "Mevcut parola", - "enterCurrentPassword": "Mevcut parolanızı girin", - "newPassword": "Yeni parola", - "enterNewPassword": "Yeni parolanızı girin", - "confirmNewPassword": "Yeni şifrenizi onaylayın", - "passwordRequirements": "Yeni şifreniz en az 8 karakterden oluşmalı ve en az bir büyük harf, bir küçük harf, bir rakam ve bir özel karakter içermelidir.", - "saving": "Kaydediliyor..." - }, - "emailSent": "E-posta başarı ile gönderildi.", - "failedToSendEmail": "E-posta gönderimi başarısız.", - "settingsTestEmailSuccess": "Test E-postası başarı ile gönderildi.", - "settingsTestEmailFailed": "Test E-postası gönderimi başarısız.", - "settingsTestEmailFailedWithReason": "Test E-postası gönderimi başarısız: {{reason}}", - "settingsTestEmailUnknownError": "Bilinmeyen hata.", - "statusMsg": { - "paused": "İzleme duraklatıldı.", - "up": "Siteniz yayında.", - "down": "Siteniz kapalı.", - "pending": "Sırada.." - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "", - "game": "", - "https": "" - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" + "auth": { + "roles": { + "admin": "Yönetici", + "demo": "Demo", + "superadmin": "Süper yönetici", + "user": "Kullanıcı" + } }, - "toasts": { - "networkError": "", - "checkConnection": "Lütfen bağlantınızı kontrol edin", - "unknownError": "Bilinmeyen hata" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "Devam et", - "back": "Geri" + "alerts": { + "pageSpeedApiKey": { + "content": "Uyarı: Henüz bir Google PageSpeed API anahtarı eklemediniz. Eklemek için Ayarlar sayfasını ziyaret edin. Bu anahtar olmadan PageSpeed monitörü çalışmayacaktır." + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "Detaylar", + "home": "Ana sayfa" + }, + "buttons": { + "addMember": "Üye ekle", + "cancel": "İptal", + "close": "Kapat", + "configure": "Yapılandır", + "confirm": "Onayla", + "create": "Oluştur", + "delete": "Sil", + "generateToken": "Token oluştur", + "incidents": "Olaylar", + "inviteMember": "Üye davet et", + "pause": "Duraklat", + "resume": "Devam et", + "save": "Kaydet", + "sendInvite": "Davet gönder", + "test": "Test", + "testNotifications": "Bildirimleri test et", + "toggleTheme": "", + "flushQueue": "Kuyruğu temizle", + "notFound": "Ana panele git", + "resetPassword": "Parolayı sıfırla", + "reset": "Sıfırla", + "clear": "Temizle", + "addDemo": "Demo monitörler ekle", + "removeMonitors": "Monitörleri kaldır", + "sendTestEmail": "Test e-postası gönder", + "exportToJSON": "JSON olarak dışa aktar", + "importFromJSON": "JSON dosyasından içe aktar", + "clearFilters": "Filtreleri temizle", + "removeUser": "Kullanıcıyı kaldır" + }, + "charts": { + "labels": { + "averageResponseTime": "Ortalama yanıt süresi", + "downtime": "Kesinti süresi", + "high": "yüksek", + "low": "düşük", + "uptime": "Çalışma süresi" }, - "inputs": { - "email": { - "label": "Eposta", - "placeholder": "", - "errors": { - "empty": "Devam etmek için eposta adresinizi girin", - "invalid": "" - } - }, - "password": { - "label": "Parola", - "rules": { - "length": { - "beginning": "", - "highlighted": "8 karakter uzunluğunda" - }, - "special": { - "beginning": "En az içermelidir: ", - "highlighted": "bir özel karakter" - }, - "number": { - "beginning": "En az içermelidir: ", - "highlighted": "bir sayı" - }, - "uppercase": { - "beginning": "En az içermelidir: ", - "highlighted": "Bir büyük harf" - }, - "lowercase": { - "beginning": "En az içermelidir: ", - "highlighted": "Bir küçük harf" - }, - "match": { - "beginning": "", - "highlighted": "eşleşmelidir" - } - }, - "errors": { - "empty": "Lütfen parolanızı girin", - "length": "Parolanız en az 8 karakter uzunluğunda olmalıdır", - "uppercase": "Parola en az 1 büyük harf içermelidir", - "lowercase": "Parola en az 1 küçük harf içermelidir", - "number": "Parola en az 1 sayı içermelidir", - "special": "Parola en az 1 özel karakter içermelidir", - "incorrect": "Parolanız sistemdeki ile uyuşmuyor" - } - }, - "passwordConfirm": { - "label": "Parolayı onayla", - "placeholder": "Onaylamak için parolayı yeniden gir", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "Jordan", - "errors": { - "empty": "Lütfen adınızı girin", - "length": "İsim 50 karakterden az olmalıdır", - "pattern": "İsim yalnızca harfler, boşluklar, kesme işaretleri veya tireler içerebilir" - } - }, - "lastName": { - "label": "Soyadı", - "placeholder": "Ellis", - "errors": { - "empty": "Lütfen soyadınızı girin", - "length": "Soyadı en çok 50 karakter olmalıdır", - "pattern": "Soyad yalnızca harfler, boşluklar, kesme işaretleri veya tireler içerebilir" + "histogram": { + "avg": "Ort: {{value}} ms", + "max": "Maks: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Bu işlem geri alınamaz.", + "title": "Bunu silmek istediğinizden emin misiniz?" + } + }, + "labels": { + "active": "Aktif", + "paused": "duraklatıldı", + "na": "N/A", + "resolved": "Çözüldü", + "responseTime": "Yanıt süresi" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Roller" } } }, - "errors": { - "validation": "Veriyi işlerken hata oluştu." - }, - "fields": { - "password": { - "errors": { - "incorrect": "" + "name": { + "option": { + "firstName": { + "label": "", + "placeholder": "Jordan" + }, + "lastName": { + "label": "Soyadı", + "placeholder": "Ellis" } - }, - "role": { - "errors": { - "min": "" + } + }, + "email": { + "option": { + "email": { + "label": "E-posta", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "Giriş yap", - "subheadings": { - "stepOne": "Eposta adresinizi girin", - "stepTwo": "Parolanızı girin" - }, - "links": { - "forgotPassword": "Parolanızı mı unuttunuz?", - "register": "Hesabınız yok mu?", - "forgotPasswordLink": "Parolayı sıfırla", - "registerLink": "Buradan kayıt olun" - }, - "toasts": { - "success": "Tekrar hoşgeldiniz! Başarıyla sisteme giriş yaptınız.", - "incorrectPassword": "Hatalı parola" - }, + "table": { + "empty": "Burada hiçbir şey yok", + "headers": { + "actions": "İşlemler", + "dateTime": "Tarih ve saat", + "message": "Mesaj", + "monitor": "Monitör", + "monitorId": "Monitör ID", + "name": "Ad", + "status": "Durum", + "type": "Tür", + "url": "URL", + "interval": "Aralık", + "active": "Aktif", + "responseTime": "Yanıt süresi" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "Yüklemek için tıklayın", + "dragAndDrop": "sürükle ve bırak", + "supportedFormats": "Desteklenen formatlar", + "maxSize": "Azami Boyut", + "orDragAndDrop": "veya sürükle bırak", "errors": { - "password": { - "incorrect": "Giriş yaptığınız parola sistemdekiyle uyuşmuyor" - } - }, - "welcome": "" + "invalidFileSize": "Dosya boyutu çok büyük!", + "invalidFileFormat": "Desteklenmeyen dosya biçimi!" + } }, - "registration": { - "heading": { - "superAdmin": "Süper yönetici oluştur", - "user": "Kayıt ol" - }, - "subheadings": { - "stepOne": "Kişisel bilgilerinizi girin", - "stepTwo": "Eposta adresinizi girin", - "stepThree": "Parolanızı oluturun" + "headerStatusPageControls": { + "publicLink": "Herkese açık bağlantı" + }, + "headerTimeRange": { + "labels": { + "day": "Gün", + "month": "Ay", + "recent": "Son", + "week": "Hafta" }, "description": { - "superAdmin": "Başlamak için süper yönetici hesabı oluşturun", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "Süper yönetici hesabı oluşturun", - "user": "" - }, - "termsAndPolicies": "Bir hesap oluşturarak Hizmet Şartları ve Gizlilik Politikası'nı kabul etmiş olursunuz.", - "links": { - "login": "Bir hesabınız mı var? Giriş yapın" - }, - "toasts": { - "success": "Hoşgeldiniz! Hesabınız başarıyla oluşturuldu." - }, - "welcome": "" + "recent": "Son 2 saatin istatistikleri gösteriliyor.", + "day": "Son 24 saatin istatistikleri gösteriliyor.", + "week": "Son 7 günün istatistikleri gösteriliyor.", + "month": "Son 30 günün istatistikleri gösteriliyor." + } }, - "forgotPassword": { - "heading": "Parolanızı mı kaybettiniz?", - "subheadings": { - "stepOne": "Endişelenmeyin, sıfırlamak için gerekli bilgileri göndereceğiz.", - "stepTwo": "Parola sıfırlama için gerekli bilgileri hesabına ilettik.", - "stepThree": "Parolanız daha önce kullandığınız paroladan farklı olmalıdır.", - "stepFour": "Parolanız başarıyla sıfırlandı. Buraya tıklayarak giriş yapabilirsiniz." + "offlineBanner": { + "serverUnreachable": "Sunucuya ulaşılamıyor", + "retry": "Tekrar dene", + "retrying": "Yeniden deneniyor...", + "reconnected": "" + }, + "sidebar": { + "menu": { + "uptime": "", + "pagespeed": "Sayfa hızı", + "infrastructure": "", + "notifications": "Bildirimler", + "checks": "Kontroller", + "incidents": "Olaylar", + "statusPages": "Durum sayfaları", + "maintenance": "Bakım", + "logs": "Günlükler", + "settings": "Ayarlar" }, - "buttons": { - "openEmail": "Eposta uygulamasını aç", - "resetPassword": "Parolayı sıfırla" + "bottomMenu": { + "support": "Destek", + "discussions": "Soru-cevap", + "docs": "Dokümanlar", + "changelog": "Değişiklik günlüğü" }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "accountMenu": { + "profile": "Profil", + "password": "Parola", + "team": "Ekip" }, - "links": { - "login": "", - "resend": "" + "starPrompt": { + "title": "Checkmate'e yıldız verin", + "description": "En son sürümleri görün ve GitHub'da topluluğun büyümesine katkıda bulunun" }, - "toasts": { - "sent": "", - "emailNotFound": "Eposta bulunamadı.", - "redirect": "", - "success": "", - "error": "" + "authFooter": { + "navControls": "Kontroller", + "logOut": "Çıkış yap", + "roles": { + "superAdmin": "Süper yönetici", + "admin": "Yönetici", + "user": "Kullanıcı", + "demoUser": "Demo kullanıcısı" + } + } + }, + "starPrompt": { + "title": "Checkmate'e yıldız verin", + "description": "En son sürümleri görün ve GitHub'da topluluğun büyümesine katkıda bulunun" + } + }, + "pages": { + "notFound": { + "title": "Eyvah! Sushini düşürdün!", + "subtitle": "URL mevcut değil veya erişim yetkiniz yok." + }, + "account": { + "tabs": { + "profile": "Profil", + "password": "Parola", + "team": "Ekip" + }, + "form": { + "name": { + "title": "Ad", + "description": "Kişisel bilgilerinizi güncelleyin" + }, + "photo": { + "title": "Profil fotoğrafı", + "description": "Bir profil fotoğrafı yükleyin" + }, + "currentPassword": { + "title": "Mevcut parola", + "description": "Kimliğinizi doğrulamak için mevcut parolanızı girin", + "option": { + "label": "Mevcut parola", + "placeholder": "Mevcut parolanızı girin" + } + }, + "newPassword": { + "title": "Yeni parola", + "description": "En az 8 karakter içeren güçlü bir parola seçin", + "option": { + "newPassword": { + "label": "Yeni parola", + "placeholder": "Yeni parolanızı girin" + }, + "confirm": { + "label": "Parolayı onaylayın", + "placeholder": "Yeni parolanızı onaylayın" + } + } + }, + "deleteAccount": { + "title": "Hesabı sil", + "description": "Bu işlem kalıcıdır ve geri alınamaz" + } + }, + "team": { + "filter": { + "placeholder": "Role göre filtrele", + "all": "Tümü", + "admin": "Yönetici", + "member": "Eleman" + }, + "table": { + "headers": { + "email": "E-posta", + "role": "Rol", + "created": "Oluşturulma" + } + }, + "addMember": { + "title": "Yeni ekip üyesi kaydet", + "description": "Yeni bir kullanıcı hesabı oluşturun. Kimlik bilgilerini oluşturduktan sonra güvenli bir şekilde paylaşın." + }, + "invite": { + "title": "Ekip üyesi davet et", + "description": "Ekibinize katılmak için bir davet gönderin", + "email": { + "label": "E-posta adresi", + "placeholder": "E-posta adresini girin" + }, + "role": { + "label": "Rol", + "placeholder": "Bir rol seçin" + }, + "linkLabel": "Davet bağlantısı" + } + } + }, + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "", + "highlighted": "8 karakter uzunluğunda" + }, + "lowercase": { + "beginning": "En az içermelidir: ", + "highlighted": "Bir küçük harf" + }, + "match": { + "beginning": "", + "highlighted": "eşleşmelidir" + }, + "number": { + "beginning": "En az içermelidir: ", + "highlighted": "bir sayı" + }, + "special": { + "beginning": "En az içermelidir: ", + "highlighted": "bir özel karakter" + }, + "uppercase": { + "beginning": "En az içermelidir: ", + "highlighted": "Bir büyük harf" + } + }, + "form": { + "option": { + "email": { + "label": "E-posta", + "placeholder": "me@example.com" + }, + "password": { + "label": "Parola", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Parolayı onaylayın" + } + } + } + }, + "login": { + "title": "Checkmate'e tekrar hoş geldiniz!", + "subtitle": "Devam etmek için giriş yapın", + "submit": "Giriş yap", + "links": { + "forgotPassword": { + "text": "Parolanızı mı unuttunuz?", + "linkText": "Parolayı sıfırla" + }, + "register": { + "text": "Hesabınız yok mu?", + "linkText": "Buradan kaydolun" + } + } + }, + "register": { + "title": "Checkmate'e tekrar hoş geldiniz!", + "subtitle": "Başlamak için kaydolun", + "submit": "Kaydol" + }, + "forgotPassword": { + "title": "Parolanızı mı unuttunuz?", + "subtitle": "Endişelenmeyin, size sıfırlama talimatları göndereceğiz.", + "submit": "Kurtarma talebi gönder", + "links": { + "login": { + "text": "Geri dön:", + "linkText": "giriş" + } + } + }, + "setNewPassword": { + "title": "Parolanızı sıfırlayın", + "subtitle": "Yeni parolanız daha önce kullandığınız parolalardan farklı olmalıdır." + } + }, + "checks": { + "selects": { + "monitor": { + "all": "Tüm monitörler" + }, + "status": { + "all": "Tümü", + "down": "Çevrimdışı", + "up": "Çevrimiçi" + } + }, + "table": { + "empty": "Bu zaman aralığında çevrimdışı kontrol yok", + "headers": { + "statusCode": "Durum kodu", + "location": "Konum" + } + } + }, + "common": { + "monitors": { + "actions": { + "configure": "Yapılandır", + "delete": "Sil", + "generateToken": "Token oluştur", + "details": "Detaylar", + "incidents": "Olaylar", + "inviteMember": "Üye davet et", + "openSite": "Siteyi aç", + "pause": "Duraklat", + "resume": "Devam et" + }, + "statBoxes": { + "activeFor": "Aktif süresi", + "certificateExpiry": "Sertifika bitiş tarihi", + "lastCheck": "Son kontrol", + "lastResponseTime": "Son yanıt süresi" + }, + "status": { + "down": "çevrimdışı", + "breached": "eşik aşıldı", + "initializing": "başlatılıyor", + "maintenance": "bakım", + "paused": "duraklatıldı", + "total": "toplam", + "up": "çevrimiçi" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Oyun", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Port", + "optionPagespeed": "PageSpeed", + "optionHardware": "Altyapı", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } + }, + "createMonitor": { + "form": { + "advanced": { + "description": "Gelişmiş kullanım senaryoları için isteğe bağlı ayarlar", + "option": { + "advancedMatching": { + "label": "Gelişmiş eşleştirme kullan" + }, + "expectedValue": { + "label": "Beklenen değer" + }, + "jsonPath": { + "description": "Bu ifade, yanıt JSON verisi üzerinde değerlendirilecek ve sonuç beklenen değerle eşleştirilecektir. Sorgu dili belgeleri için jmespath.org sayfasına bakın.", + "label": "JSONPath ifadesi" + }, + "matchMethod": { + "label": "Eşleştirme yöntemi", + "equal": "", + "include": "", + "regex": "" + } + }, + "title": "Gelişmiş ayarlar" + }, + "frequency": { + "description": "Bu monitörün durumunu ne sıklıkla kontrol etmek istiyorsunuz?", + "option": { + "frequency": { + "label": "Kontrol sıklığı", + "value": { + "fifteenMinutes": "15 dakika", + "fifteenSeconds": "", + "fiveMinutes": "5 dakika", + "fourMinutes": "", + "oneMinute": "", + "tenMinutes": "10 dakika", + "thirtyMinutes": "30 dakika", + "thirtySeconds": "", + "threeMinutes": "", + "twoMinutes": "" + } + } + }, + "title": "Kontrol sıklığı" + }, + "general": { + "option": { + "container": { + "label": "Container adı/ID", + "placeholder": "uygulamam veya abcd1234" + }, + "host": { + "label": "Ana bilgisayar", + "placeholder": "192.168.1.100 veya example.com" + }, + "name": { + "label": "Görünen ad", + "placeholder": "örn. Web Sitem" + }, + "secret": { + "label": "Yetkilendirme anahtarı", + "placeholder": "Gizli anahtarınızı girin" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "", + "placeholder": "" + }, + "port": { + "label": "İzlenecek port", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Servis adı", + "placeholder": "örn. my.service.v1 (genel sağlık için boş bırakın)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Genel ayarlar", + "description": { + "http": "İzlenecek URL veya IP adresini girin (örn. https://example.com/ veya 192.168.1.100) ve panoda görünecek açık bir ad ekleyin.", + "ping": "", + "port": "", + "docker": "", + "game": "", + "pagespeed": "Web sitenizin sayfa yükleme performansını, Core Web Vitals metriklerini ve optimizasyon puanlarını takip edin.", + "grpc": "gRPC sunucusunun ana bilgisayar adını ve portunu girin, isteğe bağlı olarak sağlık kontrolü için bir servis adı belirtin ve bir görünen ad ekleyin.", + "websocket": "İzlenecek WebSocket URL'sini girin (örn. wss://example.com/socket) ve bir görünen ad ekleyin.", + "hardware": "Altyapınız için CPU, bellek, disk kullanımı ve sıcaklığı izleyin." + } + }, + "ignoreTls": { + "description": "HTTPS bağlantıları için TLS/SSL sertifika doğrulamasını yapılandırın.", + "option": { + "tls": { + "label": "TLS/SSL hatalarını yoksay" + } + }, + "title": "TLS/SSL ayarları" + }, + "incidents": { + "description": "Bir monitörün ne zaman çevrimdışı olduğunu belirlemek için kayan pencere kullanılır. Monitörün durumu, yalnızca kayan penceredeki kontrol yüzdesi belirlenen değeri karşıladığında değişir.", + "option": { + "checks": { + "label": "Kayan penceredeki kontrol sayısı" + }, + "percentage": { + "label": "Monitör durumu değişmeden önce kayan penceredeki kontrollerin yüzde kaçı başarısız/başarılı olmalı?" + } + }, + "title": "Olaylar" + }, + "notifications": { + "description": "Kullanmak istediğiniz bildirim kanallarını seçin", + "title": "Bildirimler" + }, + "type": { + "description": "Gerçekleştirilecek kontrol türünü seçin", + "optionDockerDescription": "Bir container'ın çalışıp çalışmadığını izlemek için Docker kullanın.", + "optionGameDescription": "Belirli bir oyun sunucusunun çevrimiçi olup olmadığını izleyin.", + "optionGrpcDescription": "Standart Health Checking Protocol kullanarak gRPC servislerini izleyin.", + "optionWebSocketDescription": "Bağlantı sağlığı ve yanıt süresi için WebSocket uç noktalarını izleyin.", + "optionHttpDescription": "Web sitenizi veya API uç noktanızı izlemek için HTTP(S) kullanın.", + "optionPingDescription": "Bir sunucunun çevrimiçi olup olmadığını izlemek için ICMP Ping kullanın.", + "optionPortDescription": "Bir sunucuda belirli bir portun açık olup olmadığını izleyin.", + "title": "Tür" + }, + "thresholds": { + "title": "Uyarı eşikleri", + "description": "Bu altyapı monitörü için uyarıların tetikleneceği eşikleri tanımlayın.", + "option": { + "cpuThreshold": { + "label": "CPU uyarı eşiği (%)" + }, + "memoryThreshold": { + "label": "Bellek uyarı eşiği (%)" + }, + "diskThreshold": { + "label": "Disk uyarı eşiği (%)" + }, + "tempThreshold": { + "label": "Sıcaklık uyarı eşiği (°C)" + } + } + }, + "geoChecks": { + "title": "Coğrafi dağıtılmış kontroller", + "description": "Küresel erişilebilirlik ve performansı izlemek için birden fazla coğrafi konumdan kontrol çalıştırın.", + "option": { + "enabled": { + "label": "Coğrafi dağıtılmış kontrolleri etkinleştir" + }, + "locations": { + "label": "Konumlar", + "placeholder": "Konum seçin", + "options": { + "EU": "Avrupa", + "NA": "Kuzey Amerika", + "AS": "Asya", + "SA": "Güney Amerika", + "AF": "Afrika", + "OC": "Okyanusya" + } + }, + "interval": { + "label": "Kontrol aralığı", + "value": { + "fiveMinutes": "5 dakika", + "tenMinutes": "10 dakika", + "fifteenMinutes": "15 dakika", + "thirtyMinutes": "30 dakika" + } + } + } + }, + "url": { + "title": "Durum sayfasında monitör IP/URL gösterimi", + "description": "Monitörün IP adresini veya URL'sini herkese açık durum sayfasında görüntüleyin. Devre dışı bırakılırsa hassas bilgileri korumak için yalnızca monitör adı gösterilir.", + "option": { + "showURL": { + "label": "Durum sayfasında IP/URL göster", + "enabled": "Etkin", + "disabled": "Devre dışı" + } + } + }, + "stats": { + "title": "Monitör geçmişi", + "description": "Ekibinizin tüm izleme geçmişini ve istatistiklerini temizleyin. Bu işlem geri alınamaz.", + "buttonText": "Tüm istatistikleri temizle", + "dialog": { + "title": "Tüm izleme geçmişi temizlensin mi?", + "description": "Bu işlem ekibinizin tüm izleme geçmişini, istatistiklerini ve kontrol verilerini kalıcı olarak silecektir. Bu işlem geri alınamaz.", + "confirm": "Evet, tüm istatistikleri temizle" + } + } + } + }, + "editUser": { + "form": { + "roles": { + "title": "Roller", + "description": "Kullanıcıya roller atayın. Birden fazla rol seçilebilir." + } + }, + "dialog": { + "removeUser": { + "title": "Kullanıcıyı kaldır", + "content": "{{name}} adlı kullanıcıyı ekibinizden kaldırmak istediğinizden emin misiniz? Bu işlem geri alınamaz." + } + } + }, + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Olayı çöz", + "option": { + "comment": { + "label": "Yorum (isteğe bağlı)", + "placeholder": "Çözüm hakkında bir yorum ekleyin..." + } + } + }, + "details": { + "analysis": "Olay analizi", + "comment": "Yorum:", + "detailsLabel": "Detaylar", + "downtime": "Kesinti süresi:", + "message": "Mesaj:", + "monitor": "Monitör:", + "overview": "Genel bakış", + "resolutionDetails": "Çözüm detayları", + "resolutionType": "Tür:", + "resolutionTypes": { + "automatic": "Otomatik", + "manual": "Manuel" + }, + "resolve": "Olayı çöz", + "resolvedAt": "Çözülme zamanı:", + "resolvedBy": "Çözen:", + "startedAt": "Başlangıç zamanı:", + "status": "Durum:", + "statusCode": "Durum kodu:", + "timeline": "Zaman çizelgesi", + "title": "Olay detayları", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Tüm monitörler", + "monitor": "Monitör", + "resolutionType": "Çözüm türü", + "resolutionTypes": { + "manual": "Manuel", + "automatic": "Otomatik", + "all": "Tümü" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Aktif olaylar", + "active_zero": "Aktif olay yok", + "active_one": "{{count}} aktif olay", + "active_other": "{{count}} aktif olay" + }, + "incidentStats": { + "avgResolutionTime": "Ort. çözüm süresi", + "mostAffectedMonitor": "En çok etkilenen monitör", + "title": "Olay istatistikleri", + "totalIncidents": "Toplam olay sayısı" + }, + "latestIncidents": { + "title": "Son olaylar" + } + }, + "table": { + "actions": { + "details": "Detaylar", + "goToMonitor": "Monitöre git", + "resolveManually": "Manuel olarak çöz" + }, + "activeIncidents": "Aktif olaylar", + "headers": { + "endTime": "Bitiş zamanı", + "resolutionType": "Çözüm türü", + "startTime": "Başlangıç saati", + "statusCode": "Durum kodu" + }, + "resolvedIncidents": "Çözülen olaylar", + "status": { + "active": "Aktif", + "resolved": "Çözüldü" + } + } + }, + "infrastructure": { + "charts": { + "labels": { + "cpu": "CPU kullanımı", + "disk": "Disk kullanımı", + "memory": "Bellek kullanımı", + "netBytesRecv": "{{name}} - Alınan bayt", + "netBytesSent": "{{name}} - Gönderilen bayt", + "temp": "Sıcaklık" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Maks frekans", + "title": "CPU kullanımı", + "upperLabel": "Mevcut frekans" + }, + "disk": { + "lowerLabel": "Boş", + "title": "Disk {{idx}} kullanımı", + "upperLabel": "Kullanılan" + }, + "memory": { + "lowerLabel": "Boş", + "title": "Bellek kullanımı", + "upperLabel": "Kullanılan" + } + }, + "statBoxes": { + "avgCpuTemperature": "Ortalama CPU Isısı", + "cpuFrequency": "CPU Frekansı", + "cpuLogical": "CPU (Mantıksal)", + "cpuPhysical": "CPU (fiziksel)", + "disk": "Disk", + "memory": "Bellek", + "os": "İşletim sistemi" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Disk", + "memory": "Bellek" + } + }, + "tabs": { + "labels": { + "network": "", + "overview": "Genel bakış" + } + }, + "fallback": { + "actionButton": "Monitör oluşturun!", + "checks": "Sunucularınızın performansını takip edin,Darboğazları belirleyin ve kullanımı optimize edin,Gerçek zamanlı izleme ile güvenilirliği sağlayın", + "title": "Bir altyapı monitörü şunlar için kullanılır:" + } + }, + "logs": { + "tabs": { + "diagnostics": "Teşhis", + "logs": "Sunucu günlükleri", + "queue": "İş kuyruğu" + }, + "logLevelSelect": { + "label": "Günlük seviyesi" + }, + "jobQueue": "İş kuyruğu", + "failedJobs": "Başarısız işler", + "noLogs": "Günlük bulunamadı", + "metrics": { + "jobs": "İşler", + "activeJobs": "Aktif işler", + "failingJobs": "Başarısız olan işler", + "totalRuns": "Toplam çalıştırma", + "totalFailures": "Toplam başarısızlık" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Olay döngüsü gecikmesi", + "uptime": "Çalışma süresi", + "usedHeapSize": "Kullanılan heap boyutu", + "totalHeapSize": "Toplam heap boyutu", + "osMemoryLimit": "İşletim sistemi bellek limiti" + }, + "gauges": { + "heapAllocation": "Heap tahsisi", + "heapUsage": "Heap kullanımı", + "heapUtilization": "Heap verimliliği", + "instantCpuUsage": "Anlık CPU kullanımı", + "availableMemoryPercentage": "% kullanılabilir bellek", + "allocatedPercentage": "% tahsis edilmiş", + "usedSPercentage": "% CPU tarafından kullanılan (1s)", + "total": "Toplam", + "used": "Kullanılan" + } + }, + "table": { + "headers": { + "timestamp": "Zaman damgası", + "level": "Seviye", + "service": "Servis", + "method": "Yöntem", + "monitorId": "Monitör ID", + "runCount": "Çalışma sayısı", + "failCount": "Başarısızlık sayısı", + "lastRunAt": "Son çalışma zamanı", + "lockedAt": "Kilitlenme zamanı", + "lastFinishedAt": "Son tamamlanma zamanı", + "lastRunTook": "Son çalışma süresi", + "lastFailedAt": "Son başarısızlık zamanı", + "failReason": "Başarısızlık nedeni" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Bakım penceresi oluşturun!", + "checks": "Bakım dönemlerinizi işaretleyin,Yanlış anlamaları ortadan kaldırın,Bakım pencerelerinde uyarı göndermeyi durdurun", + "title": "Bakım penceresi şunlar için kullanılır:" + }, + "table": { + "headers": { + "nextWindow": "Sonraki pencere", + "repeat": "Tekrarla" + } + }, + "form": { + "general": { + "title": "Genel ayarlar", + "description": "Bakım pencereniz için bir ad ve tekrar seçeneği belirleyin.", + "option": { + "name": { + "label": "Ad", + "placeholder": "örn. Haftalık Bakım" + }, + "repeat": { + "label": "Tekrarla" + } + } + }, + "startDate": { + "title": "Başlangıç tarihi", + "description": "Bakım pencereniz için başlangıç tarihini seçin.", + "option": { + "startDate": { + "label": "Başlangıç tarihi" + } + } + }, + "startTime": { + "title": "Başlangıç saati", + "description": "Bakım pencereniz için başlangıç saati ve süreyi ayarlayın. Tüm değerler UTC cinsindendir", + "option": { + "duration": { + "label": "Süre" + }, + "startTime": { + "label": "Başlangıç saati" + } + }, + "monitors": { + "title": "Monitörler", + "description": "Bakım penceresinin uygulanacağı monitörler", + "option": { + "addMonitors": { + "label": "Monitör ekle" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "Kanal oluştur", + "checks": "Kesinti veya performans sorunları hakkında ekipleri uyarın,Olaylar meydana geldiğinde mühendisleri bilgilendirin,Yöneticileri sistem değişiklikleri hakkında haberdar edin", + "title": "Bildirim kanalları şunlar için kullanılır:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Erişim token'ı", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "Bildirimlerin gönderileceği adres.", + "optionAddress": "Adres", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Adres" + }, + "homeServer": { + "optionHomeServer": "Ana sunucu", + "placeholder": "example.com" + }, + "matrix": { + "description": "Bildirimler için Matrix ana sunucu bağlantınızı yapılandırın.", + "title": "Matrix yapılandırması" + }, + "notificationName": { + "description": "Bildirim kanalı için açıklayıcı bir ad", + "optionName": "Kanal adı", + "placeholder": "örn. Üretim Uyarıları", + "title": "Kanal adı" + }, + "pagerDuty": { + "description": "Uyarıları almak için PagerDuty entegrasyon anahtarınız.", + "optionIntegrationKey": "Entegrasyon anahtarı", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Entegrasyon anahtarı" + }, + "roomId": { + "optionRoomId": "Oda ID", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Oluşturulacak bildirim kanalı türünü seçin.", + "optionType": "Tür", + "title": "Kanal türü" + }, + "telegram": { + "title": "Telegram yapılandırması", + "description": "Telegram bildirimlerini etkinleştirmek için, Telegram botlarını oluşturup yönetmek için kullanılan resmi bot BotFather ile bir Telegram botu oluşturun. Daha sonra, API jetonunu (token) ve sohbet kimliğini (chat ID) alın ve buraya yazın.", + "optionBotToken": "Bot token", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "Sohbet ID", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Hedef" + } + } + }, + "pageSpeed": { + "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)" + }, + "legend": { + "title": "PageSpeed raporu", + "weight": "Ağırlık" + }, + "pie": { + "title": "Performans raporu" + } + }, + "table": { + "headers": { + "pageSpeedScore": "PageSpeed puanı" + } + }, + "fallback": { + "actionButton": "Monitör oluşturun!", + "checks": "Bir sayfanın kullanıcı deneyimini raporlayın,Web sayfası hızını analiz etmeye yardımcı olun,Sayfanın nasıl iyileştirilebileceğine dair öneriler sunun", + "title": "Bir PageSpeed monitörü şunlar için kullanılır:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Görüntüleme saat dilimi", + "description": "Uygulama genelinde tarih ve saatleri görüntülemek için kullanılacak saat dilimini seçin.", + "option": { + "timezone": { + "label": "Görüntüleme saat dilimi" + } + } + }, + "ui": { + "title": "Görünüm", + "description": "Açık ve koyu mod arasında geçiş yapın, dili değiştirin veya grafik görüntüleme türünü özelleştirin.", + "option": { + "theme": { + "label": "Tema modu", + "light": "Açık", + "dark": "Koyu" + }, + "language": { + "label": "Dil" + }, + "chartType": { + "label": "Grafik türü", + "histogram": "Histogram", + "heatmap": "Isı haritası" + } + } + }, + "pagespeed": { + "title": "Google PageSpeed API anahtarı", + "description": "Google PageSpeed izlemesini etkinleştirmek için Google PageSpeed API anahtarınızı girin. Anahtarı güncellemek için Sıfırla'ya tıklayın.", + "option": { + "apiKey": { + "label": "PageSpeed API anahtarı", + "labelSet": "API anahtarı ayarlandı. Değiştirmek için Sıfırla'ya tıklayın.", + "placeholder": "Google PageSpeed API anahtarınızı girin" + } + } + }, + "url": { + "title": "Durum sayfasında monitör IP/URL gösterimi", + "description": "Monitörün IP adresini veya URL'sini herkese açık durum sayfasında görüntüleyin. Devre dışı bırakılırsa hassas bilgileri korumak için yalnızca monitör adı gösterilir.", + "option": { + "showURL": { + "label": "Durum sayfasında IP/URL göster", + "enabled": "Etkin", + "disabled": "Devre dışı" + } + } + }, + "stats": { + "title": "Monitör geçmişi", + "description": "Ekibinizin tüm izleme geçmişini ve istatistiklerini temizleyin. Bu işlem geri alınamaz.", + "option": { + "clear": { + "label": "Tüm istatistikleri temizle. Bu işlem geri alınamaz." + } + }, + "dialog": { + "title": "Tüm izleme geçmişi temizlensin mi?", + "description": "Bu işlem geri alınamaz" + } + }, + "retention": { + "title": "Kontrol saklama", + "description": "Kontrol verilerinin otomatik temizlenmeden önce ne kadar süre saklanacağını ayarlayın.", + "option": { + "days": { + "label": "Saklama süresi (gün)", + "unlimited": "Sınırsız" + } + } + }, + "thresholds": { + "title": "Genel eşikler", + "description": "Altyapı izleme için genel CPU, bellek, disk ve sıcaklık eşiklerini ayarlayın. Bunlar, geçersiz kılınmadığı sürece tüm donanım monitörlerine uygulanır.", + "option": { + "cpu": { + "label": "CPU eşiği (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Bellek eşiği (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Disk eşiği (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Sıcaklık eşiği (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "E-posta ayarları", + "description": "Sisteminiz için e-posta ayarlarını yapılandırın. Bu, bildirim ve uyarı göndermek için kullanılır.", + "descriptionTransport": "", + "descriptionTransportLink": "Özellikleri burada görün", + "option": { + "host": { + "label": "E-posta sunucusu - Bağlanılacak ana bilgisayar adı veya IP adresi", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "E-posta portu - Bağlanılacak port", + "placeholder": "587" + }, + "address": { + "label": "E-posta adresi - Kimlik doğrulama için kullanılır", + "placeholder": "you@example.com" + }, + "user": { + "label": "E-posta kullanıcısı - Kimlik doğrulama için kullanıcı adı, belirtilirse e-posta adresini geçersiz kılar", + "placeholder": "Gerekli değilse boş bırakın" + }, + "password": { + "label": "E-posta parolası - Kimlik doğrulama için parola", + "labelSet": "Parola ayarlandı. Değiştirmek için Sıfırla'ya tıklayın.", + "placeholder": "Parolanızı girin" + }, + "tlsServername": { + "label": "TLS sunucu adı - Ana bilgisayar IP olduğunda TLS doğrulama için isteğe bağlı ana bilgisayar adı", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "E-posta bağlantı sunucusu - HELO/EHLO karşılamasında kullanılacak ana bilgisayar adı", + "placeholder": "localhost" + }, + "secure": { + "label": "SSL kullan (önerilen): Bağlantıyı SSL/TLS ile şifreleyin" + }, + "pool": { + "label": "Bağlantı havuzunu etkinleştir: Performansı artırmak için mevcut bağlantıları yeniden kullanın" + }, + "ignoreTLS": { + "label": "STARTTLS'yi devre dışı bırak: Sunucu desteklese bile TLS kullanma" + }, + "requireTLS": { + "label": "STARTTLS'yi zorunlu kıl: TLS yükseltmesini zorunlu kıl, desteklenmiyorsa başarısız ol" + }, + "rejectUnauthorized": { + "label": "Geçersiz sertifikaları reddet: Kendinden imzalı veya güvenilmeyen sertifikalı bağlantıları reddet" + } + } + }, + "demoMonitors": { + "title": "Demo monitörler", + "description": "Gösterim amaçlı örnek monitörler ekleyin." + }, + "removeMonitors": { + "title": "Sistem sıfırlama", + "description": "Sisteminizdeki tüm monitörleri kaldırın.", + "dialog": { + "title": "Tüm monitörler kaldırılsın mı?", + "description": "Bu işlem geri alınamaz." + } + }, + "exportMonitors": { + "title": "Monitörleri dışa aktar" + }, + "importExportMonitors": { + "title": "Monitörleri içe / dışa aktar", + "description": "Yedekleme veya aktarım için monitör verilerinizi JSON dosyası olarak içe veya dışa aktarın." + }, + "about": { + "title": "Hakkında", + "developedBy": "Bluewave Labs tarafından geliştirildi" + }, + "validation": { + "errorMessage": "Lütfen aşağıdaki doğrulama hatalarını düzeltin:" + } + } + }, + "statusPages": { + "deleteSuccess": "Durum sayfası başarıyla silindi", + "fallback": { + "title": "Bir durum sayfası şunlar için kullanılır:", + "checks": "Kullanıcılara ve paydaşlara sistem durumunu iletin,Gerçek zamanlı çalışma süresi bilgilerini herkese açık olarak görüntüleyin,Şeffaf hizmet izleme ile güven oluşturun,Olaylar sırasında destek taleplerini azaltın", + "actionButton": "Durum sayfası oluşturun!" + }, + "monitorsList": { + "chartTypeHeatmap": "Isı haritası", + "chartTypeHistogram": "Histogram", + "noData": "Veri mevcut değil", + "infrastructure": { + "title": "Altyapı", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Bellek", + "disk": "Disk", + "usage": "Kullanım", + "used": "Kullanılan", + "total": "Toplam" + }, + "uptime": { + "title": "Çalışma süresi", + "responseTime": "Yanıt süresi" + } + }, + "statusBar": { + "allDown": "Tüm sistemler çevrimdışı", + "allUp": "Tüm sistemler çalışıyor", + "degraded": "Bazı sistemlerde sorun yaşanıyor", + "unknown": "Sistem durumu belirlenemiyor" + }, + "table": { + "headers": { + "name": "Durum sayfası adı", + "url": "Herkese açık URL" + }, + "published": "Yayında", + "unpublished": "Yayında değil" + }, + "title": "Durum sayfaları", + "details": { + "empty": { + "title": "Burada henüz hiçbir şey yok", + "addMonitor": "Başlamak için bir monitör ekleyin" + } + }, + "form": { + "access": { + "title": "Erişim", + "description": "Durum sayfanız hazırsa yayında olarak işaretleyebilirsiniz.", + "option": { + "published": { + "name": "Yayında ve herkese açık" + } + } + }, + "basicInfo": { + "title": "Temel bilgiler", + "description": "Şirket adını ve durum sayfanızın işaret ettiği alt alan adını tanımlayın.", + "option": { + "name": { + "label": "Şirket adı", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Durum sayfası adresiniz", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "Monitörler", + "description": "Durum sayfanızda görüntülenecek monitörleri seçin.", + "noMonitors": "Monitör seçilmedi", + "option": { + "monitors": { + "label": "Monitör seçin", + "placeholder": "Monitör arayın ve seçin..." + } + } + }, + "timezone": { + "title": "Saat dilimi", + "description": "Durum sayfanızın görüntüleneceği saat dilimini seçin.", + "option": { + "timezone": { + "label": "Saat dilimi", + "placeholder": "Saat dilimi seçin..." + } + } + }, + "appearance": { + "title": "Görünüm", + "description": "Herkese açık durum sayfanızın varsayılan görünümünü ve hissini tanımlayın.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Marka rengi" + } + } + }, + "features": { + "title": "Özellikler", + "description": "Durum sayfanızda hangi bilgilerin görüntüleneceğini yapılandırın.", + "option": { + "showCharts": { + "label": "Yanıt süresi grafiklerini göster" + }, + "showUptimePercentage": { + "label": "Çalışma süresi yüzdesini göster" + }, + "showAdminLoginLink": { + "label": "Yönetici giriş bağlantısını göster" + }, + "showInfrastructure": { + "label": "Altyapı metriklerini göster" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Monitör ara..." + } + }, + "table": { + "headers": { + "responseTime": "Yanıt süresi" + } + }, + "fallback": { + "actionButton": "Monitör oluşturun!", + "checks": "Web sitelerinin veya sunucuların çevrimiçi ve yanıt verip vermediğini kontrol edin,Kesinti veya performans sorunları hakkında ekipleri uyarın,HTTP uç noktalarını, ping'leri, container'ları ve portları izleyin,Geçmiş çalışma süresi ve güvenilirlik trendlerini takip edin", + "title": "Bir çalışma süresi monitörü şunlar için kullanılır:" } } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" - }, - "alertBox": "Sunucu bağlantı hatası", - "description": "", - "retryButton": { - "default": "Bağlantıyı tekrarla", - "processing": "Bağlanıyor..." - } - } - }, - "createNotifications": { - "title": "Bildirim kanalları oluştur", - "nameSettings": { - "title": "İsim", - "description": "Entegrasyon için açıklama", - "nameLabel": "İsim", - "namePlaceholder": "örn. Slack bildirimleri" - }, - "typeSettings": { - "title": "Tür", - "description": "", - "typeLabel": "Tür" - }, - "emailSettings": { - "title": "Eposta", - "description": "Hedef eposta adresi", - "emailLabel": "Eposta adresi", - "emailPlaceholder": "örn. john@example.com" - }, - "slackSettings": { - "title": "Slack", - "description": "Slack webhook'u burada tanımlayın", - "webhookLabel": "Slack webhook adresi", - "webhookPlaceholder": "https://hooks.slack.com/services/...\n" - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "PagerDuty entegrasyonunu burada yapın", - "integrationKeyLabel": "Entegrasyon anahtarı", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discord", - "description": "Discord webhook'u buradan ayarlayın", - "webhookLabel": "Discord Webhook adresi", - "webhookPlaceholder": "https://sunucu-adı.com/webhook\n" - }, - "webhookSettings": { - "title": "Webhook", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "https://sunucu-adı.com/webhook\n" - }, - "testNotification": "Test bildirimi", - "dialogDeleteTitle": "Gerçekten bu bildirimi silmek istiyor musunuz?", - "dialogDeleteConfirm": "Sil" - }, - "notificationConfig": { - "title": "Bildirimler", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "ayakta", - "down": "çalışmıyor", - "paused": "beklemede" - }, - "advancedMatching": "Gelişmiş eşleştirme", - "sendTestNotifications": "Bildirimleri dene", - "selectAll": "Tümünü seç", - "showAdminLoginLink": "", - "logsPage": { - "title": "Günlükler", - "description": "Sistem günlükleri - son 1000 satır", - "tabs": { - "queue": "İş kuyruğu", - "logs": "Sunucu günlükleri", - "diagnostics": "Teşhis" - }, - "toast": { - "fetchLogsSuccess": "Günlükler başarıyla alındı" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" - } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "Aktif", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "Son tamamlama", - "lastRunTookHeader": "", - "intervalHeader": "" - }, - "metricsTable": { - "title": "", - "metricHeader": "Metrik", - "valueHeader": "Değer" - }, - "failedJobTable": { - "title": "Tamamlanamayan işler", - "monitorIdHeader": "Monitör ID", - "monitorUrlHeader": "Monitör URL", - "failCountHeader": "Hata sayısı", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "Monitörleri dışarı al", - "success": "Monitörler başarıyla dışarıya alındı", - "failed": "Monitörleri dışarıya alırken bir hata oluştu" - }, - "monitorActions": { - "title": "Dışarı/İçeri al", - "import": "", - "export": "", - "deleteSuccess": "Monitör başarıyla silindi", - "deleteFailed": "Monitör silinemedi", - "details": "Detaylar" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" - }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "Eğer gerekmiyorsa boş bırakabilirsiniz", - "title": "Eposta", - "toastEmailRequiredFieldsError": "" - }, - "pageSpeedSettings": { - "description": "Google PageSpeed izlemeyi etkinleştirmek için Google PageSpeed API anahtarınızı girin. Anahtarı güncellemek için Sıfırla'ya tıklayın.", - "labelApiKeySet": "API key tanımlandı. Sıfırla düğmesine tıklayarak değiştirebilirsiniz.", - "labelApiKey": "PageSpeed API anahtarı", - "title": "Google PageSpeed API anahtarı" - }, - "saveButtonLabel": "Kaydet", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "Sonsuz için 0", - "title": "İzleme geçmişi" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "Tüm monitörleri sil", - "description": "Sistemden tüm monitörleri sil", - "dialogConfirm": "Evet, tüm monitörleri sil", - "dialogDescription": "", - "dialogTitle": "Gerçekten tüm monitörleri silmek istiyor musunuz?", - "title": "Sistemi sıfırla" - }, - "timezoneSettings": { - "description": "", - "label": "Görünüm zaman dilimi", - "title": "Görünüm zaman dilimi" - }, - "title": "Ayarlar", - "uiSettings": { - "description": "Aydınlık ve karanlık mod arasında geçiş yapın ya da kullanıcı arayüzü dilini değiştirin.", - "labelLanguage": "Dil", - "labelTheme": "Tema modu", - "title": "Görünüm" - }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "Kapalı", - "selectEnabled": "Etkin", - "title": "" - }, - "globalThresholds": { - "title": "", - "description": "" - } - }, - "statusPageCreate": { - "buttonSave": "" - }, - "incidentsOptionsHeaderFilterResolved": "", - "settingsSave": "", - "statusPageCreateAppearanceTitle": "", - "confirmPassword": "", - "monitorHooks": { - "failureAddDemoMonitors": "", - "successAddDemoMonitors": "" - }, - "settingsAppearance": "", - "settingsDisplayTimezone": "", - "settingsGeneralSettings": "", - "incidentsOptionsHeaderTotalIncidents": "", - "statusPage": { - "deleteSuccess": "Durum sayfası başarıyla silindi", - "deleteFailed": "Durum sayfası silinemedi", - "createSuccess": "Durum sayfası başarıyla oluşturuldu", - "updateSuccess": "Durum sayfası başarıyla güncellendi", - "generalSettings": "Genel ayarlar", - "contents": "İçerikler", - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "testNotificationsDisabled": "Bu izleyici için ayarlanmış bir bildirim yok. 'Yapılandır' düğmesine tıklayarak bir bildirim eklemeniz gerekiyor.", - "incidentsTableResolvedAt": "", - "incidentsTableActionResolve": "Çözümle", - "checkHooks": { - "failureResolveOne": "Olay çözümlenirken bir hata oluştu.", - "failureResolveAll": "Tüm olayları çözümlerken bir hata oluştu.", - "failureResolveMonitor": "" - }, - "checkFormError": "Formda hatalar var. Lütfen kontrol edin.", - "diagnosticsPage": { - "diagnosticDescription": "", - "statsDescription": "", - "gauges": { - "heapAllocationTitle": "", - "heapAllocationSubtitle": "", - "heapUsageTitle": "", - "heapUsageSubtitle": "", - "heapUtilizationTitle": "", - "heapUtilizationSubtitle": "", - "instantCpuUsageTitle": "", - "instantCpuUsageSubtitle": "" - }, - "stats": { - "eventLoopDelayTitle": "", - "uptimeTitle": "", - "usedHeapSizeTitle": "", - "totalHeapSizeTitle": "", - "osMemoryLimitTitle": "" - } - }, - "pageSpeedLighthouseAPI": "", - "time": { - "threeMinutes": "", - "fiveMinutes": "", - "tenMinutes": "", - "twentyMinutes": "", - "oneHour": "1 saat", - "oneDay": "1 gün", - "oneWeek": "1 hafta", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" - }, - "general": { - "noOptionsFound": "" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "pageSpeed": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "" - } - }, - "editUserPage": { - "form": { - "email": "", - "firstName": "", - "lastName": "", - "role": "", - "save": "" - }, - "table": { - "actionHeader": "", - "roleHeader": "" - }, - "title": "", - "toast": { - "successUserUpdate": "", - "validationErrors": "" - } - }, - "incidentsPageActionResolveMonitor": "", - "incidentsPageActionResolveAll": "", - "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + } } diff --git a/client/src/locales/uk.json b/client/src/locales/uk.json index 22b96487c..3872638aa 100644 --- a/client/src/locales/uk.json +++ b/client/src/locales/uk.json @@ -1,838 +1,1305 @@ { - "submit": "", - "title": "", - "distributedStatusHeaderText": "", - "distributedStatusSubHeaderText": "", - "settingsDisabled": "", - "settingsSuccessSaved": "", - "settingsFailedToSave": "", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", - "https": "", - "http": "", - "monitor": "", - "aboutus": "", - "now": "", - "delete": "", - "configure": "", - "responseTime": "", - "ms": "", - "bar": "", - "area": "", - "country": "", - "city": "", - "response": "", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "", - "webhookSendError": "", - "webhookUnsupportedPlatform": "", - "distributedRightCategoryTitle": "", - "distributedStatusServerMonitors": "", - "distributedStatusServerMonitorsDescription": "", - "distributedUptimeCreateSelectURL": "", - "distributedUptimeCreateChecks": "", - "distributedUptimeCreateChecksDescription": "", - "distributedUptimeCreateIncidentNotification": "", - "distributedUptimeCreateIncidentDescription": "", - "distributedUptimeCreateAdvancedSettings": "", - "distributedUptimeDetailsNoMonitorHistory": "", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "", - "notifications": { - "enableNotifications": "", - "testNotification": "", - "addOrEditNotifications": "", - "slack": { - "label": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "discord": { - "label": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "telegram": { - "label": "", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "", - "chatIdLabel": "", - "chatIdPlaceholder": "", - "fieldsRequired": "" - }, - "webhook": { - "label": "", - "description": "", - "urlLabel": "", - "urlPlaceholder": "", - "urlRequired": "" - }, - "testNotificationDevelop": "", - "integrationButton": "", - "testSuccess": "", - "testFailed": "", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [""] - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "", - "add": "", - "monitors": "", - "distributedUptimeStatusCreateStatusPage": "", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "", - "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "", - "distributedUptimeStatus60Days": "", - "distributedUptimeStatus90Days": "", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "", - "incidentsTableDateTime": "", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", - "incidentsOptionsHeaderLastHour": "", - "incidentsOptionsHeaderLastDay": "", - "incidentsOptionsHeaderLastWeek": "", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "", - "infrastructureServerUrlLabel": "", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "", - "cpu": "", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "", - "avgCpuTemperature": "", - "memory": "", - "disk": "", - "uptime": "", - "os": "", - "host": "", - "actions": "", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "", - "integrationsSlackInfo": "", - "integrationsDiscord": "", - "integrationsDiscordInfo": "", - "integrationsZapier": "", - "integrationsZapierInfo": "", - "commonSave": "", - "createYour": "", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", - "timezone": "", - "features": "", - "administrator": "", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "", - "window": "", - "cancel": "", - "message": "", - "low": "", - "high": "", - "statusCode": "", - "date&Time": "", - "type": "", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", - "createA": "", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "", - "chooseGame": "", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", - "ago": "", - "companyName": "", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "", - "showUptimePercentage": "", - "removeLogo": "", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "", - "invalidFileFormat": "", - "invalidFileSize": "", - "ClickUpload": "", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", - "EmailDescriptionText": "", - "YourPhoto": "", - "PhotoDescriptionText": "", - "save": "", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", - "DeleteWarningTitle": "", - "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "", - "fallbackPage": "" - }, - "DeleteAccountTitle": "", - "DeleteAccountButton": "", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "", - "greeting": { - "prepend": "", - "append": "", - "overview": "" - }, - "roles": { - "superAdmin": "", - "admin": "", - "teamMember": "", - "demoUser": "" - }, - "teamPanel": { - "teamMembers": "", - "filter": { - "all": "", - "member": "" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "", - "selectRole": "", - "inviteLink": "", - "cancel": "", - "noMembers": "", - "getToken": "", - "emailToken": "", - "table": { - "name": "", - "email": "", - "role": "", - "created": "" - } - }, - "monitorState": { - "paused": "", - "resumed": "", - "active": "" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "", - "incidents": "", - "statusPages": "", - "maintenance": "", - "integrations": "", - "settings": "", - "support": "", - "discussions": "", - "docs": "", - "changelog": "", - "profile": "", - "password": "", - "team": "", - "logOut": "", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", - "commonSaving": "", - "navControls": "", - "incidentsPageTitle": "", - "passwordPanel": { - "passwordChangedSuccess": "", - "passwordInputIncorrect": "", - "currentPassword": "", - "enterCurrentPassword": "", - "newPassword": "", - "enterNewPassword": "", - "confirmNewPassword": "", - "passwordRequirements": "", - "saving": "" - }, - "emailSent": "", - "failedToSendEmail": "", - "settingsTestEmailSuccess": "", - "settingsTestEmailFailed": "", - "settingsTestEmailFailedWithReason": "", - "settingsTestEmailUnknownError": "", - "statusMsg": { - "paused": "", - "up": "", - "down": "", - "pending": "" - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "", - "game": "" - }, "common": { - "appName": "", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" + "auth": { + "roles": { + "admin": "Адміністратор", + "demo": "Демо", + "superadmin": "Суперадміністратор", + "user": "Користувач" + } }, - "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" + "alerts": { + "pageSpeedApiKey": { + "content": "Увага: Ви ще не додали ключ API Google PageSpeed. Перейдіть у Налаштування, щоб додати його. Без нього моніторинг PageSpeed не працюватиме." + } + }, + "appName": "", + "breadcrumbs": { + "details": "Деталі", + "home": "Головна" + }, + "buttons": { + "addMember": "Додати учасника", + "cancel": "Скасувати", + "close": "Закрити", + "configure": "Налаштувати", + "confirm": "Підтвердити", + "create": "Створити", + "delete": "Видалити", + "generateToken": "Згенерувати токен", + "incidents": "Інциденти", + "inviteMember": "Запросити учасника", + "pause": "Призупинити", + "resume": "Відновити", + "save": "", + "sendInvite": "Надіслати запрошення", + "test": "Тест", + "testNotifications": "Тестові сповіщення", + "toggleTheme": "", + "flushQueue": "Очистити чергу", + "notFound": "Перейти на головну панель", + "resetPassword": "", + "reset": "", + "clear": "Очистити", + "addDemo": "Додати демо-монітори", + "removeMonitors": "Видалити монітори", + "sendTestEmail": "Надіслати тестовий лист", + "exportToJSON": "Експортувати в JSON", + "importFromJSON": "Імпортувати з JSON", + "clearFilters": "Очистити фільтри", + "removeUser": "Видалити користувача" + }, + "charts": { + "labels": { + "averageResponseTime": "Середній час відповіді", + "downtime": "Час простою", + "high": "", + "low": "", + "uptime": "Час роботи" + }, + "histogram": { + "avg": "Сер: {{value}} мс", + "max": "Макс: {{value}} мс" + } + }, + "dialogs": { + "delete": { + "description": "Цю дію не можна скасувати.", + "title": "Ви впевнені, що хочете це видалити?" + } + }, + "labels": { + "active": "Активний", + "paused": "призупинено", + "na": "Н/Д", + "resolved": "Вирішено", + "responseTime": "Час відповіді" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Ролі" + } + } + }, + "name": { + "option": { + "firstName": { + "label": "", + "placeholder": "" + }, + "lastName": { + "label": "", + "placeholder": "" + } + } + }, + "email": { + "option": { + "email": { + "label": "Електронна пошта", + "placeholder": "you@example.com" + } + } + } + }, + "table": { + "empty": "Тут нічого немає", + "headers": { + "actions": "", + "dateTime": "Дата і час", + "message": "Повідомлення", + "monitor": "Монітор", + "monitorId": "ID монітора", + "name": "Ім'я", + "status": "Статус", + "type": "", + "url": "URL", + "interval": "Інтервал", + "active": "Активний", + "responseTime": "Час відповіді" + } } }, - "auth": { - "common": { - "navigation": { - "continue": "", - "back": "" - }, - "inputs": { - "email": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "invalid": "" - } - }, - "password": { - "label": "", - "rules": { - "length": { - "beginning": "", - "highlighted": "" - }, - "special": { - "beginning": "", - "highlighted": "" - }, - "number": { - "beginning": "", - "highlighted": "" - }, - "uppercase": { - "beginning": "", - "highlighted": "" - }, - "lowercase": { - "beginning": "", - "highlighted": "" - }, - "match": { - "beginning": "", - "highlighted": "" - } - }, - "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" - } - }, - "passwordConfirm": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - }, - "lastName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - } - }, + "components": { + "imageUpload": { + "clickToUpload": "Натисніть, щоб завантажити", + "dragAndDrop": "", + "supportedFormats": "", + "maxSize": "", + "orDragAndDrop": "або перетягніть файл", "errors": { - "validation": "" - }, - "fields": { - "password": { - "errors": { - "incorrect": "" - } - } + "invalidFileSize": "", + "invalidFileFormat": "" } }, - "login": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "" - }, - "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "", - "incorrectPassword": "" - }, - "errors": { - "password": { - "incorrect": "" - } - } + "headerStatusPageControls": { + "publicLink": "" }, - "registration": { - "heading": { - "superAdmin": "", - "user": "" - }, - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" + "headerTimeRange": { + "labels": { + "day": "День", + "month": "Місяць", + "recent": "Нещодавно", + "week": "Тиждень" }, "description": { - "superAdmin": "", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "", - "user": "" - }, - "termsAndPolicies": "", - "links": { - "login": "" - }, - "toasts": { - "success": "" + "recent": "Показано статистику за останні 2 години.", + "day": "Показано статистику за останні 24 години.", + "week": "Показано статистику за останні 7 днів.", + "month": "Показано статистику за останні 30 днів." } }, - "forgotPassword": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" + "offlineBanner": { + "serverUnreachable": "Не вдається з'єднатися з сервером", + "retry": "Повторити", + "retrying": "Повторна спроба...", + "reconnected": "" + }, + "sidebar": { + "menu": { + "uptime": "", + "pagespeed": "", + "infrastructure": "", + "notifications": "", + "checks": "Перевірки", + "incidents": "", + "statusPages": "", + "maintenance": "", + "logs": "", + "settings": "" }, - "buttons": { - "openEmail": "", - "resetPassword": "" + "bottomMenu": { + "support": "", + "discussions": "", + "docs": "", + "changelog": "" }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "accountMenu": { + "profile": "Профіль", + "password": "Пароль", + "team": "Команда" }, - "links": { - "login": "", - "resend": "" + "starPrompt": { + "title": "Оцінити Checkmate", + "description": "Переглядайте останні оновлення та допомагайте розвивати спільноту на GitHub" }, - "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" + "authFooter": { + "navControls": "", + "logOut": "", + "roles": { + "superAdmin": "Суперадміністратор", + "admin": "Адміністратор", + "user": "Користувач", + "demoUser": "" + } } + }, + "starPrompt": { + "title": "Оцінити Checkmate", + "description": "Переглядайте останні оновлення та допомагайте розвивати спільноту на GitHub" } }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" + "pages": { + "notFound": { + "title": "Ой ні! Ви впустили суші!", + "subtitle": "Або URL не існує, або у Вас немає доступу до нього." + }, + "account": { + "tabs": { + "profile": "Профіль", + "password": "Пароль", + "team": "Команда" }, - "alertBox": "", - "description": "", - "retryButton": { - "default": "", - "processing": "" + "form": { + "name": { + "title": "Ім'я", + "description": "Оновіть Вашу особисту інформацію" + }, + "photo": { + "title": "Фото профілю", + "description": "Завантажте фото профілю" + }, + "currentPassword": { + "title": "Поточний пароль", + "description": "Введіть поточний пароль для підтвердження Вашої особи", + "option": { + "label": "Поточний пароль", + "placeholder": "Введіть поточний пароль" + } + }, + "newPassword": { + "title": "Новий пароль", + "description": "Оберіть надійний пароль довжиною не менше 8 символів", + "option": { + "newPassword": { + "label": "Новий пароль", + "placeholder": "Введіть новий пароль" + }, + "confirm": { + "label": "Підтвердити пароль", + "placeholder": "Підтвердити новий пароль" + } + } + }, + "deleteAccount": { + "title": "Видалити обліковий запис", + "description": "Ця дія є остаточною і не може бути скасована" + } + }, + "team": { + "filter": { + "placeholder": "Фільтрувати за роллю", + "all": "", + "admin": "Адміністратор", + "member": "" + }, + "table": { + "headers": { + "email": "Електронна пошта", + "role": "", + "created": "" + } + }, + "addMember": { + "title": "Зареєструвати нового учасника команди", + "description": "Створіть новий обліковий запис. Безпечно передайте облікові дані після створення." + }, + "invite": { + "title": "Запросити учасника команди", + "description": "Надіслати запрошення приєднатися до Вашої команди", + "email": { + "label": "Адреса електронної пошти", + "placeholder": "Введіть адресу електронної пошти" + }, + "role": { + "label": "Роль", + "placeholder": "Оберіть роль" + }, + "linkLabel": "Посилання для запрошення" + } } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" }, - "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" - }, - "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - } - }, - "notificationConfig": { - "title": "", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "", - "sendTestNotifications": "", - "testNotificationsDisabled": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "", + "highlighted": "" + }, + "lowercase": { + "beginning": "", + "highlighted": "" + }, + "match": { + "beginning": "", + "highlighted": "" + }, + "number": { + "beginning": "", + "highlighted": "" + }, + "special": { + "beginning": "", + "highlighted": "" + }, + "uppercase": { + "beginning": "", + "highlighted": "" + } + }, + "form": { + "option": { + "email": { + "label": "Електронна пошта", + "placeholder": "me@example.com" + }, + "password": { + "label": "Пароль", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Підтвердити пароль" + } + } + } + }, + "login": { + "title": "Ласкаво просимо до Checkmate!", + "subtitle": "Увійдіть, щоб продовжити", + "submit": "Увійти", + "links": { + "forgotPassword": { + "text": "Забули пароль?", + "linkText": "Скинути пароль" + }, + "register": { + "text": "Не маєте облікового запису?", + "linkText": "Зареєструватися тут" + } + } + }, + "register": { + "title": "Ласкаво просимо до Checkmate!", + "subtitle": "Зареєструйтесь, щоб розпочати", + "submit": "Зареєструватися" + }, + "forgotPassword": { + "title": "Забули пароль?", + "subtitle": "Не хвилюйтесь, ми надішлемо Вам інструкції для скидання.", + "submit": "Запит на відновлення", + "links": { + "login": { + "text": "Повернутися до", + "linkText": "входу" + } + } + }, + "setNewPassword": { + "title": "Скинути пароль", + "subtitle": "Ваш новий пароль повинен відрізнятися від раніше використаних паролів." } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "" }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" + "checks": { + "selects": { + "monitor": { + "all": "Усі монітори" + }, + "status": { + "all": "Усі", + "down": "Недоступний", + "up": "Доступний" + } + }, + "table": { + "empty": "Немає невдалих перевірок за цей період", + "headers": { + "statusCode": "Код статусу", + "location": "Розташування" + } + } }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" + "common": { + "monitors": { + "actions": { + "configure": "Налаштувати", + "delete": "Видалити", + "generateToken": "Згенерувати токен", + "details": "Деталі", + "incidents": "Інциденти", + "inviteMember": "Запросити учасника", + "openSite": "Відкрити сайт", + "pause": "Призупинити", + "resume": "Відновити" + }, + "statBoxes": { + "activeFor": "Активний протягом", + "certificateExpiry": "Закінчення сертифіката", + "lastCheck": "Остання перевірка", + "lastResponseTime": "Останній час відповіді" + }, + "status": { + "down": "недоступний", + "breached": "поріг перевищено", + "initializing": "ініціалізація", + "maintenance": "обслуговування", + "paused": "призупинено", + "total": "усього", + "up": "доступний" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Гра", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Порт", + "optionPagespeed": "PageSpeed", + "optionHardware": "Інфраструктура", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" + "createMonitor": { + "form": { + "advanced": { + "description": "Додаткові налаштування для складних випадків", + "option": { + "advancedMatching": { + "label": "Використовувати розширене зіставлення" + }, + "expectedValue": { + "label": "Очікуване значення" + }, + "jsonPath": { + "description": "Цей вираз буде обчислено для JSON-даних відповіді, а результат буде зіставлено з очікуваним значенням. Дивіться документацію мови запитів на jmespath.org.", + "label": "Вираз JSONPath" + }, + "matchMethod": { + "label": "Метод зіставлення", + "equal": "Дорівнює", + "include": "Містить", + "regex": "Regex" + } + }, + "title": "Розширені налаштування" + }, + "frequency": { + "description": "Як часто Ви хочете перевіряти стан цього монітора?", + "option": { + "frequency": { + "label": "Частота перевірки", + "value": { + "fifteenMinutes": "15 хвилин", + "fifteenSeconds": "15 секунд", + "fiveMinutes": "5 хвилин", + "fourMinutes": "4 хвилини", + "oneMinute": "1 хвилина", + "tenMinutes": "10 хвилин", + "thirtyMinutes": "30 хвилин", + "thirtySeconds": "30 секунд", + "threeMinutes": "3 хвилини", + "twoMinutes": "2 хвилини" + } + } + }, + "title": "Частота перевірки" + }, + "general": { + "option": { + "container": { + "label": "Ім'я/ID контейнера", + "placeholder": "my-app або abcd1234" + }, + "host": { + "label": "Хост", + "placeholder": "192.168.1.100 або example.com" + }, + "name": { + "label": "Відображуване ім'я", + "placeholder": "напр. Мій сайт" + }, + "secret": { + "label": "Секрет авторизації", + "placeholder": "Введіть Ваш секретний ключ" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "Обрати гру", + "placeholder": "Оберіть гру" + }, + "port": { + "label": "Порт для моніторингу", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Назва сервісу", + "placeholder": "напр. my.service.v1 (залиште порожнім для загальної перевірки)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Загальні налаштування", + "description": { + "http": "Введіть URL або IP для моніторингу (напр., https://example.com/ або 192.168.1.100) та додайте зрозуміле ім'я для відображення на панелі.", + "ping": "", + "port": "", + "docker": "", + "game": "", + "pagespeed": "Відстежуйте швидкість завантаження сторінки, показники Core Web Vitals та оцінки оптимізації Вашого сайту.", + "grpc": "Введіть ім'я хоста та порт gRPC-сервера, за бажанням вкажіть назву сервісу для перевірки стану та додайте відображуване ім'я.", + "websocket": "Введіть WebSocket URL для моніторингу (напр., wss://example.com/socket) та додайте відображуване ім'я.", + "hardware": "Моніторинг CPU, пам'яті, використання диска та температури Вашої інфраструктури." + } + }, + "ignoreTls": { + "description": "Налаштуйте перевірку сертифікатів TLS/SSL для HTTPS-з'єднань.", + "option": { + "tls": { + "label": "Ігнорувати помилки TLS/SSL" + } + }, + "title": "Налаштування TLS/SSL" + }, + "incidents": { + "description": "Ковзне вікно використовується для визначення моменту, коли монітор стає недоступним. Статус монітора зміниться лише тоді, коли відсоток перевірок у ковзному вікні досягне вказаного значення.", + "option": { + "checks": { + "label": "Кількість перевірок у ковзному вікні" + }, + "percentage": { + "label": "Який відсоток перевірок у ковзному вікні має зазнати невдачі/успіху перед зміною статусу монітора?" + } + }, + "title": "Інциденти" + }, + "notifications": { + "description": "Оберіть канали сповіщень, які Ви хочете використовувати", + "title": "Сповіщення" + }, + "type": { + "description": "Оберіть тип перевірки", + "optionDockerDescription": "Використовуйте Docker для моніторингу роботи контейнера.", + "optionGameDescription": "Моніторинг доступності конкретного ігрового сервера.", + "optionGrpcDescription": "Моніторинг gRPC-сервісів за допомогою стандартного протоколу перевірки стану.", + "optionWebSocketDescription": "Моніторинг WebSocket-ендпоінтів на предмет стану з'єднання та часу відповіді.", + "optionHttpDescription": "Використовуйте HTTP(S) для моніторингу Вашого сайту або API-ендпоінту.", + "optionPingDescription": "Використовуйте ICMP Ping для моніторингу доступності сервера.", + "optionPortDescription": "Моніторинг відкритості конкретного порту на сервері.", + "title": "Тип" + }, + "thresholds": { + "title": "Порогові значення сповіщень", + "description": "Визначте порогові значення, при яких мають спрацьовувати сповіщення для цього монітора обладнання.", + "option": { + "cpuThreshold": { + "label": "Поріг сповіщення CPU (%)" + }, + "memoryThreshold": { + "label": "Поріг сповіщення пам'яті (%)" + }, + "diskThreshold": { + "label": "Поріг сповіщення диска (%)" + }, + "tempThreshold": { + "label": "Поріг сповіщення температури (°C)" + } + } + }, + "geoChecks": { + "title": "Геоперевірки", + "description": "Запускайте перевірки з різних географічних локацій для моніторингу глобальної доступності та продуктивності.", + "option": { + "enabled": { + "label": "Увімкнути геоперевірки" + }, + "locations": { + "label": "Розташування", + "placeholder": "Оберіть розташування", + "options": { + "EU": "Європа", + "NA": "Північна Америка", + "AS": "Азія", + "SA": "Південна Америка", + "AF": "Африка", + "OC": "Океанія" + } + }, + "interval": { + "label": "Інтервал перевірки", + "value": { + "fiveMinutes": "5 хвилин", + "tenMinutes": "10 хвилин", + "fifteenMinutes": "15 хвилин", + "thirtyMinutes": "30 хвилин" + } + } + } + }, + "url": { + "title": "IP/URL монітора на сторінці статусу", + "description": "Відображати IP-адресу або URL монітора на публічній сторінці статусу. Якщо вимкнено, буде показано лише назву монітора для захисту конфіденційної інформації.", + "option": { + "showURL": { + "label": "Відображати IP/URL на сторінці статусу", + "enabled": "Увімкнено", + "disabled": "Вимкнено" + } + } + }, + "stats": { + "title": "Історія моніторингу", + "description": "Очистити всю історію моніторингу та статистику Вашої команди. Ця дія незворотна.", + "buttonText": "Очистити всю статистику", + "dialog": { + "title": "Очистити всю історію моніторингу?", + "description": "Це назавжди видалить всю історію моніторингу, статистику та дані перевірок Вашої команди. Цю дію не можна скасувати.", + "confirm": "Так, очистити всю статистику" + } + } + } }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "" + "editUser": { + "form": { + "roles": { + "title": "Ролі", + "description": "Призначте ролі користувачу. Можна обрати кілька ролей." + } + }, + "dialog": { + "removeUser": { + "title": "Видалити користувача", + "content": "Ви впевнені, що хочете видалити {{name}} з Вашої команди? Цю дію не можна скасувати." + } + } }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Вирішити інцидент", + "option": { + "comment": { + "label": "Коментар (необов'язково)", + "placeholder": "Додайте коментар щодо вирішення..." + } + } + }, + "details": { + "analysis": "Аналіз інциденту", + "comment": "Коментар:", + "detailsLabel": "Деталі", + "downtime": "Час простою:", + "message": "Повідомлення:", + "monitor": "Монітор:", + "overview": "Огляд", + "resolutionDetails": "Деталі вирішення", + "resolutionType": "Тип:", + "resolutionTypes": { + "automatic": "Автоматично", + "manual": "Вручну" + }, + "resolve": "Вирішити інцидент", + "resolvedAt": "Вирішено о:", + "resolvedBy": "Вирішив:", + "startedAt": "Початок:", + "status": "Статус:", + "statusCode": "Код статусу:", + "timeline": "Хронологія", + "title": "Деталі інциденту", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Усі монітори", + "monitor": "Монітор", + "resolutionType": "Тип вирішення", + "resolutionTypes": { + "manual": "Вручну", + "automatic": "Автоматично", + "all": "Усі" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Активні інциденти", + "active_zero": "Немає активних інцидентів", + "active_one": "{{count}} активний інцидент", + "active_other": "{{count}} активних інцидентів" + }, + "incidentStats": { + "avgResolutionTime": "Середній час вирішення", + "mostAffectedMonitor": "Найвразливіший монітор", + "title": "Статистика інцидентів", + "totalIncidents": "Усього інцидентів" + }, + "latestIncidents": { + "title": "Останні інциденти" + } + }, + "table": { + "actions": { + "details": "Деталі", + "goToMonitor": "Перейти до монітора", + "resolveManually": "Вирішити вручну" + }, + "activeIncidents": "Активні інциденти", + "headers": { + "endTime": "Час завершення", + "resolutionType": "Тип вирішення", + "startTime": "", + "statusCode": "Код статусу" + }, + "resolvedIncidents": "Вирішені інциденти", + "status": { + "active": "Активний", + "resolved": "Вирішено" + } + } }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" + "infrastructure": { + "charts": { + "labels": { + "cpu": "Використання CPU", + "disk": "Використання диска", + "memory": "Використання пам'яті", + "netBytesRecv": "{{name}} - Отримано байтів", + "netBytesSent": "{{name}} - Надіслано байтів", + "temp": "Темп." + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Макс. частота", + "title": "Використання CPU", + "upperLabel": "Поточна частота" + }, + "disk": { + "lowerLabel": "Вільно", + "title": "Використання диска {{idx}}", + "upperLabel": "Використано" + }, + "memory": { + "lowerLabel": "Вільно", + "title": "Використання пам'яті", + "upperLabel": "Використано" + } + }, + "statBoxes": { + "avgCpuTemperature": "", + "cpuFrequency": "", + "cpuLogical": "", + "cpuPhysical": "", + "disk": "Диск", + "memory": "Пам'ять", + "os": "" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Диск", + "memory": "Пам'ять" + } + }, + "tabs": { + "labels": { + "network": "Мережа", + "overview": "Огляд" + } + }, + "fallback": { + "actionButton": "Створити монітор!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Монітор інфраструктури використовується для:" + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" + "logs": { + "tabs": { + "diagnostics": "Діагностика", + "logs": "", + "queue": "" + }, + "logLevelSelect": { + "label": "Рівень логів" + }, + "jobQueue": "Черга завдань", + "failedJobs": "Невдалі завдання", + "noLogs": "Логи не знайдено", + "metrics": { + "jobs": "Завдання", + "activeJobs": "Активні завдання", + "failingJobs": "Невдалі завдання", + "totalRuns": "Усього запусків", + "totalFailures": "Усього помилок" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Затримка циклу подій", + "uptime": "Час роботи", + "usedHeapSize": "Використаний розмір купи", + "totalHeapSize": "Загальний розмір купи", + "osMemoryLimit": "Обмеження пам'яті ОС" + }, + "gauges": { + "heapAllocation": "Виділення купи", + "heapUsage": "Використання купи", + "heapUtilization": "Утилізація купи", + "instantCpuUsage": "Миттєве використання CPU", + "availableMemoryPercentage": "% доступної пам'яті", + "allocatedPercentage": "% виділено", + "usedSPercentage": "% від 1с використано CPU", + "total": "Усього", + "used": "Використано" + } + }, + "table": { + "headers": { + "timestamp": "Мітка часу", + "level": "Рівень", + "service": "Сервіс", + "method": "Метод", + "monitorId": "ID монітора", + "runCount": "Кількість запусків", + "failCount": "Кількість помилок", + "lastRunAt": "Останній запуск", + "lockedAt": "Заблоковано о", + "lastFinishedAt": "Останнє завершення", + "lastRunTook": "Останній запуск тривав", + "lastFailedAt": "Остання помилка", + "failReason": "Причина помилки" + } + } }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "" + "maintenanceWindow": { + "fallback": { + "actionButton": "Створити вікно обслуговування!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Вікно обслуговування використовується для:" + }, + "table": { + "headers": { + "nextWindow": "", + "repeat": "" + } + }, + "form": { + "general": { + "title": "Загальні налаштування", + "description": "Встановіть назву та параметр повтору для вікна обслуговування.", + "option": { + "name": { + "label": "Ім'я", + "placeholder": "напр. Щотижневе обслуговування" + }, + "repeat": { + "label": "Повтор" + } + } + }, + "startDate": { + "title": "Дата початку", + "description": "Оберіть дату початку вікна обслуговування.", + "option": { + "startDate": { + "label": "Дата початку" + } + } + }, + "startTime": { + "title": "Час початку", + "description": "Встановіть час початку та тривалість вікна обслуговування. Усі значення вказані в UTC", + "option": { + "duration": { + "label": "Тривалість" + }, + "startTime": { + "label": "Час початку" + } + }, + "monitors": { + "title": "Монітори", + "description": "Монітори, до яких застосовується вікно обслуговування", + "option": { + "addMonitors": { + "label": "Додати монітори" + } + } + } + } + } }, - "title": "", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" + "notifications": { + "fallback": { + "actionButton": "Створити канал", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Канали сповіщень використовуються для:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Токен доступу", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "Адреса, на яку надсилатимуться сповіщення.", + "optionAddress": "Адреса", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Адреса" + }, + "homeServer": { + "optionHomeServer": "Домашній сервер", + "placeholder": "example.com" + }, + "matrix": { + "description": "Налаштуйте підключення до Matrix homeserver для сповіщень.", + "title": "Налаштування Matrix" + }, + "notificationName": { + "description": "Описова назва каналу сповіщень", + "optionName": "Назва каналу", + "placeholder": "напр. Сповіщення продакшену", + "title": "Назва каналу" + }, + "pagerDuty": { + "description": "Ваш ключ інтеграції PagerDuty для отримання сповіщень.", + "optionIntegrationKey": "Ключ інтеграції", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Ключ інтеграції" + }, + "roomId": { + "optionRoomId": "ID кімнати", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Оберіть тип каналу сповіщень для створення.", + "optionType": "Тип", + "title": "Тип каналу" + }, + "telegram": { + "title": "Налаштування Telegram", + "description": "", + "optionBotToken": "Токен бота", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "ID чату", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Призначення" + } + } }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" + "pageSpeed": { + "charts": { + "common": { + "cls": "Сукупний зсув макету (CLS)", + "fcp": "Перше відображення вмісту (FCP)", + "lcp": "Найбільше відображення вмісту (LCP)", + "si": "Індекс швидкості (SI)", + "tbt": "Загальний час блокування (TBT)" + }, + "legend": { + "title": "Звіт PageSpeed", + "weight": "Вага" + }, + "pie": { + "title": "Звіт продуктивності" + } + }, + "table": { + "headers": { + "pageSpeedScore": "Оцінка PageSpeed" + } + }, + "fallback": { + "actionButton": "Створити монітор!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "Монітор PageSpeed використовується для:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Часовий пояс відображення", + "description": "Оберіть часовий пояс для відображення дат і часу у додатку.", + "option": { + "timezone": { + "label": "Часовий пояс відображення" + } + } + }, + "ui": { + "title": "Зовнішній вигляд", + "description": "Перемикайтесь між світлою та темною темами, змінюйте мову або налаштовуйте тип відображення графіків.", + "option": { + "theme": { + "label": "Режим теми", + "light": "Світла", + "dark": "Темна" + }, + "language": { + "label": "Мова" + }, + "chartType": { + "label": "Тип графіка", + "histogram": "Гістограма", + "heatmap": "Теплова карта" + } + } + }, + "pagespeed": { + "title": "Ключ API Google PageSpeed", + "description": "Введіть Ваш ключ API Google PageSpeed для увімкнення моніторингу PageSpeed. Натисніть Скинути, щоб оновити ключ.", + "option": { + "apiKey": { + "label": "Ключ API PageSpeed", + "labelSet": "Ключ API встановлено. Натисніть Скинути, щоб змінити.", + "placeholder": "Введіть Ваш ключ API Google PageSpeed" + } + } + }, + "url": { + "title": "IP/URL монітора на сторінці статусу", + "description": "Відображати IP-адресу або URL монітора на публічній сторінці статусу. Якщо вимкнено, буде показано лише назву монітора для захисту конфіденційної інформації.", + "option": { + "showURL": { + "label": "Відображати IP/URL на сторінці статусу", + "enabled": "Увімкнено", + "disabled": "Вимкнено" + } + } + }, + "stats": { + "title": "Історія моніторингу", + "description": "Очистити всю історію моніторингу та статистику Вашої команди. Ця дія незворотна.", + "option": { + "clear": { + "label": "Очистити всю статистику. Це незворотно." + } + }, + "dialog": { + "title": "Очистити всю історію моніторингу?", + "description": "Це не можна скасувати" + } + }, + "retention": { + "title": "Збереження перевірок", + "description": "Встановіть, як довго зберігаються дані перевірок перед автоматичним очищенням.", + "option": { + "days": { + "label": "Період збереження (дні)", + "unlimited": "Необмежено" + } + } + }, + "thresholds": { + "title": "Глобальні порогові значення", + "description": "Встановіть глобальні порогові значення CPU, пам'яті, диска та температури для моніторингу інфраструктури. Вони застосовуються до всіх моніторів обладнання, якщо не перевизначені.", + "option": { + "cpu": { + "label": "Поріг CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Поріг пам'яті (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Поріг диска (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Поріг температури (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Налаштування пошти", + "description": "Налаштуйте параметри електронної пошти для Вашої системи. Використовується для надсилання сповіщень та попереджень.", + "descriptionTransport": "", + "descriptionTransportLink": "Дивіться специфікації тут", + "option": { + "host": { + "label": "Хост пошти — ім'я хоста або IP-адреса для підключення", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Порт пошти — порт для підключення", + "placeholder": "587" + }, + "address": { + "label": "Адреса пошти — використовується для автентифікації", + "placeholder": "you@example.com" + }, + "user": { + "label": "Користувач пошти — ім'я для автентифікації, замінює адресу пошти, якщо вказано", + "placeholder": "Залиште порожнім, якщо не потрібно" + }, + "password": { + "label": "Пароль пошти — пароль для автентифікації", + "labelSet": "Пароль встановлено. Натисніть Скинути, щоб змінити.", + "placeholder": "Введіть Ваш пароль" + }, + "tlsServername": { + "label": "TLS Servername — необов'язкове ім'я хоста для валідації TLS, коли хост є IP", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "Хост з'єднання пошти — ім'я хоста для HELO/EHLO привітання", + "placeholder": "localhost" + }, + "secure": { + "label": "Використовувати SSL (рекомендовано): шифрувати з'єднання за допомогою SSL/TLS" + }, + "pool": { + "label": "Увімкнути пул з'єднань: повторне використання з'єднань для покращення продуктивності" + }, + "ignoreTLS": { + "label": "Вимкнути STARTTLS: не використовувати TLS, навіть якщо сервер його підтримує" + }, + "requireTLS": { + "label": "Примусовий STARTTLS: вимагати оновлення до TLS, помилка якщо не підтримується" + }, + "rejectUnauthorized": { + "label": "Відхиляти недійсні сертифікати: відхиляти з'єднання із самопідписаними або ненадійними сертифікатами" + } + } + }, + "demoMonitors": { + "title": "Демо-монітори", + "description": "Додайте зразки моніторів для демонстрації." + }, + "removeMonitors": { + "title": "Скидання системи", + "description": "Видалити всі монітори з Вашої системи.", + "dialog": { + "title": "Видалити всі монітори?", + "description": "Це не можна скасувати." + } + }, + "exportMonitors": { + "title": "Експортувати монітори" + }, + "importExportMonitors": { + "title": "Імпорт / Експорт моніторів", + "description": "Імпортуйте або експортуйте дані моніторів у файлі JSON для резервного копіювання або перенесення." + }, + "about": { + "title": "Про додаток", + "developedBy": "Розроблено Bluewave Labs" + }, + "validation": { + "errorMessage": "Будь ласка, виправте такі помилки валідації:" + } + } + }, + "statusPages": { + "deleteSuccess": "Сторінку статусу успішно видалено", + "fallback": { + "title": "Сторінка статусу використовується для:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "Створити сторінку статусу!" + }, + "monitorsList": { + "chartTypeHeatmap": "Теплова карта", + "chartTypeHistogram": "Гістограма", + "noData": "Немає доступних даних", + "infrastructure": { + "title": "Інфраструктура", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Пам'ять", + "disk": "Диск", + "usage": "Використання", + "used": "Використано", + "total": "Усього" + }, + "uptime": { + "title": "Час роботи", + "responseTime": "Час відповіді" + } + }, + "statusBar": { + "allDown": "Усі системи недоступні", + "allUp": "Усі системи працюють", + "degraded": "Деякі системи мають проблеми", + "unknown": "Неможливо визначити стан системи" + }, + "table": { + "headers": { + "name": "Назва сторінки статусу", + "url": "Публічний URL" + }, + "published": "Опубліковано", + "unpublished": "Неопубліковано" + }, + "title": "Сторінки статусу", + "details": { + "empty": { + "title": "Тут ще нічого немає", + "addMonitor": "Додайте монітор, щоб розпочати" + } + }, + "form": { + "access": { + "title": "Доступ", + "description": "Якщо Ваша сторінка статусу готова, Ви можете позначити її як опубліковану.", + "option": { + "published": { + "name": "Опубліковано та видно для всіх" + } + } + }, + "basicInfo": { + "title": "Основна інформація", + "description": "Визначте назву компанії та піддомен, на який вказує Ваша сторінка статусу.", + "option": { + "name": { + "label": "Назва компанії", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Адреса Вашої сторінки статусу", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "Монітори", + "description": "Оберіть монітори для відображення на Вашій сторінці статусу.", + "noMonitors": "Монітори не обрані", + "option": { + "monitors": { + "label": "Оберіть монітори", + "placeholder": "Шукати та обрати монітори..." + } + } + }, + "timezone": { + "title": "Часовий пояс", + "description": "Оберіть часовий пояс для відображення Вашої сторінки статусу.", + "option": { + "timezone": { + "label": "Часовий пояс", + "placeholder": "Оберіть часовий пояс..." + } + } + }, + "appearance": { + "title": "Зовнішній вигляд", + "description": "Визначте стандартний вигляд Вашої публічної сторінки статусу.", + "option": { + "logo": { + "label": "Логотип" + }, + "color": { + "label": "Колір бренду" + } + } + }, + "features": { + "title": "Функції", + "description": "Налаштуйте, яка інформація відображається на Вашій сторінці статусу.", + "option": { + "showCharts": { + "label": "Показувати графіки часу відповіді" + }, + "showUptimePercentage": { + "label": "Показувати відсоток часу роботи" + }, + "showAdminLoginLink": { + "label": "Показувати посилання для входу адміністратора" + }, + "showInfrastructure": { + "label": "Показувати метрики інфраструктури" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Шукати монітори..." + } + }, + "table": { + "headers": { + "responseTime": "Час відповіді" + } + }, + "fallback": { + "actionButton": "Створити монітор!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Монітор доступності використовується для:" + } } } } diff --git a/client/src/locales/vi.json b/client/src/locales/vi.json index 22b96487c..f9963958b 100644 --- a/client/src/locales/vi.json +++ b/client/src/locales/vi.json @@ -1,838 +1,1305 @@ { - "submit": "", - "title": "", - "distributedStatusHeaderText": "", - "distributedStatusSubHeaderText": "", - "settingsDisabled": "", - "settingsSuccessSaved": "", - "settingsFailedToSave": "", - "settingsStatsCleared": "", - "settingsFailedToClearStats": "", - "settingsFailedToAddDemoMonitors": "", - "settingsMonitorsDeleted": "", - "settingsFailedToDeleteMonitors": "", - "starPromptTitle": "", - "starPromptDescription": "", - "https": "", - "http": "", - "monitor": "", - "aboutus": "", - "now": "", - "delete": "", - "configure": "", - "responseTime": "", - "ms": "", - "bar": "", - "area": "", - "country": "", - "city": "", - "response": "", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "", - "webhookSendError": "", - "webhookUnsupportedPlatform": "", - "distributedRightCategoryTitle": "", - "distributedStatusServerMonitors": "", - "distributedStatusServerMonitorsDescription": "", - "distributedUptimeCreateSelectURL": "", - "distributedUptimeCreateChecks": "", - "distributedUptimeCreateChecksDescription": "", - "distributedUptimeCreateIncidentNotification": "", - "distributedUptimeCreateIncidentDescription": "", - "distributedUptimeCreateAdvancedSettings": "", - "distributedUptimeDetailsNoMonitorHistory": "", - "distributedUptimeDetailsStatusHeaderUptime": "", - "distributedUptimeDetailsStatusHeaderLastUpdate": "", - "notifications": { - "enableNotifications": "", - "testNotification": "", - "addOrEditNotifications": "", - "slack": { - "label": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "discord": { - "label": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "", - "webhookRequired": "" - }, - "telegram": { - "label": "", - "description": "", - "tokenLabel": "", - "tokenPlaceholder": "", - "chatIdLabel": "", - "chatIdPlaceholder": "", - "fieldsRequired": "" - }, - "webhook": { - "label": "", - "description": "", - "urlLabel": "", - "urlPlaceholder": "", - "urlRequired": "" - }, - "testNotificationDevelop": "", - "integrationButton": "", - "testSuccess": "", - "testFailed": "", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [""] - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "", - "add": "", - "monitors": "", - "distributedUptimeStatusCreateStatusPage": "", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "", - "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "", - "distributedUptimeStatus60Days": "", - "distributedUptimeStatus90Days": "", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", - "distributedUptimeStatusUpt": "", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "", - "incidentsTableDateTime": "", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", - "incidentsOptionsHeaderLastHour": "", - "incidentsOptionsHeaderLastDay": "", - "incidentsOptionsHeaderLastWeek": "", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "", - "infrastructureServerUrlLabel": "", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", - "memoryUsage": "", - "cpu": "", - "cpuUsage": "", - "cpuTemperature": "", - "diskUsage": "", - "used": "", - "total": "", - "cores": "", - "frequency": "", - "status": "", - "cpuPhysical": "", - "cpuLogical": "", - "cpuFrequency": "", - "avgCpuTemperature": "", - "memory": "", - "disk": "", - "uptime": "", - "os": "", - "host": "", - "actions": "", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "", - "integrationsSlackInfo": "", - "integrationsDiscord": "", - "integrationsDiscordInfo": "", - "integrationsZapier": "", - "integrationsZapierInfo": "", - "commonSave": "", - "createYour": "", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", - "timezone": "", - "features": "", - "administrator": "", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "", - "window": "", - "cancel": "", - "message": "", - "low": "", - "high": "", - "statusCode": "", - "date&Time": "", - "type": "", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", - "createA": "", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "", - "chooseGame": "", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "", - "deleteDialogDescription": "", - "pageSpeedMonitor": "", - "shown": "", - "ago": "", - "companyName": "", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "", - "statusPageCreateSettings": "", - "basicInformation": "", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "", - "showUptimePercentage": "", - "removeLogo": "", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "", - "invalidFileFormat": "", - "invalidFileSize": "", - "ClickUpload": "", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", - "EmailDescriptionText": "", - "YourPhoto": "", - "PhotoDescriptionText": "", - "save": "", - "DeleteDescriptionText": "", - "DeleteAccountWarning": "", - "DeleteWarningTitle": "", - "bulkImport": { - "title": "", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "", - "fallbackPage": "" - }, - "DeleteAccountTitle": "", - "DeleteAccountButton": "", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "", - "greeting": { - "prepend": "", - "append": "", - "overview": "" - }, - "roles": { - "superAdmin": "", - "admin": "", - "teamMember": "", - "demoUser": "" - }, - "teamPanel": { - "teamMembers": "", - "filter": { - "all": "", - "member": "" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "", - "selectRole": "", - "inviteLink": "", - "cancel": "", - "noMembers": "", - "getToken": "", - "emailToken": "", - "table": { - "name": "", - "email": "", - "role": "", - "created": "" - } - }, - "monitorState": { - "paused": "", - "resumed": "", - "active": "" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "", - "incidents": "", - "statusPages": "", - "maintenance": "", - "integrations": "", - "settings": "", - "support": "", - "discussions": "", - "docs": "", - "changelog": "", - "profile": "", - "password": "", - "team": "", - "logOut": "", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", - "commonSaving": "", - "navControls": "", - "incidentsPageTitle": "", - "passwordPanel": { - "passwordChangedSuccess": "", - "passwordInputIncorrect": "", - "currentPassword": "", - "enterCurrentPassword": "", - "newPassword": "", - "enterNewPassword": "", - "confirmNewPassword": "", - "passwordRequirements": "", - "saving": "" - }, - "emailSent": "", - "failedToSendEmail": "", - "settingsTestEmailSuccess": "", - "settingsTestEmailFailed": "", - "settingsTestEmailFailedWithReason": "", - "settingsTestEmailUnknownError": "", - "statusMsg": { - "paused": "", - "up": "", - "down": "", - "pending": "" - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "", - "game": "" - }, "common": { - "appName": "", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" + "auth": { + "roles": { + "admin": "Quản trị viên", + "demo": "Demo", + "superadmin": "Quản trị viên cấp cao", + "user": "Người dùng" + } }, - "toasts": { - "networkError": "", - "checkConnection": "", - "unknownError": "" + "alerts": { + "pageSpeedApiKey": { + "content": "Cảnh báo: Bạn chưa thêm khóa API Google PageSpeed. Truy cập Cài đặt để thêm. Nếu không có khóa này, trình giám sát PageSpeed sẽ không hoạt động." + } + }, + "appName": "", + "breadcrumbs": { + "details": "Chi tiết", + "home": "Trang chủ" + }, + "buttons": { + "addMember": "Thêm thành viên", + "cancel": "Hủy", + "close": "Đóng", + "configure": "Cấu hình", + "confirm": "Xác nhận", + "create": "Tạo", + "delete": "Xóa", + "generateToken": "Tạo token", + "incidents": "Sự cố", + "inviteMember": "Mời thành viên", + "pause": "Tạm dừng", + "resume": "Tiếp tục", + "save": "", + "sendInvite": "Gửi lời mời", + "test": "Kiểm tra", + "testNotifications": "Kiểm tra thông báo", + "toggleTheme": "", + "flushQueue": "Xóa hàng đợi", + "notFound": "Về bảng điều khiển chính", + "resetPassword": "", + "reset": "", + "clear": "Xóa", + "addDemo": "Thêm trình giám sát mẫu", + "removeMonitors": "Xóa trình giám sát", + "sendTestEmail": "Gửi email kiểm tra", + "exportToJSON": "Xuất ra JSON", + "importFromJSON": "Nhập từ JSON", + "clearFilters": "Xóa bộ lọc", + "removeUser": "Xóa người dùng" + }, + "charts": { + "labels": { + "averageResponseTime": "Thời gian phản hồi trung bình", + "downtime": "Thời gian ngừng hoạt động", + "high": "", + "low": "", + "uptime": "Thời gian hoạt động" + }, + "histogram": { + "avg": "TB: {{value}} ms", + "max": "Tối đa: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Hành động này không thể hoàn tác.", + "title": "Bạn có chắc chắn muốn xóa không?" + } + }, + "labels": { + "active": "Đang hoạt động", + "paused": "tạm dừng", + "na": "N/A", + "resolved": "Đã giải quyết", + "responseTime": "Thời gian phản hồi" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Vai trò" + } + } + }, + "name": { + "option": { + "firstName": { + "label": "", + "placeholder": "" + }, + "lastName": { + "label": "", + "placeholder": "" + } + } + }, + "email": { + "option": { + "email": { + "label": "Email", + "placeholder": "you@example.com" + } + } + } + }, + "table": { + "empty": "Không có dữ liệu", + "headers": { + "actions": "", + "dateTime": "Ngày & giờ", + "message": "Tin nhắn", + "monitor": "Trình giám sát", + "monitorId": "ID trình giám sát", + "name": "Tên", + "status": "Trạng thái", + "type": "", + "url": "Url", + "interval": "Khoảng thời gian", + "active": "Đang hoạt động", + "responseTime": "Thời gian phản hồi" + } } }, - "auth": { - "common": { - "navigation": { - "continue": "", - "back": "" - }, - "inputs": { - "email": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "invalid": "" - } - }, - "password": { - "label": "", - "rules": { - "length": { - "beginning": "", - "highlighted": "" - }, - "special": { - "beginning": "", - "highlighted": "" - }, - "number": { - "beginning": "", - "highlighted": "" - }, - "uppercase": { - "beginning": "", - "highlighted": "" - }, - "lowercase": { - "beginning": "", - "highlighted": "" - }, - "match": { - "beginning": "", - "highlighted": "" - } - }, - "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" - } - }, - "passwordConfirm": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - }, - "lastName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - } - }, + "components": { + "imageUpload": { + "clickToUpload": "Nhấn để tải lên", + "dragAndDrop": "", + "supportedFormats": "", + "maxSize": "", + "orDragAndDrop": "hoặc kéo và thả", "errors": { - "validation": "" - }, - "fields": { - "password": { - "errors": { - "incorrect": "" - } - } + "invalidFileSize": "", + "invalidFileFormat": "" } }, - "login": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "" - }, - "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "", - "incorrectPassword": "" - }, - "errors": { - "password": { - "incorrect": "" - } - } + "headerStatusPageControls": { + "publicLink": "" }, - "registration": { - "heading": { - "superAdmin": "", - "user": "" - }, - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" + "headerTimeRange": { + "labels": { + "day": "Ngày", + "month": "Tháng", + "recent": "Gần đây", + "week": "Tuần" }, "description": { - "superAdmin": "", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "", - "user": "" - }, - "termsAndPolicies": "", - "links": { - "login": "" - }, - "toasts": { - "success": "" + "recent": "Hiển thị thống kê trong 2 giờ qua.", + "day": "Hiển thị thống kê trong 24 giờ qua.", + "week": "Hiển thị thống kê trong 7 ngày qua.", + "month": "Hiển thị thống kê trong 30 ngày qua." } }, - "forgotPassword": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" + "offlineBanner": { + "serverUnreachable": "Không thể kết nối đến máy chủ", + "retry": "Thử lại", + "retrying": "Đang thử lại...", + "reconnected": "" + }, + "sidebar": { + "menu": { + "uptime": "", + "pagespeed": "", + "infrastructure": "", + "notifications": "", + "checks": "Kiểm tra", + "incidents": "", + "statusPages": "", + "maintenance": "", + "logs": "", + "settings": "" }, - "buttons": { - "openEmail": "", - "resetPassword": "" + "bottomMenu": { + "support": "", + "discussions": "", + "docs": "", + "changelog": "" }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "accountMenu": { + "profile": "Hồ sơ", + "password": "Mật khẩu", + "team": "Nhóm" }, - "links": { - "login": "", - "resend": "" + "starPrompt": { + "title": "Gắn sao Checkmate", + "description": "Xem các bản phát hành mới nhất và giúp phát triển cộng đồng trên GitHub" }, - "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" + "authFooter": { + "navControls": "", + "logOut": "", + "roles": { + "superAdmin": "Quản trị viên cấp cao", + "admin": "Quản trị viên", + "user": "Người dùng", + "demoUser": "" + } } + }, + "starPrompt": { + "title": "Gắn sao Checkmate", + "description": "Xem các bản phát hành mới nhất và giúp phát triển cộng đồng trên GitHub" } }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" + "pages": { + "notFound": { + "title": "Ôi không! Bạn đã làm rơi sushi!", + "subtitle": "Either the URL doesn’t exist, or you don’t have access to it." + }, + "account": { + "tabs": { + "profile": "Hồ sơ", + "password": "Mật khẩu", + "team": "Nhóm" }, - "alertBox": "", - "description": "", - "retryButton": { - "default": "", - "processing": "" + "form": { + "name": { + "title": "Tên", + "description": "Cập nhật thông tin cá nhân của bạn" + }, + "photo": { + "title": "Ảnh hồ sơ", + "description": "Tải lên ảnh hồ sơ" + }, + "currentPassword": { + "title": "Mật khẩu hiện tại", + "description": "Nhập mật khẩu hiện tại để xác minh danh tính", + "option": { + "label": "Mật khẩu hiện tại", + "placeholder": "Nhập mật khẩu hiện tại" + } + }, + "newPassword": { + "title": "Mật khẩu mới", + "description": "Chọn mật khẩu mạnh có ít nhất 8 ký tự", + "option": { + "newPassword": { + "label": "Mật khẩu mới", + "placeholder": "Nhập mật khẩu mới" + }, + "confirm": { + "label": "Xác nhận mật khẩu", + "placeholder": "Xác nhận mật khẩu mới" + } + } + }, + "deleteAccount": { + "title": "Xóa tài khoản", + "description": "Hành động này là vĩnh viễn và không thể hoàn tác" + } + }, + "team": { + "filter": { + "placeholder": "Lọc theo vai trò", + "all": "", + "admin": "Quản trị viên", + "member": "" + }, + "table": { + "headers": { + "email": "Email", + "role": "", + "created": "" + } + }, + "addMember": { + "title": "Đăng ký thành viên mới", + "description": "Tạo tài khoản người dùng mới. Chia sẻ thông tin đăng nhập một cách an toàn sau khi tạo." + }, + "invite": { + "title": "Mời thành viên nhóm", + "description": "Gửi lời mời tham gia nhóm của bạn", + "email": { + "label": "Địa chỉ email", + "placeholder": "Nhập địa chỉ email" + }, + "role": { + "label": "Vai trò", + "placeholder": "Chọn vai trò" + }, + "linkLabel": "Liên kết mời" + } } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" }, - "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" - }, - "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - } - }, - "notificationConfig": { - "title": "", - "description": "" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "", - "sendTestNotifications": "", - "testNotificationsDisabled": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "", + "highlighted": "" + }, + "lowercase": { + "beginning": "", + "highlighted": "" + }, + "match": { + "beginning": "", + "highlighted": "" + }, + "number": { + "beginning": "", + "highlighted": "" + }, + "special": { + "beginning": "", + "highlighted": "" + }, + "uppercase": { + "beginning": "", + "highlighted": "" + } + }, + "form": { + "option": { + "email": { + "label": "Email", + "placeholder": "me@example.com" + }, + "password": { + "label": "Mật khẩu", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Xác nhận mật khẩu" + } + } + } + }, + "login": { + "title": "Chào mừng bạn trở lại Checkmate!", + "subtitle": "Đăng nhập để tiếp tục", + "submit": "Đăng nhập", + "links": { + "forgotPassword": { + "text": "Quên mật khẩu?", + "linkText": "Đặt lại mật khẩu" + }, + "register": { + "text": "Chưa có tài khoản?", + "linkText": "Đăng ký tại đây" + } + } + }, + "register": { + "title": "Chào mừng bạn trở lại Checkmate!", + "subtitle": "Đăng ký để bắt đầu", + "submit": "Đăng ký" + }, + "forgotPassword": { + "title": "Quên mật khẩu?", + "subtitle": "Đừng lo, chúng tôi sẽ gửi hướng dẫn đặt lại mật khẩu cho bạn.", + "submit": "Yêu cầu khôi phục", + "links": { + "login": { + "text": "Quay lại", + "linkText": "đăng nhập" + } + } + }, + "setNewPassword": { + "title": "Đặt lại mật khẩu", + "subtitle": "Mật khẩu mới phải khác với các mật khẩu đã sử dụng trước đó." } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "" }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" + "checks": { + "selects": { + "monitor": { + "all": "Tất cả trình giám sát" + }, + "status": { + "all": "Tất cả", + "down": "Ngừng hoạt động", + "up": "Hoạt động" + } + }, + "table": { + "empty": "Không có kiểm tra lỗi trong khoảng thời gian này", + "headers": { + "statusCode": "Mã trạng thái", + "location": "Vị trí" + } + } }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" + "common": { + "monitors": { + "actions": { + "configure": "Cấu hình", + "delete": "Xóa", + "generateToken": "Tạo token", + "details": "Chi tiết", + "incidents": "Sự cố", + "inviteMember": "Mời thành viên", + "openSite": "Mở trang web", + "pause": "Tạm dừng", + "resume": "Tiếp tục" + }, + "statBoxes": { + "activeFor": "Hoạt động từ", + "certificateExpiry": "Hạn chứng chỉ", + "lastCheck": "Lần kiểm tra cuối", + "lastResponseTime": "Thời gian phản hồi cuối" + }, + "status": { + "down": "ngừng hoạt động", + "breached": "vượt ngưỡng", + "initializing": "đang khởi tạo", + "maintenance": "bảo trì", + "paused": "tạm dừng", + "total": "tổng", + "up": "hoạt động" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Trò chơi", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Cổng", + "optionPagespeed": "PageSpeed", + "optionHardware": "Hạ tầng", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" + "createMonitor": { + "form": { + "advanced": { + "description": "Cài đặt tùy chọn cho các trường hợp sử dụng nâng cao", + "option": { + "advancedMatching": { + "label": "Sử dụng đối sánh nâng cao" + }, + "expectedValue": { + "label": "Giá trị mong đợi" + }, + "jsonPath": { + "description": "Biểu thức này sẽ được đánh giá với dữ liệu JSON phản hồi và kết quả sẽ được dùng để đối sánh với giá trị mong đợi. Xem jmespath.org để biết tài liệu ngôn ngữ truy vấn.", + "label": "Biểu thức JSONPath" + }, + "matchMethod": { + "label": "Phương thức đối sánh", + "equal": "Bằng", + "include": "Bao gồm", + "regex": "Regex" + } + }, + "title": "Cài đặt nâng cao" + }, + "frequency": { + "description": "Bạn muốn kiểm tra trạng thái trình giám sát này bao lâu một lần?", + "option": { + "frequency": { + "label": "Tần suất kiểm tra", + "value": { + "fifteenMinutes": "15 phút", + "fifteenSeconds": "15 giây", + "fiveMinutes": "5 phút", + "fourMinutes": "4 phút", + "oneMinute": "1 phút", + "tenMinutes": "10 phút", + "thirtyMinutes": "30 phút", + "thirtySeconds": "30 giây", + "threeMinutes": "3 phút", + "twoMinutes": "2 phút" + } + } + }, + "title": "Tần suất kiểm tra" + }, + "general": { + "option": { + "container": { + "label": "Tên/ID container", + "placeholder": "my-app hoặc abcd1234" + }, + "host": { + "label": "Máy chủ", + "placeholder": "192.168.1.100 hoặc example.com" + }, + "name": { + "label": "Tên hiển thị", + "placeholder": "vd: Website của tôi" + }, + "secret": { + "label": "Khóa xác thực", + "placeholder": "Nhập khóa bí mật của bạn" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "Chọn trò chơi", + "placeholder": "Chọn một trò chơi" + }, + "port": { + "label": "Cổng cần giám sát", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "Tên dịch vụ", + "placeholder": "vd: my.service.v1 (để trống cho kiểm tra sức khỏe tổng thể)" + }, + "wsUrl": { + "label": "URL WebSocket", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Cài đặt chung", + "description": { + "http": "Nhập URL hoặc IP cần giám sát (vd: https://example.com/ hoặc 192.168.1.100) và thêm tên hiển thị rõ ràng trên bảng điều khiển.", + "ping": "", + "port": "", + "docker": "", + "game": "", + "pagespeed": "Theo dõi hiệu suất tải trang, Core Web Vitals và điểm tối ưu hóa cho trang web của bạn.", + "grpc": "Nhập tên máy chủ và cổng của máy chủ gRPC, tùy chọn chỉ định tên dịch vụ cho kiểm tra sức khỏe và thêm tên hiển thị.", + "websocket": "Nhập URL WebSocket cần giám sát (vd: wss://example.com/socket) và thêm tên hiển thị.", + "hardware": "Giám sát CPU, bộ nhớ, dung lượng ổ đĩa và nhiệt độ cho hạ tầng của bạn." + } + }, + "ignoreTls": { + "description": "Cấu hình xác thực chứng chỉ TLS/SSL cho kết nối HTTPS.", + "option": { + "tls": { + "label": "Bỏ qua lỗi TLS/SSL" + } + }, + "title": "Cài đặt TLS/SSL" + }, + "incidents": { + "description": "Cửa sổ trượt được sử dụng để xác định khi nào trình giám sát ngừng hoạt động. Trạng thái của trình giám sát chỉ thay đổi khi tỷ lệ kiểm tra trong cửa sổ trượt đạt giá trị được chỉ định.", + "option": { + "checks": { + "label": "Số lần kiểm tra trong cửa sổ trượt" + }, + "percentage": { + "label": "Bao nhiêu phần trăm kiểm tra trong cửa sổ trượt thất bại/thành công trước khi trạng thái trình giám sát thay đổi?" + } + }, + "title": "Sự cố" + }, + "notifications": { + "description": "Chọn các kênh thông báo bạn muốn sử dụng", + "title": "Thông báo" + }, + "type": { + "description": "Chọn loại kiểm tra để thực hiện", + "optionDockerDescription": "Sử dụng Docker để giám sát xem container có đang chạy không.", + "optionGameDescription": "Giám sát xem máy chủ trò chơi cụ thể có trực tuyến không.", + "optionGrpcDescription": "Giám sát dịch vụ gRPC bằng Giao thức Kiểm tra Sức khỏe tiêu chuẩn.", + "optionWebSocketDescription": "Giám sát điểm cuối WebSocket về sức khỏe kết nối và thời gian phản hồi.", + "optionHttpDescription": "Sử dụng HTTP(S) để giám sát trang web hoặc điểm cuối API của bạn.", + "optionPingDescription": "Sử dụng ICMP Ping để giám sát xem máy chủ có trực tuyến không.", + "optionPortDescription": "Giám sát xem một cổng cụ thể trên máy chủ có mở không.", + "title": "Loại" + }, + "thresholds": { + "title": "Ngưỡng cảnh báo", + "description": "Xác định các ngưỡng kích hoạt cảnh báo cho trình giám sát phần cứng này.", + "option": { + "cpuThreshold": { + "label": "Ngưỡng cảnh báo CPU (%)" + }, + "memoryThreshold": { + "label": "Ngưỡng cảnh báo bộ nhớ (%)" + }, + "diskThreshold": { + "label": "Ngưỡng cảnh báo ổ đĩa (%)" + }, + "tempThreshold": { + "label": "Ngưỡng cảnh báo nhiệt độ (°C)" + } + } + }, + "geoChecks": { + "title": "Kiểm tra phân tán địa lý", + "description": "Chạy kiểm tra từ nhiều vị trí địa lý để giám sát tính khả dụng và hiệu suất toàn cầu.", + "option": { + "enabled": { + "label": "Bật kiểm tra phân tán địa lý" + }, + "locations": { + "label": "Vị trí", + "placeholder": "Chọn vị trí", + "options": { + "EU": "Châu Âu", + "NA": "Bắc Mỹ", + "AS": "Châu Á", + "SA": "Nam Mỹ", + "AF": "Châu Phi", + "OC": "Châu Đại Dương" + } + }, + "interval": { + "label": "Khoảng thời gian kiểm tra", + "value": { + "fiveMinutes": "5 phút", + "tenMinutes": "10 phút", + "fifteenMinutes": "15 phút", + "thirtyMinutes": "30 phút" + } + } + } + }, + "url": { + "title": "Giám sát IP/URL trên trang trạng thái", + "description": "Hiển thị địa chỉ IP hoặc URL của trình giám sát trên trang trạng thái công khai. Nếu tắt, chỉ tên trình giám sát được hiển thị để bảo vệ thông tin nhạy cảm.", + "option": { + "showURL": { + "label": "Hiển thị IP/URL trên trang trạng thái", + "enabled": "Đã bật", + "disabled": "Đã tắt" + } + } + }, + "stats": { + "title": "Lịch sử giám sát", + "description": "Xóa tất cả lịch sử giám sát và thống kê cho nhóm của bạn. Hành động này không thể hoàn tác.", + "buttonText": "Xóa tất cả thống kê", + "dialog": { + "title": "Xóa tất cả lịch sử giám sát?", + "description": "Điều này sẽ xóa vĩnh viễn tất cả lịch sử giám sát, thống kê và dữ liệu kiểm tra cho nhóm của bạn. Hành động này không thể hoàn tác.", + "confirm": "Có, xóa tất cả thống kê" + } + } + } }, - "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "" + "editUser": { + "form": { + "roles": { + "title": "Vai trò", + "description": "Gán vai trò cho người dùng. Có thể chọn nhiều vai trò." + } + }, + "dialog": { + "removeUser": { + "title": "Xóa người dùng", + "content": "Bạn có chắc chắn muốn xóa {{name}} khỏi nhóm không? Hành động này không thể hoàn tác." + } + } }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "Giải quyết sự cố", + "option": { + "comment": { + "label": "Nhận xét (tùy chọn)", + "placeholder": "Thêm nhận xét về cách giải quyết..." + } + } + }, + "details": { + "analysis": "Phân tích sự cố", + "comment": "Nhận xét:", + "detailsLabel": "Chi tiết", + "downtime": "Thời gian ngừng:", + "message": "Tin nhắn:", + "monitor": "Trình giám sát:", + "overview": "Tổng quan", + "resolutionDetails": "Chi tiết giải quyết", + "resolutionType": "Loại:", + "resolutionTypes": { + "automatic": "Tự động", + "manual": "Thủ công" + }, + "resolve": "Giải quyết sự cố", + "resolvedAt": "Đã giải quyết lúc:", + "resolvedBy": "Giải quyết bởi:", + "startedAt": "Bắt đầu lúc:", + "status": "Trạng thái:", + "statusCode": "Mã trạng thái:", + "timeline": "Dòng thời gian", + "title": "Chi tiết sự cố", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Tất cả trình giám sát", + "monitor": "Trình giám sát", + "resolutionType": "Loại giải quyết", + "resolutionTypes": { + "manual": "Thủ công", + "automatic": "Tự động", + "all": "Tất cả" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Sự cố đang xảy ra", + "active_zero": "Không có sự cố đang xảy ra", + "active_one": "{{count}} sự cố đang xảy ra", + "active_other": "{{count}} sự cố đang xảy ra" + }, + "incidentStats": { + "avgResolutionTime": "Thời gian giải quyết TB", + "mostAffectedMonitor": "Trình giám sát bị ảnh hưởng nhiều nhất", + "title": "Thống kê sự cố", + "totalIncidents": "Tổng số sự cố" + }, + "latestIncidents": { + "title": "Sự cố mới nhất" + } + }, + "table": { + "actions": { + "details": "Chi tiết", + "goToMonitor": "Đi đến trình giám sát", + "resolveManually": "Giải quyết thủ công" + }, + "activeIncidents": "Sự cố đang xảy ra", + "headers": { + "endTime": "Thời gian kết thúc", + "resolutionType": "Loại giải quyết", + "startTime": "", + "statusCode": "Mã trạng thái" + }, + "resolvedIncidents": "Sự cố đã giải quyết", + "status": { + "active": "Đang hoạt động", + "resolved": "Đã giải quyết" + } + } }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" + "infrastructure": { + "charts": { + "labels": { + "cpu": "Sử dụng CPU", + "disk": "Sử dụng ổ đĩa", + "memory": "Sử dụng bộ nhớ", + "netBytesRecv": "{{name}} - Byte nhận", + "netBytesSent": "{{name}} - Byte gửi", + "temp": "Nhiệt độ" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Tần số tối đa", + "title": "Sử dụng CPU", + "upperLabel": "Tần số hiện tại" + }, + "disk": { + "lowerLabel": "Còn trống", + "title": "Sử dụng ổ đĩa {{idx}}", + "upperLabel": "Đã dùng" + }, + "memory": { + "lowerLabel": "Còn trống", + "title": "Sử dụng bộ nhớ", + "upperLabel": "Đã dùng" + } + }, + "statBoxes": { + "avgCpuTemperature": "", + "cpuFrequency": "", + "cpuLogical": "", + "cpuPhysical": "", + "disk": "Ổ đĩa", + "memory": "Bộ nhớ", + "os": "" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Ổ đĩa", + "memory": "Bộ nhớ" + } + }, + "tabs": { + "labels": { + "network": "Mạng", + "overview": "Tổng quan" + } + }, + "fallback": { + "actionButton": "Tạo trình giám sát!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "Trình giám sát hạ tầng dùng để:" + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" + "logs": { + "tabs": { + "diagnostics": "Chẩn đoán", + "logs": "", + "queue": "" + }, + "logLevelSelect": { + "label": "Mức nhật ký" + }, + "jobQueue": "Hàng đợi công việc", + "failedJobs": "Công việc thất bại", + "noLogs": "Không tìm thấy nhật ký", + "metrics": { + "jobs": "Công việc", + "activeJobs": "Công việc đang chạy", + "failingJobs": "Công việc đang lỗi", + "totalRuns": "Tổng số lần chạy", + "totalFailures": "Tổng số lỗi" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "Độ trễ vòng lặp sự kiện", + "uptime": "Thời gian hoạt động", + "usedHeapSize": "Kích thước heap đã dùng", + "totalHeapSize": "Tổng kích thước heap", + "osMemoryLimit": "Giới hạn bộ nhớ hệ điều hành" + }, + "gauges": { + "heapAllocation": "Cấp phát heap", + "heapUsage": "Sử dụng heap", + "heapUtilization": "Hiệu suất sử dụng heap", + "instantCpuUsage": "Sử dụng CPU tức thời", + "availableMemoryPercentage": "% bộ nhớ khả dụng", + "allocatedPercentage": "% đã cấp phát", + "usedSPercentage": "% của 1 giây CPU sử dụng", + "total": "Tổng", + "used": "Đã dùng" + } + }, + "table": { + "headers": { + "timestamp": "Thời gian", + "level": "Mức", + "service": "Dịch vụ", + "method": "Phương thức", + "monitorId": "ID trình giám sát", + "runCount": "Số lần chạy", + "failCount": "Số lần lỗi", + "lastRunAt": "Chạy lần cuối lúc", + "lockedAt": "Khóa lúc", + "lastFinishedAt": "Hoàn thành lần cuối lúc", + "lastRunTook": "Lần chạy cuối mất", + "lastFailedAt": "Lỗi lần cuối lúc", + "failReason": "Lý do lỗi" + } + } }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "" + "maintenanceWindow": { + "fallback": { + "actionButton": "Tạo cửa sổ bảo trì!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "Cửa sổ bảo trì dùng để:" + }, + "table": { + "headers": { + "nextWindow": "", + "repeat": "" + } + }, + "form": { + "general": { + "title": "Cài đặt chung", + "description": "Đặt tên và tùy chọn lặp lại cho cửa sổ bảo trì.", + "option": { + "name": { + "label": "Tên", + "placeholder": "vd: Bảo trì hàng tuần" + }, + "repeat": { + "label": "Lặp lại" + } + } + }, + "startDate": { + "title": "Ngày bắt đầu", + "description": "Chọn ngày bắt đầu cho cửa sổ bảo trì.", + "option": { + "startDate": { + "label": "Ngày bắt đầu" + } + } + }, + "startTime": { + "title": "Thời gian bắt đầu", + "description": "Đặt thời gian bắt đầu và thời lượng cho cửa sổ bảo trì. Tất cả giá trị theo UTC", + "option": { + "duration": { + "label": "Thời lượng" + }, + "startTime": { + "label": "Thời gian bắt đầu" + } + }, + "monitors": { + "title": "Trình giám sát", + "description": "Trình giám sát mà cửa sổ bảo trì áp dụng", + "option": { + "addMonitors": { + "label": "Thêm trình giám sát" + } + } + } + } + } }, - "title": "", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" + "notifications": { + "fallback": { + "actionButton": "Tạo kênh", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "Kênh thông báo dùng để:" + }, + "form": { + "accessToken": { + "optionAccessToken": "Token truy cập", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "Địa chỉ nơi thông báo sẽ được gửi đến.", + "optionAddress": "Địa chỉ", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Địa chỉ" + }, + "homeServer": { + "optionHomeServer": "Máy chủ chính", + "placeholder": "example.com" + }, + "matrix": { + "description": "Cấu hình kết nối máy chủ Matrix cho thông báo.", + "title": "Cấu hình Matrix" + }, + "notificationName": { + "description": "Tên mô tả cho kênh thông báo", + "optionName": "Tên kênh", + "placeholder": "vd: Cảnh báo sản xuất", + "title": "Tên kênh" + }, + "pagerDuty": { + "description": "Khóa tích hợp PagerDuty để nhận cảnh báo.", + "optionIntegrationKey": "Khóa tích hợp", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Khóa tích hợp" + }, + "roomId": { + "optionRoomId": "ID phòng", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Chọn loại kênh thông báo để tạo.", + "optionType": "Loại", + "title": "Loại kênh" + }, + "telegram": { + "title": "Cấu hình Telegram", + "description": "", + "optionBotToken": "Token bot", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "ID cuộc trò chuyện", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "Đích đến" + } + } }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" + "pageSpeed": { + "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)" + }, + "legend": { + "title": "Báo cáo PageSpeed", + "weight": "Trọng số" + }, + "pie": { + "title": "Báo cáo hiệu suất" + } + }, + "table": { + "headers": { + "pageSpeedScore": "Điểm PageSpeed" + } + }, + "fallback": { + "actionButton": "Tạo trình giám sát!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "Trình giám sát PageSpeed dùng để:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "Múi giờ hiển thị", + "description": "Chọn múi giờ dùng để hiển thị ngày và giờ trong toàn bộ ứng dụng.", + "option": { + "timezone": { + "label": "Múi giờ hiển thị" + } + } + }, + "ui": { + "title": "Giao diện", + "description": "Chuyển đổi giữa chế độ sáng và tối, thay đổi ngôn ngữ hoặc tùy chỉnh kiểu hiển thị biểu đồ.", + "option": { + "theme": { + "label": "Chế độ giao diện", + "light": "Sáng", + "dark": "Tối" + }, + "language": { + "label": "Ngôn ngữ" + }, + "chartType": { + "label": "Loại biểu đồ", + "histogram": "Biểu đồ cột", + "heatmap": "Bản đồ nhiệt" + } + } + }, + "pagespeed": { + "title": "Khóa API Google PageSpeed", + "description": "Nhập khóa API Google PageSpeed để bật giám sát PageSpeed. Nhấn Đặt lại để cập nhật khóa.", + "option": { + "apiKey": { + "label": "Khóa API PageSpeed", + "labelSet": "Khóa API đã được đặt. Nhấn Đặt lại để thay đổi.", + "placeholder": "Nhập khóa API Google PageSpeed của bạn" + } + } + }, + "url": { + "title": "Giám sát IP/URL trên trang trạng thái", + "description": "Hiển thị địa chỉ IP hoặc URL của trình giám sát trên trang trạng thái công khai. Nếu tắt, chỉ tên trình giám sát được hiển thị để bảo vệ thông tin nhạy cảm.", + "option": { + "showURL": { + "label": "Hiển thị IP/URL trên trang trạng thái", + "enabled": "Đã bật", + "disabled": "Đã tắt" + } + } + }, + "stats": { + "title": "Lịch sử giám sát", + "description": "Xóa tất cả lịch sử giám sát và thống kê cho nhóm của bạn. Hành động này không thể hoàn tác.", + "option": { + "clear": { + "label": "Xóa tất cả thống kê. Hành động này không thể hoàn tác." + } + }, + "dialog": { + "title": "Xóa tất cả lịch sử giám sát?", + "description": "Không thể hoàn tác" + } + }, + "retention": { + "title": "Lưu giữ dữ liệu kiểm tra", + "description": "Đặt thời gian lưu giữ dữ liệu kiểm tra trước khi tự động dọn dẹp.", + "option": { + "days": { + "label": "Thời gian lưu giữ (ngày)", + "unlimited": "Không giới hạn" + } + } + }, + "thresholds": { + "title": "Ngưỡng toàn cục", + "description": "Đặt ngưỡng CPU, Bộ nhớ, Ổ đĩa và Nhiệt độ toàn cục cho giám sát hạ tầng. Áp dụng cho tất cả trình giám sát phần cứng trừ khi bị ghi đè.", + "option": { + "cpu": { + "label": "Ngưỡng CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Ngưỡng bộ nhớ (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Ngưỡng ổ đĩa (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Ngưỡng nhiệt độ (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Cài đặt email", + "description": "Cấu hình cài đặt email cho hệ thống. Được sử dụng để gửi thông báo và cảnh báo.", + "descriptionTransport": "", + "descriptionTransportLink": "Xem thông số kỹ thuật tại đây", + "option": { + "host": { + "label": "Máy chủ email - Tên máy chủ hoặc địa chỉ IP để kết nối", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Cổng email - Cổng để kết nối", + "placeholder": "587" + }, + "address": { + "label": "Địa chỉ email - Dùng để xác thực", + "placeholder": "you@example.com" + }, + "user": { + "label": "Người dùng email - Tên đăng nhập để xác thực, ghi đè địa chỉ email nếu được chỉ định", + "placeholder": "Để trống nếu không cần" + }, + "password": { + "label": "Mật khẩu email - Mật khẩu để xác thực", + "labelSet": "Mật khẩu đã được đặt. Nhấn Đặt lại để thay đổi.", + "placeholder": "Nhập mật khẩu của bạn" + }, + "tlsServername": { + "label": "Tên máy chủ TLS - Tên máy chủ tùy chọn để xác thực TLS khi host là IP", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "Máy chủ kết nối email - Tên máy chủ dùng trong lời chào HELO/EHLO", + "placeholder": "localhost" + }, + "secure": { + "label": "Sử dụng SSL (khuyến nghị): Mã hóa kết nối bằng SSL/TLS" + }, + "pool": { + "label": "Bật gộp kết nối: Tái sử dụng kết nối hiện có để cải thiện hiệu suất" + }, + "ignoreTLS": { + "label": "Tắt STARTTLS: Không sử dụng TLS ngay cả khi máy chủ hỗ trợ" + }, + "requireTLS": { + "label": "Bắt buộc STARTTLS: Yêu cầu nâng cấp TLS, thất bại nếu không hỗ trợ" + }, + "rejectUnauthorized": { + "label": "Từ chối chứng chỉ không hợp lệ: Từ chối kết nối với chứng chỉ tự ký hoặc không đáng tin cậy" + } + } + }, + "demoMonitors": { + "title": "Trình giám sát mẫu", + "description": "Thêm trình giám sát mẫu cho mục đích trình diễn." + }, + "removeMonitors": { + "title": "Đặt lại hệ thống", + "description": "Xóa tất cả trình giám sát khỏi hệ thống.", + "dialog": { + "title": "Xóa tất cả trình giám sát?", + "description": "Không thể hoàn tác." + } + }, + "exportMonitors": { + "title": "Xuất trình giám sát" + }, + "importExportMonitors": { + "title": "Nhập / Xuất trình giám sát", + "description": "Nhập hoặc xuất dữ liệu trình giám sát dưới dạng tệp JSON để sao lưu hoặc chuyển giao." + }, + "about": { + "title": "Giới thiệu", + "developedBy": "Phát triển bởi Bluewave Labs" + }, + "validation": { + "errorMessage": "Vui lòng sửa các lỗi xác thực sau:" + } + } + }, + "statusPages": { + "deleteSuccess": "Đã xóa trang trạng thái thành công", + "fallback": { + "title": "Trang trạng thái dùng để:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "Tạo trang trạng thái!" + }, + "monitorsList": { + "chartTypeHeatmap": "Bản đồ nhiệt", + "chartTypeHistogram": "Biểu đồ cột", + "noData": "Không có dữ liệu", + "infrastructure": { + "title": "Hạ tầng", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Bộ nhớ", + "disk": "Ổ đĩa", + "usage": "Sử dụng", + "used": "Đã dùng", + "total": "Tổng" + }, + "uptime": { + "title": "Thời gian hoạt động", + "responseTime": "Thời gian phản hồi" + } + }, + "statusBar": { + "allDown": "Tất cả hệ thống đều ngừng hoạt động", + "allUp": "Tất cả hệ thống đang hoạt động", + "degraded": "Một số hệ thống đang gặp sự cố", + "unknown": "Không thể xác định trạng thái hệ thống" + }, + "table": { + "headers": { + "name": "Tên trang trạng thái", + "url": "URL công khai" + }, + "published": "Đã xuất bản", + "unpublished": "Chưa xuất bản" + }, + "title": "Trang trạng thái", + "details": { + "empty": { + "title": "Chưa có gì ở đây", + "addMonitor": "Thêm trình giám sát để bắt đầu" + } + }, + "form": { + "access": { + "title": "Truy cập", + "description": "Nếu trang trạng thái đã sẵn sàng, bạn có thể đánh dấu là đã xuất bản.", + "option": { + "published": { + "name": "Đã xuất bản và hiển thị công khai" + } + } + }, + "basicInfo": { + "title": "Thông tin cơ bản", + "description": "Xác định tên công ty và tên miền phụ mà trang trạng thái trỏ đến.", + "option": { + "name": { + "label": "Tên công ty", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Địa chỉ trang trạng thái", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "Trình giám sát", + "description": "Chọn trình giám sát hiển thị trên trang trạng thái.", + "noMonitors": "Chưa chọn trình giám sát", + "option": { + "monitors": { + "label": "Chọn trình giám sát", + "placeholder": "Tìm kiếm và chọn trình giám sát..." + } + } + }, + "timezone": { + "title": "Múi giờ", + "description": "Chọn múi giờ hiển thị cho trang trạng thái.", + "option": { + "timezone": { + "label": "Múi giờ", + "placeholder": "Chọn múi giờ..." + } + } + }, + "appearance": { + "title": "Giao diện", + "description": "Xác định giao diện mặc định cho trang trạng thái công khai.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Màu thương hiệu" + } + } + }, + "features": { + "title": "Tính năng", + "description": "Cấu hình thông tin hiển thị trên trang trạng thái.", + "option": { + "showCharts": { + "label": "Hiển thị biểu đồ thời gian phản hồi" + }, + "showUptimePercentage": { + "label": "Hiển thị phần trăm thời gian hoạt động" + }, + "showAdminLoginLink": { + "label": "Hiển thị liên kết đăng nhập quản trị" + }, + "showInfrastructure": { + "label": "Hiển thị số liệu hạ tầng" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Tìm kiếm trình giám sát..." + } + }, + "table": { + "headers": { + "responseTime": "Thời gian phản hồi" + } + }, + "fallback": { + "actionButton": "Tạo trình giám sát!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "Trình giám sát hoạt động dùng để:" + } } } } diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json index 67b419445..d6580e851 100644 --- a/client/src/locales/zh-CN.json +++ b/client/src/locales/zh-CN.json @@ -1,1132 +1,1305 @@ { - "submit": "提交", - "title": "标题", - "distributedStatusHeaderText": "实时、真实设备覆盖", - "distributedStatusSubHeaderText": "在全球数百万设备的支持下,按全球区域、国家或城市查看系统性能", - "settingsDisabled": "禁用", - "settingsSuccessSaved": "设置保存成功", - "settingsFailedToSave": "保存设置失败", - "settingsStatsCleared": "统计数据已成功清除", - "settingsFailedToClearStats": "清除统计数据失败", - "settingsMonitorsDeleted": "已成功删除所有监视器", - "settingsFailedToDeleteMonitors": "删除所有监视器失败", - "starPromptTitle": "打星Checkmate", - "starPromptDescription": "在 GitHub 上查看最新版本,并帮助社区发展壮大", - "https": "HTTPS", - "http": "HTTP", - "monitor": "监控", - "aboutus": "关于我们", - "now": "现在", - "delete": "删除", - "configure": "配置", - "responseTime": "响应时间", - "ms": "毫秒", - "bar": "条", - "area": "区域", - "country": "国家", - "city": "城市", - "response": "响应", - "monitorStatusUp": "监控目标 {name} ({url}) 现在处于正常状态并正在响应", - "monitorStatusDown": "监控目标 {name} ({url}) 已下线且无响应", - "webhookSendSuccess": "Webhook通知发送成功", - "webhookSendError": "向 {platform} 发送 Webhook 通知时出错", - "webhookUnsupportedPlatform": "不支持的平台:{platform}", - "distributedRightCategoryTitle": "监控", - "distributedStatusServerMonitors": "服务器监控", - "distributedStatusServerMonitorsDescription": "监控相关服务器的状态", - "distributedUptimeCreateSelectURL": "在这里,您可以选择主机的URL以及监控类型。", - "distributedUptimeCreateChecks": "分布式正常运行时间创建检查", - "distributedUptimeCreateChecksDescription": "在添加站点之后,您随时可以增加或删除检查项。", - "distributedUptimeCreateIncidentNotification": "事件通知", - "distributedUptimeCreateIncidentDescription": "当发生事件时,通知用户。", - "distributedUptimeCreateAdvancedSettings": "高级设置", - "distributedUptimeDetailsNoMonitorHistory": "此监控器尚未有检查历史记录。", - "distributedUptimeDetailsStatusHeaderUptime": "运行时间:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "最后更新于", - "notifications": { - "enableNotifications": "启用 {{platform}} 通知", - "testNotification": "测试通知", - "addOrEditNotifications": "添加或修改通知方式", - "slack": { - "label": "Slack", - "description": "要启用 Slack 通知,请先创建一个 Slack 应用并启用传入的 Webhook。之后,只需在此处提供 Webhook URL 即可。", - "webhookLabel": "Webhook 网址", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "需要 Slack Webhook URL" - }, - "discord": { - "label": "Discord", - "description": "要通过 Discord 通知将数据从 Checkmate 发送到 Discord 频道,可以使用 Discord 的入站 WebHooks 功能。", - "webhookLabel": "Discord 接收网址", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "需要 Discord Webhook URL" - }, - "telegram": { - "label": "Telegram", - "description": "要启用 Telegram 通知,请使用 BotFather创建一个 Telegram 机器人。然后,获取 API 令牌和聊天 ID 并在此处填写。", - "tokenLabel": "你的机器人token", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "你的Chat ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "需要 Telegram 令牌和聊天 ID" - }, - "webhook": { - "label": "Webhooks", - "description": "你可以设置自定义的 Webhook,以便在事件发生时接收通知。", - "urlLabel": "Webhook URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "Webhook URL 是必填项" - }, - "testNotificationDevelop": "测试通知 2", - "integrationButton": "通知集成", - "testSuccess": "测试通知发送成功!", - "testFailed": "测试通知发送失败", - "unsupportedType": "不支持的通知类型", - "networkError": "发生网络错误", - "fallback": { - "title": "通知通道用于:", - "checks": [ - "提醒团队有关停机时间或性能问题", - "让工程师知道事件发生时", - "让管理员了解系统更改" - ], - "actionButton": "让我们创建您的第一个通知渠道!" - }, - "createButton": "创建通知渠道", - "createTitle": "通知渠道", - "create": { - "success": "通知创建成功", - "failed": "通知创建失败" - }, - "fetch": { - "success": "成功获取通知", - "failed": "无法获取通知" - }, - "delete": { - "success": "通知已成功删除", - "failed": "删除通知失败" - }, - "edit": { - "success": "通知已成功更新", - "failed": "无法更新通知" - }, - "test": { - "success": "测试通知发送成功", - "failed": "测试通知发送失败" - } - }, - "testLocale": "测试区域设置", - "add": "添加", - "monitors": "监视器", - "distributedUptimeStatusCreateStatusPage": "状态页", - "distributedUptimeStatusCreateStatusPageAccess": "访问", - "distributedUptimeStatusCreateStatusPageReady": "如果状态页面准备就绪,您可以标记为已发布。", - "distributedUptimeStatusBasicInfoHeader": "基础信息", - "distributedUptimeStatusBasicInfoDescription": "定义公司名称和状态页指向的子域名。", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "为您的状态页面上传一个 Logo", - "distributedUptimeStatusLogoUploadButton": "上传 Logo", - "distributedUptimeStatusStandardMonitorsHeader": "标准监视器", - "distributedUptimeStatusStandardMonitorsDescription": "将标准监视器附加到您的状态页面。", - "distributedUptimeStatusCreateYour": "创建您的", - "distributedUptimeStatusEditYour": "编辑您的", - "distributedUptimeStatusPublishedLabel": "发布并公开可见", - "distributedUptimeStatusCompanyNameLabel": "公司名称", - "distributedUptimeStatusPageAddressLabel": "您的状态页面地址", - "distributedUptimeStatus30Days": "30 天", - "distributedUptimeStatus60Days": "60 天", - "distributedUptimeStatus90Days": "90 天", - "distributedUptimeStatusPageNotSetUp": "未设置状态页面。", - "distributedUptimeStatusContactAdmin": "请联系您的管理员", - "distributedUptimeStatusPageNotPublic": "此状态页面不公开。", - "distributedUptimeStatusPageDeleteDialog": "你想删除这个状态页吗?", - "distributedUptimeStatusPageDeleteConfirm": "确定,删除状态页", - "distributedUptimeStatusPageDeleteDescription": "删除后将无法恢复您的状态页面。", - "distributedUptimeStatusDevices": "设备", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "分布式正常运行时间状态运行已完成", - "distributedUptimeStatusUptLogo": "分布式UptimeStatusUptLogo", - "incidentsTableNoIncidents": "无事件记录", - "incidentsTablePaginationLabel": "事件", - "incidentsTableMonitorName": "监控名称", - "incidentsTableStatus": "状态", - "incidentsTableDateTime": "日期&时间", - "incidentsTableStatusCode": "状态码", - "incidentsTableMessage": "信息", - "incidentsOptionsHeader": "事件:", - "incidentsOptionsHeaderFilterBy": "筛选:", - "incidentsOptionsHeaderFilterAll": "全部", - "incidentsOptionsHeaderFilterDown": "离线", - "incidentsOptionsHeaderFilterCannotResolve": "无法解析", - "incidentsOptionsHeaderShow": "显示:", - "incidentsOptionsHeaderLastHour": "过去一小时", - "incidentsOptionsHeaderLastDay": "过去一天", - "incidentsOptionsHeaderLastWeek": "过去一周", - "incidentsOptionsPlaceholderAllServers": "所有服务器", - "infrastructureCreateYour": "创建你的", - "infrastructureCreateGeneralSettingsDescription": "在这里您可以选择主机的 URL,以及连接到服务器代理时使用的友好名称和授权密钥。", - "infrastructureServerRequirement": "你正在监控的服务器必须运行在", - "infrastructureCustomizeAlerts": "自定义告警", - "infrastructureAlertNotificationDescription": "当阈值超过指定百分比时向用户(们)发送通知。", - "infrastructureCreateMonitor": "创建基础设施监控", - "infrastructureProtocol": "协议", - "infrastructureServerUrlLabel": "服务器 URL", - "infrastructureDisplayNameLabel": "显示名称", - "infrastructureAuthorizationSecretLabel": "授权密钥", - "gb": "GB", - "mb": "MB", - "mem": "内存", - "memoryUsage": "内存使用率", - "cpu": "CPU", - "cpuUsage": "CPU 使用率", - "cpuTemperature": "CPU 温度", - "diskUsage": "硬盘使用率", - "used": "已使用", - "total": "总共", - "cores": "核心", - "frequency": "频率", - "status": "状态", - "cpuPhysical": "CPU(物理核心)", - "cpuLogical": "CPU(逻辑核心)", - "cpuFrequency": "CPU 频率", - "avgCpuTemperature": "CPU 平均温度", - "memory": "内存", - "disk": "硬盘", - "uptime": "运行时间", - "os": "系统", - "host": "主机", - "actions": "动作", - "integrations": "继承", - "integrationsPrism": "将 Prism 连接到您喜欢的服务。", - "integrationsSlack": "集成Slack", - "integrationsSlackInfo": "集成SlackInfo", - "integrationsDiscord": "集成Discord", - "integrationsDiscordInfo": "集成Discord信息", - "integrationsZapier": "集成Zapier", - "integrationsZapierInfo": "集成ZapierInfo", - "commonSave": "保存", - "createYour": "创建你的", - "createMonitor": "创建监控", - "pause": "暂停", - "resume": "恢复", - "editing": "编辑中...", - "url": "网址", - "access": "访问", - "timezone": "时区", - "features": "特征", - "administrator": "管理员?", - "loginHere": "点击此处登录", - "displayName": "显示名称", - "urlMonitor": "监控的 URL", - "portToMonitor": "监控端口", - "websiteMonitoring": "网站监控", - "websiteMonitoringDescription": "使用 HTTP(s)监控您的网站或 API 端点。", - "pingMonitoring": "ping监控", - "pingMonitoringDescription": "检查您的服务器是否可用。", - "dockerContainerMonitoring": "Docker容器监控", - "dockerContainerMonitoringDescription": "检查你的 Docker 容器是否正在运行。", - "portMonitoring": "端口监控", - "portMonitoringDescription": "检查你的端口是否已打开。", - "createMaintenanceWindow": "创建维护窗口", - "createMaintenance": "创建维护", - "editMaintenance": "编辑维护", - "maintenanceWindowName": "维护窗口名称", - "friendlyNameInput": "友好名称", - "friendlyNamePlaceholder": "维护时间于 __:__ ,持续 ___ 分钟", - "maintenanceRepeat": "维护重复", - "maintenance": "维护", - "duration": "期间", - "addMonitors": "添加监视器", - "window": "窗口", - "cancel": "取消", - "message": "信息", - "low": "低的", - "high": "高的", - "statusCode": "状态码", - "date&Time": "日期&时间", - "type": "类型", - "statusPageName": "状态页名称", - "publicURL": "公共 URL", - "repeat": "重复", - "edit": "编辑", - "createA": "创建A", - "remove": "移除", - "maintenanceWindowDescription": "在维护时段内,所选监视器的所有监视都将暂停。不会执行任何网络检查,从而阻止触发任何状态更新或通知。您的显示器将冻结在其最后的已知状态,并且状态页面将显示维护指示器。维护时段结束后,监控会自动恢复,如果检测到问题,将触发警报。维护期不计入正常运行时间计算。", - "startTime": "开始时间", - "timeZoneInfo": "所有日期和时间均为 GMT+0 时区。", - "monitorsToApply": "监控器应用", - "nextWindow": "下一个窗口", - "notFoundButton": "未找到按钮", - "pageSpeedConfigureSettingsDescription": "您可以在此处选择主机的 URL 以及监视器类型。", - "monitorDisplayName": "监视器显示名称", - "whenNewIncident": "当新事件发生时", - "notifySMS": "通过短信通知(即将推出)", - "notifyEmails": "还通过电子邮件通知多个地址(即将推出)", - "seperateEmails": "单独的电子邮件", - "checkFrequency": "检查频率", - "matchMethod": "match方法", - "expectedValue": "预期值", - "deleteDialogTitle": "确定要删除此监视器吗?", - "deleteDialogDescription": "一旦删除,将无法检索此监视器。", - "pageSpeedMonitor": "页面速度监控器", - "shown": "显示", - "ago": "前", - "companyName": "公司名称", - "pageSpeedDetailsPerformanceReport": "值是估计值,可能会有所不同。", - "pageSpeedDetailsPerformanceReportCalculator": "页面速度详情性能报告计算器", - "checkingEvery": "检查每一个", - "statusPageCreateSettings": "如果您的状态页面已准备就绪,您可以将其标记为已发布。", - "basicInformation": "基础信息", - "statusPageCreateBasicInfoDescription": "定义公司名称和状态页面指向的子域。", - "statusPageCreateSelectTimeZoneDescription": "选择状态页面的显示时区。", - "statusPageCreateAppearanceDescription": "定义公共状态页面的默认外观。", - "statusPageCreateSettingsCheckboxLabel": "已发布并向公众可见", - "statusPageCreateBasicInfoStatusPageAddress": "您的状态页面地址", - "statusPageCreateTabsContent": "状态页服务器", - "statusPageCreateTabsContentDescription": "您可以将监视的任意数量的服务器添加到状态页面。您还可以重新排序它们以获得最佳观看体验。", - "statusPageCreateTabsContentFeaturesDescription": "在状态页上显示更多详细信息", - "showCharts": "显示图表", - "showUptimePercentage": "显示正常运行时间百分比", - "removeLogo": "移除Logo", - "statusPageStatus": "未设置公开状态页面。", - "statusPageStatusContactAdmin": "请联系您的管理员", - "statusPageStatusNotPublic": "此状态页面不公开。", - "statusPageStatusNoPage": "这里没有状态页面。", - "statusPageStatusServiceStatus": "服务状态", - "deleteStatusPage": "要删除此状态页面吗?", - "deleteStatusPageConfirm": "是,删除状态页", - "deleteStatusPageDescription": "一旦删除,您的状态页面将无法检索。", - "uptimeCreate": "期望值用于与响应结果进行匹配,匹配确定状态。", - "uptimeCreateJsonPath": "将根据响应 JSON 数据评估此表达式,并将结果用于与预期值进行匹配。看", - "uptimeCreateJsonPathQuery": "查询语言文档。", - "maintenanceTableActionMenuDialogTitle": "您真的想删除此维护窗口吗?", - "infrastructureEditYour": "编辑您的", - "infrastructureEditMonitor": "保存基础架构监视器", - "infrastructureMonitorCreated": "基础设施监控创建成功!", - "infrastructureMonitorUpdated": "基础设施监控更新成功!", - "errorInvalidTypeId": "提供的通知类型无效", - "errorInvalidFieldId": "提供的字段 ID 无效", - "inviteNoTokenFound": "未找到邀请令牌", - "pageSpeedWarning": "警告:您尚未添加 Google PageSpeed API 密钥。没有它,PageSpeed 监视器将无法运行。", - "pageSpeedLearnMoreLink": "点击这里", - "pageSpeedAddApiKey": "以添加您的 API 密钥。", - "update": "更新", - "invalidFileFormat": "不支持的文件格式!", - "invalidFileSize": "文件太大了!", - "ClickUpload": "点击上传", - "DragandDrop": "拖放", - "MaxSize": "最大尺寸", - "SupportedFormats": "支持的格式", - "FirstName": "名", - "LastName": "姓", - "EmailDescriptionText": "这是您当前的电子邮件地址 - 无法更改。", - "YourPhoto": "个人资料照片", - "PhotoDescriptionText": "这张照片将显示在您的个人资料页面中。", - "save": "保存", - "DeleteDescriptionText": "这将从服务器中删除帐户和所有关联数据。这是不可逆的。", - "DeleteAccountWarning": "删除您的帐户意味着您将无法再次登录,并且您的所有数据都将被删除。这是不可逆的。", - "DeleteWarningTitle": "真的删除这个帐户?", - "bulkImport": { - "title": "批量导入", - "selectFileTips": "选择要上传的 CSV 文件", - "selectFileDescription": "您可以下载我们的或示例", - "selectFile": "选择文件", - "parsingFailed": "解析失败", - "uploadSuccess": "监视器创建成功!", - "validationFailed": "验证失败", - "noFileSelected": "未选择文件", - "fallbackPage": "导入文件以批量上传服务器列表", - "invalidFileType": "文件类型无效", - "uploadFailed": "上传失败" - }, - "DeleteAccountTitle": "删除账户", - "DeleteAccountButton": "删除帐户", - "publicLink": "公共链接", - "maskedPageSpeedKeyPlaceholder": "*************************************", - "reset": "重置", - "ignoreTLSError": "忽略 TLS/SSL 错误", - "tlsErrorIgnored": "已忽略 TLS/SSL 错误", - "ignoreTLSErrorDescription": "忽略 TLS/SSL 错误并继续检查网站可用性", - "createNew": "新建", - "greeting": { - "prepend": "你好", - "append": "下午时间,尽情发挥!", - "overview": "以下是您的 {{type}} 监视器的概述。" - }, - "roles": { - "superAdmin": "超级管理员", - "admin": "管理", - "teamMember": "团队成员", - "demoUser": "演示用户" - }, - "teamPanel": { - "teamMembers": "团队成员", - "filter": { - "all": "全部", - "member": "成员" - }, - "inviteTeamMember": "邀请新团队成员", - "inviteNewTeamMember": "邀请新团队成员", - "inviteDescription": "当您添加新的团队成员时,他们将可以访问所有监视器。", - "email": "电子邮件", - "selectRole": "选择角色", - "inviteLink": "邀请链接", - "cancel": "取消", - "noMembers": "没有具有此角色的团队成员", - "getToken": "获取Token", - "emailToken": "电子邮件令牌", - "table": { - "name": "名字", - "email": "邮箱", - "role": "角色", - "created": "创建" - }, - "addTeamMember": { - "addMemberMenu": "添加团队成员", - "title": "注册新团队成员", - "description": "创建一个新用户并与他们共享凭据。此方法使成员能够立即访问所有监视器。", - "addButton": "添加成员" - }, - "register": "注册团队成员", - "registerToast": { - "success": "用户创建,安全地与成员共享凭据。", - "dbUserExists": "用户已存在。", - "unknownError": "发生未知错误。" - }, - "registerTeamMember": { - "title": "注册团队成员", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "请输入姓名", - "pattern": "名称只能包含字母、空格、撇号或连字符" - } - }, - "lastName": { - "errors": { - "empty": "请输入姓氏", - "pattern": "姓氏只能包含字母、空格、撇号或连字符" - } - }, - "email": { - "errors": { - "empty": "要继续,请输入电子邮件地址", - "invalid": "请重新检查输入的电子邮件地址的有效性" - } - }, - "role": { - "errors": { - "empty": "角色是必需的" - } - } - } - } - } - }, - "role": "角色", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "暂停", - "resumed": "恢复", - "active": "已激活" - }, - "menu": { - "uptime": "上线时间", - "pagespeed": "页面速度", - "infrastructure": "基础设施", - "incidents": "事件", - "statusPages": "状态页", - "maintenance": "维护", - "integrations": "集成", - "settings": "设置", - "support": "支持", - "discussions": "讨论", - "docs": "文档", - "changelog": "变更日志", - "profile": "用户配置", - "password": "密码", - "team": "团队", - "logOut": "登出", - "notifications": "通知", - "logs": "日志" - }, - "settingsEmailUser": "电子邮件用户 - 用于身份验证的用户名,如果指定,则覆盖电子邮件地址", - "state": "状态", - "statusBreadCrumbsStatusPages": "状态页", - "statusBreadCrumbsDetails": "详情", - "commonSaving": "保存中...", - "navControls": "控制", - "incidentsPageTitle": "事件页面标题", - "passwordPanel": { - "passwordChangedSuccess": "您的密码已成功更改。", - "passwordInputIncorrect": "您的密码输入错误。", - "currentPassword": "当前密码", - "enterCurrentPassword": "输入当前密码", - "newPassword": "新密码", - "enterNewPassword": "输入新密码", - "confirmNewPassword": "确认新密码", - "passwordRequirements": "新密码必须至少包含 8 个字符,并且必须至少包含一个大写字母、一个小写字母、一个数字和一个特殊字符。", - "saving": "保存..." - }, - "emailSent": "电子邮件已发送", - "failedToSendEmail": "电子邮件发送失败", - "settingsTestEmailSuccess": "设置测试电子邮件成功", - "settingsTestEmailFailed": "发送测试电子邮件失败", - "settingsTestEmailFailedWithReason": "发送测试电子邮件失败: {{reason}}", - "settingsTestEmailUnknownError": "未知错误", - "statusMsg": { - "paused": "监视已暂停。", - "up": "您的网站已上线。", - "down": "您的网站已下线。", - "pending": "等待中..." - }, - "uptimeGeneralInstructions": { - "http": "输入要监控的 URL 或 IP(例如.. https://example.com/ or 192.168.1.100)并添加仪表板上显示的清晰显示名称。", - "ping": "输入要 ping 的 IP 地址或主机名(例如.. 192.168.1.100 或 example.com),并添加显示在仪表板上的清晰显示名称。", - "docker": "码输入 Docker 容器名称或 ID。您可以使用容器名称(例如.. my-app)或容器 ID(完整的 64 字符 ID 或短 ID)。", - "port": "输入服务器的 URL 或 IP、端口号和仪表板上显示的清晰显示名称。", - "game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。", - "https": "" - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "捕获", - "buttons": { - "toggleTheme": "切换主题明&暗" + "auth": { + "roles": { + "admin": "管理员", + "demo": "演示", + "superadmin": "超级管理员", + "user": "用户" + } }, - "toasts": { - "networkError": "网络错误", - "checkConnection": "请检查你的网络连接", - "unknownError": "未知错误" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "继续", - "back": "返回" + "alerts": { + "pageSpeedApiKey": { + "content": "警告:您尚未添加 Google PageSpeed API 密钥。请访问设置添加。没有密钥,PageSpeed 监控将无法运行。" + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "详情", + "home": "首页" + }, + "buttons": { + "addMember": "添加成员", + "cancel": "取消", + "close": "关闭", + "configure": "配置", + "confirm": "确认", + "create": "创建", + "delete": "删除", + "generateToken": "生成令牌", + "incidents": "事件", + "inviteMember": "邀请成员", + "pause": "暂停", + "resume": "恢复", + "save": "保存", + "sendInvite": "发送邀请", + "test": "测试", + "testNotifications": "测试通知", + "toggleTheme": "切换主题明&暗", + "flushQueue": "清空队列", + "notFound": "前往主控制台", + "resetPassword": "重置密码", + "reset": "重置", + "clear": "清除", + "addDemo": "添加演示监控", + "removeMonitors": "移除监控", + "sendTestEmail": "发送测试邮件", + "exportToJSON": "导出为 JSON", + "importFromJSON": "从 JSON 导入", + "clearFilters": "清除筛选", + "removeUser": "移除用户" + }, + "charts": { + "labels": { + "averageResponseTime": "平均响应时间", + "downtime": "停机时间", + "high": "高的", + "low": "低的", + "uptime": "正常运行时间" }, - "inputs": { - "email": { - "label": "电子邮件", - "placeholder": "jordan.ellis@domain.com", - "errors": { - "empty": "要继续,请输入您的电子邮件地址", - "invalid": "请重新检查输入的电子邮件地址的有效性" - } - }, - "password": { - "label": "密码", - "rules": { - "length": { - "beginning": "必须至少", - "highlighted": "8 个字符长" - }, - "special": { - "beginning": "必须至少包含", - "highlighted": "一个特殊字符" - }, - "number": { - "beginning": "必须至少包含", - "highlighted": "一个数字" - }, - "uppercase": { - "beginning": "必须至少包含", - "highlighted": "一个大写字母" - }, - "lowercase": { - "beginning": "必须至少包含", - "highlighted": "一个小写字母" - }, - "match": { - "beginning": "密码", - "highlighted": "必须匹配" - } - }, - "errors": { - "empty": "请输入您的密码", - "length": "密码长度必须至少为 8 个字符", - "uppercase": "密码必须至少包含 1 个大写字母", - "lowercase": "密码必须至少包含 1 个小写字母", - "number": "密码必须至少包含 1 个数字", - "special": "密码必须至少包含 1 个特殊字符", - "incorrect": "您提供的密码与我们的记录不符" - } - }, - "passwordConfirm": { - "label": "确认密码", - "placeholder": "重新输入密码以确认", - "errors": { - "empty": "请再次输入您的密码以进行确认(有助于解决拼写错误)", - "different": "输入的密码不匹配,因此其中一个密码可能输入错误" - } - }, - "firstName": { - "label": "名字", - "placeholder": "Jordan", - "errors": { - "empty": "请输入您的姓名", - "length": "姓名必须少于 50 个字符", - "pattern": "名称只能包含字母、空格、撇号或连字符" - } - }, - "lastName": { - "label": "姓", - "placeholder": "Ellis", - "errors": { - "empty": "请输入您的姓氏", - "length": "姓氏必须少于 50 个字符", - "pattern": "姓氏只能包含字母、空格、撇号或连字符" + "histogram": { + "avg": "平均: {{value}} ms", + "max": "最大: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "此操作无法撤销。", + "title": "确定要删除吗?" + } + }, + "labels": { + "active": "活跃", + "paused": "已暂停", + "na": "不适用", + "resolved": "已解决", + "responseTime": "响应时间" + }, + "form": { + "role": { + "option": { + "role": { + "label": "角色" } } }, - "errors": { - "validation": "验证数据时出错。" - }, - "fields": { - "password": { - "errors": { - "incorrect": "您提供的密码与我们的记录不符" + "name": { + "option": { + "firstName": { + "label": "名字", + "placeholder": "Jordan" + }, + "lastName": { + "label": "姓", + "placeholder": "Ellis" } - }, - "role": { - "errors": { - "min": "必须至少设置一个角色" + } + }, + "email": { + "option": { + "email": { + "label": "邮箱", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "登录以继续", - "subheadings": { - "stepOne": "输入您的电子邮件", - "stepTwo": "输入您的密码" - }, - "links": { - "forgotPassword": "忘记密码?", - "register": "没有帐户?", - "forgotPasswordLink": "重置密码", - "registerLink": "注册链接" - }, - "toasts": { - "success": "欢迎回来!您已成功登录。", - "incorrectPassword": "密码错误" - }, + "table": { + "empty": "暂无数据", + "headers": { + "actions": "动作", + "dateTime": "日期和时间", + "message": "消息", + "monitor": "监控", + "monitorId": "监控 ID", + "name": "名称", + "status": "状态", + "type": "类型", + "url": "URL", + "interval": "间隔", + "active": "活跃", + "responseTime": "响应时间" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "点击上传", + "dragAndDrop": "拖放", + "supportedFormats": "支持的格式", + "maxSize": "最大尺寸", + "orDragAndDrop": "或拖放文件", "errors": { - "password": { - "incorrect": "您提供的密码与我们的记录不符" - } - }, - "welcome": "欢迎回到 Checkmate!" + "invalidFileSize": "文件太大了!", + "invalidFileFormat": "不支持的文件格式!" + } }, - "registration": { - "heading": { - "superAdmin": "创建超级管理员", - "user": "注册" - }, - "subheadings": { - "stepOne": "输入您的个人详细信息", - "stepTwo": "输入您的电子邮件", - "stepThree": "创建密码" + "headerStatusPageControls": { + "publicLink": "公共链接" + }, + "headerTimeRange": { + "labels": { + "day": "日", + "month": "月", + "recent": "最近", + "week": "周" }, "description": { - "superAdmin": "创建超级用户帐户以开始使用", - "user": "注册为用户并请求超级管理员访问您的监视器" - }, - "gettingStartedButton": { - "superAdmin": "创建超级管理员帐户", - "user": "注册普通用户" - }, - "termsAndPolicies": "创建帐户即表示您同意我们的服务条款和隐私政策 。", - "links": { - "login": "已拥有帐号? 登录" - }, - "toasts": { - "success": "欢迎!您的账号已成功创建。" - }, - "welcome": "欢迎来到Checkmate!" - }, - "forgotPassword": { - "heading": "忘记密码?", - "subheadings": { - "stepOne": "不用担心,我们会向您发送重置说明。", - "stepTwo": "我们已发送密码重置链接到 ", - "stepThree": "您的新密码必须与以前使用的密码不同。", - "stepFour": "您的密码已成功重置。点击下方登录。" - }, - "buttons": { - "openEmail": "打开电子邮件应用", - "resetPassword": "重置密码" - }, - "imageAlts": { - "passwordKey": "密码图标", - "email": "电子邮件图标", - "lock": "锁定图标", - "passwordConfirm": "密码确认图标" - }, - "links": { - "login": "返回到登录", - "resend": "没有收到电子邮件? 点击重新发送" - }, - "toasts": { - "sent": "指令发送至 .", - "emailNotFound": "未找到电子邮件。", - "redirect": "将在秒后重定向...", - "success": "您的密码已成功重置。", - "error": "无法重置密码。请稍后重试或联系支持人员。" + "recent": "显示过去 2 小时的统计数据。", + "day": "显示过去 24 小时的统计数据。", + "week": "显示过去 7 天的统计数据。", + "month": "显示过去 30 天的统计数据。" } - } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "已成功重新连接到服务器。", - "stillUnreachable": "服务器仍然无法访问。请稍后重试。" + }, + "offlineBanner": { + "serverUnreachable": "无法连接到服务器", + "retry": "重试", + "retrying": "重试中...", + "reconnected": "已成功重新连接到服务器。" + }, + "sidebar": { + "menu": { + "uptime": "上线时间", + "pagespeed": "页面速度", + "infrastructure": "基础设施", + "notifications": "通知", + "checks": "检查", + "incidents": "事件", + "statusPages": "状态页", + "maintenance": "维护", + "logs": "日志", + "settings": "设置" }, - "alertBox": "服务器连接错误", - "description": "我们无法连接到服务器。如果问题仍然存在,请检查您的互联网连接或验证您的部署配置。", - "retryButton": { - "default": "尝试重连", - "processing": "连接中..." + "bottomMenu": { + "support": "支持", + "discussions": "讨论", + "docs": "文档", + "changelog": "变更日志" + }, + "accountMenu": { + "profile": "个人资料", + "password": "密码", + "team": "团队" + }, + "starPrompt": { + "title": "Star Checkmate", + "description": "查看最新版本并在 GitHub 上帮助发展社区" + }, + "authFooter": { + "navControls": "控制", + "logOut": "登出", + "roles": { + "superAdmin": "超级管理员", + "admin": "管理员", + "user": "用户", + "demoUser": "演示用户" + } } + }, + "starPrompt": { + "title": "Star Checkmate", + "description": "查看最新版本并在 GitHub 上帮助发展社区" } }, - "createNotifications": { - "title": "创建通知通道", - "nameSettings": { - "title": "名字", - "description": "集成的描述性名称。", - "nameLabel": "名字", - "namePlaceholder": "例如.. Slack 通知" + "pages": { + "notFound": { + "title": "哦不!你的寿司掉了!", + "subtitle": "该 URL 不存在,或者您没有访问权限。" }, - "typeSettings": { - "title": "类型", - "description": "选择要创建的通知通道类型。", - "typeLabel": "类型" - }, - "emailSettings": { - "title": "邮箱", - "description": "目标电子邮件地址。", - "emailLabel": "电子邮件地址", - "emailPlaceholder": "例如.. john@example.com" - }, - "slackSettings": { - "title": "Slack", - "description": "在此处配置您的 Slack Webhook", - "webhookLabel": "Slack webhook 网址", - "webhookPlaceholder": "https://hooks.slack.com/services/..." - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "在此处配置您的 PagerDuty 集成", - "integrationKeyLabel": "集成密钥", - "integrationKeyPlaceholder": "1234567890" - }, - "discordSettings": { - "title": "Discord", - "description": "在此处配置您的 Discord Webhook", - "webhookLabel": "Discord Webhook 网址", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "webhookSettings": { - "title": "Webhook", - "description": "在此处配置 Webhook", - "webhookLabel": "Webhook网址", - "webhookPlaceholder": "https://your-server.com/webhook" - }, - "testNotification": "测试通知", - "dialogDeleteTitle": "你确认要删除此通知吗?", - "dialogDeleteConfirm": "删除" - }, - "notificationConfig": { - "title": "通知", - "description": "选择要使用的通知渠道" - }, - "monitorStatus": { - "checkingEvery": "每隔 {{interval}} 检查一次", - "withCaptureAgent": "使用 Capture 代理 {{version}}", - "up": "在线", - "down": "离线", - "paused": "暂停" - }, - "advancedMatching": "高级匹配", - "sendTestNotifications": "发送测试通知", - "selectAll": "全选", - "showAdminLoginLink": "显示“管理员?在此处登录“状态页面上的链接", - "logsPage": { - "title": "日志", - "description": "系统日志 - 最后 1000 行", - "tabs": { - "queue": "作业队列", - "logs": "服务器日志", - "diagnostics": "诊断" - }, - "toast": { - "fetchLogsSuccess": "日志获取成功" - }, - "logLevelSelect": { - "title": "日志等级", - "values": { - "all": "全部", - "info": "信息", - "warn": "警告", - "error": "错误", - "debug": "调试" + "account": { + "tabs": { + "profile": "个人资料", + "password": "密码", + "team": "团队" + }, + "form": { + "name": { + "title": "名称", + "description": "更新您的个人信息" + }, + "photo": { + "title": "个人头像", + "description": "上传个人头像" + }, + "currentPassword": { + "title": "当前密码", + "description": "输入当前密码以验证您的身份", + "option": { + "label": "当前密码", + "placeholder": "输入当前密码" + } + }, + "newPassword": { + "title": "新密码", + "description": "请选择至少 8 个字符的强密码", + "option": { + "newPassword": { + "label": "新密码", + "placeholder": "输入新密码" + }, + "confirm": { + "label": "确认密码", + "placeholder": "确认新密码" + } + } + }, + "deleteAccount": { + "title": "删除账户", + "description": "此操作是永久的,无法撤销" + } + }, + "team": { + "filter": { + "placeholder": "按角色筛选", + "all": "全部", + "admin": "管理员", + "member": "成员" + }, + "table": { + "headers": { + "email": "邮箱", + "role": "角色", + "created": "创建" + } + }, + "addMember": { + "title": "注册新团队成员", + "description": "创建新用户账户。创建后请安全地分享凭据。" + }, + "invite": { + "title": "邀请团队成员", + "description": "发送邀请以加入您的团队", + "email": { + "label": "邮箱地址", + "placeholder": "输入邮箱地址" + }, + "role": { + "label": "角色", + "placeholder": "选择角色" + }, + "linkLabel": "邀请链接" + } } - } - }, - "queuePage": { - "title": "队列", - "refreshButton": "刷新", - "flushButton": "刷新队列", - "jobTable": { - "title": "当前在队列中的作业", - "idHeader": "监视器 ID", - "urlHeader": "网址", - "typeHeader": "类型", - "activeHeader": "有效", - "lockedAtHeader": "锁定在", - "runCountHeader": "运行计数", - "failCountHeader": "失败计数", - "lastRunHeader": "上次运行时间", - "lastFinishedAtHeader": "最后完成时间", - "lastRunTookHeader": "上次运行花费了", - "intervalHeader": "间隔" }, - "metricsTable": { - "title": "队列指标", - "metricHeader": "度量", - "valueHeader": "值" + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "必须至少", + "highlighted": "8 个字符长" + }, + "lowercase": { + "beginning": "必须至少包含", + "highlighted": "一个小写字母" + }, + "match": { + "beginning": "密码", + "highlighted": "必须匹配" + }, + "number": { + "beginning": "必须至少包含", + "highlighted": "一个数字" + }, + "special": { + "beginning": "必须至少包含", + "highlighted": "一个特殊字符" + }, + "uppercase": { + "beginning": "必须至少包含", + "highlighted": "一个大写字母" + } + }, + "form": { + "option": { + "email": { + "label": "邮箱", + "placeholder": "me@example.com" + }, + "password": { + "label": "密码", + "placeholder": "********" + }, + "confirmPassword": { + "label": "确认密码" + } + } + } + }, + "login": { + "title": "欢迎回到 Checkmate!", + "subtitle": "登录以继续", + "submit": "登录", + "links": { + "forgotPassword": { + "text": "忘记密码?", + "linkText": "重置密码" + }, + "register": { + "text": "还没有账户?", + "linkText": "在此注册" + } + } + }, + "register": { + "title": "欢迎回到 Checkmate!", + "subtitle": "注册以开始使用", + "submit": "注册" + }, + "forgotPassword": { + "title": "忘记密码了?", + "subtitle": "别担心,我们会给您发送重置说明。", + "submit": "请求恢复", + "links": { + "login": { + "text": "返回到", + "linkText": "登录" + } + } + }, + "setNewPassword": { + "title": "重置您的密码", + "subtitle": "您的新密码必须与之前使用过的密码不同。" + } }, - "failedJobTable": { - "title": "失败的作业", - "monitorIdHeader": "监视器ID", - "monitorUrlHeader": "监控网址", - "failCountHeader": "失败计数", - "failedAtHeader": "上次失败时于", - "failReasonHeader": "失败原因" - } - }, - "export": { - "title": "导出监视器", - "success": "监视器导出成功!", - "failed": "导出监视器失败" - }, - "monitorActions": { - "title": "导出/导入", - "import": "导入监视器", - "export": "导出监视器", - "deleteSuccess": "监视项删除成功", - "deleteFailed": "删除监视项失败", - "details": "详情" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "由 Bluewave Labs 开发", - "labelVersion": "版本", - "title": "关于" + "checks": { + "selects": { + "monitor": { + "all": "所有监控" + }, + "status": { + "all": "全部", + "down": "宕机", + "up": "正常" + } + }, + "table": { + "empty": "此时间范围内没有宕机检查", + "headers": { + "statusCode": "状态码", + "location": "位置" + } + } }, - "demoMonitorsSettings": { - "buttonAddMonitors": "添加演示监视器", - "description": "添加示例监视器以进行演示。", - "title": "示例监视器" + "common": { + "monitors": { + "actions": { + "configure": "配置", + "delete": "删除", + "generateToken": "生成令牌", + "details": "详情", + "incidents": "事件", + "inviteMember": "邀请成员", + "openSite": "打开站点", + "pause": "暂停", + "resume": "恢复" + }, + "statBoxes": { + "activeFor": "已活跃", + "certificateExpiry": "证书到期", + "lastCheck": "上次检查", + "lastResponseTime": "上次响应时间" + }, + "status": { + "down": "宕机", + "breached": "超出阈值", + "initializing": "初始化中", + "maintenance": "维护中", + "paused": "已暂停", + "total": "总计", + "up": "正常" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "游戏", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "端口", + "optionPagespeed": "PageSpeed", + "optionHardware": "基础设施", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } }, - "emailSettings": { - "buttonSendTestEmail": "发送测试电子邮件", - "description": "配置系统的电子邮件设置。这用于发送通知和警报。", - "descriptionTransport": "这将为 NodeMailer 构建 SMTP 传输", - "labelAddress": "电子邮件地址 - 用于身份验证", - "labelConnectionHost": "电子邮件连接主机 - 要在 HELO/EHLO 问候语中使用的主机名", - "labelHost": "电子邮件主机 - 要连接到的主机名或 IP 地址", - "labelIgnoreTLS": "禁用 STARTTLS:即使服务器支持 TLS,也不要使用 TLS", - "labelPassword": "电子邮件密码 - 身份验证密码", - "labelPasswordSet": "已设置密码。单击重置以更改它。", - "labelPool": "启用连接池:重用现有连接以提高性能", - "labelPort": "电子邮件端口 - 要连接的端口", - "labelRejectUnauthorized": "拒绝无效证书:拒绝具有自签名或不受信任的证书的连接", - "labelRequireTLS": "强制 STARTTLS:需要 TLS 升级,如果不支持则失败", - "labelSecure": "使用 SSL(推荐):使用 SSL/TLS 加密连接", - "labelTLSServername": "TLS 服务器名称 - 当主机是 IP 时,用于 TLS 验证的可选主机名", - "labelUser": "电子邮件用户 - 用于身份验证的用户名,如果指定,则覆盖电子邮件地址", - "linkTransport": "在此处查看规格", - "placeholderUser": "如果不需要,请留空", - "title": "电子邮件", - "toastEmailRequiredFieldsError": "需要输入电子邮件地址、主机、端口和密码" + "createMonitor": { + "form": { + "advanced": { + "description": "高级用例的可选设置", + "option": { + "advancedMatching": { + "label": "使用高级匹配" + }, + "expectedValue": { + "label": "期望值" + }, + "jsonPath": { + "description": "此表达式将针对响应 JSON 数据进行计算,结果将用于与期望值进行匹配。查询语言文档请参见 jmespath.org。", + "label": "JSONPath 表达式" + }, + "matchMethod": { + "label": "匹配方法", + "equal": "相等", + "include": "包含", + "regex": "正则表达式" + } + }, + "title": "高级设置" + }, + "frequency": { + "description": "您希望多久检查一次此监控的状态?", + "option": { + "frequency": { + "label": "检查频率", + "value": { + "fifteenMinutes": "15 分钟", + "fifteenSeconds": "15 秒", + "fiveMinutes": "5 分钟", + "fourMinutes": "4 分钟", + "oneMinute": "1 分钟", + "tenMinutes": "10 分钟", + "thirtyMinutes": "30 分钟", + "thirtySeconds": "30 秒", + "threeMinutes": "3 分钟", + "twoMinutes": "2 分钟" + } + } + }, + "title": "检查频率" + }, + "general": { + "option": { + "container": { + "label": "容器名称/ID", + "placeholder": "my-app 或 abcd1234" + }, + "host": { + "label": "主机", + "placeholder": "192.168.1.100 或 example.com" + }, + "name": { + "label": "显示名称", + "placeholder": "例如 My Website" + }, + "secret": { + "label": "授权密钥", + "placeholder": "输入您的密钥" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "丢包监控网址", + "placeholder": "localhost" + }, + "port": { + "label": "要监控的端口", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "服务名称", + "placeholder": "例如 my.service.v1(留空则检查整体健康状态)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "常规设置", + "description": { + "http": "输入要监控的 URL 或 IP(例如 https://example.com/ 或 192.168.1.100),并添加显示在仪表板上的清晰显示名称。", + "ping": "输入要 ping 的 IP 地址或主机名(例如.. 192.168.1.100 或 example.com),并添加显示在仪表板上的清晰显示名称。", + "port": "输入服务器的 URL 或 IP、端口号和仪表板上显示的清晰显示名称。", + "docker": "码输入 Docker 容器名称或 ID。您可以使用容器名称(例如.. my-app)或容器 ID(完整的 64 字符 ID 或短 ID)。", + "game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。", + "pagespeed": "跟踪网站的页面加载性能、Core Web Vitals 和优化评分。", + "grpc": "输入 gRPC 服务器的主机名和端口,可选指定健康检查的服务名称,并添加显示名称。", + "websocket": "输入要监控的 WebSocket URL(例如 wss://example.com/socket),并添加显示名称。", + "hardware": "监控基础设施的 CPU、内存、磁盘使用率和温度。" + } + }, + "ignoreTls": { + "description": "配置 HTTPS 连接的 TLS/SSL 证书验证。", + "option": { + "tls": { + "label": "忽略 TLS/SSL 错误" + } + }, + "title": "TLS/SSL 设置" + }, + "incidents": { + "description": "滑动窗口用于判断监控何时宕机。监控的状态只有在滑动窗口中的检查百分比达到指定值时才会改变。", + "option": { + "checks": { + "label": "滑动窗口中的检查次数" + }, + "percentage": { + "label": "滑动窗口中多少百分比的检查失败/成功后监控状态才会改变?" + } + }, + "title": "事件" + }, + "notifications": { + "description": "选择您要使用的通知渠道", + "title": "通知" + }, + "type": { + "description": "选择要执行的检查类型", + "optionDockerDescription": "使用 Docker 监控容器是否正在运行。", + "optionGameDescription": "监控特定游戏服务器是否在线。", + "optionGrpcDescription": "使用标准健康检查协议监控 gRPC 服务。", + "optionWebSocketDescription": "监控 WebSocket 端点的连接健康状况和响应时间。", + "optionHttpDescription": "使用 HTTP(S) 监控您的网站或 API 端点。", + "optionPingDescription": "使用 ICMP Ping 监控服务器是否在线。", + "optionPortDescription": "监控服务器上的特定端口是否开放。", + "title": "类型" + }, + "thresholds": { + "title": "告警阈值", + "description": "定义此硬件监控触发告警的阈值。", + "option": { + "cpuThreshold": { + "label": "CPU 告警阈值 (%)" + }, + "memoryThreshold": { + "label": "内存告警阈值 (%)" + }, + "diskThreshold": { + "label": "磁盘告警阈值 (%)" + }, + "tempThreshold": { + "label": "温度告警阈值 (°C)" + } + } + }, + "geoChecks": { + "title": "地理分布式检查", + "description": "从多个地理位置运行检查,以监控全球可用性和性能。", + "option": { + "enabled": { + "label": "启用地理分布式检查" + }, + "locations": { + "label": "位置", + "placeholder": "选择位置", + "options": { + "EU": "欧洲", + "NA": "北美洲", + "AS": "亚洲", + "SA": "南美洲", + "AF": "非洲", + "OC": "大洋洲" + } + }, + "interval": { + "label": "检查间隔", + "value": { + "fiveMinutes": "5 分钟", + "tenMinutes": "10 分钟", + "fifteenMinutes": "15 分钟", + "thirtyMinutes": "30 分钟" + } + } + } + }, + "url": { + "title": "在状态页上监控 IP/URL", + "description": "在公共状态页上显示监控的 IP 地址或 URL。如果禁用,将仅显示监控名称以保护敏感信息。", + "option": { + "showURL": { + "label": "在状态页上显示 IP/URL", + "enabled": "已启用", + "disabled": "已禁用" + } + } + }, + "stats": { + "title": "监控历史", + "description": "清除您团队的所有监控历史和统计数据。此操作不可逆。", + "buttonText": "清除所有统计", + "dialog": { + "title": "清除所有监控历史?", + "description": "这将永久删除您团队的所有监控历史、统计数据和检查数据。此操作无法撤销。", + "confirm": "是的,清除所有统计" + } + } + } }, - "pageSpeedSettings": { - "description": "输入您的 Google PageSpeed API 密钥以启用 Google PageSpeed 监控。单击重置以更新密钥。", - "labelApiKeySet": "API 密钥已设置。单击重置以更改它。", - "labelApiKey": "ageSpeed API 密钥", - "title": "Google PageSpeed API 密钥" + "editUser": { + "form": { + "roles": { + "title": "角色", + "description": "为用户分配角色。可以选择多个角色。" + } + }, + "dialog": { + "removeUser": { + "title": "移除用户", + "content": "确定要将 {{name}} 从您的团队中移除吗?此操作无法撤销。" + } + } }, - "saveButtonLabel": "保存", - "statsSettings": { - "clearAllStatsButton": "清除所有统计", - "clearAllStatsDescription": "清除所有统计数据。这是不可逆转的。", - "clearAllStatsDialogConfirm": "是的,清除所有统计数据", - "clearAllStatsDialogDescription": "一旦删除,监控历史记录和统计信息将无法检索。", - "clearAllStatsDialogTitle": "你想清除所有的统计数据吗?", - "description": "定义要保留历史数据的时间。您还可以清除所有现有数据。", - "labelTTL": "要保留监控历史记录的天数。", - "labelTTLOptional": "0 表示无限", - "title": "查看历史记录" + "incidents": { + "dialog": { + "resolveIncident": { + "title": "解决事件", + "option": { + "comment": { + "label": "备注(可选)", + "placeholder": "添加关于解决方案的备注..." + } + } + }, + "details": { + "analysis": "事件分析", + "comment": "备注:", + "detailsLabel": "详情", + "downtime": "停机时间:", + "message": "消息:", + "monitor": "监控:", + "overview": "概览", + "resolutionDetails": "解决详情", + "resolutionType": "类型:", + "resolutionTypes": { + "automatic": "自动", + "manual": "手动" + }, + "resolve": "解决事件", + "resolvedAt": "解决于:", + "resolvedBy": "解决人:", + "startedAt": "开始于:", + "status": "状态:", + "statusCode": "状态码:", + "timeline": "时间线", + "title": "事件详情", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "所有监控", + "monitor": "监控", + "resolutionType": "解决类型", + "resolutionTypes": { + "manual": "手动", + "automatic": "自动", + "all": "全部" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "活跃事件", + "active_zero": "没有活跃事件", + "active_one": "{{count}} 个活跃事件", + "active_other": "{{count}} 个活跃事件" + }, + "incidentStats": { + "avgResolutionTime": "平均解决时间", + "mostAffectedMonitor": "受影响最大的监控", + "title": "事件统计", + "totalIncidents": "总事件数" + }, + "latestIncidents": { + "title": "最新事件" + } + }, + "table": { + "actions": { + "details": "详情", + "goToMonitor": "前往监控", + "resolveManually": "手动解决" + }, + "activeIncidents": "活跃事件", + "headers": { + "endTime": "结束时间", + "resolutionType": "解决类型", + "startTime": "开始时间", + "statusCode": "状态码" + }, + "resolvedIncidents": "已解决事件", + "status": { + "active": "活跃", + "resolved": "已解决" + } + } }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "移除所有监视项", - "description": "从系统中移除所有监视项。", - "dialogConfirm": "确定,移除所有监视项", - "dialogDescription": "移除后,无法恢复这些监视项。", - "dialogTitle": "你想移除所有的监视项吗?", - "title": "系统复位" - }, - "timezoneSettings": { - "description": "选择用于在整个应用程序中显示日期和时间时区。", - "label": "显示时区", - "title": "显示时区" - }, - "title": "设置", - "uiSettings": { - "description": "在亮色和暗色模式之间切换,或更改用户界面语言。", - "labelLanguage": "语言", - "labelTheme": "主题模式", - "title": "外观" - }, - "urlSettings": { - "description": "在公共状态页面上显示监控的 IP 地址或 URL。如果禁用,则仅显示监控名称以保护敏感信息。", - "label": "在状态页面显示 IP/URL", - "selectDisabled": "禁用", - "selectEnabled": "启用", - "title": "在状态页面上监控 IP/URL" - }, - "globalThresholds": { - "title": "全局阈值", - "description": "配置 CPU、内存、磁盘和温度阈值。如果提供了阈值,则会自动启用监控。" - } - }, - "statusPageCreate": { - "buttonSave": "保存" - }, - "incidentsOptionsHeaderFilterResolved": "已解决", - "settingsSave": "保存", - "statusPageCreateAppearanceTitle": "外观", - "confirmPassword": "确认密码", - "monitorHooks": { - "failureAddDemoMonitors": "未能添加示例监视器", - "successAddDemoMonitors": "成功添加示例监视器" - }, - "settingsAppearance": "外观", - "settingsDisplayTimezone": "显示时区", - "settingsGeneralSettings": "常规设置", - "incidentsOptionsHeaderTotalIncidents": "事件总数", - "statusPage": { - "deleteSuccess": "状态页面已成功删除", - "deleteFailed": "删除状态页面失败", - "createSuccess": "状态页创建成功", - "updateSuccess": "状态页更新成功", - "generalSettings": "常规设置", - "contents": "内容", - "fallback": { - "checks": [ - "实时监控和显示服务的运行状况", - "跟踪多个服务并共享其状态", - "让用户了解中断和性能" - ], - "title": "状态页用于:", - "actionButton": "让我们创建你的第一个状态页!" - } - }, - "testNotificationsDisabled": "此监视器没有通知设置。您需要通过单击‘配置’按钮来添加一个", - "incidentsTableResolvedAt": "解决于", - "incidentsTableActionResolve": "解决", - "checkHooks": { - "failureResolveOne": "未能解决事件。", - "failureResolveAll": "未能解决所有事件。", - "failureResolveMonitor": "故障排除监控事件失败。" - }, - "checkFormError": "请检查表单中的错误。", - "diagnosticsPage": { - "diagnosticDescription": "系统诊断", - "statsDescription": "系统统计数据", - "gauges": { - "heapAllocationTitle": "堆分配", - "heapAllocationSubtitle": "% 的可用内存占用率", - "heapUsageTitle": "堆使用情况", - "heapUsageSubtitle": "% 的可用内存占用率", - "heapUtilizationTitle": "堆利用率", - "heapUtilizationSubtitle": "% 的分配百分比", - "instantCpuUsageTitle": "当前 CPU 使用率", - "instantCpuUsageSubtitle": "% 的1秒CPU占用率" - }, - "stats": { - "eventLoopDelayTitle": "事件循环延迟", - "uptimeTitle": "运行时间", - "usedHeapSizeTitle": "已用堆内存大小", - "totalHeapSizeTitle": "总堆大小", - "osMemoryLimitTitle": "系统内存限制" - } - }, - "pageSpeedLighthouseAPI": "使用 Lighthouse PageSpeed API 监控您的网站", - "time": { - "threeMinutes": "3 分钟", - "fiveMinutes": "5 分钟", - "tenMinutes": "10 分钟", - "twentyMinutes": "20 分钟", - "oneHour": "1 小时", - "oneDay": "1 天", - "oneWeek": "1 周", - "fourMinutes": "4 分钟", - "oneMinute": "1 分钟", - "twoMinutes": "2 分钟", - "fifteenSeconds": "15 秒", - "thirtySeconds": "30 秒" - }, - "general": { - "noOptionsFound": "{{unit}} 未找到" - }, - "infrastructureMonitor": { - "fallback": { - "checks": [ - "追踪服务器性能表现", - "识别瓶颈并优化资源利用率", - "通过实时监控保障系统可靠性" - ], - "title": "基础设施监视器用于:", - "actionButton": "让我们创建你的第一个基础设施视器!" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": ["标记维护时段", "消除所有误解", "停止在维护窗口内发送警报"], - "title": "维护时段用于:", - "actionButton": "让我们创建您的第一个维护时段!" - } - }, - "pageSpeed": { - "fallback": { - "checks": ["报告页面的用户体验", "帮助分析网页速度", "就如何改进页面提出建议"], - "title": "PageSpeed 监视器用于:", - "actionButton": "让我们创建您的第一个 PageSpeed 监视器!" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [ - "检测网站或服务器是否在线及响应正常", - "向团队发送停机或性能问题警报", - "监控 HTTP 端点、Ping 测试、容器及端口状态", - "追踪历史运行时长与可靠性趋势" - ], - "title": "正常运行时间监视器用于:", - "actionButton": "让我们创建您的第一个正常运行时间监视器!" - } - }, - "editUserPage": { - "form": { - "email": "电子邮件", - "firstName": "名", - "lastName": "姓氏", - "role": "角色", - "save": "保存" - }, - "table": { - "actionHeader": "操作", - "roleHeader": "角色" - }, - "title": "编辑用户", - "toast": { - "successUserUpdate": "用户更新成功", - "validationErrors": "验证错误" - } - }, - "incidentsPageActionResolveMonitor": "解决监控事件", - "incidentsPageActionResolveAll": "解决所有事件", - "matchMethodOptions": { - "equal": "相等", - "equalPlaceholder": "成功", - "include": "包含", - "includePlaceholder": "OK", - "regex": "正则表达式", - "regexPlaceholder": "^(success|ok)$", - "text": "匹配方法" - }, - "monitorType": { - "docker": { - "label": "容器 ID", - "namePlaceholder": "我的容器", - "placeholder": "我的应用程序或 abcd1234" - }, - "http": { - "label": "要监控的 URL", - "namePlaceholder": "Google", - "placeholder": "google.com" - }, - "ping": { - "label": "要监控的 IP 地址", - "namePlaceholder": "Google", - "placeholder": "1.1.1.1" - }, - "port": { - "label": "要监控的 URL", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" - }, - "game": { - "label": "丢包监控网址", - "namePlaceholder": "localhost:5173", - "placeholder": "localhost" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "JSON 路径" - }, - "bytesPerSecond": "每秒字节数", - "bytesReceived": "已接收字节数", - "bytesSent": "已发送字节数", - "chooseGame": "选择游戏", - "createMonitorPage": { - "incidentConfigDescription": "滑动窗口用于确定监视器何时发生故障。仅当滑动窗口中的检查百分比满足指定值时,监视器的状态才会改变。", - "incidentConfigStatusWindowLabel": "在滑动窗口中应包含多少次检测?", - "incidentConfigStatusWindowThresholdLabel": "在监视器状态改变之前,滑动窗口中的检查失败/成功的百分比是多少?", - "incidentConfigTitle": "事件", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "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": "选择您要监控的特定磁盘。" + "charts": { + "labels": { + "cpu": "CPU 使用率", + "disk": "磁盘使用率", + "memory": "内存使用率", + "netBytesRecv": "{{name}} - 接收字节", + "netBytesSent": "{{name}} - 发送字节", + "temp": "温度" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "最大频率", + "title": "CPU 使用率", + "upperLabel": "当前频率" + }, + "disk": { + "lowerLabel": "可用", + "title": "磁盘 {{idx}} 使用率", + "upperLabel": "已使用" + }, + "memory": { + "lowerLabel": "可用", + "title": "内存使用率", + "upperLabel": "已使用" + } + }, + "statBoxes": { + "avgCpuTemperature": "CPU 平均温度", + "cpuFrequency": "CPU 频率", + "cpuLogical": "CPU(逻辑核心)", + "cpuPhysical": "CPU(物理核心)", + "disk": "磁盘", + "memory": "内存", + "os": "系统" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "磁盘", + "memory": "内存" + } + }, + "tabs": { + "labels": { + "network": "网络", + "overview": "概览" + } + }, + "fallback": { + "actionButton": "创建监控!", + "checks": "Track the performance of your servers,Identify bottlenecks and optimize usage,Ensure reliability with real-time monitoring", + "title": "基础设施监控用于:" + } + }, + "logs": { + "tabs": { + "diagnostics": "诊断", + "logs": "服务器日志", + "queue": "作业队列" + }, + "logLevelSelect": { + "label": "日志级别" + }, + "jobQueue": "作业队列", + "failedJobs": "失败作业", + "noLogs": "未找到日志", + "metrics": { + "jobs": "作业", + "activeJobs": "活跃作业", + "failingJobs": "失败中的作业", + "totalRuns": "总运行次数", + "totalFailures": "总失败次数" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "事件循环延迟", + "uptime": "正常运行时间", + "usedHeapSize": "已使用堆大小", + "totalHeapSize": "总堆大小", + "osMemoryLimit": "操作系统内存限制" + }, + "gauges": { + "heapAllocation": "堆分配", + "heapUsage": "堆使用率", + "heapUtilization": "堆利用率", + "instantCpuUsage": "即时 CPU 使用率", + "availableMemoryPercentage": "可用内存百分比", + "allocatedPercentage": "已分配百分比", + "usedSPercentage": "CPU 1 秒使用百分比", + "total": "总计", + "used": "已使用" + } + }, + "table": { + "headers": { + "timestamp": "时间戳", + "level": "级别", + "service": "服务", + "method": "方法", + "monitorId": "监控 ID", + "runCount": "运行次数", + "failCount": "失败次数", + "lastRunAt": "上次运行于", + "lockedAt": "锁定于", + "lastFinishedAt": "上次完成于", + "lastRunTook": "上次运行耗时", + "lastFailedAt": "上次失败于", + "failReason": "失败原因" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "创建维护窗口!", + "checks": "Mark your maintenance periods,Eliminate any misunderstandings,Stop sending alerts in maintenance windows", + "title": "维护窗口用于:" + }, + "table": { + "headers": { + "nextWindow": "下一个窗口", + "repeat": "重复" + } + }, + "form": { + "general": { + "title": "常规设置", + "description": "为维护窗口设置名称和重复选项。", + "option": { + "name": { + "label": "名称", + "placeholder": "例如 每周维护" + }, + "repeat": { + "label": "重复" + } + } + }, + "startDate": { + "title": "开始日期", + "description": "选择维护窗口的开始日期。", + "option": { + "startDate": { + "label": "开始日期" + } + } + }, + "startTime": { + "title": "开始时间", + "description": "设置维护窗口的开始时间和持续时间。所有时间均为 UTC", + "option": { + "duration": { + "label": "持续时间" + }, + "startTime": { + "label": "开始时间" + } + }, + "monitors": { + "title": "监控", + "description": "维护窗口应用的监控对象", + "option": { + "addMonitors": { + "label": "添加监控" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "创建渠道", + "checks": "Alert teams about downtime or performance issues,Let engineers know when incidents happen,Keep administrators informed of system changes", + "title": "通知渠道用于:" + }, + "form": { + "accessToken": { + "optionAccessToken": "访问令牌", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "通知将发送到的地址。", + "optionAddress": "地址", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "地址" + }, + "homeServer": { + "optionHomeServer": "主服务器", + "placeholder": "example.com" + }, + "matrix": { + "description": "配置 Matrix 主服务器连接以接收通知。", + "title": "Matrix 配置" + }, + "notificationName": { + "description": "通知渠道的描述性名称", + "optionName": "渠道名称", + "placeholder": "例如 生产告警", + "title": "渠道名称" + }, + "pagerDuty": { + "description": "您的 PagerDuty 集成密钥,用于接收告警。", + "optionIntegrationKey": "集成密钥", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "集成密钥" + }, + "roomId": { + "optionRoomId": "房间 ID", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "选择要创建的通知渠道类型。", + "optionType": "类型", + "title": "渠道类型" + }, + "telegram": { + "title": "Telegram 配置", + "description": "要启用 Telegram 通知,请使用 BotFather创建一个 Telegram 机器人。然后,获取 API 令牌和聊天 ID 并在此处填写。", + "optionBotToken": "机器人令牌", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "聊天 ID", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "目标" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "累积布局偏移 (CLS)", + "fcp": "首次内容绘制 (FCP)", + "lcp": "最大内容绘制 (LCP)", + "si": "速度指数 (SI)", + "tbt": "总阻塞时间 (TBT)" + }, + "legend": { + "title": "PageSpeed 报告", + "weight": "权重" + }, + "pie": { + "title": "性能报告" + } + }, + "table": { + "headers": { + "pageSpeedScore": "PageSpeed 评分" + } + }, + "fallback": { + "actionButton": "创建监控!", + "checks": "Report on the user experience of a page,Help analyze webpage speed,Give suggestions on how the page can be improved", + "title": "PageSpeed 监控用于:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "显示时区", + "description": "选择应用程序中用于显示日期和时间的时区。", + "option": { + "timezone": { + "label": "显示时区" + } + } + }, + "ui": { + "title": "外观", + "description": "在浅色和深色模式之间切换、更改语言或自定义图表显示类型。", + "option": { + "theme": { + "label": "主题模式", + "light": "浅色", + "dark": "深色" + }, + "language": { + "label": "语言" + }, + "chartType": { + "label": "图表类型", + "histogram": "柱状图", + "heatmap": "热力图" + } + } + }, + "pagespeed": { + "title": "Google PageSpeed API 密钥", + "description": "输入您的 Google PageSpeed API 密钥以启用 Google PageSpeed 监控。点击重置以更新密钥。", + "option": { + "apiKey": { + "label": "PageSpeed API 密钥", + "labelSet": "API 密钥已设置。点击重置以更改。", + "placeholder": "输入您的 Google PageSpeed API 密钥" + } + } + }, + "url": { + "title": "在状态页上监控 IP/URL", + "description": "在公共状态页上显示监控的 IP 地址或 URL。如果禁用,将仅显示监控名称以保护敏感信息。", + "option": { + "showURL": { + "label": "在状态页上显示 IP/URL", + "enabled": "已启用", + "disabled": "已禁用" + } + } + }, + "stats": { + "title": "监控历史", + "description": "清除您团队的所有监控历史和统计数据。此操作不可逆。", + "option": { + "clear": { + "label": "清除所有统计数据。此操作不可逆。" + } + }, + "dialog": { + "title": "清除所有监控历史?", + "description": "此操作无法撤销" + } + }, + "retention": { + "title": "检查数据保留", + "description": "设置检查数据在自动清理前保留多长时间。", + "option": { + "days": { + "label": "保留期限(天)", + "unlimited": "无限制" + } + } + }, + "thresholds": { + "title": "全局阈值", + "description": "设置基础设施监控的全局 CPU、内存、磁盘和温度阈值。除非被覆盖,否则适用于所有硬件监控。", + "option": { + "cpu": { + "label": "CPU 阈值 (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "内存阈值 (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "磁盘阈值 (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "温度阈值 (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "邮件设置", + "description": "配置系统的邮件设置。用于发送通知和告警。", + "descriptionTransport": "这将为 NodeMailer 构建 SMTP 传输", + "descriptionTransportLink": "在此查看规范", + "option": { + "host": { + "label": "邮件主机 - 要连接的主机名或 IP 地址", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "邮件端口 - 要连接的端口", + "placeholder": "587" + }, + "address": { + "label": "邮箱地址 - 用于身份验证", + "placeholder": "you@example.com" + }, + "user": { + "label": "邮件用户 - 用于身份验证的用户名,如果指定则覆盖邮箱地址", + "placeholder": "如不需要请留空" + }, + "password": { + "label": "邮件密码 - 用于身份验证的密码", + "labelSet": "密码已设置。点击重置以更改。", + "placeholder": "输入您的密码" + }, + "tlsServername": { + "label": "TLS 服务器名 - 当主机为 IP 时用于 TLS 验证的可选主机名", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "邮件连接主机 - 在 HELO/EHLO 问候中使用的主机名", + "placeholder": "localhost" + }, + "secure": { + "label": "使用 SSL(推荐):使用 SSL/TLS 加密连接" + }, + "pool": { + "label": "启用连接池:重用现有连接以提高性能" + }, + "ignoreTLS": { + "label": "禁用 STARTTLS:即使服务器支持也不使用 TLS" + }, + "requireTLS": { + "label": "强制 STARTTLS:要求 TLS 升级,不支持则失败" + }, + "rejectUnauthorized": { + "label": "拒绝无效证书:拒绝自签名或不受信任证书的连接" + } + } + }, + "demoMonitors": { + "title": "演示监控", + "description": "添加用于演示目的的示例监控。" + }, + "removeMonitors": { + "title": "系统重置", + "description": "从系统中移除所有监控。", + "dialog": { + "title": "移除所有监控?", + "description": "此操作无法撤销。" + } + }, + "exportMonitors": { + "title": "导出监控" + }, + "importExportMonitors": { + "title": "导入/导出监控", + "description": "将监控数据导入或导出为 JSON 文件,用于备份或迁移。" + }, + "about": { + "title": "关于", + "developedBy": "由 Bluewave Labs 开发" + }, + "validation": { + "errorMessage": "请修复以下验证错误:" + } + } + }, + "statusPages": { + "deleteSuccess": "状态页已成功删除", + "fallback": { + "title": "状态页用于:", + "checks": "Communicate system status to users and stakeholders,Display real-time uptime information publicly,Build trust with transparent service monitoring,Reduce support requests during incidents", + "actionButton": "创建状态页!" + }, + "monitorsList": { + "chartTypeHeatmap": "热力图", + "chartTypeHistogram": "柱状图", + "noData": "无可用数据", + "infrastructure": { + "title": "基础设施", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "内存", + "disk": "磁盘", + "usage": "使用率", + "used": "已使用", + "total": "总计" + }, + "uptime": { + "title": "正常运行时间", + "responseTime": "响应时间" + } + }, + "statusBar": { + "allDown": "所有系统已宕机", + "allUp": "所有系统运行正常", + "degraded": "部分系统正在出现问题", + "unknown": "无法确定系统状态" + }, + "table": { + "headers": { + "name": "状态页名称", + "url": "公共 URL" + }, + "published": "已发布", + "unpublished": "未发布" + }, + "title": "状态页", + "details": { + "empty": { + "title": "这里还没有内容", + "addMonitor": "添加监控以开始" + } + }, + "form": { + "access": { + "title": "访问", + "description": "如果您的状态页已准备就绪,可以将其标记为已发布。", + "option": { + "published": { + "name": "已发布且公众可见" + } + } + }, + "basicInfo": { + "title": "基本信息", + "description": "定义公司名称和状态页指向的子域名。", + "option": { + "name": { + "label": "公司名称", + "placeholder": "Acme Inc." + }, + "url": { + "label": "您的状态页地址", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "监控", + "description": "选择要在状态页上显示的监控。", + "noMonitors": "未选择监控", + "option": { + "monitors": { + "label": "选择监控", + "placeholder": "搜索并选择监控..." + } + } + }, + "timezone": { + "title": "时区", + "description": "选择状态页显示的时区。", + "option": { + "timezone": { + "label": "时区", + "placeholder": "选择时区..." + } + } + }, + "appearance": { + "title": "外观", + "description": "定义公共状态页的默认外观。", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "品牌颜色" + } + } + }, + "features": { + "title": "功能", + "description": "配置状态页上显示的信息。", + "option": { + "showCharts": { + "label": "显示响应时间图表" + }, + "showUptimePercentage": { + "label": "显示正常运行时间百分比" + }, + "showAdminLoginLink": { + "label": "显示管理员登录链接" + }, + "showInfrastructure": { + "label": "显示基础设施指标" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "搜索监控..." + } + }, + "table": { + "headers": { + "responseTime": "响应时间" + } + }, + "fallback": { + "actionButton": "创建监控!", + "checks": "Check if websites or servers are online & responsive,Alert teams about downtime or performance issues,Monitor HTTP endpoints, pings, containers & ports,Track historical uptime and reliability trends", + "title": "正常运行时间监控用于:" + } } } } diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json index e5ee58e58..16aca7ed8 100644 --- a/client/src/locales/zh-TW.json +++ b/client/src/locales/zh-TW.json @@ -1,1112 +1,1323 @@ { - "submit": "提交", - "title": "名稱", - "distributedStatusHeaderText": "實時、真實設備覆蓋", - "distributedStatusSubHeaderText": "由全球數百萬個設備提供支持,您可以按全球區域、國家或城市查看系統效能", - "settingsDisabled": "已停用", - "settingsSuccessSaved": "設定已成功儲存", - "settingsFailedToSave": "無法儲存設定", - "settingsStatsCleared": "統計數據已成功清除", - "settingsFailedToClearStats": "無法清除統計數據", - "settingsMonitorsDeleted": "已成功刪除所有監視器", - "settingsFailedToDeleteMonitors": "無法刪除所有監視器", - "starPromptTitle": "星標Checkmate", - "starPromptDescription": "查看最新版本發布及在 GitHub 上幫助社區成長", - "https": "HTTPS", - "http": "HTTP", - "monitor": "監控", - "aboutus": "關於我們", - "now": "現在", - "delete": "刪除", - "configure": "設定", - "responseTime": "回應時間", - "ms": "毫秒", - "bar": "條", - "area": "區域", - "country": "國家", - "city": "城市", - "response": "回應", - "monitorStatusUp": "", - "monitorStatusDown": "", - "webhookSendSuccess": "Webhook 通知發送成功", - "webhookSendError": "", - "webhookUnsupportedPlatform": "不支援的平台: {platform}", - "distributedRightCategoryTitle": "監控", - "distributedStatusServerMonitors": "伺服器監控", - "distributedStatusServerMonitorsDescription": "監控相關伺服器狀態", - "distributedUptimeCreateSelectURL": "你可以在這裡選擇主機的 URL,同時選擇監視器類型。", - "distributedUptimeCreateChecks": "要執行的檢查項目", - "distributedUptimeCreateChecksDescription": "你可以在新增網站後,隨時新增或移除檢查項目。", - "distributedUptimeCreateIncidentNotification": "事故通知", - "distributedUptimeCreateIncidentDescription": "事故發生時,向用戶發送通知。", - "distributedUptimeCreateAdvancedSettings": "進階設定", - "distributedUptimeDetailsNoMonitorHistory": "此監控尚無檢查記錄。", - "distributedUptimeDetailsStatusHeaderUptime": "正常運行時間:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "最後更新", - "notifications": { - "enableNotifications": "", - "testNotification": "測試通知", - "addOrEditNotifications": "新增或編輯通知", - "slack": { - "label": "Slack", - "description": "要啟用 Slack 通知,請建立一個 Slack 應用程式並啟用傳入 Webhook。完成後,請在此提供 Webhook URL 即可。", - "webhookLabel": "Webhook URL", - "webhookPlaceholder": "https://hooks.slack.com/services/...", - "webhookRequired": "Slack webhook URL is required" - }, - "discord": { - "label": "Discord", - "description": "", - "webhookLabel": "Discord Webhook URL", - "webhookPlaceholder": "https://discord.com/api/webhooks/...", - "webhookRequired": "" - }, - "telegram": { - "label": "Telegram", - "description": "要啟用 Telegram 通知,請使用 BotFather 用於創建和管理 Telegram 的官方機器來建立一個 Telegram 機器人。接著,取得 API 金鑰和聊天室 ID,並在此處填寫。", - "tokenLabel": "你的機器人金鑰", - "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", - "chatIdLabel": "你的聊天 ID", - "chatIdPlaceholder": "-1001234567890", - "fieldsRequired": "" - }, - "webhook": { - "label": "Webhooks", - "description": "你可以設定自訂 webhook,在事件發生時接收通知。", - "urlLabel": "Webhook URL", - "urlPlaceholder": "https://your-server.com/webhook", - "urlRequired": "" - }, - "testNotificationDevelop": "測試通知 2", - "integrationButton": "", - "testSuccess": "", - "testFailed": "", - "unsupportedType": "", - "networkError": "", - "fallback": { - "title": "", - "checks": [ - "提醒團隊有關停機或性能問題", - "讓工程師知道事件發生時的情況", - "讓管理員了解系統變更情況" - ], - "actionButton": "讓我們建立您的第一個通知頻道!" - }, - "createButton": "", - "createTitle": "", - "create": { - "success": "", - "failed": "" - }, - "fetch": { - "success": "", - "failed": "" - }, - "delete": { - "success": "", - "failed": "" - }, - "edit": { - "success": "", - "failed": "" - }, - "test": { - "success": "", - "failed": "" - } - }, - "testLocale": "測試語言環境", - "add": "新增", - "monitors": "", - "distributedUptimeStatusCreateStatusPage": "", - "distributedUptimeStatusCreateStatusPageAccess": "", - "distributedUptimeStatusCreateStatusPageReady": "", - "distributedUptimeStatusBasicInfoHeader": "基本資料", - "distributedUptimeStatusBasicInfoDescription": "", - "distributedUptimeStatusLogoHeader": "標誌", - "distributedUptimeStatusLogoDescription": "", - "distributedUptimeStatusLogoUploadButton": "上傳標誌", - "distributedUptimeStatusStandardMonitorsHeader": "", - "distributedUptimeStatusStandardMonitorsDescription": "", - "distributedUptimeStatusCreateYour": "", - "distributedUptimeStatusEditYour": "", - "distributedUptimeStatusPublishedLabel": "", - "distributedUptimeStatusCompanyNameLabel": "公司名稱", - "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "30日", - "distributedUptimeStatus60Days": "60日", - "distributedUptimeStatus90Days": "90日", - "distributedUptimeStatusPageNotSetUp": "", - "distributedUptimeStatusContactAdmin": "", - "distributedUptimeStatusPageNotPublic": "", - "distributedUptimeStatusPageDeleteDialog": "", - "distributedUptimeStatusPageDeleteConfirm": "", - "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "設備", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "", - "distributedUptimeStatusUptLogo": "", - "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", - "incidentsTableMonitorName": "", - "incidentsTableStatus": "", - "incidentsTableDateTime": "", - "incidentsTableStatusCode": "", - "incidentsTableMessage": "", - "incidentsOptionsHeader": "", - "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", - "incidentsOptionsHeaderFilterDown": "", - "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", - "incidentsOptionsHeaderLastHour": "", - "incidentsOptionsHeaderLastDay": "", - "incidentsOptionsHeaderLastWeek": "", - "incidentsOptionsPlaceholderAllServers": "", - "infrastructureCreateYour": "", - "infrastructureCreateGeneralSettingsDescription": "", - "infrastructureServerRequirement": "", - "infrastructureCustomizeAlerts": "", - "infrastructureAlertNotificationDescription": "", - "infrastructureCreateMonitor": "", - "infrastructureProtocol": "", - "infrastructureServerUrlLabel": "", - "infrastructureDisplayNameLabel": "", - "infrastructureAuthorizationSecretLabel": "", - "gb": "GB", - "mb": "MB", - "mem": "Mem", - "memoryUsage": "", - "cpu": "CPU", - "cpuUsage": "CPU 使用量", - "cpuTemperature": "CPU 溫度", - "diskUsage": "", - "used": "", - "total": "", - "cores": "核", - "frequency": "", - "status": "", - "cpuPhysical": "CPU (實體核心)", - "cpuLogical": "CPU (邏輯核心)", - "cpuFrequency": "CPU 頻率", - "avgCpuTemperature": "平均 CPU 溫度", - "memory": "", - "disk": "", - "uptime": "", - "os": "OS", - "host": "", - "actions": "行動", - "integrations": "", - "integrationsPrism": "", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "", - "integrationsDiscord": "", - "integrationsDiscordInfo": "", - "integrationsZapier": "", - "integrationsZapierInfo": "", - "commonSave": "儲存", - "createYour": "", - "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "UR:", - "access": "存取", - "timezone": "時區", - "features": "", - "administrator": "系統管理員?", - "loginHere": "", - "displayName": "", - "urlMonitor": "", - "portToMonitor": "", - "websiteMonitoring": "網站監控", - "websiteMonitoringDescription": "", - "pingMonitoring": "", - "pingMonitoringDescription": "", - "dockerContainerMonitoring": "", - "dockerContainerMonitoringDescription": "", - "portMonitoring": "", - "portMonitoringDescription": "", - "createMaintenanceWindow": "", - "createMaintenance": "", - "editMaintenance": "", - "maintenanceWindowName": "", - "friendlyNameInput": "", - "friendlyNamePlaceholder": "", - "maintenanceRepeat": "", - "maintenance": "", - "duration": "", - "addMonitors": "新增監視器", - "window": "", - "cancel": "取消", - "message": "", - "low": "低", - "high": "高", - "statusCode": "", - "date&Time": "日期與時間", - "type": "類型", - "statusPageName": "", - "publicURL": "", - "repeat": "", - "edit": "", - "createA": "", - "remove": "", - "maintenanceWindowDescription": "", - "startTime": "", - "timeZoneInfo": "", - "monitorsToApply": "", - "nextWindow": "", - "notFoundButton": "", - "pageSpeedConfigureSettingsDescription": "", - "monitorDisplayName": "", - "whenNewIncident": "", - "notifySMS": "", - "notifyEmails": "", - "seperateEmails": "", - "checkFrequency": "檢查頻率", - "matchMethod": "", - "expectedValue": "", - "deleteDialogTitle": "您確定要刪除此監視器嗎?", - "deleteDialogDescription": "一旦刪除,該監視器無法恢復。", - "pageSpeedMonitor": "", - "shown": "", - "ago": "", - "companyName": "公司名稱", - "pageSpeedDetailsPerformanceReport": "", - "pageSpeedDetailsPerformanceReportCalculator": "", - "checkingEvery": "", - "statusPageCreateSettings": "", - "basicInformation": "基本資料", - "statusPageCreateBasicInfoDescription": "", - "statusPageCreateSelectTimeZoneDescription": "", - "statusPageCreateAppearanceDescription": "", - "statusPageCreateSettingsCheckboxLabel": "", - "statusPageCreateBasicInfoStatusPageAddress": "", - "statusPageCreateTabsContent": "", - "statusPageCreateTabsContentDescription": "", - "statusPageCreateTabsContentFeaturesDescription": "", - "showCharts": "", - "showUptimePercentage": "", - "removeLogo": "", - "statusPageStatus": "", - "statusPageStatusContactAdmin": "", - "statusPageStatusNotPublic": "", - "statusPageStatusNoPage": "", - "statusPageStatusServiceStatus": "", - "deleteStatusPage": "", - "deleteStatusPageConfirm": "", - "deleteStatusPageDescription": "", - "uptimeCreate": "", - "uptimeCreateJsonPath": "", - "uptimeCreateJsonPathQuery": "", - "maintenanceTableActionMenuDialogTitle": "", - "infrastructureEditYour": "", - "infrastructureEditMonitor": "", - "infrastructureMonitorCreated": "", - "infrastructureMonitorUpdated": "", - "errorInvalidTypeId": "", - "errorInvalidFieldId": "", - "inviteNoTokenFound": "", - "pageSpeedWarning": "", - "pageSpeedLearnMoreLink": "", - "pageSpeedAddApiKey": "", - "update": "更新", - "invalidFileFormat": "", - "invalidFileSize": "", - "ClickUpload": "點擊上傳", - "DragandDrop": "", - "MaxSize": "", - "SupportedFormats": "", - "FirstName": "", - "LastName": "", - "EmailDescriptionText": "", - "YourPhoto": "頭像", - "PhotoDescriptionText": "", - "save": "", - "DeleteDescriptionText": "這將刪除帳號以及所有相關的伺服器資料,此操作不可逆轉。", - "DeleteAccountWarning": "刪除您的帳號表示您將無法再次登入,且所有資料都會被移除。此操作不可逆轉。", - "DeleteWarningTitle": "", - "bulkImport": { - "title": "批量匯入", - "selectFileTips": "", - "selectFileDescription": "", - "selectFile": "", - "parsingFailed": "", - "uploadSuccess": "", - "validationFailed": "", - "noFileSelected": "", - "fallbackPage": "", - "invalidFileType": "", - "uploadFailed": "" - }, - "DeleteAccountTitle": "刪除帳戶", - "DeleteAccountButton": "刪除帳戶", - "publicLink": "", - "maskedPageSpeedKeyPlaceholder": "", - "reset": "", - "ignoreTLSError": "", - "tlsErrorIgnored": "", - "ignoreTLSErrorDescription": "", - "createNew": "", - "greeting": { - "prepend": "", - "append": "", - "overview": "" - }, - "roles": { - "superAdmin": "", - "admin": "管理員", - "teamMember": "", - "demoUser": "示範用戶" - }, - "teamPanel": { - "teamMembers": "", - "filter": { - "all": "全部", - "member": "" - }, - "inviteTeamMember": "", - "inviteNewTeamMember": "", - "inviteDescription": "", - "email": "", - "selectRole": "", - "inviteLink": "", - "cancel": "取消", - "noMembers": "", - "getToken": "", - "emailToken": "", - "table": { - "name": "", - "email": "", - "role": "", - "created": "" - }, - "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" - }, - "register": "", - "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" - }, - "registerTeamMember": { - "title": "", - "auth": { - "common": { - "inputs": { - "firstName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "lastName": { - "errors": { - "empty": "", - "pattern": "" - } - }, - "email": { - "errors": { - "empty": "", - "invalid": "" - } - }, - "role": { - "errors": { - "empty": "" - } - } - } - } - } - }, - "role": "", - "changeTeamPassword": { - "changePasswordMenu": "", - "title": "", - "description": "", - "success": "" - } - }, - "monitorState": { - "paused": "", - "resumed": "", - "active": "啟用中" - }, - "menu": { - "uptime": "", - "pagespeed": "", - "infrastructure": "", - "incidents": "", - "statusPages": "", - "maintenance": "", - "integrations": "", - "settings": "", - "support": "", - "discussions": "", - "docs": "", - "changelog": "版本變更記錄", - "profile": "", - "password": "", - "team": "", - "logOut": "", - "notifications": "", - "logs": "" - }, - "settingsEmailUser": "", - "state": "", - "statusBreadCrumbsStatusPages": "", - "statusBreadCrumbsDetails": "", - "commonSaving": "儲存中...", - "navControls": "", - "incidentsPageTitle": "", - "passwordPanel": { - "passwordChangedSuccess": "", - "passwordInputIncorrect": "", - "currentPassword": "當前密碼", - "enterCurrentPassword": "", - "newPassword": "", - "enterNewPassword": "", - "confirmNewPassword": "確認新密碼", - "passwordRequirements": "", - "saving": "" - }, - "emailSent": "", - "failedToSendEmail": "", - "settingsTestEmailSuccess": "", - "settingsTestEmailFailed": "", - "settingsTestEmailFailedWithReason": "", - "settingsTestEmailUnknownError": "", - "statusMsg": { - "paused": "", - "up": "", - "down": "", - "pending": "" - }, - "uptimeGeneralInstructions": { - "http": "", - "ping": "", - "docker": "", - "port": "", - "game": "", - "https": "" - }, "common": { - "appName": "Checkmate", - "monitoringAgentName": "", - "buttons": { - "toggleTheme": "" + "auth": { + "roles": { + "admin": "管理員", + "demo": "示範", + "superadmin": "超級管理員", + "user": "使用者" + } }, - "toasts": { - "networkError": "", - "checkConnection": "請檢查您的連線", - "unknownError": "" - } - }, - "auth": { - "common": { - "navigation": { - "continue": "繼續", - "back": "返回" + "alerts": { + "pageSpeedApiKey": { + "content": "警告:您尚未新增 Google PageSpeed API 金鑰。請前往設定新增。若未設定,PageSpeed 監控將無法運作。" + } + }, + "appName": "Checkmate", + "breadcrumbs": { + "details": "詳細資料", + "home": "首頁" + }, + "buttons": { + "addMember": "新增成員", + "cancel": "取消", + "close": "關閉", + "configure": "設定", + "confirm": "確認", + "create": "建立", + "delete": "刪除", + "generateToken": "產生令牌", + "incidents": "事件", + "inviteMember": "邀請成員", + "pause": "暫停", + "resume": "恢復", + "save": "儲存", + "sendInvite": "發送邀請", + "test": "測試", + "testNotifications": "測試通知", + "toggleTheme": "切換淺色和深色模式", + "flushQueue": "清除佇列", + "notFound": "前往主儀表板", + "resetPassword": "重設密碼", + "reset": "重設", + "clear": "清除", + "addDemo": "新增示範監控", + "removeMonitors": "移除監控", + "sendTestEmail": "發送測試郵件", + "exportToJSON": "匯出為 JSON", + "importFromJSON": "從 JSON 匯入", + "clearFilters": "清除篩選條件", + "removeUser": "移除使用者" + }, + "charts": { + "labels": { + "averageResponseTime": "平均回應時間", + "downtime": "停機時間", + "high": "高", + "low": "低", + "uptime": "正常運行時間" }, - "inputs": { - "email": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "invalid": "" - } - }, - "password": { - "label": "", - "rules": { - "length": { - "beginning": "必須至少", - "highlighted": "" - }, - "special": { - "beginning": "必須至少包含", - "highlighted": "" - }, - "number": { - "beginning": "必須至少包含", - "highlighted": "" - }, - "uppercase": { - "beginning": "必須至少包含", - "highlighted": "" - }, - "lowercase": { - "beginning": "必須至少包含", - "highlighted": "" - }, - "match": { - "beginning": "", - "highlighted": "" - } - }, - "errors": { - "empty": "", - "length": "", - "uppercase": "", - "lowercase": "", - "number": "", - "special": "", - "incorrect": "" - } - }, - "passwordConfirm": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "different": "" - } - }, - "firstName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" - } - }, - "lastName": { - "label": "", - "placeholder": "", - "errors": { - "empty": "", - "length": "", - "pattern": "" + "histogram": { + "avg": "平均:{{value}} ms", + "max": "最大:{{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "此操作無法復原。", + "title": "您確定要刪除嗎?" + } + }, + "labels": { + "active": "啟用中", + "paused": "已暫停", + "na": "N/A", + "resolved": "已解決", + "responseTime": "回應時間" + }, + "form": { + "role": { + "option": { + "role": { + "label": "角色" } } }, - "errors": { - "validation": "" - }, - "fields": { - "password": { - "errors": { - "incorrect": "" + "name": { + "option": { + "firstName": { + "label": "名字", + "placeholder": "輸入您的名字" + }, + "lastName": { + "label": "姓氏", + "placeholder": "輸入您的姓氏" } - }, - "role": { - "errors": { - "min": "" + } + }, + "email": { + "option": { + "email": { + "label": "電子郵件", + "placeholder": "you@example.com" } } } }, - "login": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "" - }, - "links": { - "forgotPassword": "", - "register": "", - "forgotPasswordLink": "", - "registerLink": "" - }, - "toasts": { - "success": "歡迎回來!您已成功登入。", - "incorrectPassword": "" - }, + "table": { + "empty": "這裡沒有資料", + "headers": { + "actions": "行動", + "dateTime": "日期和時間", + "message": "訊息", + "monitor": "監控", + "monitorId": "監控 ID", + "name": "名稱", + "status": "狀態", + "type": "類型", + "url": "URL", + "interval": "間隔", + "active": "啟用中", + "responseTime": "回應時間" + } + } + }, + "components": { + "imageUpload": { + "clickToUpload": "點擊上傳", + "dragAndDrop": "或拖放檔案", + "supportedFormats": "支援的格式", + "maxSize": "最大大小", + "orDragAndDrop": "或拖放檔案", "errors": { - "password": { - "incorrect": "" - } - }, - "welcome": "" + "invalidFileSize": "檔案大小過大!", + "invalidFileFormat": "不支援的檔案格式!" + } }, - "registration": { - "heading": { - "superAdmin": "", - "user": "" - }, - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "" + "headerStatusPageControls": { + "publicLink": "公開連結" + }, + "headerTimeRange": { + "labels": { + "day": "日", + "month": "月", + "recent": "最近", + "week": "週" }, "description": { - "superAdmin": "", - "user": "" - }, - "gettingStartedButton": { - "superAdmin": "", - "user": "" - }, - "termsAndPolicies": "", - "links": { - "login": "" - }, - "toasts": { - "success": "歡迎!您的帳號已成功建立。" - }, - "welcome": "" + "recent": "顯示過去 2 小時的統計資料。", + "day": "顯示過去 24 小時的統計資料。", + "week": "顯示過去 7 天的統計資料。", + "month": "顯示過去 30 天的統計資料。" + } }, - "forgotPassword": { - "heading": "", - "subheadings": { - "stepOne": "", - "stepTwo": "", - "stepThree": "", - "stepFour": "" + "offlineBanner": { + "serverUnreachable": "無法連線至伺服器", + "retry": "重試", + "retrying": "重試中...", + "reconnected": "連線已恢復" + }, + "sidebar": { + "menu": { + "uptime": "正常運行時間", + "pagespeed": "PageSpeed", + "infrastructure": "基礎架構", + "notifications": "通知", + "checks": "檢查", + "incidents": "事件", + "statusPages": "狀態頁面", + "maintenance": "維護", + "logs": "日誌", + "settings": "設定" }, - "buttons": { - "openEmail": "", - "resetPassword": "" + "bottomMenu": { + "support": "支援", + "discussions": "討論", + "docs": "文件", + "changelog": "版本變更記錄" }, - "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "accountMenu": { + "profile": "個人檔案", + "password": "密碼", + "team": "團隊" }, - "links": { - "login": "", - "resend": "" + "starPrompt": { + "title": "為 Checkmate 加星", + "description": "查看最新版本並在 GitHub 上幫助社群成長" }, - "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "您的密碼已成功重設。", - "error": "" + "authFooter": { + "navControls": "控制項", + "logOut": "登出", + "roles": { + "superAdmin": "超級管理員", + "admin": "管理員", + "user": "使用者", + "demoUser": "示範用戶" + } + } + }, + "starPrompt": { + "title": "為 Checkmate 加星", + "description": "查看最新版本並在 GitHub 上幫助社群成長" + } + }, + "pages": { + "notFound": { + "title": "哎呀!您的壽司掉了!", + "subtitle": "該 URL 不存在,或您沒有存取權限。" + }, + "account": { + "tabs": { + "profile": "個人檔案", + "password": "密碼", + "team": "團隊" + }, + "form": { + "name": { + "title": "名稱", + "description": "更新您的個人資訊" + }, + "photo": { + "title": "個人頭像", + "description": "上傳個人頭像" + }, + "currentPassword": { + "title": "目前密碼", + "description": "輸入您目前的密碼以驗證身份", + "option": { + "label": "目前密碼", + "placeholder": "輸入目前密碼" + } + }, + "newPassword": { + "title": "新密碼", + "description": "請選擇至少 8 個字元的強密碼", + "option": { + "newPassword": { + "label": "新密碼", + "placeholder": "輸入新密碼" + }, + "confirm": { + "label": "確認密碼", + "placeholder": "確認新密碼" + } + } + }, + "deleteAccount": { + "title": "刪除帳號", + "description": "此操作為永久性,無法復原" + } + }, + "team": { + "filter": { + "placeholder": "依角色篩選", + "all": "全部", + "admin": "管理員", + "member": "成員" + }, + "table": { + "headers": { + "email": "電子郵件", + "role": "角色", + "created": "建立時間" + } + }, + "addMember": { + "title": "註冊新團隊成員", + "description": "建立新的使用者帳號。建立後請安全地分享登入憑證。" + }, + "invite": { + "title": "邀請團隊成員", + "description": "發送加入團隊的邀請", + "email": { + "label": "電子郵件地址", + "placeholder": "輸入電子郵件地址" + }, + "role": { + "label": "角色", + "placeholder": "選擇角色" + }, + "linkLabel": "邀請連結" + } + } + }, + "auth": { + "common": { + "passwordRules": { + "length": { + "beginning": "必須至少", + "highlighted": "8 個字元" + }, + "lowercase": { + "beginning": "必須至少包含", + "highlighted": "一個小寫字母" + }, + "match": { + "beginning": "密碼", + "highlighted": "必須相符" + }, + "number": { + "beginning": "必須至少包含", + "highlighted": "一個數字" + }, + "special": { + "beginning": "必須至少包含", + "highlighted": "一個特殊字元 (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" + }, + "uppercase": { + "beginning": "必須至少包含", + "highlighted": "一個大寫字母" + } + }, + "form": { + "option": { + "email": { + "label": "電子郵件", + "placeholder": "me@example.com" + }, + "password": { + "label": "密碼", + "placeholder": "********" + }, + "confirmPassword": { + "label": "確認密碼" + } + } + } + }, + "login": { + "title": "歡迎回到 Checkmate!", + "subtitle": "請登入以繼續", + "submit": "登入", + "links": { + "forgotPassword": { + "text": "忘記密碼?", + "linkText": "重設密碼" + }, + "register": { + "text": "還沒有帳號?", + "linkText": "在此註冊" + } + } + }, + "register": { + "title": "歡迎回到 Checkmate!", + "subtitle": "註冊以開始使用", + "submit": "註冊" + }, + "forgotPassword": { + "title": "忘記密碼了嗎?", + "subtitle": "別擔心,我們會發送重設密碼的說明給您。", + "submit": "請求復原", + "links": { + "login": { + "text": "返回", + "linkText": "登入" + } + } + }, + "setNewPassword": { + "title": "重設您的密碼", + "subtitle": "您的新密碼必須與先前使用過的密碼不同。" + } + }, + "checks": { + "selects": { + "monitor": { + "all": "所有監控" + }, + "status": { + "all": "全部", + "down": "離線", + "up": "上線" + } + }, + "table": { + "empty": "此時間範圍內沒有離線檢查", + "headers": { + "statusCode": "狀態碼", + "location": "位置" + } + } + }, + "common": { + "monitors": { + "actions": { + "configure": "設定", + "delete": "刪除", + "generateToken": "產生令牌", + "details": "詳細資料", + "incidents": "事件", + "inviteMember": "邀請成員", + "openSite": "開啟網站", + "pause": "暫停", + "resume": "恢復" + }, + "statBoxes": { + "activeFor": "已啟用", + "certificateExpiry": "憑證到期", + "lastCheck": "最後檢查", + "lastResponseTime": "最後回應時間" + }, + "status": { + "down": "離線", + "breached": "超過閾值", + "initializing": "初始化中", + "maintenance": "維護中", + "paused": "已暫停", + "total": "總計", + "up": "上線" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "遊戲", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "連接埠", + "optionPagespeed": "PageSpeed", + "optionHardware": "基礎架構", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket" + } + } + }, + "createMonitor": { + "form": { + "advanced": { + "description": "進階使用案例的選用設定", + "option": { + "advancedMatching": { + "label": "使用進階配對" + }, + "expectedValue": { + "label": "預期值" + }, + "jsonPath": { + "description": "此表達式將對回應的 JSON 資料進行評估,結果將用於與預期值進行配對。查詢語法文件請參閱 jmespath.org。", + "label": "JSONPath 表達式" + }, + "matchMethod": { + "label": "配對方式", + "equal": "等於", + "include": "包含", + "regex": "正規表達式" + } + }, + "title": "進階設定" + }, + "frequency": { + "description": "您希望多久檢查一次此監控的狀態?", + "option": { + "frequency": { + "label": "檢查頻率", + "value": { + "fifteenMinutes": "15 分鐘", + "fifteenSeconds": "15 秒", + "fiveMinutes": "5 分鐘", + "fourMinutes": "4 分鐘", + "oneMinute": "1 分鐘", + "tenMinutes": "10 分鐘", + "thirtyMinutes": "30 分鐘", + "thirtySeconds": "30 秒", + "threeMinutes": "3分鐘", + "twoMinutes": "2 分鐘" + } + } + }, + "title": "檢查頻率" + }, + "general": { + "option": { + "container": { + "label": "容器名稱/ID", + "placeholder": "my-app 或 abcd1234" + }, + "host": { + "label": "主機", + "placeholder": "192.168.1.100 或 example.com" + }, + "name": { + "label": "顯示名稱", + "placeholder": "例如:我的網站" + }, + "secret": { + "label": "授權密鑰", + "placeholder": "輸入您的密鑰" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "選擇遊戲", + "placeholder": "選擇遊戲" + }, + "port": { + "label": "監控的連接埠", + "placeholder": "80" + }, + "grpcServiceName": { + "label": "服務名稱", + "placeholder": "例如:my.service.v1(留空表示整體健康狀態)" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "一般設定", + "description": { + "http": "輸入要監控的 URL 或 IP(例如 https://example.com/ 或 192.168.1.100),並新增一個顯示在儀表板上的清晰顯示名稱。", + "ping": "輸入要 Ping 的 IP 位址或主機名稱(例如 192.168.1.100 或 example.com),並新增一個顯示在儀表板上的清晰顯示名稱。", + "port": "輸入伺服器的 URL 或 IP、連接埠號碼,以及顯示在儀表板上的清晰顯示名稱。", + "docker": "輸入 Docker 容器名稱或 ID。您可以使用容器名稱(例如 my-app)或容器 ID(完整的 64 字元 ID 或短 ID)。", + "game": "輸入要 Ping 的 IP 位址或主機名稱及連接埠號碼(例如 192.168.1.100 或 example.com),並選擇遊戲類型。", + "pagespeed": "追蹤您網站的頁面載入效能、Core Web Vitals 和最佳化分數。", + "grpc": "輸入 gRPC 伺服器的主機名稱和連接埠,可選擇指定健康檢查的服務名稱,並新增一個顯示名稱。", + "websocket": "輸入要監控的 WebSocket URL(例如 wss://example.com/socket),並新增一個顯示名稱。", + "hardware": "監控您基礎架構的 CPU、記憶體、磁碟使用量和溫度。" + } + }, + "ignoreTls": { + "description": "設定 HTTPS 連線的 TLS/SSL 憑證驗證。", + "option": { + "tls": { + "label": "忽略 TLS/SSL 錯誤" + } + }, + "title": "TLS/SSL 設定" + }, + "incidents": { + "description": "使用滑動視窗來判斷監控何時離線。監控的狀態只有在滑動視窗內的檢查百分比達到指定值時才會變更。", + "option": { + "checks": { + "label": "滑動視窗中的檢查次數" + }, + "percentage": { + "label": "滑動視窗中有多少百分比的檢查失敗/成功後,監控狀態才會變更?" + } + }, + "title": "事件" + }, + "notifications": { + "description": "選擇您要使用的通知頻道", + "title": "通知" + }, + "type": { + "description": "選擇要執行的檢查類型", + "optionDockerDescription": "使用 Docker 監控容器是否正在執行。", + "optionGameDescription": "監控特定遊戲伺服器是否上線。", + "optionGrpcDescription": "使用標準健康檢查協定監控 gRPC 服務。", + "optionWebSocketDescription": "監控 WebSocket 端點的連線健康狀態和回應時間。", + "optionHttpDescription": "使用 HTTP(S) 監控您的網站或 API 端點。", + "optionPingDescription": "使用 ICMP Ping 監控伺服器是否上線。", + "optionPortDescription": "監控伺服器上的特定連接埠是否開啟。", + "title": "類型" + }, + "thresholds": { + "title": "警報閾值", + "description": "定義此硬體監控應觸發警報的閾值。", + "option": { + "cpuThreshold": { + "label": "CPU 警報閾值 (%)" + }, + "memoryThreshold": { + "label": "記憶體警報閾值 (%)" + }, + "diskThreshold": { + "label": "磁碟警報閾值 (%)" + }, + "tempThreshold": { + "label": "溫度警報閾值 (°C)" + } + } + }, + "geoChecks": { + "title": "地理分散式檢查", + "description": "從多個地理位置執行檢查,以監控全球可用性和效能。", + "option": { + "enabled": { + "label": "啟用地理分散式檢查" + }, + "locations": { + "label": "位置", + "placeholder": "選擇位置", + "options": { + "EU": "歐洲", + "NA": "北美洲", + "AS": "亞洲", + "SA": "南美洲", + "AF": "非洲", + "OC": "大洋洲" + } + }, + "interval": { + "label": "檢查間隔", + "value": { + "fiveMinutes": "5 分鐘", + "tenMinutes": "10 分鐘", + "fifteenMinutes": "15 分鐘", + "thirtyMinutes": "30 分鐘" + } + } + } + }, + "url": { + "title": "在狀態頁面上顯示監控 IP/URL", + "description": "在公開狀態頁面上顯示監控的 IP 位址或 URL。若停用此選項,將僅顯示監控名稱以保護敏感資訊。", + "option": { + "showURL": { + "label": "在狀態頁面上顯示 IP/URL", + "enabled": "啟用", + "disabled": "停用" + } + } + }, + "stats": { + "title": "監控歷史", + "description": "清除團隊的所有監控歷史和統計資料。此操作無法復原。", + "buttonText": "清除所有統計資料", + "dialog": { + "title": "清除所有監控歷史?", + "description": "這將永久刪除團隊的所有監控歷史、統計資料和檢查資料。此操作無法復原。", + "confirm": "是的,清除所有統計資料" + } + } + } + }, + "editUser": { + "form": { + "roles": { + "title": "角色", + "description": "為使用者指派角色。可以選擇多個角色。" + } + }, + "dialog": { + "removeUser": { + "title": "移除使用者", + "content": "您確定要將 {{name}} 從團隊中移除嗎?此操作無法復原。" + } + } + }, + "incidents": { + "dialog": { + "resolveIncident": { + "title": "解決事件", + "option": { + "comment": { + "label": "備註(選填)", + "placeholder": "新增關於解決方案的備註..." + } + } + }, + "details": { + "analysis": "事件分析", + "comment": "備註:", + "detailsLabel": "詳細資料", + "downtime": "停機時間:", + "message": "訊息:", + "monitor": "監控:", + "overview": "概覽", + "resolutionDetails": "解決詳情", + "resolutionType": "類型:", + "resolutionTypes": { + "automatic": "自動", + "manual": "手動" + }, + "resolve": "解決事件", + "resolvedAt": "解決於:", + "resolvedBy": "解決者:", + "startedAt": "開始於:", + "status": "狀態:", + "statusCode": "狀態碼:", + "timeline": "時間軸", + "title": "事件詳情", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "所有監控", + "monitor": "監控", + "resolutionType": "解決類型", + "resolutionTypes": { + "manual": "手動", + "automatic": "自動", + "all": "全部" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "進行中的事件", + "active_zero": "沒有進行中的事件", + "active_one": "{{count}} 個進行中的事件", + "active_other": "{{count}} 個進行中的事件" + }, + "incidentStats": { + "avgResolutionTime": "平均解決時間", + "mostAffectedMonitor": "受影響最多的監控", + "title": "事件統計", + "totalIncidents": "事件總數" + }, + "latestIncidents": { + "title": "最新事件" + } + }, + "table": { + "actions": { + "details": "詳細資料", + "goToMonitor": "前往監控", + "resolveManually": "手動解決" + }, + "activeIncidents": "進行中的事件", + "headers": { + "endTime": "結束時間", + "resolutionType": "解決類型", + "startTime": "開始時間", + "statusCode": "狀態碼" + }, + "resolvedIncidents": "已解決的事件", + "status": { + "active": "啟用中", + "resolved": "已解決" + } + } + }, + "infrastructure": { + "charts": { + "labels": { + "cpu": "CPU 使用率", + "disk": "磁碟使用率", + "memory": "記憶體使用率", + "netBytesRecv": "{{name}} - 接收位元組", + "netBytesSent": "{{name}} - 發送位元組", + "temp": "溫度" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "最大頻率", + "title": "CPU 使用率", + "upperLabel": "目前頻率" + }, + "disk": { + "lowerLabel": "可用", + "title": "磁碟 {{idx}} 使用率", + "upperLabel": "已使用" + }, + "memory": { + "lowerLabel": "可用", + "title": "記憶體使用率", + "upperLabel": "已使用" + } + }, + "statBoxes": { + "avgCpuTemperature": "平均 CPU 溫度", + "cpuFrequency": "CPU 頻率", + "cpuLogical": "CPU (邏輯核心)", + "cpuPhysical": "CPU (實體核心)", + "disk": "磁碟", + "memory": "記憶體", + "os": "作業系統" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "磁碟", + "memory": "記憶體" + } + }, + "tabs": { + "labels": { + "network": "網路", + "overview": "概覽" + } + }, + "fallback": { + "actionButton": "建立監控!", + "checks": [ + "追蹤您伺服器的效能", + "找出瓶頸並最佳化使用", + "透過即時監控確保可靠性" + ], + "title": "基礎架構監控用於:" + } + }, + "logs": { + "tabs": { + "diagnostics": "診斷", + "logs": "日誌", + "queue": "工作佇列" + }, + "logLevelSelect": { + "label": "日誌等級" + }, + "jobQueue": "工作佇列", + "failedJobs": "失敗的工作", + "noLogs": "找不到日誌", + "metrics": { + "jobs": "工作", + "activeJobs": "進行中的工作", + "failingJobs": "失敗中的工作", + "totalRuns": "總執行次數", + "totalFailures": "總失敗次數" + }, + "diagnostics": { + "stats": { + "eventLoopDelay": "事件迴圈延遲", + "uptime": "正常運行時間", + "usedHeapSize": "已使用堆積大小", + "totalHeapSize": "堆積總大小", + "osMemoryLimit": "作業系統記憶體限制" + }, + "gauges": { + "heapAllocation": "堆積配置", + "heapUsage": "堆積使用率", + "heapUtilization": "堆積利用率", + "instantCpuUsage": "即時 CPU 使用率", + "availableMemoryPercentage": "可用記憶體百分比", + "allocatedPercentage": "已配置百分比", + "usedSPercentage": "CPU 使用 1 秒百分比", + "total": "總計", + "used": "已使用" + } + }, + "table": { + "headers": { + "timestamp": "時間戳", + "level": "等級", + "service": "服務", + "method": "方法", + "monitorId": "監控 ID", + "runCount": "執行次數", + "failCount": "失敗次數", + "lastRunAt": "最後執行於", + "lockedAt": "鎖定於", + "lastFinishedAt": "最後完成於", + "lastRunTook": "最後執行耗時", + "lastFailedAt": "最後失敗於", + "failReason": "失敗原因" + } + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "建立維護時段!", + "checks": ["標記您的維護期間", "消除任何誤解", "在維護時段停止發送警報"], + "title": "維護時段用於:" + }, + "table": { + "headers": { + "nextWindow": "下一個時段", + "repeat": "重複" + } + }, + "form": { + "general": { + "title": "一般設定", + "description": "為維護時段設定名稱和重複選項。", + "option": { + "name": { + "label": "名稱", + "placeholder": "例如:每週維護" + }, + "repeat": { + "label": "重複" + } + } + }, + "startDate": { + "title": "開始日期", + "description": "選擇維護時段的開始日期。", + "option": { + "startDate": { + "label": "開始日期" + } + } + }, + "startTime": { + "title": "開始時間", + "description": "設定維護時段的開始時間和持續時間。所有數值為 UTC 時間", + "option": { + "duration": { + "label": "持續時間" + }, + "startTime": { + "label": "開始時間" + } + }, + "monitors": { + "title": "監控", + "description": "維護時段應套用到的監控", + "option": { + "addMonitors": { + "label": "新增監控" + } + } + } + } + } + }, + "notifications": { + "fallback": { + "actionButton": "建立頻道", + "checks": [ + "針對停機或效能問題通知團隊", + "在事件發生時通知工程師", + "讓管理員隨時了解系統變更" + ], + "title": "通知頻道用於:" + }, + "form": { + "accessToken": { + "optionAccessToken": "存取令牌", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "通知將發送到的地址。", + "optionAddress": "地址", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "地址" + }, + "homeServer": { + "optionHomeServer": "主伺服器", + "placeholder": "example.com" + }, + "matrix": { + "description": "設定您的 Matrix 主伺服器連線以接收通知。", + "title": "Matrix 設定" + }, + "notificationName": { + "description": "通知頻道的描述性名稱", + "optionName": "頻道名稱", + "placeholder": "例如:正式環境警報", + "title": "頻道名稱" + }, + "pagerDuty": { + "description": "用於接收警報的 PagerDuty 整合金鑰。", + "optionIntegrationKey": "整合金鑰", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "整合金鑰" + }, + "roomId": { + "optionRoomId": "房間 ID", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "選擇要建立的通知頻道類型。", + "optionType": "類型", + "title": "頻道類型" + }, + "telegram": { + "title": "Telegram 設定", + "description": "要啟用 Telegram 通知,請使用 BotFather 用於創建和管理 Telegram 的官方機器來建立一個 Telegram 機器人。接著,取得 API 金鑰和聊天室 ID,並在此處填寫。", + "optionBotToken": "機器人令牌", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "聊天室 ID", + "placeholderChatId": "-1001234567890" + } + }, + "table": { + "headers": { + "destination": "目的地" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "累計版面配置位移 (CLS)", + "fcp": "首次內容繪製 (FCP)", + "lcp": "最大內容繪製 (LCP)", + "si": "速度指數 (SI)", + "tbt": "總阻塞時間 (TBT)" + }, + "legend": { + "title": "PageSpeed 報告", + "weight": "權重" + }, + "pie": { + "title": "效能報告" + } + }, + "table": { + "headers": { + "pageSpeedScore": "PageSpeed 分數" + } + }, + "fallback": { + "actionButton": "建立監控!", + "checks": ["報告頁面的使用者體驗", "協助分析網頁速度", "提供頁面改善建議"], + "title": "PageSpeed 監控用於:" + } + }, + "settings": { + "form": { + "timezone": { + "title": "顯示時區", + "description": "選擇用於在整個應用程式中顯示日期和時間的時區。", + "option": { + "timezone": { + "label": "顯示時區" + } + } + }, + "ui": { + "title": "外觀", + "description": "切換淺色和深色模式、變更語言或自訂圖表顯示類型。", + "option": { + "theme": { + "label": "佈景主題模式", + "light": "淺色", + "dark": "深色" + }, + "language": { + "label": "語言" + }, + "chartType": { + "label": "圖表類型", + "histogram": "直方圖", + "heatmap": "熱力圖" + } + } + }, + "pagespeed": { + "title": "Google PageSpeed API 金鑰", + "description": "輸入您的 Google PageSpeed API 金鑰以啟用 Google PageSpeed 監控。點擊重設以更新金鑰。", + "option": { + "apiKey": { + "label": "PageSpeed API 金鑰", + "labelSet": "API 金鑰已設定。點擊重設以變更。", + "placeholder": "輸入您的 Google PageSpeed API 金鑰" + } + } + }, + "url": { + "title": "在狀態頁面上顯示監控 IP/URL", + "description": "在公開狀態頁面上顯示監控的 IP 位址或 URL。若停用此選項,將僅顯示監控名稱以保護敏感資訊。", + "option": { + "showURL": { + "label": "在狀態頁面上顯示 IP/URL", + "enabled": "啟用", + "disabled": "停用" + } + } + }, + "stats": { + "title": "監控歷史", + "description": "清除團隊的所有監控歷史和統計資料。此操作無法復原。", + "option": { + "clear": { + "label": "清除所有統計資料。此操作無法復原。" + } + }, + "dialog": { + "title": "清除所有監控歷史?", + "description": "此操作無法復原" + } + }, + "retention": { + "title": "檢查資料保留", + "description": "設定檢查資料在自動清除之前的保留時間。", + "option": { + "days": { + "label": "保留期間(天)", + "unlimited": "無限制" + } + } + }, + "thresholds": { + "title": "全域閾值", + "description": "設定基礎架構監控的全域 CPU、記憶體、磁碟和溫度閾值。這些設定將套用到所有硬體監控,除非被個別覆寫。", + "option": { + "cpu": { + "label": "CPU 閾值 (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "記憶體閾值 (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "磁碟閾值 (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "溫度閾值 (°C)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "電子郵件設定", + "description": "設定系統的電子郵件設定。此設定用於發送通知和警報。", + "descriptionTransport": "這會為 NodeMailer 建立 SMTP 傳輸", + "descriptionTransportLink": "在此查看規格", + "option": { + "host": { + "label": "電子郵件主機 - 要連線的主機名稱或 IP 位址", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "電子郵件連接埠 - 要連線的連接埠", + "placeholder": "587" + }, + "address": { + "label": "電子郵件地址 - 用於驗證", + "placeholder": "you@example.com" + }, + "user": { + "label": "電子郵件使用者 - 用於驗證的使用者名稱,若指定則覆寫電子郵件地址", + "placeholder": "若非必要請留空" + }, + "password": { + "label": "電子郵件密碼 - 用於驗證的密碼", + "labelSet": "密碼已設定。點擊重設以變更。", + "placeholder": "輸入您的密碼" + }, + "tlsServername": { + "label": "TLS 伺服器名稱 - 當主機為 IP 時用於 TLS 驗證的選用主機名稱", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "電子郵件連線主機 - 用於 HELO/EHLO 問候的主機名稱", + "placeholder": "localhost" + }, + "secure": { + "label": "使用 SSL(建議):使用 SSL/TLS 加密連線" + }, + "pool": { + "label": "啟用連線池:重複使用現有連線以提升效能" + }, + "ignoreTLS": { + "label": "停用 STARTTLS:即使伺服器支援也不使用 TLS" + }, + "requireTLS": { + "label": "強制 STARTTLS:要求 TLS 升級,若不支援則失敗" + }, + "rejectUnauthorized": { + "label": "拒絕無效憑證:拒絕使用自簽或不受信任憑證的連線" + } + } + }, + "demoMonitors": { + "title": "示範監控", + "description": "新增範例監控以供示範用途。" + }, + "removeMonitors": { + "title": "系統重設", + "description": "從系統中移除所有監控。", + "dialog": { + "title": "移除所有監控?", + "description": "此操作無法復原。" + } + }, + "exportMonitors": { + "title": "匯出監控" + }, + "importExportMonitors": { + "title": "匯入/匯出監控", + "description": "將監控資料匯入或匯出為 JSON 檔案以進行備份或轉移。" + }, + "about": { + "title": "關於", + "developedBy": "由 Bluewave Labs 開發" + }, + "validation": { + "errorMessage": "請修正以下驗證錯誤:" + } + } + }, + "statusPages": { + "deleteSuccess": "狀態頁面已成功刪除", + "fallback": { + "title": "狀態頁面用於:", + "checks": [ + "向使用者和利害關係人傳達系統狀態", + "公開顯示即時正常運行時間資訊", + "透過透明的服務監控建立信任", + "在事件期間減少支援請求" + ], + "actionButton": "建立狀態頁面!" + }, + "monitorsList": { + "chartTypeHeatmap": "熱力圖", + "chartTypeHistogram": "直方圖", + "noData": "沒有可用資料", + "infrastructure": { + "title": "基礎架構", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "記憶體", + "disk": "磁碟", + "usage": "使用量", + "used": "已使用", + "total": "總計" + }, + "uptime": { + "title": "正常運行時間", + "responseTime": "回應時間" + } + }, + "statusBar": { + "allDown": "所有系統已停機", + "allUp": "所有系統運作正常", + "degraded": "部分系統正在發生問題", + "unknown": "無法判斷系統狀態" + }, + "table": { + "headers": { + "name": "狀態頁面名稱", + "url": "公開 URL" + }, + "published": "已發佈", + "unpublished": "未發佈" + }, + "title": "狀態頁面", + "details": { + "empty": { + "title": "這裡還沒有任何內容", + "addMonitor": "新增一個監控以開始使用" + } + }, + "form": { + "access": { + "title": "存取", + "description": "如果您的狀態頁面已準備就緒,可以將其標記為已發佈。", + "option": { + "published": { + "name": "已發佈且公開可見" + } + } + }, + "basicInfo": { + "title": "基本資訊", + "description": "定義公司名稱和狀態頁面指向的子網域。", + "option": { + "name": { + "label": "公司名稱", + "placeholder": "Acme Inc." + }, + "url": { + "label": "您的狀態頁面地址", + "placeholder": "my-status-page" + } + } + }, + "monitors": { + "title": "監控", + "description": "選擇要在狀態頁面上顯示的監控。", + "noMonitors": "未選擇監控", + "option": { + "monitors": { + "label": "選擇監控", + "placeholder": "搜尋並選擇監控..." + } + } + }, + "timezone": { + "title": "時區", + "description": "選擇狀態頁面顯示的時區。", + "option": { + "timezone": { + "label": "時區", + "placeholder": "選擇時區..." + } + } + }, + "appearance": { + "title": "外觀", + "description": "定義公開狀態頁面的預設外觀。", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "品牌顏色" + } + } + }, + "features": { + "title": "功能", + "description": "設定狀態頁面上顯示的資訊。", + "option": { + "showCharts": { + "label": "顯示回應時間圖表" + }, + "showUptimePercentage": { + "label": "顯示正常運行時間百分比" + }, + "showAdminLoginLink": { + "label": "顯示管理員登入連結" + }, + "showInfrastructure": { + "label": "顯示基礎架構指標" + } + } + } + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "搜尋監控..." + } + }, + "table": { + "headers": { + "responseTime": "回應時間" + } + }, + "fallback": { + "actionButton": "建立監控!", + "checks": [ + "檢查網站或伺服器是否上線並回應", + "針對停機或效能問題通知團隊", + "監控 HTTP 端點、Ping、容器和連接埠", + "追蹤歷史正常運行時間和可靠性趨勢" + ], + "title": "正常運行時間監控用於:" } } - }, - "errorPages": { - "serverUnreachable": { - "toasts": { - "reconnected": "", - "stillUnreachable": "" - }, - "alertBox": "伺服器連線錯誤", - "description": "我們無法連接到伺服器。請檢查您的網路連線,若問題持續,請確認您的部署設定是否正確。", - "retryButton": { - "default": "重新嘗試連線", - "processing": "" - } - } - }, - "createNotifications": { - "title": "", - "nameSettings": { - "title": "姓名", - "description": "為您的整合設定描述名稱。", - "nameLabel": "", - "namePlaceholder": "" - }, - "typeSettings": { - "title": "類型", - "description": "請選擇您想要建立的通知頻道類型。", - "typeLabel": "類型" - }, - "emailSettings": { - "title": "電郵", - "description": "目的地電子郵件位址。", - "emailLabel": "", - "emailPlaceholder": "" - }, - "slackSettings": { - "title": "Slack", - "description": "在此配置您的 Slack webhook", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "pagerdutySettings": { - "title": "PagerDuty", - "description": "在此配置您的 PagerDuty 整合", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" - }, - "discordSettings": { - "title": "Discord", - "description": "在此配置您的 Discord webhook", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "webhookSettings": { - "title": "Webhook", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" - }, - "testNotification": "", - "dialogDeleteTitle": "", - "dialogDeleteConfirm": "" - }, - "notificationConfig": { - "title": "通知", - "description": "請選擇您想要使用的通知頻道" - }, - "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" - }, - "advancedMatching": "進階匹配", - "sendTestNotifications": "", - "selectAll": "", - "showAdminLoginLink": "", - "logsPage": { - "title": "", - "description": "", - "tabs": { - "queue": "", - "logs": "", - "diagnostics": "" - }, - "toast": { - "fetchLogsSuccess": "" - }, - "logLevelSelect": { - "title": "", - "values": { - "all": "全部", - "info": "", - "warn": "", - "error": "", - "debug": "除錯" - } - } - }, - "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", - "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "類型", - "activeHeader": "啟用中", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "", - "intervalHeader": "" - }, - "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" - }, - "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" - } - }, - "export": { - "title": "", - "success": "", - "failed": "" - }, - "monitorActions": { - "title": "", - "import": "", - "export": "", - "deleteSuccess": "", - "deleteFailed": "", - "details": "" - }, - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "關於" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" - }, - "emailSettings": { - "buttonSendTestEmail": "發送測試電子郵件", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "電郵", - "toastEmailRequiredFieldsError": "" - }, - "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "Google PageSpeed API 金鑰" - }, - "saveButtonLabel": "", - "statsSettings": { - "clearAllStatsButton": "清除所有統計數據", - "clearAllStatsDescription": "清除所有統計數據,此操作不可逆轉。", - "clearAllStatsDialogConfirm": "是,請清除所有統計數據", - "clearAllStatsDialogDescription": "一旦刪除,監控歷史和統計數據將無法復原。", - "clearAllStatsDialogTitle": "您是否要清除所有統計數據?", - "description": "", - "labelTTL": "", - "labelTTLOptional": "0 表示無限", - "title": "" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "系統重設" - }, - "timezoneSettings": { - "description": "", - "label": "", - "title": "顯示時區" - }, - "title": "設定", - "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "外觀" - }, - "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" - }, - "globalThresholds": { - "title": "", - "description": "" - } - }, - "statusPageCreate": { - "buttonSave": "儲存" - }, - "incidentsOptionsHeaderFilterResolved": "", - "settingsSave": "", - "statusPageCreateAppearanceTitle": "", - "confirmPassword": "確認密碼", - "monitorHooks": { - "failureAddDemoMonitors": "", - "successAddDemoMonitors": "" - }, - "settingsAppearance": "", - "settingsDisplayTimezone": "", - "settingsGeneralSettings": "", - "incidentsOptionsHeaderTotalIncidents": "", - "statusPage": { - "deleteSuccess": "", - "deleteFailed": "刪除狀態頁面失敗", - "createSuccess": "", - "updateSuccess": "", - "generalSettings": "", - "contents": "內容", - "fallback": { - "checks": [""], - "title": "", - "actionButton": "讓我們建立您的第一個狀態頁面!" - } - }, - "testNotificationsDisabled": "", - "incidentsTableResolvedAt": "", - "incidentsTableActionResolve": "", - "checkHooks": { - "failureResolveOne": "", - "failureResolveAll": "", - "failureResolveMonitor": "" - }, - "checkFormError": "", - "diagnosticsPage": { - "diagnosticDescription": "", - "statsDescription": "", - "gauges": { - "heapAllocationTitle": "", - "heapAllocationSubtitle": "", - "heapUsageTitle": "", - "heapUsageSubtitle": "", - "heapUtilizationTitle": "", - "heapUtilizationSubtitle": "", - "instantCpuUsageTitle": "", - "instantCpuUsageSubtitle": "" - }, - "stats": { - "eventLoopDelayTitle": "", - "uptimeTitle": "", - "usedHeapSizeTitle": "", - "totalHeapSizeTitle": "", - "osMemoryLimitTitle": "" - } - }, - "pageSpeedLighthouseAPI": "", - "time": { - "threeMinutes": "3分鐘", - "fiveMinutes": "5分鐘", - "tenMinutes": "10分鐘", - "twentyMinutes": "20分鐘", - "oneHour": "1小時", - "oneDay": "1日", - "oneWeek": "1星期", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" - }, - "general": { - "noOptionsFound": "" - }, - "infrastructureMonitor": { - "fallback": { - "checks": ["追蹤伺服器的效能", "識別瓶頸並優化資源使用", "透過即時監控確保可靠性"], - "title": "", - "actionButton": "讓我們建立您的第一個基礎設施監視器!" - } - }, - "maintenanceWindow": { - "fallback": { - "checks": ["標記您的維護期間", "消除任何誤解", "在維護時段停止發送警報"], - "title": "", - "actionButton": "讓我們建立您的第一個維護時段!" - } - }, - "pageSpeed": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "讓我們建立您的第一個 PageSpeed 監視器!" - } - }, - "uptimeMonitor": { - "fallback": { - "checks": [""], - "title": "", - "actionButton": "讓我們建立您的第一個正常運行時間監視器!" - } - }, - "editUserPage": { - "form": { - "email": "電郵", - "firstName": "名", - "lastName": "姓", - "role": "角色", - "save": "儲存" - }, - "table": { - "actionHeader": "行動", - "roleHeader": "角色" - }, - "title": "編輯用戶", - "toast": { - "successUserUpdate": "", - "validationErrors": "" - } - }, - "incidentsPageActionResolveMonitor": "解決監視器事故", - "incidentsPageActionResolveAll": "解決所有事故", - "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" - }, - "monitorType": { - "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - }, - "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" - } - }, - "uptimeAdvancedMatching": { - "jsonPath": "" - }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", - "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "", - "incidentConfigDescriptionV2": "", - "incidentConfigStatusCheckNumber": "", - "intervalTitle": "", - "intervalDescription": "" - }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + } } diff --git a/client/vite.config.ts b/client/vite.config.ts index 373a50763..a490d321d 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export default defineConfig(({}) => { - let version = "3.5.1"; + let version = "3.6.0"; return { base: "/", diff --git a/docker/coolify/build_images.bat b/docker/coolify/build_images.bat new file mode 100644 index 000000000..60fc9aa4a --- /dev/null +++ b/docker/coolify/build_images.bat @@ -0,0 +1,36 @@ +@echo off +setlocal enabledelayedexpansion + +REM Change directory to root Server directory for correct Docker Context +cd /d "%~dp0" +cd ../.. + +REM Define service names and their corresponding Dockerfiles +set services[0]=bluewaveuptime/uptime_client +set dockerfiles[0]=.\docker\coolify\client.Dockerfile + +set services[1]=bluewaveuptime/uptime_database_mongo +set dockerfiles[1]=.\docker\coolify\mongoDB.Dockerfile + +set services[2]=bluewaveuptime/uptime_redis +set dockerfiles[2]=.\docker\coolify\redis.Dockerfile + +set services[3]=bluewaveuptime/uptime_server +set dockerfiles[3]=.\docker\coolify\server.Dockerfile + +REM Loop through each service and build the corresponding image +for /L %%i in (0,1,2,3) do ( + set service=!services[%%i]! + set dockerfile=!dockerfiles[%%i]! + + docker build -f "!dockerfile!" -t "!service!" . + + REM Check if the build succeeded + if errorlevel 1 ( + echo Error building !service! image. Exiting... + exit /b 1 + ) +) + +echo All images built successfully +exit /b 0 diff --git a/docker/coolify/build_images.sh b/docker/coolify/build_images.sh index 179b50b5b..04dfe324c 100755 --- a/docker/coolify/build_images.sh +++ b/docker/coolify/build_images.sh @@ -1,14 +1,14 @@ #!/bin/bash # Change directory to root Server directory for correct Docker Context cd "$(dirname "$0")" -cd ../../../ +cd ../../ # Define an array of services and their Dockerfiles declare -A services=( - ["bluewaveuptime/uptime_client"]="./server/docker/dist/client.Dockerfile" - ["bluewaveuptime/uptime_database_mongo"]="./server/docker/dist/mongoDB.Dockerfile" - ["bluewaveuptime/uptime_redis"]="./server/docker/dist/redis.Dockerfile" - ["bluewaveuptime/uptime_server"]="./server/docker/dist/server.Dockerfile" + ["bluewaveuptime/uptime_client"]="./docker/coolify/client.Dockerfile" + ["bluewaveuptime/uptime_database_mongo"]="./docker/coolify/mongoDB.Dockerfile" + ["bluewaveuptime/uptime_redis"]="./docker/coolify/redis.Dockerfile" + ["bluewaveuptime/uptime_server"]="./docker/coolify/server.Dockerfile" ) # Loop through each service and build the corresponding image diff --git a/docker/dev/build_images.bat b/docker/dev/build_images.bat new file mode 100644 index 000000000..ec3889418 --- /dev/null +++ b/docker/dev/build_images.bat @@ -0,0 +1,33 @@ +@echo off +setlocal enabledelayedexpansion + +REM Change directory to root Server directory for correct Docker Context +cd /d "%~dp0" +cd ../.. + +REM Define service names and their corresponding Dockerfiles +set services[0]=uptime_client +set dockerfiles[0]=.\docker\dev\client.Dockerfile + +set services[1]=uptime_mongo +set dockerfiles[1]=.\docker\dev\mongoDB.Dockerfile + +set services[2]=uptime_server +set dockerfiles[2]=.\docker\dev\server.Dockerfile + +REM Loop through each service and build the corresponding image +for /L %%i in (0,1,2) do ( + set service=!services[%%i]! + set dockerfile=!dockerfiles[%%i]! + + docker build -f "!dockerfile!" -t "!service!" . + + REM Check if the build succeeded + if errorlevel 1 ( + echo Error building !service! image. Exiting... + exit /b 1 + ) +) + +echo All images built successfully +exit /b 0 \ No newline at end of file diff --git a/docker/dist-mono/build_images.bat b/docker/dist-mono/build_images.bat new file mode 100644 index 000000000..3e1d39fe1 --- /dev/null +++ b/docker/dist-mono/build_images.bat @@ -0,0 +1,30 @@ +@echo off +setlocal enabledelayedexpansion + +REM Change directory to root Server directory for correct Docker Context +cd /d "%~dp0" +cd ../.. + +REM Define service names and their corresponding Dockerfiles +set services[0]=uptime_database_mongo +set dockerfiles[0]=.\docker\dist-mono\mongoDB.Dockerfile + +set services[1]=uptime_server +set dockerfiles[1]=.\docker\dist-mono\server.Dockerfile + +REM Loop through each service and build the corresponding image +for /L %%i in (0,1) do ( + set service=!services[%%i]! + set dockerfile=!dockerfiles[%%i]! + + docker build -f "!dockerfile!" -t "!service!" . + + REM Check if the build succeeded + if errorlevel 1 ( + echo Error building !service! image. Exiting... + exit /b 1 + ) +) + +echo All images built successfully +exit /b 0 diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile index 84b7127ed..5e1bf5c13 100644 --- a/docker/dist-mono/server.Dockerfile +++ b/docker/dist-mono/server.Dockerfile @@ -19,10 +19,10 @@ WORKDIR /app/server COPY server ./ - RUN npm install RUN npm run build +RUN cp -r src/templates dist/templates COPY --from=frontend-build /app/client/dist ./public @@ -30,4 +30,4 @@ RUN chmod +x ./scripts/inject-vars.sh EXPOSE 52345 -CMD ./scripts/inject-vars.sh && node ./dist/index.js +CMD ./scripts/inject-vars.sh && node ./dist/index.js \ No newline at end of file diff --git a/docker/dist/build_images.bat b/docker/dist/build_images.bat new file mode 100644 index 000000000..92b5c4a9d --- /dev/null +++ b/docker/dist/build_images.bat @@ -0,0 +1,33 @@ +@echo off +setlocal enabledelayedexpansion + +REM Change directory to root Server directory for correct Docker Context +cd /d "%~dp0" +cd ../.. + +REM Define service names and their corresponding Dockerfiles +set services[0]=bluewaveuptime/uptime_client +set dockerfiles[0]=.\docker\dist\client.Dockerfile + +set services[1]=bluewaveuptime/uptime_database_mongo +set dockerfiles[1]=.\docker\dist\mongoDB.Dockerfile + +set services[2]=bluewaveuptime/uptime_server +set dockerfiles[2]=.\docker\dist\server.Dockerfile + +REM Loop through each service and build the corresponding image +for /L %%i in (0,1,2) do ( + set service=!services[%%i]! + set dockerfile=!dockerfiles[%%i]! + + docker build -f "!dockerfile!" -t "!service!" . + + REM Check if the build succeeded + if errorlevel 1 ( + echo Error building !service! image. Exiting... + exit /b 1 + ) +) + +echo All images built successfully +exit /b 0 diff --git a/docker/prod/build_images.bat b/docker/prod/build_images.bat new file mode 100644 index 000000000..79403db3f --- /dev/null +++ b/docker/prod/build_images.bat @@ -0,0 +1,33 @@ +@echo off +setlocal enabledelayedexpansion + +REM Change directory to root Server directory for correct Docker Context +cd /d "%~dp0" +cd ../.. + +REM Define service names and their corresponding Dockerfiles +set services[0]=uptime_client +set dockerfiles[0]=.\docker\prod\client.Dockerfile + +set services[1]=uptime_database_mongo +set dockerfiles[1]=.\docker\prod\mongoDB.Dockerfile + +set services[2]=uptime_server +set dockerfiles[2]=.\docker\prod\server.Dockerfile + +REM Loop through each service and build the corresponding image +for /L %%i in (0,1,2) do ( + set service=!services[%%i]! + set dockerfile=!dockerfiles[%%i]! + + docker build -f "!dockerfile!" -t "!service!" . + + REM Check if the build succeeded + if errorlevel 1 ( + echo Error building !service! image. Exiting... + exit /b 1 + ) +) + +echo All images built successfully +exit /b 0 diff --git a/docker/prod/docker-compose.yaml b/docker/prod/docker-compose.yaml index 6e29fd90a..02a9fd919 100755 --- a/docker/prod/docker-compose.yaml +++ b/docker/prod/docker-compose.yaml @@ -3,8 +3,8 @@ services: image: ghcr.io/bluewave-labs/checkmate:frontend-demo restart: always environment: - UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1" - UPTIME_APP_CLIENT_HOST: "https://checkmate-demo.bluewavelabs.ca" + UPTIME_APP_API_BASE_URL: "https://demo.checkmate.so/api/v1" + UPTIME_APP_CLIENT_HOST: "https://demo.checkmate.so" ports: - "80:80" - "443:443" @@ -12,16 +12,7 @@ services: - server volumes: - ./nginx/conf.d:/etc/nginx/conf.d/:ro - - ./certbot/www:/var/www/certbot/:ro - - ./certbot/conf/:/etc/nginx/ssl/:ro - - certbot: - image: certbot/certbot:latest - restart: always - volumes: - - ./certbot/www/:/var/www/certbot/:rw - - ./certbot/conf/:/etc/letsencrypt/:rw - entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'" + - /etc/letsencrypt:/etc/nginx/ssl:ro server: image: ghcr.io/bluewave-labs/checkmate:backend-demo diff --git a/docker/prod/nginx/conf.d/default.conf b/docker/prod/nginx/conf.d/default.conf index 8afeb93b7..0e0e55a2b 100755 --- a/docker/prod/nginx/conf.d/default.conf +++ b/docker/prod/nginx/conf.d/default.conf @@ -40,8 +40,8 @@ server { server_name checkmate-demo.bluewavelabs.ca; - ssl_certificate /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/privkey.pem; + ssl_certificate /etc/nginx/ssl/live/demo.checkmate.so/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/demo.checkmate.so/privkey.pem; location / { root /usr/share/nginx/html; diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile index 7341b1e9b..8be8200ee 100755 --- a/docker/prod/server.Dockerfile +++ b/docker/prod/server.Dockerfile @@ -17,6 +17,8 @@ COPY ./server ./ RUN npm run build +RUN cp -r src/templates dist/templates + EXPOSE 52345 CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/staging/build_images.bat b/docker/staging/build_images.bat new file mode 100644 index 000000000..c8be0378a --- /dev/null +++ b/docker/staging/build_images.bat @@ -0,0 +1,33 @@ +@echo off +setlocal enabledelayedexpansion + +REM Change directory to root Server directory for correct Docker Context +cd /d "%~dp0" +cd ../.. + +REM Define service names and their corresponding Dockerfiles +set services[0]=uptime_client +set dockerfiles[0]=.\docker\staging\client.Dockerfile + +set services[1]=uptime_database_mongo +set dockerfiles[1]=.\docker\staging\mongoDB.Dockerfile + +set services[2]=uptime_server +set dockerfiles[2]=.\docker\staging\server.Dockerfile + +REM Loop through each service and build the corresponding image +for /L %%i in (0,1,2) do ( + set service=!services[%%i]! + set dockerfile=!dockerfiles[%%i]! + + docker build -f "!dockerfile!" -t "!service!" . + + REM Check if the build succeeded + if errorlevel 1 ( + echo Error building !service! image. Exiting... + exit /b 1 + ) +) + +echo All images built successfully +exit /b 0 diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile index 7341b1e9b..8be8200ee 100755 --- a/docker/staging/server.Dockerfile +++ b/docker/staging/server.Dockerfile @@ -17,6 +17,8 @@ COPY ./server ./ RUN npm run build +RUN cp -r src/templates dist/templates + EXPOSE 52345 CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/unified/build_images.bat b/docker/unified/build_images.bat new file mode 100644 index 000000000..a3d54e60d --- /dev/null +++ b/docker/unified/build_images.bat @@ -0,0 +1,33 @@ +@echo off +setlocal enabledelayedexpansion + +REM Change directory to root Server directory for correct Docker Context +cd /d "%~dp0" +cd ../.. + +REM Define service names and their corresponding Dockerfiles +set services[0]=uptime_client +set dockerfiles[0]=.\docker\unified\client.Dockerfile + +set services[1]=uptime_database_mongo +set dockerfiles[1]=.\docker\unified\mongoDB.Dockerfile + +set services[2]=uptime_server +set dockerfiles[2]=.\docker\unified\server.Dockerfile + +REM Loop through each service and build the corresponding image +for /L %%i in (0,1,2) do ( + set service=!services[%%i]! + set dockerfile=!dockerfiles[%%i]! + + docker build -f "!dockerfile!" -t "!service!" . + + REM Check if the build succeeded + if errorlevel 1 ( + echo Error building !service! image. Exiting... + exit /b 1 + ) +) + +echo All images built successfully +exit /b 0 diff --git a/docker/unified/build_images.sh b/docker/unified/build_images.sh new file mode 100755 index 000000000..21b866bbe --- /dev/null +++ b/docker/unified/build_images.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Change directory to root directory for correct Docker Context +cd "$(dirname "$0")" +cd ../../ + +# Define an array of services and their Dockerfiles +declare -A services=( + ["uptime_client"]="./docker/unified/client.Dockerfile" + ["uptime_database_mongo"]="./docker/unified/mongoDB.Dockerfile" + ["uptime_server"]="./docker/unified/server.Dockerfile" +) + +# Loop through each service and build the corresponding image +for service in "${!services[@]}"; do + docker build -f "${services[$service]}" -t "$service" . + + # Check if the build succeeded + if [ $? -ne 0 ]; then + echo "Error building $service image. Exiting..." + exit 1 + fi +done + +echo "All images built successfully" diff --git a/docker/unified/certbot-compose.yaml b/docker/unified/certbot-compose.yaml new file mode 100755 index 000000000..9220d07a1 --- /dev/null +++ b/docker/unified/certbot-compose.yaml @@ -0,0 +1,19 @@ +version: "3" + +services: + webserver: + image: nginx:latest + ports: + - 80:80 + - 443:443 + restart: always + volumes: + - ./nginx/conf.d/:/etc/nginx/conf.d/:ro + - ./certbot/www/:/var/www/certbot/:ro + certbot: + image: certbot/certbot:latest + volumes: + - ./certbot/www/:/var/www/certbot/:rw + - ./certbot/conf/:/etc/letsencrypt/:rw + depends_on: + - webserver diff --git a/docker/unified/client.Dockerfile b/docker/unified/client.Dockerfile new file mode 100755 index 000000000..b4c685e3e --- /dev/null +++ b/docker/unified/client.Dockerfile @@ -0,0 +1,20 @@ +FROM node:20-slim AS build + +ENV NODE_OPTIONS="--max-old-space-size=4096" + +WORKDIR /app + +COPY ./client/package*.json ./ + +RUN npm install + +COPY ./client . + +RUN npm run build + +FROM nginx:1.27.1-alpine + +COPY --from=build /app/dist /usr/share/nginx/html +COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh +RUN chmod +x /docker-entrypoint.d/env.sh +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/docker/unified/docker-compose.yaml b/docker/unified/docker-compose.yaml new file mode 100755 index 000000000..199d0da21 --- /dev/null +++ b/docker/unified/docker-compose.yaml @@ -0,0 +1,114 @@ +# sudo certbot certonly --standalone --http-01-port 8080 +# cerbot needs to run on 8080 since port 80 is used by the gateway + +services: + # Gateway (Single Entry Point) + gateway: + image: nginx:1.27.1-alpine + container_name: checkmate-gateway + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - /etc/letsencrypt:/etc/nginx/ssl:ro + - ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro + + # Production Stack ----- + client-prod: + image: ghcr.io/bluewave-labs/checkmate:frontend-demo + container_name: client-prod + restart: always + environment: + UPTIME_APP_API_BASE_URL: "https://demo.checkmate.so/api/v1" + UPTIME_APP_CLIENT_HOST: "https://demo.checkmate.so" + expose: + - "80" + depends_on: + - server-prod + volumes: + - ./nginx/conf.d/prod.conf:/etc/nginx/conf.d/default.conf:ro + + server-prod: + image: ghcr.io/bluewave-labs/checkmate:backend-demo + container_name: server-prod + restart: always + env_file: + - server-prod.env + depends_on: + - mongodb-prod + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + mongodb-prod: + image: ghcr.io/bluewave-labs/checkmate:mongo-demo + container_name: mongodb-prod + restart: always + command: [ "mongod", "--quiet", "--bind_ip_all" ] + volumes: + - ./mongo/data-prod:/data/db + env_file: + - mongo-prod.env + healthcheck: + test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet" ] + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 + + # Staging Stack ----- + client-staging: + image: ghcr.io/bluewave-labs/checkmate:frontend-staging + container_name: client-staging + restart: always + environment: + UPTIME_APP_API_BASE_URL: "https://staging.checkmate.so/api/v1" + UPTIME_APP_CLIENT_HOST: "https://staging.checkmate.so" + expose: + - "80" + depends_on: + - server-staging + volumes: + - ./nginx/conf.d/staging.conf:/etc/nginx/conf.d/default.conf:ro + + server-staging: + image: ghcr.io/bluewave-labs/checkmate:backend-staging + container_name: server-staging + restart: always + env_file: + - server-staging.env + depends_on: + - mongodb-staging + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + mongodb-staging: + image: ghcr.io/bluewave-labs/checkmate:mongo-staging + container_name: mongodb-staging + restart: always + command: [ "mongod", "--quiet", "--bind_ip_all" ] + volumes: + - ./mongo/data-staging:/data/db + env_file: + - mongo-staging.env + healthcheck: + test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet" ] + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 + + # Shared Utils + capture: + image: ghcr.io/bluewave-labs/capture:latest + container_name: capture + restart: unless-stopped + ports: + - "59232:59232" + environment: + - API_SECRET=my_secret + - GIN_MODE=release + volumes: + - /etc/os-release:/etc/os-release:ro diff --git a/docker/unified/gateway/nginx.conf b/docker/unified/gateway/nginx.conf new file mode 100644 index 000000000..47ad4695e --- /dev/null +++ b/docker/unified/gateway/nginx.conf @@ -0,0 +1,55 @@ +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; + + +server { + listen 80; + server_name demo.checkmate.so staging.checkmate.so; + + location /.well-known/acme-challenge/ { + # Forward the challenge to Certbot listening on 8080 + proxy_pass http://172.17.0.1:8080; + } + + location / { + return 301 https://$host$request_uri; + } +} + + +# --- PRODUCTION: demo.checkmate.so --- +server { + listen 443 ssl; + server_name demo.checkmate.so; + + ssl_certificate /etc/nginx/ssl/live/demo.checkmate.so/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/demo.checkmate.so/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + # Passes traffic to the prod client on port 80 + proxy_pass http://client-prod:80; + } +} + +# --- STAGING: staging.checkmate.so --- + + server { + listen 443 ssl; + server_name staging.checkmate.so; + + ssl_certificate /etc/nginx/ssl/live/staging.checkmate.so/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/staging.checkmate.so/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + # Passes traffic to the staging client on port 80 + proxy_pass http://client-staging:80; + } + } diff --git a/docker/unified/mongoDB.Dockerfile b/docker/unified/mongoDB.Dockerfile new file mode 100755 index 000000000..969a320c2 --- /dev/null +++ b/docker/unified/mongoDB.Dockerfile @@ -0,0 +1,3 @@ +FROM mongo +EXPOSE 27017 +CMD ["mongod"] diff --git a/docker/unified/nginx/conf.d/prod.conf b/docker/unified/nginx/conf.d/prod.conf new file mode 100755 index 000000000..63a42a401 --- /dev/null +++ b/docker/unified/nginx/conf.d/prod.conf @@ -0,0 +1,31 @@ +server { + listen 80; + listen [::]:80; + + server_name _; + server_tokens off; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://server-prod:52345/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api-docs/ { + proxy_pass http://server-prod:52345/api-docs/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/docker/unified/nginx/conf.d/staging.conf b/docker/unified/nginx/conf.d/staging.conf new file mode 100755 index 000000000..6b640a06b --- /dev/null +++ b/docker/unified/nginx/conf.d/staging.conf @@ -0,0 +1,31 @@ +server { + listen 80; + listen [::]:80; + + server_name _; + server_tokens off; + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://server-staging:52345/api/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api-docs/ { + proxy_pass http://server-staging:52345/api-docs/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/docker/unified/server.Dockerfile b/docker/unified/server.Dockerfile new file mode 100755 index 000000000..7341b1e9b --- /dev/null +++ b/docker/unified/server.Dockerfile @@ -0,0 +1,22 @@ +FROM node:20-slim + +# Install ping +RUN apt-get update \ + && apt-get install -y iputils-ping \ + && rm -rf /var/lib/apt/lists/* + +ENV NODE_OPTIONS="--max-old-space-size=2048" + +WORKDIR /app + +COPY ./server/package*.json ./ + +RUN npm install + +COPY ./server ./ + +RUN npm run build + +EXPOSE 52345 + +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docs/i18n-contribution.md b/docs/i18n-contribution.md new file mode 100644 index 000000000..6dbec34b0 --- /dev/null +++ b/docs/i18n-contribution.md @@ -0,0 +1,31 @@ +# Contributing Translations + +Checkmate supports multiple languages. This guide explains how to contribute translations. + +## How to Add a New Language + +1. Create a new JSON file in `client/src/locales/` named with the language code (e.g., `it.json` for Italian) + +2. Copy the structure from `en.json` and translate all values + +3. Add the language to the language selector in the UI + +## Translation Guidelines + +- en.json is the reference localization file, all keys should be declared here and mirrored in other localization files +- Translate only the values +- Use formal/informal tone consistently +- Test your translations in the UI + +## File Structure +client/src/locales/ +├── en.json # Source language +├── es.json # Spanish +├── de.json # German +└── [lang].json # Your language +## Testing Translations + +1. Start the development server: `npm run dev` +2. Go to Settings → Language +3. Select your language +4. Verify all strings display correctly diff --git a/server/README.md b/server/README.md index db609cb02..09e055d02 100755 --- a/server/README.md +++ b/server/README.md @@ -8,7 +8,7 @@ Checkmate's backend is designed to be lightweight and scalable, ensuring reliabl ## Installation & documentation -For setup instructions, configuration details, and deployment guidelines, please visit our official documentation at [docs.checkmate.so](https://docs.checkmate.so). +For setup instructions, configuration details, and deployment guidelines, please visit our official documentation at [docs.checkmate.so](https://checkmate.so/docs). ## Related repositories diff --git a/server/eslint.config.js b/server/eslint.config.js index c854029a0..817f54bc2 100755 --- a/server/eslint.config.js +++ b/server/eslint.config.js @@ -32,11 +32,14 @@ export default [ languageOptions: { parser: tseslint.parser, parserOptions: { - projectService: true, + projectService: { + allowDefaultProject: ["jest.config.ts"], + }, tsconfigRootDir: import.meta.dirname, }, }, rules: { + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore unused variables that start with '_' "@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types "mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing }, diff --git a/server/jest.config.ts b/server/jest.config.ts index fd0c87ad7..be7d359b1 100644 --- a/server/jest.config.ts +++ b/server/jest.config.ts @@ -9,8 +9,7 @@ const config: Config = { }, moduleNameMapper: { "^@/validation/(.*)\\.js$": "/src/validation/$1.js", - "^@/utils/(AppError)\\.js$": "/src/utils/$1.ts", - "^@/utils/(.*)\\.js$": "/src/utils/$1.js", + "^@/utils/(.*)\\.js$": "/src/utils/$1.ts", "^@/(.*)\\.ts$": "/src/$1.ts", "^@/(.*)\\.js$": "/src/$1.ts", "^@/(.*)$": "/src/$1", diff --git a/server/openapi.json b/server/openapi.json index 1a6bbbfbf..35d109693 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -510,42 +510,24 @@ "summary": "Get monitors by team", "description": "Get monitors filtered by team with optional parameters", "parameters": [ - { - "name": "status", - "in": "query", - "description": "Filter by monitor status (up/down)", - "schema": { - "type": "boolean" - } - }, { "name": "type", "in": "query", "description": "Filter by monitor type", "schema": { - "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] - } - }, - { - "name": "page", - "in": "query", - "description": "Page number for pagination", - "schema": { - "type": "integer", - "minimum": 1, - "default": 1 - } - }, - { - "name": "rowsPerPage", - "in": "query", - "description": "Number of monitors per page", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100, - "default": 10 + "oneOf": [ + { + "type": "string", + "enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"] + } + } + ] } }, { @@ -555,15 +537,6 @@ "schema": { "type": "string" } - }, - { - "name": "field", - "in": "query", - "description": "Field to filter on", - "schema": { - "type": "string", - "enum": ["name", "url", "description"] - } } ], "responses": { @@ -756,7 +729,7 @@ } ] }, - "put": { + "patch": { "tags": ["monitors"], "summary": "Update monitor", "description": "Update an existing monitor", @@ -3568,22 +3541,19 @@ }, "CreateMonitorRequest": { "type": "object", - "required": ["name", "description", "type", "url", "interval"], + "required": ["name", "type", "url"], "properties": { "name": { "type": "string", - "minLength": 1, - "maxLength": 100, "example": "My Website Monitor" }, "description": { "type": "string", - "maxLength": 500, "example": "Monitors the main website homepage" }, "type": { "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"], + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"], "example": "http" }, "url": { @@ -3593,14 +3563,34 @@ }, "interval": { "type": "integer", - "minimum": 30, - "maximum": 3600, "example": 300, "description": "Check interval in seconds" }, + "statusWindowSize": { + "type": "integer", + "minimum": 1, + "maximum": 20, + "default": 5 + }, + "statusWindowThreshold": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 60 + }, "isActive": { + "type": "boolean" + }, + "ignoreTlsErrors": { "type": "boolean", - "default": true + "default": false + }, + "useAdvancedMatching": { + "type": "boolean", + "default": false + }, + "port": { + "type": "integer" }, "notifications": { "type": "array", @@ -3609,51 +3599,62 @@ }, "description": "Array of notification IDs to associate with this monitor" }, - "httpOptions": { - "type": "object", - "properties": { - "method": { - "type": "string", - "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"], - "default": "GET" - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "string", - "description": "Request body for POST/PUT requests" - }, - "timeout": { - "type": "integer", - "minimum": 1000, - "maximum": 30000, - "default": 5000, - "description": "Request timeout in milliseconds" - } - } + "secret": { + "type": "string" }, - "assertions": { + "jsonPath": { + "type": "string" + }, + "expectedValue": { + "type": "string" + }, + "matchMethod": { + "type": "string", + "enum": ["equal", "include", "regex", ""] + }, + "cpuAlertThreshold": { + "type": "number" + }, + "memoryAlertThreshold": { + "type": "number" + }, + "diskAlertThreshold": { + "type": "number" + }, + "tempAlertThreshold": { + "type": "number" + }, + "gameId": { + "type": "string" + }, + "grpcServiceName": { + "type": "string", + "default": "" + }, + "selectedDisks": { "type": "array", "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["status-code", "response-time", "body-contains", "header-contains"] - }, - "comparison": { - "type": "string", - "enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"] - }, - "value": { - "type": "string" - } - } + "type": "string" } + }, + "group": { + "type": "string", + "nullable": true, + "maxLength": 50 + }, + "geoCheckEnabled": { + "type": "boolean" + }, + "geoCheckLocations": { + "type": "array", + "items": { + "type": "string", + "enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"] + } + }, + "geoCheckInterval": { + "type": "integer", + "minimum": 300000 } } }, @@ -3661,21 +3662,32 @@ "type": "object", "properties": { "name": { + "type": "string" + }, + "type": { "type": "string", - "minLength": 1, - "maxLength": 100 + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] + }, + "url": { + "type": "string" + }, + "statusWindowSize": { + "type": "integer", + "minimum": 1, + "maximum": 20, + "default": 5 + }, + "statusWindowThreshold": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 60 }, "description": { - "type": "string", - "maxLength": 500 + "type": "string" }, "interval": { - "type": "integer", - "minimum": 30, - "maximum": 3600 - }, - "isActive": { - "type": "boolean" + "type": "integer" }, "notifications": { "type": "array", @@ -3683,47 +3695,72 @@ "type": "string" } }, - "httpOptions": { - "type": "object", - "properties": { - "method": { - "type": "string", - "enum": ["GET", "POST", "PUT", "DELETE", "HEAD"] - }, - "headers": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "body": { - "type": "string" - }, - "timeout": { - "type": "integer", - "minimum": 1000, - "maximum": 30000 - } - } + "secret": { + "type": "string" }, - "assertions": { + "ignoreTlsErrors": { + "type": "boolean" + }, + "useAdvancedMatching": { + "type": "boolean" + }, + "jsonPath": { + "type": "string" + }, + "expectedValue": { + "type": "string" + }, + "matchMethod": { + "type": "string", + "enum": ["equal", "include", "regex", ""] + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "cpuAlertThreshold": { + "type": "number" + }, + "memoryAlertThreshold": { + "type": "number" + }, + "diskAlertThreshold": { + "type": "number" + }, + "tempAlertThreshold": { + "type": "number" + }, + "gameId": { + "type": "string" + }, + "grpcServiceName": { + "type": "string" + }, + "selectedDisks": { "type": "array", "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["status-code", "response-time", "body-contains", "header-contains"] - }, - "comparison": { - "type": "string", - "enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"] - }, - "value": { - "type": "string" - } - } + "type": "string" } + }, + "group": { + "type": "string", + "nullable": true, + "maxLength": 50 + }, + "geoCheckEnabled": { + "type": "boolean" + }, + "geoCheckLocations": { + "type": "array", + "items": { + "type": "string", + "enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"] + } + }, + "geoCheckInterval": { + "type": "integer", + "minimum": 300000 } } }, diff --git a/server/package-lock.json b/server/package-lock.json index 5d9d920ee..8423aa8d0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -14,7 +14,6 @@ "@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", @@ -22,7 +21,7 @@ "dockerode": "4.0.6", "dotenv": "^16.4.5", "express": "^4.19.2", - "express-rate-limit": "8.0.1", + "express-rate-limit": "8.3.2", "gamedig": "5.2.0", "got": "14.4.7", "handlebars": "^4.7.8", @@ -36,11 +35,11 @@ "mjml": "^5.0.0-alpha.4", "mongoose": "^8.3.3", "multer": "^1.4.5-lts.1", - "nodemailer": "8.0.1", + "nodemailer": "8.0.5", "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", - "super-simple-scheduler": "1.4.5", + "super-simple-scheduler": "1.9.1", "swagger-ui-express": "5.0.1", "winston": "^3.13.0", "ws": "^8.19.0", @@ -67,7 +66,6 @@ "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", - "c8": "10.1.3", "eslint": "^9.17.0", "eslint-plugin-mocha": "^10.5.0", "esm": "3.2.25", @@ -799,40 +797,20 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz", - "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz", + "integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.0", - "fast-xml-parser": "5.4.1", + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", - "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "fast-xml-builder": "^1.0.0", - "strnum": "^2.1.2" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", @@ -873,6 +851,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1375,16 +1354,6 @@ "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", "license": "Apache-2.0" }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -1502,6 +1471,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1546,6 +1516,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3195,84 +3166,6 @@ "sparse-bitfield": "^3.0.3" } }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", - "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", - "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", - "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", - "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", - "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", - "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -3850,9 +3743,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz", - "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4337,6 +4230,7 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4497,6 +4391,7 @@ "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" } @@ -4734,6 +4629,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -4942,9 +4838,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5347,6 +5243,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5544,14 +5441,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", + "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" } }, "node_modules/babel-jest": { @@ -5884,9 +5781,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -5925,6 +5822,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6016,21 +5914,6 @@ "node": ">=10.0.0" } }, - "node_modules/bullmq": { - "version": "5.41.2", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.2.tgz", - "integrity": "sha512-wqsUIHW2Td86mTKTepqQpKLLUtP4gmX89bUO1YL2fAorxwj3da1GYtroGZMCg/zgB/+zMRsbylL6DHyMUWX7fA==", - "license": "MIT", - "dependencies": { - "cron-parser": "^4.9.0", - "ioredis": "^5.4.1", - "msgpackr": "^1.11.2", - "node-abort-controller": "^3.1.1", - "semver": "^7.5.4", - "tslib": "^2.0.0", - "uuid": "^9.0.0" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -6051,40 +5934,6 @@ "node": ">= 0.8" } }, - "node_modules/c8": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", - "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.1", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^3.1.1", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.6", - "test-exclude": "^7.0.1", - "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "monocart-coverage-reports": "^2" - }, - "peerDependenciesMeta": { - "monocart-coverage-reports": { - "optional": true - } - } - }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -6867,6 +6716,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" @@ -7312,13 +7162,10 @@ } }, "node_modules/dompurify": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", - "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", + "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", "license": "(MPL-2.0 OR Apache-2.0)", - "engines": { - "node": ">=20" - }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -7634,6 +7481,7 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7995,6 +7843,7 @@ "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", @@ -8037,12 +7886,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz", - "integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", + "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "10.1.0" }, "engines": { "node": ">= 16" @@ -8127,9 +7976,9 @@ "license": "MIT" }, "node_modules/fast-xml-builder": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", - "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "dev": true, "funding": [ { @@ -8137,7 +7986,31 @@ "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT" + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } }, "node_modules/fastq": { "version": "1.20.1", @@ -8278,9 +8151,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -8291,9 +8164,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -8727,9 +8600,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8829,9 +8702,9 @@ "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "license": "MIT", "dependencies": { "minimist": "^1.2.5", @@ -9210,9 +9083,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -9639,6 +9512,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10130,9 +10004,9 @@ } }, "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==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -10667,9 +10541,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -11211,9 +11085,9 @@ } }, "node_modules/mjml-cli/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -11703,37 +11577,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/msgpackr": { - "version": "1.11.5", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz", - "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==", - "license": "MIT", - "optionalDependencies": { - "msgpackr-extract": "^3.0.2" - } - }, - "node_modules/msgpackr-extract": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", - "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-gyp-build-optional-packages": "5.2.2" - }, - "bin": { - "download-msgpackr-prebuilds": "bin/download-prebuilds.js" - }, - "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" - } - }, "node_modules/multer": { "version": "1.4.5-lts.2", "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", @@ -11843,12 +11686,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, - "node_modules/node-abort-controller": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", - "license": "MIT" - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -11911,21 +11748,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/node-gyp-build-optional-packages": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", - "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.1" - }, - "bin": { - "node-gyp-build-optional-packages": "bin.js", - "node-gyp-build-optional-packages-optional": "optional.js", - "node-gyp-build-optional-packages-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -11940,9 +11762,9 @@ "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz", - "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==", + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", + "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -12307,6 +12129,22 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.1.tgz", + "integrity": "sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -12343,9 +12181,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/path-type": { @@ -12365,9 +12203,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -12509,6 +12347,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13162,9 +13001,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -13199,10 +13038,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/pstree.remy": { "version": "1.1.8", @@ -14344,9 +14186,9 @@ } }, "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", "dev": true, "funding": [ { @@ -14373,25 +14215,12 @@ } }, "node_modules/super-simple-scheduler": { - "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==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.9.1.tgz", + "integrity": "sha512-JSiEQLk7MsnlSkJ87uoyFKIseG2N27ikB0FSxlh25nnQHWC7R5Lrd0VcoLpwSBanyLzlAslhK4W2rQ0mCU8jCQ==", "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" + "uuid": "11.1.0" } }, "node_modules/super-simple-scheduler/node_modules/uuid": { @@ -14407,28 +14236,6 @@ "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", @@ -14516,47 +14323,6 @@ "node": ">=6" } }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -14599,11 +14365,12 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -14772,6 +14539,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14846,6 +14614,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, "license": "0BSD" }, "node_modules/tsx": { @@ -14934,6 +14703,7 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14987,9 +14757,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", - "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "license": "MIT", "engines": { "node": ">=18.17" @@ -15657,9 +15427,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { diff --git a/server/package.json b/server/package.json index 7bceee766..4251a2f9d 100755 --- a/server/package.json +++ b/server/package.json @@ -5,10 +5,13 @@ "main": "index.js", "type": "module", "scripts": { - "test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand", + "test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage", + "test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/**/*.ts' test/unit", + "test:unit:services": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/service/**/*.ts' test/unit/services test/unit/providers/network test/unit/providers/notifications", + "test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --no-coverage test/integration", "dev": "nodemon --exec tsx src/index.js", "start": "node --watch ./dist/index.js", - "build": "tsc && tsc-alias", + "build": "tsc && tsc-alias && cp -r src/templates dist/templates", "lint": "eslint .", "lint-fix": "eslint --fix .", "format": "prettier --write .", @@ -29,7 +32,6 @@ "@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", @@ -37,7 +39,7 @@ "dockerode": "4.0.6", "dotenv": "^16.4.5", "express": "^4.19.2", - "express-rate-limit": "8.0.1", + "express-rate-limit": "8.3.2", "gamedig": "5.2.0", "got": "14.4.7", "handlebars": "^4.7.8", @@ -51,11 +53,11 @@ "mjml": "^5.0.0-alpha.4", "mongoose": "^8.3.3", "multer": "^1.4.5-lts.1", - "nodemailer": "8.0.1", + "nodemailer": "8.0.5", "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", - "super-simple-scheduler": "1.4.5", + "super-simple-scheduler": "1.9.1", "swagger-ui-express": "5.0.1", "winston": "^3.13.0", "ws": "^8.19.0", @@ -82,7 +84,6 @@ "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", - "c8": "10.1.3", "eslint": "^9.17.0", "eslint-plugin-mocha": "^10.5.0", "esm": "3.2.25", diff --git a/server/src/app.ts b/server/src/app.ts index 58684532c..da1cd8ad9 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -4,7 +4,7 @@ import cors from "cors"; import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; -import swaggerUi from "swagger-ui-express"; +import swaggerUi, { type JsonObject } from "swagger-ui-express"; import { handleErrors } from "@/middleware/handleErrors.js"; import { generalApiLimiter } from "@/middleware/rateLimiter.js"; import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js"; @@ -24,7 +24,7 @@ export const createApp = ({ controllers: InitializedControllers; envSettings: EnvConfig; frontendPath: string; - openApiSpec: any; + openApiSpec: JsonObject; }) => { const allowedOrigin = envSettings.clientHost; const app = express(); diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 98e6ded8e..00769f545 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -31,7 +31,7 @@ export interface InitializedControllers { export const initializeControllers = (services: InitializedServices): InitializedControllers => { return { authController: new AuthController(services.userService), - monitorController: new MonitorController(services.monitorService), + monitorController: new MonitorController(services.monitorService, services.notificationsService), settingsController: new SettingsController(services.settingsService, services.emailService), checkController: new CheckController(services.checkService), geoCheckController: new GeoCheckController(services.geoChecksService), diff --git a/server/src/config/routes.ts b/server/src/config/routes.ts index 672866dc1..f36f56e16 100644 --- a/server/src/config/routes.ts +++ b/server/src/config/routes.ts @@ -1,6 +1,9 @@ +import type { Application } from "express"; import { createVerifyJWT } from "../middleware/verifyJWT.js"; import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js"; import { authApiLimiter } from "../middleware/rateLimiter.js"; +import type { InitializedControllers } from "./controllers.js"; +import type { InitializedServices } from "./services.js"; import AuthRoutes from "../routes/authRoute.js"; import InviteRoutes from "../routes/inviteRoute.js"; @@ -17,7 +20,7 @@ import NotificationRoutes from "../routes/notificationRoute.js"; import IncidentRoutes from "../routes/incidentRoute.js"; -export const setupRoutes = (app: any, controllers: Record, services: Record) => { +export const setupRoutes = (app: Application, controllers: InitializedControllers, services: InitializedServices) => { const verifyJWT = createVerifyJWT(services.settingsService); const authRoutes = new AuthRoutes(controllers.authController, verifyJWT); const monitorRoutes = new MonitorRoutes(controllers.monitorController); diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 65377eeb4..c7e1a4469 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -27,6 +27,10 @@ import { DiscordProvider, PagerDutyProvider, MatrixProvider, + TeamsProvider, + TelegramProvider, + PushoverProvider, + TwilioProvider, // Interfaces INetworkService, IEmailService, @@ -45,6 +49,7 @@ import { IIncidentService, INotificationMessageBuilder, ISettingsService, + SettingsService, EnvConfig, } from "@/service/index.js"; @@ -91,9 +96,10 @@ import { MongoInvitesRepository, MongoRecoveryTokensRepository, MongoNotificationsRepository, - MongoIncidentRepository, + MongoIncidentsRepository, MongoTeamsRepository, MongoMaintenanceWindowsRepository, + MongoSettingsRepository, IMonitorsRepository, IChecksRepository, IGeoChecksRepository, @@ -109,6 +115,7 @@ import { IMaintenanceWindowsRepository, } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; +import { AppError } from "@/utils/AppError.js"; export type InitializedServices = { settingsService: ISettingsService; @@ -151,32 +158,63 @@ export const initializeServices = async ({ logger, envSettings, settingsService, - settingsRepository, }: { logger: ILogger; envSettings: EnvConfig; settingsService: ISettingsService; - settingsRepository: ISettingsRepository; }): Promise => { // Create DB - const db = new MongoDB(logger, envSettings); + const dbType = envSettings.dbType; + + let db: IDb | null = null; + + if (dbType === "mongodb") { + db = new MongoDB(logger, envSettings); + } + + if (!db) { + throw new AppError({ message: "Unsupported database type", status: 500 }); + } await db.connect(); + let monitorsRepository: IMonitorsRepository; + let checksRepository: IChecksRepository; + let geoChecksRepository: IGeoChecksRepository; + let monitorStatsRepository: IMonitorStatsRepository; + let statusPagesRepository: IStatusPagesRepository; + let usersRepository: IUsersRepository; + let invitesRepository: IInvitesRepository; + let recoveryTokensRepository: IRecoveryTokensRepository; + let settingsRepository: ISettingsRepository; + let notificationsRepository: INotificationsRepository; + let incidentsRepository: IIncidentsRepository; + let teamsRepository: ITeamsRepository; + let maintenanceWindowsRepository: IMaintenanceWindowsRepository; + // Repositories - const monitorsRepository = new MongoMonitorsRepository(); - const checksRepository = new MongoChecksRepository(logger); - const geoChecksRepository = new MongoGeoChecksRepository(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(); + + if (dbType === "mongodb") { + monitorsRepository = new MongoMonitorsRepository(); + checksRepository = new MongoChecksRepository(logger); + geoChecksRepository = new MongoGeoChecksRepository(logger); + monitorStatsRepository = new MongoMonitorStatsRepository(); + statusPagesRepository = new MongoStatusPagesRepository(); + usersRepository = new MongoUsersRepository(); + invitesRepository = new MongoInvitesRepository(); + recoveryTokensRepository = new MongoRecoveryTokensRepository(); + settingsRepository = new MongoSettingsRepository(); + notificationsRepository = new MongoNotificationsRepository(); + incidentsRepository = new MongoIncidentsRepository(); + teamsRepository = new MongoTeamsRepository(); + maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); + } else { + throw new AppError({ message: "Unsupported database type", status: 500 }); + } + + // Inject settings repository into settings service (now that DB is connected) + (settingsService as SettingsService).setRepository(settingsRepository); // Network providers const pingProvider = new PingProvider(ping); @@ -228,6 +266,10 @@ export const initializeServices = async ({ const discordProvider = new DiscordProvider(logger); const pagerDutyProvider = new PagerDutyProvider(logger); const matrixProvider = new MatrixProvider(logger); + const teamsProvider = new TeamsProvider(logger); + const telegramProvider = new TelegramProvider(logger); + const pushoverProvider = new PushoverProvider(logger); + const twilioProvider = new TwilioProvider(logger); const notificationsService = new NotificationsService( notificationsRepository, @@ -238,6 +280,10 @@ export const initializeServices = async ({ discordProvider, pagerDutyProvider, matrixProvider, + teamsProvider, + telegramProvider, + pushoverProvider, + twilioProvider, settingsService, logger, notificationMessageBuilder @@ -249,6 +295,7 @@ export const initializeServices = async ({ statusService, notificationsService, checkService, + settingsService, bufferService, incidentService, maintenanceWindowsRepository, @@ -291,7 +338,6 @@ export const initializeServices = async ({ }); const monitorService = new MonitorService({ jobQueue: superSimpleQueue, - emailService, logger, games, monitorsRepository, diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index f196e4dd5..20303b4eb 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -1,7 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { AppError } from "@/utils/AppError.js"; -import { requireTeamId, requireUserRoles } from "./controllerUtils.js"; -import type { UserRole } from "@/types/user.js"; +import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/controllers/controllerUtils.js"; import { registrationBodyValidation, @@ -20,15 +19,33 @@ import { editSuperadminUserByIdBodyValidation, editUserPasswordByIdBodyValidation, } from "@/validation/userValidation.js"; +import { IUserService } from "@/service/index.js"; const SERVICE_NAME = "authController"; -class AuthController { +export interface IAuthController { + registerUser(req: Request, res: Response, next: NextFunction): Promise; + createUser(req: Request, res: Response, next: NextFunction): Promise; + loginUser(req: Request, res: Response, next: NextFunction): Promise; + editUser(req: Request, res: Response, next: NextFunction): Promise; + checkSuperadminExists(req: Request, res: Response, next: NextFunction): Promise; + requestRecovery(req: Request, res: Response, next: NextFunction): Promise; + validateRecovery(req: Request, res: Response, next: NextFunction): Promise; + resetPassword(req: Request, res: Response, next: NextFunction): Promise; + deleteUser(req: Request, res: Response, next: NextFunction): Promise; + deleteUserById(req: Request, res: Response, next: NextFunction): Promise; + getAllUsers(req: Request, res: Response, next: NextFunction): Promise; + getUserById(req: Request, res: Response, next: NextFunction): Promise; + editUserById(req: Request, res: Response, next: NextFunction): Promise; + editUserPasswordById(req: Request, res: Response, next: NextFunction): Promise; +} + +class AuthController implements IAuthController { static SERVICE_NAME = SERVICE_NAME; - private userService: any; + private userService: IUserService; - constructor(userService: any) { + constructor(userService: IUserService) { this.userService = userService; } @@ -41,11 +58,12 @@ class AuthController { const newUser = req.body.user; const newUserToken = req.body.token; if (newUser?.email) { - newUser.email = newUser.email.toLowerCase(); + const newUserEmail = requireUserEmail(newUser.email); + newUser.email = newUserEmail.toLowerCase(); } - registrationBodyValidation.parse(newUser); + const validatedBody = registrationBodyValidation.parse(newUser); - const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); + const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null); res.status(200).json({ success: true, msg: "User registered successfully", @@ -62,11 +80,11 @@ class AuthController { if (userData?.email) { userData.email = userData.email.toLowerCase(); } - createUserBodyValidation.parse(userData); + const validatedBody = createUserBodyValidation.parse(userData); const teamId = requireTeamId(req.user?.teamId); - const actorRoles = requireUserRoles(req.user?.role) as UserRole[]; - const newUser = await this.userService.createUser(userData, teamId, actorRoles, req.file); + const actorRoles = requireUserRoles(req.user?.role); + const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null); res.status(201).json({ success: true, msg: "User created successfully", @@ -100,8 +118,10 @@ class AuthController { editUser = async (req: Request, res: Response, next: NextFunction) => { try { - editUserBodyValidation.parse(req.body); - const updatedUser = await this.userService.editUser(req.body, req.file, req.user); + const validatedBody = editUserBodyValidation.parse(req.body); + const userId = requireUserId(req.user?.id); + const userEmail = requireUserEmail(req.user?.email); + const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail); res.status(200).json({ success: true, @@ -170,7 +190,15 @@ class AuthController { deleteUser = async (req: Request, res: Response, next: NextFunction) => { try { - await this.userService.deleteUser(req.user); + const userId = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); + const roles = requireUserRoles(req.user?.role); + + await this.userService.deleteUser({ + userId, + teamId, + roles, + }); return res.status(200).json({ success: true, msg: "User deleted successfully", @@ -182,9 +210,12 @@ class AuthController { deleteUserById = async (req: Request, res: Response, next: NextFunction) => { try { - getUserByIdParamValidation.parse(req.params); - const targetUserId = req.params.userId; - await this.userService.deleteUserById(req.user, targetUserId); + const validatedParams = getUserByIdParamValidation.parse(req.params); + const targetUserId = validatedParams.userId; + const actorId = requireUserId(req.user?.id); + const actorTeamId = requireTeamId(req.user?.teamId); + const actorRoles = requireUserRoles(req.user?.role); + await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId }); return res.status(200).json({ success: true, msg: "User removed successfully", @@ -209,19 +240,9 @@ class AuthController { getUserById = async (req: Request, res: Response, next: NextFunction) => { try { - getUserByIdParamValidation.parse(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); + const validatedParams = getUserByIdParamValidation.parse(req.params); + const actorRoles = requireUserRoles(req.user?.role); + const user = await this.userService.getUserById(actorRoles, validatedParams.userId); return res.status(200).json({ success: true, msg: "ok", data: user }); } catch (error) { @@ -231,24 +252,19 @@ class AuthController { editUserById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = req?.user?.role; + const actorRoles = requireUserRoles(req.user?.role); + const actorId = requireUserId(req.user?.id); - if (!roles || !roles.includes("superadmin")) { + if (!actorRoles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } - const userId = req.params.userId as string; - const user = { ...req.body }; - - editUserByIdParamValidation.parse(req.params); + const validatedParams = editUserByIdParamValidation.parse(req.params); // If this is superadmin self edit, allow "superadmin" role - if (userId === req.user?.id) { - editSuperadminUserByIdBodyValidation.parse(req.body); - } else { - editUserByIdBodyValidation.parse(req.body); - } + const validatedBody = + validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body); - await this.userService.editUserById(userId, user); + await this.userService.editUserById(validatedParams.userId, validatedBody); return res.status(200).json({ success: true, msg: "ok" }); } catch (error) { next(error); @@ -257,16 +273,14 @@ class AuthController { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { try { - const roles = req?.user?.role; - if (!roles || !roles.includes("superadmin")) { + const actorRoles = requireUserRoles(req.user?.role); + if (!actorRoles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } - const userId = req.params.userId as string; - editUserByIdParamValidation.parse(req.params); - editUserPasswordByIdBodyValidation.parse(req.body); - const updatedPassword = req.body.password; - await this.userService.setPasswordByUserId(userId, updatedPassword); + const validatedParams = editUserByIdParamValidation.parse(req.params); + const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body); + await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password); return res.status(200).json({ success: true, msg: "Password reset successfully" }); } catch (error) { next(error); diff --git a/server/src/controllers/checkController.ts b/server/src/controllers/checkController.ts index ec6722bd3..dfe64e80a 100755 --- a/server/src/controllers/checkController.ts +++ b/server/src/controllers/checkController.ts @@ -4,18 +4,28 @@ import { getChecksParamValidation, getChecksQueryValidation, getTeamChecksQueryValidation, + getChecksSummaryByTeamIdQueryValidation, deleteChecksParamValidation, - deleteChecksByTeamIdParamValidation, - updateChecksTTLBodyValidation, } from "@/validation/checkValidation.js"; +import { ICheckService } from "@/service/index.js"; +import { requireTeamId } from "@/controllers/controllerUtils.js"; const SERVICE_NAME = "checkController"; -class CheckController { +export interface ICheckController { + serviceName: string; + getChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise; + getChecksByTeam: (req: Request, res: Response, next: NextFunction) => Promise; + getChecksSummaryByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; + deleteChecks: (req: Request, res: Response, next: NextFunction) => Promise; + deleteChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; +} + +class CheckController implements ICheckController { static SERVICE_NAME = SERVICE_NAME; - private checkService: any; - constructor(checkService: any) { + private checkService: ICheckService; + constructor(checkService: ICheckService) { this.checkService = checkService; } @@ -25,13 +35,20 @@ class CheckController { getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getChecksParamValidation.parse(req.params); - getChecksQueryValidation.parse(req.query); + const validatedParams = getChecksParamValidation.parse(req.params); + const validatedQuery = getChecksQueryValidation.parse(req.query); + + const teamId = requireTeamId(req.user?.teamId); const result = await this.checkService.getChecksByMonitor({ - monitorId: req?.params?.monitorId, - query: req?.query, - teamId: req?.user?.teamId, + monitorId: validatedParams.monitorId, + teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + filter: validatedQuery.filter, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + status: validatedQuery.status, }); return res.status(200).json({ @@ -46,10 +63,16 @@ class CheckController { getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { try { - getTeamChecksQueryValidation.parse(req.query); + const validatedQuery = getTeamChecksQueryValidation.parse(req.query); + const teamId = requireTeamId(req.user?.teamId); + const checkData = await this.checkService.getChecksByTeam({ - teamId: req?.user?.teamId, - query: req?.query, + teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + filter: validatedQuery.filter, }); return res.status(200).json({ success: true, @@ -63,8 +86,11 @@ class CheckController { getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const dateRange = req.query?.dateRange || "hour"; - const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId, dateRange }); + const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query); + const teamId = requireTeamId(req.user?.teamId); + const dateRange = validatedQuery.dateRange ?? "hour"; + + const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange }); return res.status(200).json({ success: true, msg: "Checks summary retrieved successfully", @@ -77,11 +103,12 @@ class CheckController { deleteChecks = async (req: Request, res: Response, next: NextFunction) => { try { - deleteChecksParamValidation.parse(req.params); + const validatedParams = deleteChecksParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); const deletedCount = await this.checkService.deleteChecks({ - monitorId: req.params.monitorId as string, - teamId: req?.user?.teamId, + monitorId: validatedParams.monitorId, + teamId, }); return res.status(200).json({ @@ -96,9 +123,9 @@ class CheckController { deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - deleteChecksByTeamIdParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); - const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); + const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId }); return res.status(200).json({ success: true, @@ -109,24 +136,6 @@ class CheckController { next(error); } }; - - updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => { - try { - updateChecksTTLBodyValidation.parse(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.ts b/server/src/controllers/controllerUtils.ts index 99c60807e..17159a03c 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -1,24 +1,25 @@ import { AppError } from "@/utils/AppError.js"; -import { type MonitorType, MonitorTypes } from "@/types/index.js"; +import { Monitor, type MonitorType, MonitorTypes, UserRole } from "@/types/index.js"; +import sslChecker, { SSLDetails } from "ssl-checker"; +type SSLCheckerType = typeof sslChecker; -const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => { +export const fetchMonitorCertificate = async (checker: SSLCheckerType, monitor: Monitor): 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 + const cert = await checker(hostname); if (cert?.validTo === null || cert?.validTo === undefined) { throw new Error("Certificate not found"); } return cert; }; -const requireString = (value: unknown, fieldName: string): string => { +export 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 => { +export const optionalString = (value: unknown, fieldName: string): string | undefined => { if (value === undefined) { return undefined; } @@ -28,7 +29,7 @@ const optionalString = (value: unknown, fieldName: string): string | undefined = throw new AppError({ message: `${fieldName} must be a string`, status: 400 }); }; -const optionalNumber = (value: unknown, fieldName: string): number | undefined => { +export const optionalNumber = (value: unknown, fieldName: string): number | undefined => { if (value === undefined) { return undefined; } @@ -44,7 +45,7 @@ const optionalNumber = (value: unknown, fieldName: string): number | undefined = throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); }; -const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { +export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { if (value === undefined) { return undefined; } @@ -62,7 +63,7 @@ const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); }; -const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { +export 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 }); @@ -82,7 +83,7 @@ const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | u return parseSingle(value); }; -const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { +export const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { if (value === undefined) { return undefined; } @@ -92,42 +93,36 @@ const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 }); }; -const requireTeamId = (teamId?: string): string => { +export const requireTeamId = (teamId?: string): string => { if (!teamId) { throw new AppError({ message: "Team ID is required", status: 400 }); } return teamId; }; -const requireUserId = (userId?: string): string => { +export const requireUserId = (userId?: string): string => { if (!userId) { throw new AppError({ message: "User ID is required", status: 400 }); } return userId; }; -const requireUserEmail = (userEmail?: string): string => { +export const requireUserEmail = (userEmail?: string): string => { if (!userEmail) { throw new AppError({ message: "User email is required", status: 400 }); } return userEmail; }; -export const requireUserRoles = (userRoles?: string[]): string[] => { +export const requireFirstName = (firstName?: string): string => { + if (!firstName) { + throw new AppError({ message: "First name is required", status: 400 }); + } + return firstName; +}; + +export const requireUserRoles = (userRoles?: UserRole[]): UserRole[] => { if (!userRoles || userRoles.length === 0) { throw new AppError({ message: "User roles are required", status: 400 }); } return userRoles; }; - -export { - fetchMonitorCertificate, - requireString, - optionalString, - optionalNumber, - optionalBoolean, - parseMonitorTypeFilter, - parseSortOrder, - requireTeamId, - requireUserId, - requireUserEmail, -}; diff --git a/server/src/controllers/diagnosticController.ts b/server/src/controllers/diagnosticController.ts index b4d59ceaf..ffe7c6f5f 100755 --- a/server/src/controllers/diagnosticController.ts +++ b/server/src/controllers/diagnosticController.ts @@ -1,12 +1,18 @@ +import { IDiagnosticService } from "@/service/index.js"; import { Request, Response, NextFunction } from "express"; const SERVICE_NAME = "diagnosticController"; -class DiagnosticController { + +export interface IDiagnosticController { + getSystemStats: (req: Request, res: Response, next: NextFunction) => Promise; +} + +class DiagnosticController implements IDiagnosticController { static SERVICE_NAME = SERVICE_NAME; - private diagnosticService: any; + private diagnosticService: IDiagnosticService; - constructor(diagnosticService: any) { + constructor(diagnosticService: IDiagnosticService) { this.diagnosticService = diagnosticService; } diff --git a/server/src/controllers/geoCheckController.ts b/server/src/controllers/geoCheckController.ts index cfa7fe056..96f9beeac 100644 --- a/server/src/controllers/geoCheckController.ts +++ b/server/src/controllers/geoCheckController.ts @@ -1,10 +1,15 @@ import { Request, Response, NextFunction } from "express"; import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js"; import type { IGeoChecksService } from "@/service/business/geoChecksService.js"; +import { requireTeamId } from "./controllerUtils.js"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "geoCheckController"; -class GeoCheckController { +export interface IGeoCheckController { + getGeoChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise; +} +class GeoCheckController implements IGeoCheckController { static SERVICE_NAME = SERVICE_NAME; private geoChecksService: IGeoChecksService; @@ -18,13 +23,19 @@ class GeoCheckController { getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getChecksParamValidation.parse(req.params); - getChecksQueryValidation.parse(req.query); + const validatedParams = getChecksParamValidation.parse(req.params); + const validatedQuery = getChecksQueryValidation.parse(req.query); + + const teamId = requireTeamId(req.user?.teamId); const result = await this.geoChecksService.getGeoChecksByMonitor({ - monitorId: req?.params?.monitorId as string, - query: req?.query, - teamId: req?.user?.teamId as string, + monitorId: validatedParams.monitorId, + teamId: teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + continent: validatedQuery.continent, }); return res.status(200).json({ diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index 0eaba7173..730ab7558 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -1,35 +1,37 @@ import { AppError } from "@/utils/AppError.js"; import { Request, Response, NextFunction } from "express"; import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js"; +import { IIncidentService } from "@/service/index.js"; +import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js"; -const SERVICE_NAME = "incidentController"; +const SERVICE_NAME = "IncidentController"; -class IncidentController { - static SERVICE_NAME = SERVICE_NAME; - - private incidentService: any; - constructor(incidentService: any) { +export interface IIncidentController { + getIncidentsByTeam: (req: Request, res: Response, next: NextFunction) => Promise; + getIncidentSummary: (req: Request, res: Response, next: NextFunction) => Promise; + getIncidentById: (req: Request, res: Response, next: NextFunction) => Promise; + resolveIncidentManually: (req: Request, res: Response, next: NextFunction) => Promise; +} +class IncidentController implements IIncidentController { + private incidentService: IIncidentService; + constructor(incidentService: IIncidentService) { 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 validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query); const teamId = requireTeamId(req.user?.teamId); const result = await this.incidentService.getIncidentsByTeam( teamId, - sortOrder, - dateRange, - page, - rowsPerPage, - status, - monitorId, - resolutionType + validatedQuery.sortOrder, + validatedQuery.dateRange, + validatedQuery.page, + validatedQuery.rowsPerPage, + validatedQuery.status, + validatedQuery.monitorId, + validatedQuery.resolutionType ); return res.status(200).json({ @@ -45,10 +47,9 @@ class IncidentController { getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => { try { const teamId = requireTeamId(req.user?.teamId); + const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query); - const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10; - - const summary = await this.incidentService.getIncidentSummary(teamId, limit); + const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit); return res.status(200).json({ success: true, msg: "Incident summary retrieved successfully", @@ -89,7 +90,8 @@ class IncidentController { throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 }); } - const resolvedIncident = await this.incidentService.resolveIncident(incidentId, userId, teamId, req?.body?.comment, userEmail); + const comment = Array.isArray(req.body.comment) ? req.body.comment[0] : req.body.comment; + const resolvedIncident = await this.incidentService.resolveIncident(incidentId, userId, teamId, comment, userEmail); return res.status(200).json({ success: true, diff --git a/server/src/controllers/inviteController.ts b/server/src/controllers/inviteController.ts index b17d32e98..700a7bc6a 100755 --- a/server/src/controllers/inviteController.ts +++ b/server/src/controllers/inviteController.ts @@ -1,12 +1,19 @@ import { Request, Response, NextFunction } from "express"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/authValidation.js"; -import { requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js"; +import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers/controllerUtils.js"; +import { IInviteService } from "@/service/index.js"; const SERVICE_NAME = "inviteController"; -class InviteController { +export interface IInviteController { + getInviteToken: (req: Request, res: Response, next: NextFunction) => Promise; + sendInviteEmail: (req: Request, res: Response, next: NextFunction) => Promise; + verifyInviteToken: (req: Request, res: Response, next: NextFunction) => Promise; +} + +class InviteController implements IInviteController { static SERVICE_NAME = SERVICE_NAME; - private inviteService: any; - constructor(inviteService: any) { + private inviteService: IInviteService; + constructor(inviteService: IInviteService) { this.inviteService = inviteService; } @@ -34,8 +41,9 @@ class InviteController { sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); - const userRoles = requireUserRoles(req?.user?.role); + const teamId = requireTeamId(req.user?.teamId); + const userRoles = requireUserRoles(req.user?.role); + const firstName = requireFirstName(req.user?.firstName); const inviteRequest = req.body; inviteRequest.teamId = teamId; @@ -43,7 +51,7 @@ class InviteController { const inviteToken = await this.inviteService.sendInviteEmail({ invite: inviteRequest, - firstName: req?.user?.firstName, + firstName, userRoles, }); return res.status(200).json({ diff --git a/server/src/controllers/logController.ts b/server/src/controllers/logController.ts index d99de1e9c..7d221c6c5 100644 --- a/server/src/controllers/logController.ts +++ b/server/src/controllers/logController.ts @@ -1,11 +1,16 @@ +import { ILogger } from "@/utils/logger.js"; import { Request, Response, NextFunction } from "express"; const SERVICE_NAME = "LogController"; +export interface ILogController { + getLogs: (req: Request, res: Response, next: NextFunction) => Promise; +} + class LogController { static SERVICE_NAME = SERVICE_NAME; - private logger: any; - constructor(logger: any) { + private logger: ILogger; + constructor(logger: ILogger) { this.logger = logger; } get serviceName() { @@ -14,7 +19,7 @@ class LogController { getLogs = async (req: Request, res: Response, next: NextFunction) => { try { - const logs = await this.logger.getLogs(); + const logs = this.logger.getLogs(); res.status(200).json({ success: true, msg: "Logs fetched successfully", diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index ed16911bf..441acfa80 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -9,26 +9,37 @@ import { deleteMaintenanceWindowByIdParamValidation, } from "@/validation/maintenanceWindowValidation.js"; import { requireTeamId } from "@/controllers/controllerUtils.js"; +import { IMaintenanceWindowService } from "@/service/index.js"; -const SERVICE_NAME = "maintenanceWindowController"; - -class MaintenanceWindowController { - static SERVICE_NAME = SERVICE_NAME; - private maintenanceWindowService: any; - constructor(maintenanceWindowService: any) { +export interface IMaintenanceWindowController { + createMaintenanceWindows: (req: Request, res: Response, next: NextFunction) => Promise; + getMaintenanceWindowById: (req: Request, res: Response, next: NextFunction) => Promise; + getMaintenanceWindowsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; + getMaintenanceWindowsByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise; + deleteMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise; + editMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise; +} +class MaintenanceWindowController implements IMaintenanceWindowController { + private maintenanceWindowService: IMaintenanceWindowService; + constructor(maintenanceWindowService: IMaintenanceWindowService) { this.maintenanceWindowService = maintenanceWindowService; } - get serviceName() { - return MaintenanceWindowController.SERVICE_NAME; - } - createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { try { - createMaintenanceWindowBodyValidation.parse(req.body); + const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body); const teamId = requireTeamId(req?.user?.teamId); - await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); + const monitorIDs = validatedBody.monitors; + const name = validatedBody.name; + const active = validatedBody.active ?? true; + const duration = validatedBody.duration; + const durationUnit = validatedBody.durationUnit; + const repeat = validatedBody.repeat; + const start = validatedBody.start; + const end = validatedBody.end; + + await this.maintenanceWindowService.createMaintenanceWindow({ teamId, monitorIDs, name, active, duration, durationUnit, repeat, start, end }); return res.status(200).json({ success: true, @@ -40,11 +51,11 @@ class MaintenanceWindowController { }; getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { try { - getMaintenanceWindowByIdParamValidation.parse(req.params); + const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params); const teamId = requireTeamId(req.user?.teamId); - const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id as string, teamId }); + const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId }); return res.status(200).json({ success: true, @@ -58,11 +69,18 @@ class MaintenanceWindowController { getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); + const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); const teamId = requireTeamId(req?.user?.teamId); - const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query }); + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ + teamId, + active: validatedQuery.active, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + field: validatedQuery.field, + order: validatedQuery.order, + }); return res.status(200).json({ success: true, @@ -76,12 +94,12 @@ class MaintenanceWindowController { getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { try { - getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); + const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ - monitorId: req.params.monitorId as string, + monitorId: validatedParams.monitorId, teamId, }); @@ -96,11 +114,11 @@ class MaintenanceWindowController { }; deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { try { - deleteMaintenanceWindowByIdParamValidation.parse(req.params); + const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params); const teamId = requireTeamId(req?.user?.teamId); - await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id as string, teamId }); + await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId }); return res.status(200).json({ success: true, @@ -113,14 +131,14 @@ class MaintenanceWindowController { editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { try { - editMaintenanceWindowByIdParamValidation.parse(req.params); - editMaintenanceByIdWindowBodyValidation.parse(req.body); + const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params); + const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body); const teamId = requireTeamId(req.user?.teamId); const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ - id: req.params.id as string, - body: req.body, + id: validatedParams.id, + body: validatedBody, teamId, }); return res.status(200).json({ diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 1d222e6e2..7f1136b7c 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -1,9 +1,8 @@ import { Request, Response, NextFunction } from "express"; - +import { updateNotificationsValidation } from "@/validation/notificationValidation.js"; import { getMonitorByIdParamValidation, getMonitorByIdQueryValidation, - getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, getMonitorsWithChecksQueryValidation, createMonitorBodyValidation, @@ -12,31 +11,48 @@ import { getCertificateParamValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, + getUptimeDetailsByIdParamValidation, + getUptimeDetailsByIdQueryValidation, + importMonitorsBodyValidation, } from "@/validation/monitorValidation.js"; import sslChecker from "ssl-checker"; -import { - fetchMonitorCertificate, - requireString, - optionalString, - optionalNumber, - optionalBoolean, - parseMonitorTypeFilter, - parseSortOrder, - requireTeamId, - requireUserId, -} from "./controllerUtils.js"; +import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; -import { IMonitorService } from "@/service/index.js"; +import { IMonitorService, INotificationsService } from "@/service/index.js"; import { GeoContinent } from "@/types/geoCheck.js"; const SERVICE_NAME = "monitorController"; -class MonitorController { + +export interface IMonitorController { + getMonitorCertificate: (req: Request, res: Response, next: NextFunction) => Promise; + getUptimeDetailsById: (req: Request, res: Response, next: NextFunction) => Promise; + getHardwareDetailsById: (req: Request, res: Response, next: NextFunction) => Promise; + getPageSpeedDetailsById: (req: Request, res: Response, next: NextFunction) => Promise; + getGeoChecksByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise; + getMonitorById: (req: Request, res: Response, next: NextFunction) => Promise; + createMonitor: (req: Request, res: Response, next: NextFunction) => Promise; + importMonitorsFromJSON: (req: Request, res: Response, next: NextFunction) => Promise; + deleteMonitor: (req: Request, res: Response, next: NextFunction) => Promise; + deleteAllMonitors: (req: Request, res: Response, next: NextFunction) => Promise; + editMonitor: (req: Request, res: Response, next: NextFunction) => Promise; + pauseMonitor: (req: Request, res: Response, next: NextFunction) => Promise; + addDemoMonitors: (req: Request, res: Response, next: NextFunction) => Promise; + getMonitorsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; + getMonitorsWithChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; + exportMonitorsToJSON: (req: Request, res: Response, next: NextFunction) => Promise; + getAllGames: (req: Request, res: Response, next: NextFunction) => Promise; + getGroupsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; + updateNotifications: (req: Request, res: Response, next: NextFunction) => Promise; +} +class MonitorController implements IMonitorController { static SERVICE_NAME = SERVICE_NAME; private monitorService: IMonitorService; + private notificationsService: INotificationsService; - constructor(monitorService: IMonitorService) { + constructor(monitorService: IMonitorService, notificationsService: INotificationsService) { this.monitorService = monitorService; + this.notificationsService = notificationsService; } get serviceName() { @@ -45,10 +61,9 @@ class MonitorController { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { - getCertificateParamValidation.parse(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 validatedParams = getCertificateParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); const certificate = await fetchMonitorCertificate(sslChecker, monitor); return res.status(200).json({ @@ -65,16 +80,18 @@ class MonitorController { 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 validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query); + + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange; + + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getUptimeDetailsById({ teamId, monitorId, dateRange, - normalize, }); return res.status(200).json({ success: true, @@ -88,12 +105,12 @@ class MonitorController { getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - getHardwareDetailsByIdParamValidation.parse(req.params); - getHardwareDetailsByIdQueryValidation.parse(req.query); + const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(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 monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getHardwareDetailsById({ teamId, @@ -112,12 +129,12 @@ class MonitorController { }; getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - getHardwareDetailsByIdParamValidation.parse(req.params); - getHardwareDetailsByIdQueryValidation.parse(req.query); + const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); - const teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getPageSpeedDetailsById({ teamId, @@ -137,18 +154,14 @@ class MonitorController { getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - getMonitorByIdQueryValidation.parse(req.query); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const validatedQuery = getMonitorByIdQueryValidation.parse(req.query); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); - const continentParam = req?.query?.continent; - const continents = continentParam - ? Array.isArray(continentParam) - ? (continentParam as GeoContinent[]) - : [continentParam as GeoContinent] - : undefined; - const teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const continentParam = validatedQuery.continent; + const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined; + const teamId = requireTeamId(req.user?.teamId); const data = await this.monitorService.getGeoChecksByMonitorId({ teamId, @@ -169,11 +182,10 @@ class MonitorController { getMonitorById = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - getMonitorByIdQueryValidation.parse(req.query); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const teamId = requireTeamId(req.user?.teamId); + const monitorId = validatedParams.monitorId; const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); @@ -189,12 +201,12 @@ class MonitorController { createMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - createMonitorBodyValidation.parse(req.body); + const validatedBody = createMonitorBodyValidation.parse(req.body); - const userId = requireString(req?.user?.id, "User ID"); - const teamId = requireTeamId(req?.user?.teamId); + const userId = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); - const monitor = await this.monitorService.createMonitor(teamId, userId, req.body); + const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); return res.status(200).json({ success: true, @@ -208,14 +220,10 @@ class MonitorController { importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); - const userId = requireUserId(req?.user?.id); - - const { monitors } = req.body; - - if (!monitors || !Array.isArray(monitors)) { - throw new AppError({ message: "Invalid request: monitors array is required", status: 400 }); - } + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); + const validatedBody = importMonitorsBodyValidation.parse(req.body); + const monitors = validatedBody.monitors; const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); @@ -231,9 +239,9 @@ class MonitorController { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const teamId = requireTeamId(req?.user?.teamId); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); + const monitorId = validatedParams.monitorId; const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); @@ -249,8 +257,7 @@ class MonitorController { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = requireTeamId(req?.user?.teamId); - + const teamId = requireTeamId(req.user?.teamId); const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); return res.status(200).json({ @@ -264,12 +271,12 @@ class MonitorController { editMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorByIdParamValidation.parse(req.params); - editMonitorBodyValidation.parse(req.body); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const teamId = requireTeamId(req?.user?.teamId); + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const validatedBody = editMonitorBodyValidation.parse(req.body); + const monitorId = validatedParams.monitorId; + const teamId = requireTeamId(req.user?.teamId); - const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); + const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); return res.status(200).json({ success: true, @@ -283,10 +290,10 @@ class MonitorController { pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { try { - pauseMonitorParamValidation.parse(req.params); + const validatedParams = pauseMonitorParamValidation.parse(req.params); - const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const teamId = requireTeamId(req?.user?.teamId); + const monitorId = validatedParams.monitorId; + const teamId = requireTeamId(req.user?.teamId); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); @@ -302,8 +309,8 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const id = requireString(req?.user?.id, "User ID"); - const teamId = requireTeamId(req?.user?.teamId); + const id = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); return res.status(200).json({ @@ -316,32 +323,13 @@ class MonitorController { } }; - 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 { - getMonitorsByTeamIdParamValidation.parse(req.params); - getMonitorsByTeamIdQueryValidation.parse(req.query); + const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); - const teamId = requireTeamId(req?.user?.teamId); - const type = parseMonitorTypeFilter(req.query?.type); - const filter = optionalString(req.query?.filter, "filter"); + const teamId = requireTeamId(req.user?.teamId); + const type = validatedQuery.type; + const filter = validatedQuery.filter; const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); @@ -357,17 +345,16 @@ class MonitorController { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - getMonitorsByTeamIdParamValidation.parse(req.params); - getMonitorsWithChecksQueryValidation.parse(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 validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query); + + const limit = validatedQuery.limit; + const page = validatedQuery.page; + const rowsPerPage = validatedQuery.rowsPerPage; + const filter = validatedQuery.filter; + const field = validatedQuery.field; + const order = validatedQuery.order; + const type = validatedQuery.type; + const teamId = requireTeamId(req.user?.teamId); const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ teamId, @@ -378,7 +365,6 @@ class MonitorController { filter, field, order, - explain, }); return res.status(200).json({ @@ -392,11 +378,7 @@ class MonitorController { 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 teamId = requireTeamId(req.user?.teamId); const json = await this.monitorService.exportMonitorsToJSON({ teamId }); return res.status(200).json({ @@ -424,11 +406,7 @@ class MonitorController { 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 teamId = requireTeamId(req.user?.teamId); const groups = await this.monitorService.getGroupsByTeamId({ teamId }); return res.status(200).json({ @@ -439,6 +417,42 @@ class MonitorController { next(error); } }; + + updateNotifications = async (req: Request, res: Response, next: NextFunction) => { + try { + updateNotificationsValidation.parse(req.body); + + const teamId = requireTeamId(req.user?.teamId); + const { monitorIds, notificationIds, action } = req.body; + + // Verify all requested notification IDs actually belong to this team + const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId); + const validNotificationIds = teamNotifications.map((n) => n.id); + + const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id)); + if (invalidIds.length > 0) { + throw new AppError({ + message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`, + status: 403, + }); + } + + const modifiedCount = await this.monitorService.updateNotifications({ + teamId, + monitorIds, + notificationIds, + action, + }); + + return res.status(200).json({ + success: true, + msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`, + data: { modifiedCount }, + }); + } catch (error) { + next(error); + } + }; } export default MonitorController; diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 0cee3726e..0a49cbdb1 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -1,16 +1,30 @@ import { Request, Response, NextFunction } from "express"; -import { Notification } from "@/types/index.js"; -import { createNotificationBodyValidation } from "@/validation/notificationValidation.js"; +import { + createNotificationBodyValidation, + deleteNotificationParamValidation, + getNotificationByIdParamValidation, + testNotificationBodyValidation, + editNotificationParamValidation, + testAllNotificationsBodyValidation, +} from "@/validation/notificationValidation.js"; import { AppError } from "@/utils/AppError.js"; -import { IMonitorsRepository } from "@/repositories/index.js"; import { INotificationsService } from "@/service/index.js"; -import { requireTeamId } from "./controllerUtils.js"; +import { requireTeamId, requireUserId } from "./controllerUtils.js"; +import { IMonitorsRepository } from "@/repositories/index.js"; const SERVICE_NAME = "NotificationController"; -class NotificationController { - static SERVICE_NAME = SERVICE_NAME; +export interface INotificationController { + testNotification: (req: Request, res: Response, next: NextFunction) => Promise; + createNotification: (req: Request, res: Response, next: NextFunction) => Promise; + getNotificationsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; + deleteNotification: (req: Request, res: Response, next: NextFunction) => Promise; + getNotificationById: (req: Request, res: Response, next: NextFunction) => Promise; + editNotification: (req: Request, res: Response, next: NextFunction) => Promise; + testAllNotifications: (req: Request, res: Response, next: NextFunction) => Promise; +} +class NotificationController implements INotificationController { private notificationsService: INotificationsService; private monitorsRepository: IMonitorsRepository; constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { @@ -18,13 +32,9 @@ class NotificationController { this.monitorsRepository = monitorsRepository; } - get serviceName() { - return NotificationController.SERVICE_NAME; - } - testNotification = async (req: Request, res: Response, next: NextFunction) => { try { - const notification: Notification = req.body; + const notification = testNotificationBodyValidation.parse(req.body); const success = await this.notificationsService.sendTestNotification(notification); if (!success) { @@ -34,6 +44,7 @@ class NotificationController { return res.status(200).json({ success: true, msg: "Notification sent successfully", + details: { service: SERVICE_NAME }, }); } catch (error) { next(error); @@ -42,23 +53,12 @@ class NotificationController { createNotification = async (req: Request, res: Response, next: NextFunction) => { try { - createNotificationBodyValidation.parse(req.body); + const validatedBody = createNotificationBodyValidation.parse(req.body); - const body = req.body; + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); - 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); + const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId); return res.status(200).json({ success: true, msg: "Notification created successfully", @@ -71,11 +71,7 @@ class NotificationController { 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 teamId = requireTeamId(req.user?.teamId); const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); return res.status(200).json({ @@ -90,17 +86,10 @@ class NotificationController { 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 teamId = requireTeamId(req.user?.teamId); + const validatedParams = deleteNotificationParamValidation.parse(req.params); - const notificationId = req.params.id as string; - if (!notificationId) { - throw new AppError({ message: "Notification ID is required", status: 400 }); - } - - await this.notificationsService.deleteById(notificationId, teamId); + await this.notificationsService.deleteById(validatedParams.id, teamId); return res.status(200).json({ success: true, msg: "Notification deleted successfully", @@ -113,12 +102,9 @@ class NotificationController { getNotificationById = async (req: Request, res: Response, next: NextFunction) => { try { const teamId = requireTeamId(req.user?.teamId); - const notificationId = req.params.id as string; - if (!notificationId) { - throw new AppError({ message: "Notification ID is required", status: 400 }); - } + const validatedParams = getNotificationByIdParamValidation.parse(req.params); - const notification = await this.notificationsService.findById(notificationId, teamId); + const notification = await this.notificationsService.findById(validatedParams.id, teamId); return res.status(200).json({ success: true, @@ -132,14 +118,13 @@ class NotificationController { editNotification = async (req: Request, res: Response, next: NextFunction) => { try { - createNotificationBodyValidation.parse(req.body); + const validatedBody = createNotificationBodyValidation.parse(req.body); + const validatedParams = editNotificationParamValidation.parse(req.params); const teamId = requireTeamId(req.user?.teamId); - const notificationId = req.params.id as string; - if (!notificationId) { - throw new AppError({ message: "Notification ID is required", status: 400 }); - } - const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body); + const notificationId = validatedParams.id; + + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); return res.status(200).json({ success: true, msg: "Notification updated successfully", @@ -152,15 +137,13 @@ class NotificationController { 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 validatedBody = testAllNotificationsBodyValidation.parse(req.body); - const monitor = await this.monitorsRepository.findById(monitorId, teamId); + const teamId = requireTeamId(req.user?.teamId); + + const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId); + const notifications = monitor.notifications || []; - const notifications = monitor.notifications; if (notifications.length === 0) { throw new AppError({ message: "No notifications", status: 400 }); } diff --git a/server/src/controllers/queueController.ts b/server/src/controllers/queueController.ts index 9885bab87..52762f42b 100755 --- a/server/src/controllers/queueController.ts +++ b/server/src/controllers/queueController.ts @@ -1,11 +1,19 @@ +import { ISuperSimpleQueue } from "@/service/index.js"; import { Request, Response, NextFunction } from "express"; const SERVICE_NAME = "JobQueueController"; -class JobQueueController { +export interface IJobQueueController { + getMetrics(req: Request, res: Response, next: NextFunction): Promise; + getJobs(req: Request, res: Response, next: NextFunction): Promise; + getAllMetrics(req: Request, res: Response, next: NextFunction): Promise; + flushQueue(req: Request, res: Response, next: NextFunction): Promise; +} + +class JobQueueController implements IJobQueueController { static SERVICE_NAME = SERVICE_NAME; - private jobQueue: any; - constructor(jobQueue: any) { + private jobQueue: ISuperSimpleQueue; + constructor(jobQueue: ISuperSimpleQueue) { this.jobQueue = jobQueue; } @@ -53,18 +61,6 @@ class JobQueueController { } }; - 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(); @@ -77,18 +73,5 @@ class JobQueueController { 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.ts b/server/src/controllers/settingsController.ts index d3caa0d8f..03bc91898 100644 --- a/server/src/controllers/settingsController.ts +++ b/server/src/controllers/settingsController.ts @@ -2,14 +2,23 @@ import { Request, Response, NextFunction } from "express"; import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js"; import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js"; import { AppError } from "@/utils/AppError.js"; +import { IEmailService, ISettingsService } from "@/service/index.js"; +import { Settings } from "@/types/settings.js"; const SERVICE_NAME = "SettingsController"; -class SettingsController { +export interface ISettingsController { + serviceName: string; + getAppSettings(req: Request, res: Response, next: NextFunction): Promise; + updateAppSettings(req: Request, res: Response, next: NextFunction): Promise; + sendTestEmail(req: Request, res: Response, next: NextFunction): Promise; +} + +class SettingsController implements ISettingsController { static SERVICE_NAME = SERVICE_NAME; - private settingsService: any; - private emailService: any; - constructor(settingsService: any, emailService: any) { + private settingsService: ISettingsService; + private emailService: IEmailService; + constructor(settingsService: ISettingsService, emailService: IEmailService) { this.settingsService = settingsService; this.emailService = emailService; } @@ -18,11 +27,11 @@ class SettingsController { return SettingsController.SERVICE_NAME; } - buildAppSettings = (dbSettings: any) => { - const sanitizedSettings = { ...dbSettings }; + buildAppSettings = (dbSettings: Settings) => { + const sanitizedSettings: Record = { ...dbSettings }; delete sanitizedSettings.version; delete sanitizedSettings.jwtSecret; - const returnSettings = { + const returnSettings: Record = { pagespeedKeySet: false, emailPasswordSet: false, settings: null, @@ -57,9 +66,9 @@ class SettingsController { updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { try { - updateAppSettingsBodyValidation.parse(req.body); + const validatedBody = updateAppSettingsBodyValidation.parse(req.body); - const updatedSettings = await this.settingsService.updateDbSettings(req.body); + const updatedSettings = await this.settingsService.updateDbSettings(validatedBody); const returnSettings = this.buildAppSettings(updatedSettings); return res.status(200).json({ success: true, @@ -95,6 +104,9 @@ class SettingsController { const context = { testName: "Monitoring System" }; const html = await this.emailService.buildEmail("testEmailTemplate", context); + if (!html) { + throw new AppError({ message: "Failed to build email template.", status: 500 }); + } const messageId = await this.emailService.sendEmail(to, subject, html, { systemEmailHost, systemEmailPort, diff --git a/server/src/controllers/statusPageController.ts b/server/src/controllers/statusPageController.ts index 8593cd53f..0c73b6e3b 100644 --- a/server/src/controllers/statusPageController.ts +++ b/server/src/controllers/statusPageController.ts @@ -15,7 +15,16 @@ import { NormalizeData } from "@/utils/dataUtils.js"; const SERVICE_NAME = "statusPageController"; -class StatusPageController { +export interface IStatusPageController { + readonly serviceName: string; + createStatusPage(req: Request, res: Response, next: NextFunction): Promise; + updateStatusPage(req: Request, res: Response, next: NextFunction): Promise; + getStatusPageByUrl(req: Request, res: Response, next: NextFunction): Promise; + getStatusPagesByTeamId(req: Request, res: Response, next: NextFunction): Promise; + deleteStatusPage(req: Request, res: Response, next: NextFunction): Promise; +} + +class StatusPageController implements IStatusPageController { static SERVICE_NAME = SERVICE_NAME; private statusPageService: IStatusPageService; private monitorsRepository: IMonitorsRepository; @@ -66,7 +75,7 @@ class StatusPageController { if (statusPage === null) { throw new AppError({ message: "Status page not found", status: 404 }); } - return res.status(200).json({ + res.status(200).json({ success: true, msg: "Status page updated successfully", data: statusPage, diff --git a/server/src/db/MongoDB.ts b/server/src/db/MongoDB.ts index 82d05c3a2..1284aef4d 100755 --- a/server/src/db/MongoDB.ts +++ b/server/src/db/MongoDB.ts @@ -34,6 +34,12 @@ class MongoDB implements IDb { setDefaultsOnInsert: true, } ); + // Run migrations BEFORE syncIndexes. The 0006 dedupe migration must run before + // the unique index on MonitorStats.monitorId is enforced, otherwise databases + // with stray duplicates (caused by the pre-fix race in updateRunningStats) + // will crash at startup when the unique index fails to build. + await runMigrations(); + // Sync indexes const models = mongoose.modelNames(); for (const modelName of models) { @@ -46,8 +52,6 @@ class MongoDB implements IDb { service: SERVICE_NAME, method: "connect", }); - - await runMigrations(); } catch (error: unknown) { this.logger.error({ message: error instanceof Error ? error.message : "Unknown error", diff --git a/server/src/db/migration/0002_convertChecksToTimeSeries.ts b/server/src/db/migration/0002_convertChecksToTimeSeries.ts index 01ed82def..cc2d20b23 100644 --- a/server/src/db/migration/0002_convertChecksToTimeSeries.ts +++ b/server/src/db/migration/0002_convertChecksToTimeSeries.ts @@ -1,4 +1,5 @@ import mongoose from "mongoose"; +import type { Document, AnyBulkWriteOperation } from "mongodb"; const CHECKS_COLLECTION = "checks"; const BACKUP_COLLECTION = "checks_backup"; @@ -85,7 +86,7 @@ const createTimeSeriesCollection = async (backedUp: boolean) => { } }; -const validateDocument = (doc: any): boolean => { +const validateDocument = (doc: Document): boolean => { if (!doc.createdAt) { return false; } @@ -95,7 +96,7 @@ const validateDocument = (doc: any): boolean => { return true; }; -const storeFailedDocument = async (doc: any, reason: string) => { +const storeFailedDocument = async (doc: Document, reason: string) => { const db = getDb(); await db.collection(FAILED_DOCS_COLLECTION).insertOne({ originalDoc: doc, @@ -124,8 +125,8 @@ const migrateBackupData = async (backedUp: boolean): Promise => stats.totalSource = await source.countDocuments(); const cursor = source.find().addCursorFlag("noCursorTimeout", true); - const operations: any[] = []; - const invalidDocs: any[] = []; + const operations: AnyBulkWriteOperation[] = []; + const invalidDocs: Document[] = []; while (await cursor.hasNext()) { const doc = await cursor.next(); @@ -139,6 +140,7 @@ const migrateBackupData = async (backedUp: boolean): Promise => } const { _id, monitorId, teamId, type, ...rest } = doc; + void _id; const metadata = { monitorId, teamId, type }; operations.push({ insertOne: { document: { ...rest, metadata } } }); diff --git a/server/src/db/migration/0003_cleanupDuplicateMonitorStats.ts b/server/src/db/migration/0003_cleanupDuplicateMonitorStats.ts index 1371ab798..c4dff2fc0 100644 --- a/server/src/db/migration/0003_cleanupDuplicateMonitorStats.ts +++ b/server/src/db/migration/0003_cleanupDuplicateMonitorStats.ts @@ -42,10 +42,10 @@ export async function cleanupDuplicateMonitorStats(): Promise { const monitorId = duplicate._id; const { ids, updatedAts } = duplicate; - type DocPair = { id: any; updatedAt: Date }; + type DocPair = { id: string; updatedAt: Date }; // Create array of {id, updatedAt} pairs and sort by updatedAt descending - const docs: DocPair[] = ids.map((id: any, index: number) => ({ + const docs: DocPair[] = ids.map((id: string, index: number) => ({ id, updatedAt: updatedAts[index], })); diff --git a/server/src/db/migration/0004_fixInfrastructureThresholds.ts b/server/src/db/migration/0004_fixInfrastructureThresholds.ts index 6d369f311..bf6458565 100644 --- a/server/src/db/migration/0004_fixInfrastructureThresholds.ts +++ b/server/src/db/migration/0004_fixInfrastructureThresholds.ts @@ -37,7 +37,8 @@ export async function fixInfrastructureThresholds(): Promise { const update: Record = {}; for (const field of thresholdFields) { - if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) { + const value = monitor.get(field); + if (value === 0 || value === 5) { update[field] = 100; } } diff --git a/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts b/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts new file mode 100644 index 000000000..0b13d1cb6 --- /dev/null +++ b/server/src/db/migration/0005_migrateStatusPageTypeToArray.ts @@ -0,0 +1,45 @@ +import mongoose from "mongoose"; +import { logger } from "@/utils/logger.js"; +import StatusPageModel from "../models/StatusPage.js"; + +export async function migrateStatusPageTypeToArray(): Promise { + const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray"; + + try { + logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" }); + + const db = mongoose.connection.db; + if (!db) { + throw new Error("Database connection is not initialized"); + } + + const result = await StatusPageModel.updateMany( + { + $expr: { + $eq: [{ $type: "$type" }, "string"], // only scalar string, not existing array + }, + }, + [ + { + $set: { + type: ["$type"], + }, + }, + ] + ); + + if (result.modifiedCount === 0) { + logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" }); + return; + } + + logger.info({ + service: SERVICE_NAME, + message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` }); + throw error; + } +} diff --git a/server/src/db/migration/0006_cleanupDuplicateMonitorStatsForUniqueIndex.ts b/server/src/db/migration/0006_cleanupDuplicateMonitorStatsForUniqueIndex.ts new file mode 100644 index 000000000..814b19776 --- /dev/null +++ b/server/src/db/migration/0006_cleanupDuplicateMonitorStatsForUniqueIndex.ts @@ -0,0 +1,78 @@ +import { MonitorStatsModel } from "../models/MonitorStats.js"; +import { logger } from "@/utils/logger.js"; + +/** + * Second-pass dedupe of MonitorStats before the unique index on monitorId is enforced. + * + * 0003 ran the same dedupe, but the pre-fix race in StatusService.updateRunningStats + * could create new duplicates after 0003 completed. This migration re-runs the dedupe so + * the unique index added in the same PR can be built without failing. + * + * Keeps the document with the highest totalChecks for each monitorId (most data) and + * deletes the rest. Falls back to most recent updatedAt when totalChecks tie. + */ +export async function cleanupDuplicateMonitorStatsForUniqueIndex(): Promise { + const SERVICE_NAME = "Migration:CleanupDuplicateMonitorStatsForUniqueIndex"; + + try { + logger.info({ service: SERVICE_NAME, message: "Starting pre-unique-index dedupe of MonitorStats" }); + + const duplicates = await MonitorStatsModel.aggregate([ + { + $group: { + _id: "$monitorId", + docs: { $push: { id: "$_id", totalChecks: "$totalChecks", updatedAt: "$updatedAt" } }, + count: { $sum: 1 }, + }, + }, + { $match: { count: { $gt: 1 } } }, + ]); + + if (duplicates.length === 0) { + logger.info({ service: SERVICE_NAME, message: "No duplicate MonitorStats found" }); + return; + } + + logger.info({ + service: SERVICE_NAME, + message: `Found ${duplicates.length} monitors with duplicate stats`, + }); + + let totalDeleted = 0; + + type DocInfo = { id: string; totalChecks: number; updatedAt: Date }; + + for (const duplicate of duplicates) { + const monitorId = duplicate._id; + const docs: DocInfo[] = duplicate.docs; + + // Sort by totalChecks desc, then updatedAt desc — keep the winner, delete the rest. + docs.sort((a, b) => { + const checksDiff = (b.totalChecks ?? 0) - (a.totalChecks ?? 0); + if (checksDiff !== 0) return checksDiff; + return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + }); + + const toDelete = docs.slice(1).map((doc) => doc.id); + + if (toDelete.length > 0) { + const result = await MonitorStatsModel.deleteMany({ _id: { $in: toDelete } }); + totalDeleted += result.deletedCount ?? 0; + + logger.debug({ + service: SERVICE_NAME, + message: `Deleted ${result.deletedCount} duplicate stats for monitor ${monitorId}`, + }); + } + } + + logger.info({ + service: SERVICE_NAME, + message: `Cleanup complete. Deleted ${totalDeleted} duplicate MonitorStats documents`, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error({ service: SERVICE_NAME, message: `Error during MonitorStats dedupe: ${errorMessage}` }); + throw error; + } +} diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index 3b7b45ada..5b74a96fc 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -3,6 +3,9 @@ import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js"; import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js"; import MigrationModel from "../models/Migration.js"; +import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js"; +import { cleanupDuplicateMonitorStatsForUniqueIndex } from "./0006_cleanupDuplicateMonitorStatsForUniqueIndex.js"; +import type { ILogger } from "@/utils/logger.js"; type MigrationEntry = { name: string; @@ -14,9 +17,11 @@ const migrations: MigrationEntry[] = [ { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, { name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats }, { name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds }, + { name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray }, + { name: "0006_cleanupDuplicateMonitorStatsForUniqueIndex", execute: cleanupDuplicateMonitorStatsForUniqueIndex }, ]; -const runMigrations = async (logger?: { info: Function; error: Function }) => { +const runMigrations = async (logger?: ILogger) => { try { logger?.info({ message: "Running migrations", service: "Migrations" }); for (const migration of migrations) { @@ -43,7 +48,7 @@ const runMigrations = async (logger?: { info: Function; error: Function }) => { 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 }); + logger?.error({ message: "Migration failed", service: "Migrations", details: { error: err?.message }, stack: err?.stack }); throw error; } }; diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index 7bd1cec6f..541abb4d9 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -21,13 +21,10 @@ type CheckMetadataDocument = Omit & { type: MonitorType; }; -type CheckDocumentBase = Omit & { +type CheckDocumentBase = Omit & { metadata: CheckMetadataDocument; - expiry: Date; - ackAt?: Date | null; createdAt: Date; updatedAt: Date; - __v: number; }; interface CheckDocument extends CheckDocumentBase { @@ -225,18 +222,7 @@ const CheckSchema = new Schema( message: { type: String, }, - expiry: { - type: Date, - default: Date.now, - }, - ack: { - type: Boolean, - default: false, - }, - ackAt: { - type: Date, - default: undefined, - }, + cpu: { type: cpuSchema, default: () => ({}), diff --git a/server/src/db/models/DLQItem.ts b/server/src/db/models/DLQItem.ts new file mode 100644 index 000000000..707846adb --- /dev/null +++ b/server/src/db/models/DLQItem.ts @@ -0,0 +1,70 @@ +import { Schema, model, type Types } from "mongoose"; +import { DLQItemTypes, DLQItemStatuses, type DLQItem } from "@/types/dlqItem.js"; + +export interface DLQItemDocument extends Omit { + _id: Types.ObjectId; + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + nextRetryAt: Date; + createdAt: Date; + updatedAt: Date; +} + +const DLQItemSchema = new Schema( + { + type: { + type: String, + enum: DLQItemTypes, + required: true, + }, + status: { + type: String, + enum: DLQItemStatuses, + default: "pending", + }, + payload: { + type: Schema.Types.Mixed, + required: true, + }, + 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, + }, + retryCount: { + type: Number, + default: 0, + }, + maxRetries: { + type: Number, + default: 5, + }, + lastError: { + type: String, + default: "", + }, + nextRetryAt: { + type: Date, + required: true, + }, + }, + { timestamps: true } +); + +DLQItemSchema.index({ status: 1, nextRetryAt: 1 }); +DLQItemSchema.index({ teamId: 1, status: 1 }); +DLQItemSchema.index({ createdAt: 1 }); + +const DLQItemModel = model("DLQItem", DLQItemSchema); + +export { DLQItemModel }; +export default DLQItemModel; diff --git a/server/src/db/models/MaintenanceWindow.ts b/server/src/db/models/MaintenanceWindow.ts index ff309a117..311cf97bf 100644 --- a/server/src/db/models/MaintenanceWindow.ts +++ b/server/src/db/models/MaintenanceWindow.ts @@ -1,5 +1,5 @@ import { Schema, model, type Types } from "mongoose"; -import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js"; +import type { MaintenanceWindow } from "@/types/maintenanceWindow.js"; type MaintenanceWindowDocumentBase = Omit & { monitorId: Types.ObjectId; diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index 2def7701c..036aeadad 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -1,6 +1,18 @@ -import { Schema, model, Types, type UpdateQuery } from "mongoose"; +import { Schema, model, Types } from "mongoose"; import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js"; import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js"; +import type { + CheckAudits, + CheckCaptureInfo, + CheckCpuInfo, + CheckDiskInfo, + CheckErrorInfo, + CheckHostInfo, + CheckMemoryInfo, + CheckNetworkInterfaceInfo, + GotTimings, + ILighthouseAudit, +} from "@/types/check.js"; type CheckSnapshotDocument = Omit & { createdAt: Date }; @@ -23,13 +35,167 @@ interface MonitorDocument extends MonitorDocumentBase { updatedAt: Date; } +const snapshotTimingPhasesSchema = new Schema( + { + wait: { type: Number }, + dns: { type: Number }, + tcp: { type: Number }, + tls: { type: Number }, + request: { type: Number }, + firstByte: { type: Number }, + download: { type: Number }, + total: { type: Number }, + }, + { _id: false } +); + +const snapshotTimingsSchema = new Schema( + { + 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: snapshotTimingPhasesSchema }, + }, + { _id: false } +); + +const snapshotCpuSchema = new Schema( + { + physical_core: { type: Number }, + logical_core: { type: Number }, + frequency: { type: Number }, + current_frequency: { type: Number }, + temperature: { type: [Number] }, + free_percent: { type: Number }, + usage_percent: { type: Number }, + }, + { _id: false } +); + +const snapshotMemorySchema = new Schema( + { + total_bytes: { type: Number }, + available_bytes: { type: Number }, + used_bytes: { type: Number }, + usage_percent: { type: Number }, + }, + { _id: false } +); + +const snapshotDiskSchema = new Schema( + { + device: { type: String }, + mountpoint: { type: String }, + total_bytes: { type: Number }, + free_bytes: { type: Number }, + used_bytes: { type: Number }, + usage_percent: { type: Number }, + total_inodes: { type: Number }, + free_inodes: { type: Number }, + used_inodes: { type: Number }, + inodes_usage_percent: { type: Number }, + read_bytes: { type: Number }, + write_bytes: { type: Number }, + read_time: { type: Number }, + write_time: { type: Number }, + }, + { _id: false } +); + +const snapshotHostSchema = new Schema( + { + os: { type: String }, + platform: { type: String }, + kernel_version: { type: String }, + pretty_name: { type: String }, + }, + { _id: false } +); + +const snapshotErrorSchema = new Schema( + { + metric: { type: [String] }, + err: { type: String }, + }, + { _id: false } +); + +const snapshotCaptureSchema = new Schema( + { + version: { type: String }, + mode: { type: String }, + }, + { _id: false } +); + +const snapshotNetworkInterfaceSchema = new Schema( + { + 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 }, + }, + { _id: false } +); + +const snapshotLighthouseAuditSchema = new Schema( + { + id: { type: String }, + title: { type: String }, + score: { type: Number }, + displayValue: { type: String }, + numericValue: { type: Number }, + numericUnit: { type: String }, + }, + { _id: false } +); + +const snapshotAuditsSchema = new Schema( + { + cls: { type: snapshotLighthouseAuditSchema }, + si: { type: snapshotLighthouseAuditSchema }, + fcp: { type: snapshotLighthouseAuditSchema }, + lcp: { type: snapshotLighthouseAuditSchema }, + tbt: { type: snapshotLighthouseAuditSchema }, + }, + { _id: false } +); + const checkSnapshotSchema = new Schema( { id: { type: String, required: true }, status: { type: Boolean, required: true }, + responseTime: { type: Number }, + timings: { type: snapshotTimingsSchema }, + statusCode: { type: Number }, + message: { type: String }, + cpu: { type: snapshotCpuSchema }, + memory: { type: snapshotMemorySchema }, + disk: { type: [snapshotDiskSchema] }, + host: { type: snapshotHostSchema }, + errors: { type: [snapshotErrorSchema] }, + capture: { type: snapshotCaptureSchema }, + net: { type: [snapshotNetworkInterfaceSchema] }, + accessibility: { type: Number }, + bestPractices: { type: Number }, + seo: { type: Number }, + performance: { type: Number }, + audits: { type: snapshotAuditsSchema }, createdAt: { type: Date, required: true }, }, - { _id: false, strict: false } + { _id: false } ); const MonitorSchema = new Schema( diff --git a/server/src/db/models/MonitorStats.ts b/server/src/db/models/MonitorStats.ts index 5780de476..4ef2dab61 100755 --- a/server/src/db/models/MonitorStats.ts +++ b/server/src/db/models/MonitorStats.ts @@ -16,7 +16,7 @@ const MonitorStatsSchema = new Schema( monitorId: { type: Schema.Types.ObjectId, ref: "Monitor", - index: true, + unique: true, required: true, }, avgResponseTime: { diff --git a/server/src/db/models/Notification.ts b/server/src/db/models/Notification.ts index 97e2d43fb..2f7effa17 100755 --- a/server/src/db/models/Notification.ts +++ b/server/src/db/models/Notification.ts @@ -25,7 +25,7 @@ const NotificationSchema = new Schema( }, type: { type: String, - enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[], + enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover", "twilio"] as NotificationChannel[], required: true, }, notificationName: { @@ -37,6 +37,8 @@ const NotificationSchema = new Schema( homeserverUrl: { type: String }, roomId: { type: String }, accessToken: { type: String }, + accountSid: { type: String }, + twilioPhoneNumber: { type: String }, }, { timestamps: true, diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts index 56c5ee299..e9b5b59aa 100644 --- a/server/src/db/models/StatusPage.ts +++ b/server/src/db/models/StatusPage.ts @@ -43,9 +43,9 @@ const StatusPageSchema = new Schema( required: true, }, type: { - type: String, + type: [String], required: true, - default: "uptime", + default: ["uptime"], enum: StatusPageTypes, }, companyName: { @@ -100,6 +100,10 @@ const StatusPageSchema = new Schema( type: Boolean, default: false, }, + showInfrastructure: { + type: Boolean, + default: false, + }, customCSS: { type: String, default: "", diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 1024b2b93..d4c8f1a3b 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -36,3 +36,6 @@ export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow export * from "@/db/models/GeoCheck.js"; export { default as GeoCheckModel } from "@/db/models/GeoCheck.js"; + +export * from "@/db/models/DLQItem.js"; +export { default as DLQItemModel } from "@/db/models/DLQItem.js"; diff --git a/server/src/index.ts b/server/src/index.ts index 2286d3887..83698b45c 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,16 +1,14 @@ import { initializeServices } from "./config/services.js"; import { initializeControllers } from "./config/controllers.js"; import { createApp } from "./app.js"; -import { initShutdownListener } from "./shutdown.js"; -import { validateEnv } from "./validation/envValidation.js"; +import { initShutdownListener } from "@/shutdown.js"; +import { validateEnv } from "@/validation/envValidation.js"; import { fileURLToPath } from "url"; import path from "path"; import fs from "fs"; -import { runMigrations } from "./db/migration/index.js"; import Logger, { ILogger } from "@/utils/logger.js"; import { SettingsService } from "@/service/index.js"; -import { MongoSettingsRepository } from "./repositories/index.js"; const SERVICE_NAME = "Server"; let logger: ILogger; @@ -25,19 +23,15 @@ const startApp = async () => { const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8")); const frontendPath = path.join(__dirname, "..", "public"); - // Create services - const settingsRepository = new MongoSettingsRepository(); - const settingsService = new SettingsService(settingsRepository, env); - + // Create settings service (env only — DB repository injected after connect) + const settingsService = new SettingsService(env); const envSettings = settingsService.loadSettings(); // Create logger logger = new Logger({ envSettings }); - // Initialize services - const services = await initializeServices({ logger, envSettings, settingsService, settingsRepository }); - - await runMigrations(logger); + // Initialize services (connects DB, creates repositories, injects settingsRepository) + const services = await initializeServices({ logger, envSettings, settingsService }); // Initialize controllers const controllers = initializeControllers(services); diff --git a/server/src/middleware/handleErrors.ts b/server/src/middleware/handleErrors.ts index 1fffc9a8c..ff591484f 100755 --- a/server/src/middleware/handleErrors.ts +++ b/server/src/middleware/handleErrors.ts @@ -1,15 +1,18 @@ import type { NextFunction, Request, Response } from "express"; import { logger } from "@/utils/logger.js"; +import { AppError } from "@/utils/AppError.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"; +const handleErrors = (error: unknown, req: Request, res: Response, _next: NextFunction) => { + const status = error instanceof AppError ? error.status || 500 : 500; + const message = error instanceof AppError ? error.message : error instanceof Error ? error.message : "Server error"; + const service = error instanceof AppError ? error.service : "unknownService"; + const method = error instanceof AppError ? error.method : "unknownMethod"; logger.error({ message: message, service: service, - method: error.method, - stack: error.stack, + method: method, + stack: error instanceof Error ? error.stack : undefined, + details: error instanceof AppError ? error.details : undefined, }); res.status(status).json({ status, diff --git a/server/src/middleware/sanitization.ts b/server/src/middleware/sanitization.ts index be971a298..f191ca6ca 100644 --- a/server/src/middleware/sanitization.ts +++ b/server/src/middleware/sanitization.ts @@ -1,7 +1,9 @@ import type { Request, Response, NextFunction } from "express"; import DOMPurify from "isomorphic-dompurify"; -export const sanitizeInput = (input: any, options = {}) => { +export function sanitizeInput(input: string, options?: object): string; +export function sanitizeInput(input: unknown, options?: object): unknown; +export function sanitizeInput(input: unknown, options = {}): unknown { if (typeof input !== "string") { return input; } @@ -15,9 +17,9 @@ export const sanitizeInput = (input: any, options = {}) => { }; return DOMPurify.sanitize(input, defaultConfig); -}; +} -export const sanitizeObject = (obj: Record, options = {}): Record => { +export const sanitizeObject = (obj: unknown, options = {}): unknown => { if (typeof obj !== "object" || obj === null) { return obj; } @@ -26,7 +28,7 @@ export const sanitizeObject = (obj: Record, options = {}): Record sanitizeObject(item, options)); } - const sanitized: Record = {}; + const sanitized: Record = {}; for (const [key, value] of Object.entries(obj)) { if (typeof value === "string") { sanitized[key] = sanitizeInput(value, options); @@ -52,7 +54,12 @@ export const sanitizeBody = (options = {}): ((req: Request, res: Response, 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); + for (const key of Object.keys(req.query)) { + const value = req.query[key]; + if (typeof value === "string") { + req.query[key] = sanitizeInput(value, options); + } + } } next(); }; diff --git a/server/src/middleware/verifyJWT.ts b/server/src/middleware/verifyJWT.ts index 6c2b0a229..63218566c 100644 --- a/server/src/middleware/verifyJWT.ts +++ b/server/src/middleware/verifyJWT.ts @@ -2,12 +2,17 @@ import { NextFunction, Request, Response } from "express"; import jwt from "jsonwebtoken"; import { AppError } from "@/utils/AppError.js"; -import { Settings } from "@/types/settings.js"; +import type { ISettingsService } from "@/service/system/settingsService.js"; +import type { User } from "@/types/user.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; -export const createVerifyJWT = (settingsService: { getSettings: () => Settings }) => { +const isUser = (payload: unknown): payload is User => { + return typeof payload === "object" && payload !== null && "id" in payload && "teamId" in payload && "role" in payload; +}; + +export const createVerifyJWT = (settingsService: ISettingsService) => { return (req: Request, res: Response, next: NextFunction) => { const token = req.headers["authorization"]; // Make sure a token is provided @@ -31,21 +36,22 @@ export const createVerifyJWT = (settingsService: { getSettings: () => Settings } next(error); return; } - jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => { + jwt.verify(parsedToken, jwtSecret, (err: jwt.VerifyErrors | null, decoded: jwt.JwtPayload | string | undefined) => { if (err) { const error = new AppError({ - message: "Failed to authenticate token", - details: err, + message: err instanceof Error ? err.message : "Failed to authenticate token", status: 401, service: SERVICE_NAME, + details: err instanceof Error ? { error: err } : undefined, method: "verifyJWT", }); next(error); return; - } else { - // Token is valid, carry on + } else if (isUser(decoded)) { req.user = decoded; next(); + } else { + next(new AppError({ message: "Invalid token payload", status: 401, service: SERVICE_NAME, method: "verifyJWT" })); } }); }; diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index c89e5d5fc..b96d14933 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -47,4 +47,5 @@ export interface IChecksRepository { deleteByMonitorId(monitorId: string): Promise; deleteByTeamId(teamId: string): Promise; deleteByMonitorIdsNotIn(monitorIds: string[]): Promise; + deleteOlderThan(date: Date): Promise; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index b019a0430..4055ddfc3 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -13,9 +13,10 @@ import type { GotTimings, MonitorType, } from "@/types/index.js"; -import { CheckModel, MonitorModel, type CheckDocument } from "@/db/models/index.js"; +import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; import { getDateForRange } from "@/utils/dataUtils.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "StatusService"; @@ -26,8 +27,8 @@ type HardwareUpChecks = { totalChecks: number }; class MongoChecksRepository implements IChecksRepository { static SERVICE_NAME = SERVICE_NAME; - private logger: any; - constructor(logger: any) { + private logger: ILogger; + constructor(logger: ILogger) { this.logger = logger; } @@ -46,13 +47,6 @@ class MongoChecksRepository implements IChecksRepository { 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, @@ -171,9 +165,6 @@ class MongoChecksRepository implements IChecksRepository { 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), @@ -186,7 +177,6 @@ class MongoChecksRepository implements IChecksRepository { seo: doc.seo, performance: doc.performance, audits: mapAudits(doc.audits), - __v: doc.__v ?? 0, createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), }; @@ -237,7 +227,7 @@ class MongoChecksRepository implements IChecksRepository { status: boolean | undefined ) => { // Match - const matchStage: Record = { + const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), ...(typeof status !== "undefined" && { status }), ...(getDateForRange(dateRange) && { @@ -289,7 +279,7 @@ class MongoChecksRepository implements IChecksRepository { }; findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => { - const matchStage: Record = { + const matchStage: Record = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), ...(getDateForRange(dateRange) && { createdAt: { @@ -358,7 +348,7 @@ class MongoChecksRepository implements IChecksRepository { ); const mapped = results.reduce((acc, { monitorId, docs }) => { - acc[monitorId] = docs.map((doc: any) => this.toEntity(doc as CheckDocument)); + acc[monitorId] = docs.map((doc: CheckDocument) => this.toEntity(doc)); return acc; }, {}); return mapped; @@ -412,6 +402,31 @@ class MongoChecksRepository implements IChecksRepository { return result.deletedCount ?? 0; }; + deleteOlderThan = async (cutoffDate: Date, batchDays: number = 30): Promise => { + // Find the oldest check that is older than the cutoff + const oldest = await CheckModel.findOne({ createdAt: { $lt: cutoffDate } }) + .sort({ createdAt: 1 }) + .select({ createdAt: 1 }); + if (!oldest?.createdAt) return 0; + + let totalDeleted = 0; + let batchStart = new Date(oldest.createdAt); + const batchMs = batchDays * 24 * 60 * 60 * 1000; + + // Delete in 30 day chunks until we reach the cutoff + while (batchStart < cutoffDate) { + // Advance startTime by batchMs to get to the end of last batch + const nextStart = batchStart.getTime() + batchMs; + const batchEnd = new Date(Math.min(nextStart, cutoffDate.getTime())); + const result = await CheckModel.deleteMany({ + createdAt: { $gte: batchStart, $lt: batchEnd }, + }); + totalDeleted += result.deletedCount ?? 0; + batchStart = batchEnd; + } + + return totalDeleted; + }; private findUptimeDateRangeChecks = async ( monitorType: Exclude, monitorObjectId: mongoose.Types.ObjectId, diff --git a/server/src/repositories/dlq/IDLQRepository.ts b/server/src/repositories/dlq/IDLQRepository.ts new file mode 100644 index 000000000..5e26e6096 --- /dev/null +++ b/server/src/repositories/dlq/IDLQRepository.ts @@ -0,0 +1,30 @@ +import type { DLQItem, DLQItemType, DLQItemStatus } from "@/types/index.js"; + +export interface DLQQueryFilters { + type?: DLQItemType; + status?: DLQItemStatus; + page: number; + rowsPerPage: number; +} + +export interface DLQStatusCount { + status: DLQItemStatus; + type: DLQItemType; + count: number; +} + +export interface IDLQRepository { + // create + create(item: Partial): Promise; + // fetch + findById(id: string): Promise; + findByTeamId(teamId: string, filters: DLQQueryFilters): Promise; + findRetryable(limit: number): Promise; + countByTeamId(teamId: string): Promise; + countByTeamIdGrouped(teamId: string): Promise; + // update + updateById(id: string, patch: Partial): Promise; + // delete + deleteById(id: string, teamId: string): Promise; + deleteOlderThan(date: Date): Promise; +} diff --git a/server/src/repositories/dlq/MongoDLQRepository.ts b/server/src/repositories/dlq/MongoDLQRepository.ts new file mode 100644 index 000000000..8c9da76ad --- /dev/null +++ b/server/src/repositories/dlq/MongoDLQRepository.ts @@ -0,0 +1,139 @@ +import { DLQItemModel } from "@/db/models/index.js"; +import type { DLQItemDocument } from "@/db/models/DLQItem.js"; +import type { DLQItem } from "@/types/index.js"; +import type { IDLQRepository, DLQQueryFilters, DLQStatusCount } from "@/repositories/dlq/IDLQRepository.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; + +class MongoDLQRepository implements IDLQRepository { + 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: DLQItemDocument): DLQItem => { + return { + id: this.toStringId(doc._id), + type: doc.type, + status: doc.status, + payload: doc.payload, + monitorId: this.toStringId(doc.monitorId), + teamId: this.toStringId(doc.teamId), + retryCount: doc.retryCount, + maxRetries: doc.maxRetries, + lastError: doc.lastError, + nextRetryAt: this.toDateString(doc.nextRetryAt), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + protected mapDocuments = (documents: DLQItemDocument[] | DLQItemDocument | null): DLQItem[] => { + if (!documents) { + return []; + } + if (Array.isArray(documents)) { + return documents.map((doc) => this.toEntity(doc)); + } + return [this.toEntity(documents)]; + }; + + async create(item: Partial): Promise { + const newItem = await DLQItemModel.create(item); + return this.toEntity(newItem); + } + + findById = async (id: string): Promise => { + const item = await DLQItemModel.findById(new mongoose.Types.ObjectId(id)); + return item ? this.toEntity(item) : null; + }; + + findByTeamId = async (teamId: string, filters: DLQQueryFilters): Promise => { + const query: Record = { + teamId: new mongoose.Types.ObjectId(teamId), + }; + if (filters.type) { + query.type = filters.type; + } + if (filters.status) { + query.status = filters.status; + } + + const items = await DLQItemModel.find(query) + .sort({ createdAt: -1 }) + .skip(filters.page * filters.rowsPerPage) + .limit(filters.rowsPerPage); + return this.mapDocuments(items); + }; + + findRetryable = async (limit: number): Promise => { + const items = await DLQItemModel.find({ + status: { $in: ["pending", "retrying"] }, + nextRetryAt: { $lte: new Date() }, + }) + .sort({ nextRetryAt: 1 }) + .limit(limit); + return this.mapDocuments(items); + }; + + countByTeamId = async (teamId: string): Promise => { + return DLQItemModel.countDocuments({ + teamId: new mongoose.Types.ObjectId(teamId), + }); + }; + + countByTeamIdGrouped = async (teamId: string): Promise => { + const results = await DLQItemModel.aggregate([ + { $match: { teamId: new mongoose.Types.ObjectId(teamId) } }, + { + $group: { + _id: { status: "$status", type: "$type" }, + count: { $sum: 1 }, + }, + }, + ]); + return results.map((r) => ({ + status: r._id.status, + type: r._id.type, + count: r.count, + })); + }; + + updateById = async (id: string, patch: Partial): Promise => { + const updated = await DLQItemModel.findOneAndUpdate( + { _id: new mongoose.Types.ObjectId(id) }, + { $set: { ...patch } }, + { new: true, runValidators: true } + ); + if (!updated) { + throw new AppError({ message: `DLQ item with id ${id} not found`, status: 404 }); + } + return this.toEntity(updated); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const result = await DLQItemModel.deleteOne({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + return result.deletedCount || 0; + }; + + deleteOlderThan = async (date: Date): Promise => { + const result = await DLQItemModel.deleteMany({ + createdAt: { $lt: date }, + }); + return result.deletedCount || 0; + }; +} + +export default MongoDLQRepository; diff --git a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts index e1db5ab1d..0adcf7c9f 100644 --- a/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts +++ b/server/src/repositories/geo-checks/MongoGeoChecksRepository.ts @@ -1,8 +1,8 @@ import { IGeoChecksRepository } from "./IGeoChecksRepository.js"; import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck } from "@/types/geoCheck.js"; import type { FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js"; -import { GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js"; -import mongoose from "mongoose"; +import { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/db/models/index.js"; +import mongoose, { PipelineStage } from "mongoose"; import { getDateForRange } from "@/utils/dataUtils.js"; import { ILogger } from "@/utils/logger.js"; @@ -31,13 +31,13 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); }; - const mapMetadata = (metadata: any): GeoCheckMetadata => ({ + const mapMetadata = (metadata: GeoCheckMetadataDocument): GeoCheckMetadata => ({ monitorId: toStringId(metadata.monitorId), teamId: toStringId(metadata.teamId), type: metadata.type, }); - const mapResults = (results: any[]): GeoCheckResult[] => { + const mapResults = (results: GeoCheckResult[]): GeoCheckResult[] => { if (!results || !Array.isArray(results)) { return []; } @@ -89,12 +89,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { })) ); return docs.map((doc) => this.toEntity(doc)); - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: `Failed to createGeoChecks: ${error.message}`, + message: error instanceof Error ? `Failed to createGeoChecks: ${error.message}` : "Failed to createGeoChecks", service: SERVICE_NAME, method: "createGeoChecks", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -109,7 +109,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { continents?: GeoContinent[] ): Promise => { try { - const matchStage: Record = { + const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), ...(getDateForRange(dateRange) && { createdAt: { @@ -125,22 +125,8 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { skip = page * rowsPerPage; } - if (continents && continents.length > 0) { - const pipeline: any[] = [ - { $match: matchStage }, - { $unwind: "$results" }, - { - $match: { - "results.location.continent": { $in: continents }, - }, - }, - ]; - } else { - const pipeline: any[] = [{ $match: matchStage }, { $unwind: "$results" }]; - } - // Common pipeline stages for both paths - const pipeline: any[] = [ + const pipeline: PipelineStage[] = [ { $match: matchStage }, { $unwind: "$results" }, // Filter by continent if specified @@ -174,7 +160,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { ]; // Count pipeline - const countPipeline: any[] = [ + const countPipeline: PipelineStage[] = [ { $match: matchStage }, { $unwind: "$results" }, ...(continents && continents.length > 0 @@ -206,12 +192,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { })); return { geoChecksCount, geoChecks }; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: `Error finding geo checks by monitor ID: ${error.message}`, + message: error instanceof Error ? `Error finding geo checks by monitor ID: ${error.message}` : "Error finding geo checks by monitor ID", service: SERVICE_NAME, method: "findByMonitorId", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -227,11 +213,15 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { }, }).sort({ createdAt: -1 }); return docs.map(this.toEntity); - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: `Error finding geo checks by monitor ID and date range: ${error.message}`, + message: + error instanceof Error + ? `Error finding geo checks by monitor ID and date range: ${error.message}` + : "Error finding geo checks by monitor ID and date range", service: SERVICE_NAME, method: "findByMonitorIdAndDateRange", + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -245,7 +235,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { continents?: GeoContinent[] ): Promise => { try { - const pipeline: any[] = [ + const pipeline: PipelineStage[] = [ // Match geo checks for this monitor in date range { $match: { @@ -325,11 +315,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { const results = await GeoCheckModel.aggregate(pipeline); return results as GroupedGeoCheck[]; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: `Error finding grouped geo checks: ${error.message}`, + message: error instanceof Error ? `Error finding grouped geo checks: ${error.message}` : "Error finding grouped geo checks", service: SERVICE_NAME, method: "findGroupedByMonitorIdAndDateRange", + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -341,11 +332,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), }); return result.deletedCount || 0; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: `Error deleting geo checks by monitor ID: ${error.message}`, + message: error instanceof Error ? `Error deleting geo checks by monitor ID: ${error.message}` : "Error deleting geo checks by monitor ID", service: SERVICE_NAME, method: "deleteByMonitorId", + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -357,11 +349,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { "metadata.teamId": new mongoose.Types.ObjectId(teamId), }); return result.deletedCount || 0; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: `Error deleting geo checks by team ID: ${error.message}`, + message: error instanceof Error ? `Error deleting geo checks by team ID: ${error.message}` : "Error deleting geo checks by team ID", service: SERVICE_NAME, method: "deleteByTeamId", + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -372,11 +365,12 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } }); return result.deletedCount || 0; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: `Error deleting orphaned geo checks: ${error.message}`, + message: error instanceof Error ? `Error deleting orphaned geo checks: ${error.message}` : "Error deleting orphaned geo checks", service: SERVICE_NAME, method: "deleteByMonitorIdsNotIn", + stack: error instanceof Error ? error.stack : undefined, }); throw error; } diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentsRepository.ts similarity index 97% rename from server/src/repositories/incidents/MongoIncidentRepository.ts rename to server/src/repositories/incidents/MongoIncidentsRepository.ts index 93ecac248..e24551462 100644 --- a/server/src/repositories/incidents/MongoIncidentRepository.ts +++ b/server/src/repositories/incidents/MongoIncidentsRepository.ts @@ -5,7 +5,7 @@ import type { IIncidentsRepository } from "@/repositories/index.js"; import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; -class MongoIncidentRepository implements IIncidentsRepository { +class MongoIncidentsRepository implements IIncidentsRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { if (!value) { return ""; @@ -32,8 +32,8 @@ class MongoIncidentRepository implements IIncidentsRepository { status?: boolean; monitorId?: string; resolutionType?: string; - }): Record { - const matchStage: Record = { + }): Record { + const matchStage: Record = { teamId: new mongoose.Types.ObjectId(teamId), ...(status !== undefined && { status }), ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), @@ -288,4 +288,4 @@ class MongoIncidentRepository implements IIncidentsRepository { return result.deletedCount ?? 0; }; } -export default MongoIncidentRepository; +export default MongoIncidentsRepository; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index c1d8c2ecf..fc5eefee9 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -26,7 +26,7 @@ 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 { default as MongoIncidentsRepository } from "@/repositories/incidents/MongoIncidentsRepository.js"; export * from "@/repositories/teams/ITeamsRepository.js"; export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js"; @@ -36,3 +36,6 @@ export { default as MongoMaintenanceWindowsRepository } from "@/repositories/mai export * from "@/repositories/geo-checks/IGeoChecksRepository.js"; export { default as MongoGeoChecksRepository } from "@/repositories/geo-checks/MongoGeoChecksRepository.js"; + +export * from "@/repositories/dlq/IDLQRepository.js"; +export { default as MongoDLQRepository } from "@/repositories/dlq/MongoDLQRepository.js"; diff --git a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts index 1bcb02347..fca63ac35 100644 --- a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts @@ -5,12 +5,12 @@ export interface IMaintenanceWindowsRepository { // 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; + findByTeamId(teamId: string, page: number, rowsPerPage: number, field?: string, order?: string, active?: boolean): Promise; // update updateById(id: string, teamId: string, data: Partial): Promise; // delete deleteById(id: string, teamId: string): Promise; // other - countByTeamId(teamId: string, active: boolean): Promise; + countByTeamId(teamId: string, active?: boolean): Promise; } diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts index 4aadf9cc2..ad96328c5 100644 --- a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -1,7 +1,7 @@ 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 mongoose, { SortOrder } from "mongoose"; import { AppError } from "@/utils/AppError.js"; class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository { @@ -75,13 +75,13 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository findByTeamId = async ( teamId: string, - active: boolean, page: number, rowsPerPage: number, - field: string, - order: string + field?: string, + order?: string, + active?: boolean ): Promise => { - const maintenanceQuery: Record = { teamId }; + const maintenanceQuery: Record = { teamId }; if (active !== undefined) maintenanceQuery.active = active; @@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository } // Sorting - const sort: Record = {}; + const sort: Record = {}; if (field !== undefined && order !== undefined) { sort[field] = order === "asc" ? 1 : -1; } @@ -126,8 +126,8 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository } return this.toEntity(deleted); }; - countByTeamId = async (teamId: string, active: boolean) => { - const maintenanceQuery: Record = { teamId }; + countByTeamId = async (teamId: string, active?: boolean) => { + const maintenanceQuery: Record = { teamId }; if (active !== undefined) maintenanceQuery.active = active; diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts index 4abdeb33f..101283e32 100644 --- a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -1,11 +1,12 @@ -import type { MonitorStats } from "@/types/index.js"; +import type { MonitorStats, CheckResultInput } from "@/types/index.js"; + export interface IMonitorStatsRepository { // create create(data: Omit): Promise; // single fetch findByMonitorId(monitorId: string): Promise; - // update - updateByMonitorId(monitorId: string, data: Omit): Promise; + // Upsert must be atomic to prevent race conditions + updateByMonitorId(monitorId: string, result: CheckResultInput): Promise; // delete deleteByMonitorId(monitorId: string): Promise; deleteByMonitorIds(monitorIds: string[]): Promise; diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts index 774405587..ea255f7ad 100644 --- a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -1,5 +1,5 @@ import { type MonitorStatsDocument, MonitorStatsModel } from "@/db/models/index.js"; -import type { MonitorStats } from "@/types/index.js"; +import type { MonitorStats, CheckResultInput } from "@/types/index.js"; import { IMonitorStatsRepository } from "@/repositories/index.js"; import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; @@ -46,10 +46,48 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository { return this.toEntity(monitorStats); }; - updateByMonitorId = async (monitorId: string, data: Omit): Promise => { - const updated = await MonitorStatsModel.findOneAndUpdate({ monitorId: new mongoose.Types.ObjectId(monitorId) }, { $set: data }, { new: true }); + updateByMonitorId = async (monitorId: string, result: CheckResultInput): Promise => { + const { status, responseTime, now } = result; + const objectId = new mongoose.Types.ObjectId(monitorId); + const upDelta = status ? 1 : 0; + const downDelta = status ? 0 : 1; + const pipeline = [ + { + $set: { + monitorId: objectId, + totalChecks: { $add: [{ $ifNull: ["$totalChecks", 0] }, 1] }, + totalUpChecks: { $add: [{ $ifNull: ["$totalUpChecks", 0] }, upDelta] }, + totalDownChecks: { $add: [{ $ifNull: ["$totalDownChecks", 0] }, downDelta] }, + avgResponseTime: { + $divide: [ + { + $add: [{ $multiply: [{ $ifNull: ["$avgResponseTime", 0] }, { $ifNull: ["$totalChecks", 0] }] }, responseTime], + }, + { $add: [{ $ifNull: ["$totalChecks", 0] }, 1] }, + ], + }, + uptimePercentage: { + $divide: [{ $add: [{ $ifNull: ["$totalUpChecks", 0] }, upDelta] }, { $add: [{ $ifNull: ["$totalChecks", 0] }, 1] }], + }, + maxResponseTime: { $max: [{ $ifNull: ["$maxResponseTime", 0] }, responseTime] }, + lastResponseTime: responseTime, + lastCheckTimestamp: now, + timeOfLastFailure: status + ? { + $cond: [{ $eq: [{ $ifNull: ["$timeOfLastFailure", 0] }, 0] }, now, "$timeOfLastFailure"], + } + : 0, + }, + }, + ]; + + const updated = await MonitorStatsModel.findOneAndUpdate({ monitorId: objectId }, pipeline, { + upsert: true, + new: true, + setDefaultsOnInsert: true, + }); if (!updated) { - throw new AppError({ message: "Monitor stats not found", status: 404 }); + throw new AppError({ message: "Failed to apply check result to monitor stats", status: 500 }); } return this.toEntity(updated); }; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 9d82d9987..941556589 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -1,4 +1,4 @@ -import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js"; +import { type MonitorType, type Monitor, type MonitorsSummary, CheckSnapshot } from "@/types/index.js"; export interface TeamQueryConfig { limit?: number; @@ -29,6 +29,15 @@ export interface IMonitorsRepository { // update updateById(monitorId: string, teamId: string, updates: Partial): Promise; + updateStatusWindowAndChecks( + monitorId: string, + teamId: string, + status: boolean, + checkSnapshot: CheckSnapshot, + windowSize: number, + maxRecentChecks: number, + statusPatch?: Partial + ): Promise; togglePauseById(monitorId: string, teamId: string): Promise; // delete deleteById(monitorId: string, teamId: string): Promise; @@ -41,6 +50,7 @@ export interface IMonitorsRepository { findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; removeNotificationFromMonitors(notificationId: string): Promise; + updateNotifications(teamId: string, monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set"): Promise; deleteByTeamIdsNotIn(teamIds: string[]): Promise; findAllMonitorIds(): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index a3b40f459..bd6d506d5 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,8 +1,9 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument, CheckSnapshotDocument } from "@/db/models/index.js"; -import type { Monitor, MonitorsSummary, CheckSnapshot, Check } from "@/types/index.js"; +import type { Monitor, MonitorsSummary, CheckSnapshot } from "@/types/index.js"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; +import { MongoBulkWriteError } from "mongodb"; import { AppError } from "@/utils/AppError.js"; class MongoMonitorsRepository implements IMonitorsRepository { @@ -20,12 +21,9 @@ class MongoMonitorsRepository implements IMonitorsRepository { try { const inserted = await MonitorModel.insertMany(payload, { ordered: false }); return this.mapDocuments(inserted); - } catch (error: any) { - if (error.name === "MongoBulkWriteError" || error.name === "BulkWriteError") { - const insertedDocs = error.insertedDocs || []; - if (insertedDocs.length > 0) { - return this.mapDocuments(insertedDocs); - } + } catch (error: unknown) { + if (error instanceof MongoBulkWriteError && "insertedDocs" in error && Array.isArray(error.insertedDocs) && error.insertedDocs.length > 0) { + return this.mapDocuments(error.insertedDocs); } throw error; } @@ -46,7 +44,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type } = config ?? {}; const query: Record = { teamId: new mongoose.Types.ObjectId(teamId), @@ -184,6 +182,33 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(updatedMonitor); }; + updateStatusWindowAndChecks = async ( + monitorId: string, + teamId: string, + status: boolean, + checkSnapshot: CheckSnapshot, + windowSize: number, + maxRecentChecks: number, + statusPatch?: Partial + ): Promise => { + const updatedMonitor = await MonitorModel.findOneAndUpdate( + { _id: monitorId, teamId }, + { + $push: { + statusWindow: { $each: [status], $slice: -windowSize }, + recentChecks: { $each: [checkSnapshot], $slice: -maxRecentChecks }, + }, + ...(statusPatch && { $set: statusPatch }), + }, + { returnDocument: "after" } + ); + + if (!updatedMonitor) { + throw new AppError({ message: `Failed to update status and checks for monitor with id ${monitorId}`, status: 500 }); + } + return this.toEntity(updatedMonitor); + }; + togglePauseById = async (monitorId: string, teamId: string) => { const monitor = await MonitorModel.findOneAndUpdate( { _id: monitorId, teamId }, @@ -298,6 +323,41 @@ class MongoMonitorsRepository implements IMonitorsRepository { await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); }; + updateNotifications = async ( + teamId: string, + monitorIds: string[], + notificationIds: string[], + action: "add" | "remove" | "set" + ): Promise => { + let objectIds; + let notificationObjectIds; + try { + objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + notificationObjectIds = notificationIds.map((id) => new mongoose.Types.ObjectId(id)); + } catch { + throw new AppError({ message: "One or more monitor or notification IDs are invalid", status: 400 }); + } + const filter = { _id: { $in: objectIds }, teamId: new mongoose.Types.ObjectId(teamId) }; + + let update; + switch (action) { + case "add": + update = { $addToSet: { notifications: { $each: notificationObjectIds } } }; + break; + case "remove": + update = { $pull: { notifications: { $in: notificationObjectIds } } }; + break; + case "set": + update = { $set: { notifications: notificationObjectIds } }; + break; + default: + throw new AppError({ message: `Invalid action: ${action}`, status: 400 }); + } + + const result = await MonitorModel.updateMany(filter, update); + return result.modifiedCount; + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; @@ -354,7 +414,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { gameId: doc.gameId ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined, group: doc.group ?? null, - recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)), + recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckInterval: doc.geoCheckInterval ?? 300000, @@ -363,7 +423,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; }; - private toEntityWithChecks = (doc: any): Monitor => { + private toEntityWithChecks = (doc: MonitorDocument): Monitor => { const toStringId = (value: unknown): string => { if (value instanceof mongoose.Types.ObjectId) { return value.toString(); @@ -413,7 +473,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { gameId: doc.gameId ?? undefined, grpcServiceName: doc.grpcServiceName ?? undefined, group: doc.group ?? null, - recentChecks: (doc.recentChecks ?? []).map((check: any) => this.toCheckSnapshot(check)), + recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), geoCheckEnabled: doc.geoCheckEnabled ?? false, geoCheckLocations: doc.geoCheckLocations ?? [], geoCheckInterval: doc.geoCheckInterval ?? 300000, diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts index 3c4225b50..5303aa6ab 100644 --- a/server/src/repositories/notifications/MongoNotificationsRepository.ts +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -32,6 +32,8 @@ class MongoNotificationsRepository implements INotificationsRepository { homeserverUrl: doc.homeserverUrl ?? undefined, roomId: doc.roomId ?? undefined, accessToken: doc.accessToken ?? undefined, + accountSid: doc.accountSid ?? undefined, + twilioPhoneNumber: doc.twilioPhoneNumber ?? undefined, createdAt: toDateString(doc.createdAt), updatedAt: toDateString(doc.updatedAt), }; diff --git a/server/src/repositories/settings/ISettingsRepository.ts b/server/src/repositories/settings/ISettingsRepository.ts index 578d7f40d..0485a5aea 100644 --- a/server/src/repositories/settings/ISettingsRepository.ts +++ b/server/src/repositories/settings/ISettingsRepository.ts @@ -1,11 +1,11 @@ -import type { Settings } from "@/types/index.js"; +import type { Settings, SettingsUpdate } from "@/types/index.js"; export interface ISettingsRepository { // create create(settings: Partial): Promise; // fetch findSingleton(): Promise; // update - update(settings: Partial): Promise; + update(settings: SettingsUpdate): Promise; // delete deleteLegacy: () => Promise; } diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts index f803ae6a9..3117a94ea 100644 --- a/server/src/repositories/settings/MongoSettingsRepository.ts +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -1,6 +1,6 @@ -import mongoose from "mongoose"; +import mongoose, { type UpdateQuery } from "mongoose"; import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; -import type { Settings } from "@/types/index.js"; +import type { Settings, SettingsUpdate } from "@/types/index.js"; import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js"; class MongoSettingsRepository implements ISettingsRepository { @@ -59,27 +59,23 @@ class MongoSettingsRepository implements ISettingsRepository { return this.toEntity(settings); }; - update = async (settings: Partial) => { - const update: Record = { $set: {}, $unset: {} }; + update = async (settings: SettingsUpdate) => { + const $set: Record = {}; + const $unset: Record = {}; // Iterate through settings and separate into $set and $unset Object.entries(settings).forEach(([key, value]) => { - if (value === undefined || value === "") { - // Unset undefined or empty string values - update.$unset[key] = ""; + if (value === undefined || value === null) { + $unset[key] = ""; } else { - // Set defined values - update.$set[key] = value; + $set[key] = value; } }); - // Clean up empty operators - if (Object.keys(update.$set).length === 0) { - delete update.$set; - } - if (Object.keys(update.$unset).length === 0) { - delete update.$unset; - } + const update: UpdateQuery = { + ...(Object.keys($set).length > 0 && { $set }), + ...(Object.keys($unset).length > 0 && { $unset }), + }; await AppSettingsModel.findOneAndUpdate({}, update, { upsert: true, diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index e19676285..d8e942271 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -57,6 +57,7 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { showCharts: doc.showCharts, showUptimePercentage: doc.showUptimePercentage, showAdminLoginLink: doc.showAdminLoginLink, + showInfrastructure: doc.showInfrastructure, customCSS: doc.customCSS, createdAt: this.toDateString(doc.createdAt), updatedAt: this.toDateString(doc.updatedAt), @@ -71,7 +72,8 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { }; create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise => { - const { logo: _logo, ...restData } = data; + const { logo, ...restData } = data; + void logo; const statusPage = new StatusPageModel({ ...restData, userId, @@ -109,7 +111,8 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { image: Express.Multer.File | undefined, patch: Partial & { removeLogo?: string } ): Promise => { - const { logo: _logo, removeLogo, ...restPatch } = patch; + const { logo, removeLogo, ...restPatch } = patch; + void logo; const updateData: StatusPageUpdateData = { ...restPatch }; if (image) { updateData.logo = { diff --git a/server/src/repositories/users/MongoUsersRepository.ts b/server/src/repositories/users/MongoUsersRepository.ts index fde134a33..a8525f2c8 100644 --- a/server/src/repositories/users/MongoUsersRepository.ts +++ b/server/src/repositories/users/MongoUsersRepository.ts @@ -117,7 +117,7 @@ class MongoUsersRepository implements IUsersRepository { delete candidateUser.deleteProfileImage; - const updateQuery: Record = { $set: candidateUser }; + const updateQuery: Record = { $set: candidateUser }; if (unsetFields) { updateQuery.$unset = unsetFields; } diff --git a/server/src/routes/authRoute.ts b/server/src/routes/authRoute.ts index 2e5fa1069..af6fd4266 100755 --- a/server/src/routes/authRoute.ts +++ b/server/src/routes/authRoute.ts @@ -1,14 +1,15 @@ import { Router, RequestHandler } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; import multer from "multer"; +import { IAuthController } from "@/controllers/authController.js"; const upload = multer(); class AuthRoutes { private router: Router; - private authController: any; + private authController: IAuthController; - constructor(authController: any, verifyJWT: RequestHandler) { + constructor(authController: IAuthController, verifyJWT: RequestHandler) { this.router = Router(); this.authController = authController; this.initRoutes(verifyJWT); @@ -31,8 +32,8 @@ class AuthRoutes { this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById); this.router.delete("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.deleteUserById); - this.router.patch("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser); - this.router.delete("/user", verifyJWT, this.authController.deleteUser); + this.router.patch("/user", verifyJWT, upload.single("profileImage"), isAllowed(["admin", "superadmin", "user"]), this.authController.editUser); + this.router.delete("/user", verifyJWT, isAllowed(["admin", "superadmin", "user"]), this.authController.deleteUser); } getRouter() { diff --git a/server/src/routes/checkRoute.ts b/server/src/routes/checkRoute.ts index 4df286846..712bae969 100755 --- a/server/src/routes/checkRoute.ts +++ b/server/src/routes/checkRoute.ts @@ -1,12 +1,13 @@ import { Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; +import { ICheckController } from "@/controllers/checkController.js"; class CheckRoutes { private router: Router; - private checkController: any; + private checkController: ICheckController; - constructor(checkController: any) { + constructor(checkController: ICheckController) { this.router = Router(); this.checkController = checkController; this.initRoutes(); @@ -15,7 +16,6 @@ class CheckRoutes { initRoutes() { this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId); this.router.get("/team", this.checkController.getChecksByTeam); - this.router.put("/team/ttl", isAllowed(["admin", "superadmin"]), this.checkController.updateChecksTTL); this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId); this.router.get("/:monitorId", this.checkController.getChecksByMonitor); this.router.delete("/:monitorId", this.checkController.deleteChecks); diff --git a/server/src/routes/diagnosticRoute.ts b/server/src/routes/diagnosticRoute.ts index b2cefe4aa..32b857e8d 100755 --- a/server/src/routes/diagnosticRoute.ts +++ b/server/src/routes/diagnosticRoute.ts @@ -1,11 +1,12 @@ import { RequestHandler, Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; +import { IDiagnosticController } from "@/controllers/diagnosticController.js"; class DiagnosticRoutes { private router: Router; - private diagnosticController: any; + private diagnosticController: IDiagnosticController; - constructor(diagnosticController: any, verifyJWT: RequestHandler) { + constructor(diagnosticController: IDiagnosticController, verifyJWT: RequestHandler) { this.router = Router(); this.diagnosticController = diagnosticController; this.initRoutes(verifyJWT); diff --git a/server/src/routes/geoCheckRoutes.ts b/server/src/routes/geoCheckRoutes.ts index 4c34df56e..1fc48d7b3 100644 --- a/server/src/routes/geoCheckRoutes.ts +++ b/server/src/routes/geoCheckRoutes.ts @@ -1,10 +1,11 @@ +import { IGeoCheckController } from "@/controllers/geoCheckController.js"; import { Router } from "express"; class GeoCheckRoutes { private router: Router; - private geoCheckController: any; + private geoCheckController: IGeoCheckController; - constructor(geoCheckController: any) { + constructor(geoCheckController: IGeoCheckController) { this.router = Router(); this.geoCheckController = geoCheckController; this.initRoutes(); diff --git a/server/src/routes/incidentRoute.ts b/server/src/routes/incidentRoute.ts index 83f1ff8f9..fdbb6f532 100644 --- a/server/src/routes/incidentRoute.ts +++ b/server/src/routes/incidentRoute.ts @@ -1,11 +1,12 @@ import { Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; +import { IIncidentController } from "@/controllers/incidentController.js"; class IncidentRoutes { private router: Router; - private incidentController: any; + private incidentController: IIncidentController; - constructor(incidentController: any) { + constructor(incidentController: IIncidentController) { this.router = Router(); this.incidentController = incidentController; this.initRoutes(); diff --git a/server/src/routes/inviteRoute.ts b/server/src/routes/inviteRoute.ts index b91f922df..4542bd21e 100755 --- a/server/src/routes/inviteRoute.ts +++ b/server/src/routes/inviteRoute.ts @@ -1,11 +1,12 @@ import { RequestHandler, Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; +import { IInviteController } from "@/controllers/inviteController.js"; class InviteRoutes { private router: Router; - private inviteController: any; + private inviteController: IInviteController; - constructor(inviteController: any, verifyJWT: RequestHandler) { + constructor(inviteController: IInviteController, verifyJWT: RequestHandler) { this.router = Router(); this.inviteController = inviteController; this.initRoutes(verifyJWT); diff --git a/server/src/routes/logRoutes.ts b/server/src/routes/logRoutes.ts index c35361fd0..ea03d690e 100755 --- a/server/src/routes/logRoutes.ts +++ b/server/src/routes/logRoutes.ts @@ -1,10 +1,11 @@ import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { isAllowed } from "@/middleware/isAllowed.js"; +import { ILogController } from "@/controllers/logController.js"; class LogRoutes { private router: Router; - private logController: any; + private logController: ILogController; - constructor(logController: any) { + constructor(logController: ILogController) { this.router = Router(); this.logController = logController; this.initRoutes(); diff --git a/server/src/routes/maintenanceWindowRoute.ts b/server/src/routes/maintenanceWindowRoute.ts index 539f2413f..1c2c139c0 100755 --- a/server/src/routes/maintenanceWindowRoute.ts +++ b/server/src/routes/maintenanceWindowRoute.ts @@ -1,10 +1,11 @@ +import { IMaintenanceWindowController } from "@/controllers/maintenanceWindowController.js"; import { Router } from "express"; class MaintenanceWindowRoutes { private router: Router; - private mwController: any; + private mwController: IMaintenanceWindowController; - constructor(maintenanceWindowController: any) { + constructor(maintenanceWindowController: IMaintenanceWindowController) { this.router = Router(); this.mwController = maintenanceWindowController; this.initRoutes(); diff --git a/server/src/routes/monitorRoute.ts b/server/src/routes/monitorRoute.ts index 476e421ce..277cd250b 100755 --- a/server/src/routes/monitorRoute.ts +++ b/server/src/routes/monitorRoute.ts @@ -1,15 +1,11 @@ import { Router } from "express"; import { isAllowed } from "@/middleware/isAllowed.js"; -import multer from "multer"; -import { fetchMonitorCertificate } from "@/controllers/controllerUtils.js"; -const upload = multer({ - storage: multer.memoryStorage(), // Store file in memory as Buffer -}); +import { IMonitorController } from "@/controllers/monitorController.js"; class MonitorRoutes { private router: Router; - private monitorController: any; - constructor(monitorController: any) { + private monitorController: IMonitorController; + constructor(monitorController: IMonitorController) { this.router = Router(); this.monitorController = monitorController; this.initRoutes(); @@ -38,10 +34,11 @@ class MonitorRoutes { // Util routes this.router.get("/certificate/:monitorId", (req, res, next) => { - this.monitorController.getMonitorCertificate(req, res, next, fetchMonitorCertificate); + this.monitorController.getMonitorCertificate(req, res, next); }); // General monitor CRUD routes + this.router.patch("/notifications", isAllowed(["admin", "superadmin"]), this.monitorController.updateNotifications); this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); @@ -50,7 +47,6 @@ class MonitorRoutes { this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON); - this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail); this.router.get("/games", this.monitorController.getAllGames); // Individual monitor CRUD routes diff --git a/server/src/routes/notificationRoute.ts b/server/src/routes/notificationRoute.ts index a1802d3b9..4ec6ce3b6 100755 --- a/server/src/routes/notificationRoute.ts +++ b/server/src/routes/notificationRoute.ts @@ -1,9 +1,10 @@ +import { INotificationController } from "@/controllers/notificationController.js"; import { Router } from "express"; class NotificationRoutes { private router: Router; - private notificationController: any; + private notificationController: INotificationController; - constructor(notificationController: any) { + constructor(notificationController: INotificationController) { this.router = Router(); this.notificationController = notificationController; this.initializeRoutes(); diff --git a/server/src/routes/queueRoute.ts b/server/src/routes/queueRoute.ts index d5b036b15..d5d35147c 100755 --- a/server/src/routes/queueRoute.ts +++ b/server/src/routes/queueRoute.ts @@ -1,20 +1,19 @@ import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { isAllowed } from "@/middleware/isAllowed.js"; +import { IJobQueueController } from "@/controllers/queueController.js"; class QueueRoutes { private router: Router; - private queueController: any; + private queueController: IJobQueueController; - constructor(queueController: any) { + constructor(queueController: IJobQueueController) { this.router = Router(); this.queueController = queueController; this.initRoutes(); } initRoutes() { this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs); - this.router.post("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.addJob); this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics); - this.router.get("/health", isAllowed(["admin", "superadmin"]), this.queueController.checkQueueHealth); this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics); this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue); } diff --git a/server/src/routes/settingsRoute.ts b/server/src/routes/settingsRoute.ts index c03c17524..aec2319c2 100755 --- a/server/src/routes/settingsRoute.ts +++ b/server/src/routes/settingsRoute.ts @@ -1,11 +1,12 @@ import { Router } from "express"; import { isAllowed } from "../middleware/isAllowed.js"; +import { ISettingsController } from "@/controllers/settingsController.js"; class SettingsRoutes { private router: Router; - private settingsController: any; + private settingsController: ISettingsController; - constructor(settingsController: any) { + constructor(settingsController: ISettingsController) { this.router = Router(); this.settingsController = settingsController; this.initRoutes(); diff --git a/server/src/routes/statusPageRoute.ts b/server/src/routes/statusPageRoute.ts index a53aa04d9..871a67300 100755 --- a/server/src/routes/statusPageRoute.ts +++ b/server/src/routes/statusPageRoute.ts @@ -1,12 +1,13 @@ +import { IStatusPageController } from "@/controllers/statusPageController.js"; import { RequestHandler, Router } from "express"; import multer from "multer"; const upload = multer(); class StatusPageRoutes { private router: Router; - private statusPageController: any; + private statusPageController: IStatusPageController; - constructor(statusPageController: any, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) { + constructor(statusPageController: IStatusPageController, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) { this.router = Router(); this.statusPageController = statusPageController; this.initRoutes(verifyJWT, verifyStatusPageAccess); diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index c83a3d411..dd2a66202 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,22 +1,45 @@ import { Types } from "mongoose"; import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; -import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js"; +import type { + MonitorStatusResponse, + CheckErrorInfo, + Check, + ILighthouseAudit, + ChecksQueryResult, + ChecksSummary, + MonitorPayloadMap, +} from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; -import { ParseBoolean } from "@/utils/utils.js"; import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "checkService"; export interface ICheckService { createChecks(checks: Check[]): Promise; - buildCheck(statusResponse: MonitorStatusResponse): Partial | undefined; - getChecksByMonitor(params: { monitorId: string; query: Record; teamId: string }): Promise; - getChecksByTeam(params: { teamId: string; query: Record }): Promise; + buildCheck(statusResponse: MonitorStatusResponse): Check | undefined; + getChecksByMonitor(params: { + monitorId: string; + teamId: string; + sortOrder: string; + dateRange: string; + page: number; + rowsPerPage: number; + filter?: string; + status?: boolean; + }): Promise; + getChecksByTeam(params: { + teamId: string; + sortOrder: string; + dateRange: string; + page: number; + rowsPerPage: number; + filter?: string; + }): Promise; getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise; deleteChecks(params: { monitorId: string; teamId: string }): Promise; deleteChecksByTeamId(params: { teamId: string }): Promise; - updateChecksTTL(params: { teamId: string; ttl: string }): Promise; + deleteOlderThan(date: Date): Promise; } export class CheckService implements ICheckService { @@ -39,12 +62,14 @@ export class CheckService implements ICheckService { return this.checksRepository.createChecks(checks); }; - buildCheck = (statusResponse: MonitorStatusResponse) => { + buildCheck = (statusResponse: MonitorStatusResponse): Check | undefined => { const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse; - const check: Partial = { + const now = new Date().toISOString(); + const check: Check = { id: new Types.ObjectId().toString(), - createdAt: new Date().toISOString(), + createdAt: now, + updatedAt: now, metadata: { monitorId, teamId, @@ -82,16 +107,16 @@ export class CheckService implements ICheckService { 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.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"]), + 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"]), }; } @@ -99,7 +124,7 @@ export class CheckService implements ICheckService { const hardwarePayload = payload as HardwareStatusPayload | undefined; const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; const errorsSource = Array.isArray(hardwarePayload?.errors) - ? hardwarePayload?.errors + ? hardwarePayload.errors : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; check.cpu = cpu; check.memory = memory; @@ -112,7 +137,25 @@ export class CheckService implements ICheckService { return check; }; - getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record; teamId: string }) => { + getChecksByMonitor = async ({ + monitorId, + teamId, + sortOrder, + dateRange, + filter, + page, + rowsPerPage, + status, + }: { + monitorId: string; + teamId: string; + sortOrder: string; + dateRange: string; + page: number; + rowsPerPage: number; + status?: boolean; + filter?: string; + }) => { if (!monitorId) { throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); } @@ -123,74 +166,54 @@ export class CheckService implements ICheckService { // For verification, throws an error if monitor doesn't belong to team await this.monitorsRepository.findById(monitorId, teamId); - const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query; - if (!sortOrder || !dateRange || !page || !rowsPerPage) { - throw new AppError({ - message: `Missing required query parameters sortOrder: ${sortOrder}, dateRange: ${dateRange}, filter: ${filter}, page: ${page}, rowsPerPage: ${rowsPerPage}`, - service: SERVICE_NAME, - method: "getChecksByMonitor", - status: 400, - }); - } - const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status); - const parsedPage = page ? parseInt(page) : 0; - const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5; + const parsedPage = page ?? 0; + const parsedRowsPerPage = rowsPerPage ?? 5; - const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus); + const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, status); return result; }; - getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record }) => { - const { sortOrder, dateRange, filter, page, rowsPerPage } = query; - if (!sortOrder || !dateRange || !page || !rowsPerPage) { - throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 }); - } - - if (!teamId) { - throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 }); - } - - const parsedPage = page ? parseInt(page) : 0; - const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5; + getChecksByTeam = async ({ + teamId, + sortOrder, + dateRange, + filter, + page, + rowsPerPage, + }: { + teamId: string; + sortOrder: string; + dateRange: string; + page: number; + rowsPerPage: number; + filter?: string; + }) => { + const parsedPage = page ?? 0; + const parsedRowsPerPage = rowsPerPage ?? 5; const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId); return checkData; }; getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: 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, dateRange); 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 verification, 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, details: { teamId, ttl } }); + deleteOlderThan = async (date: Date) => { + const deletedCount = await this.checksRepository.deleteOlderThan(date); + return deletedCount; }; } diff --git a/server/src/service/business/diagnosticService.ts b/server/src/service/business/diagnosticService.ts index f9c53f633..45a5592f5 100644 --- a/server/src/service/business/diagnosticService.ts +++ b/server/src/service/business/diagnosticService.ts @@ -24,8 +24,7 @@ export class DiagnosticService implements IDiagnosticService { * Clears performance marks after each measurement to prevent memory leaks. */ const obs = new PerformanceObserver((items) => { - // Get the first entry but we don't need to store it - items.getEntries()[0]; + items.getEntries(); performance.clearMarks(); }); obs.observe({ entryTypes: ["measure"] }); diff --git a/server/src/service/business/geoChecksService.ts b/server/src/service/business/geoChecksService.ts index f35826a8f..95b945f2a 100644 --- a/server/src/service/business/geoChecksService.ts +++ b/server/src/service/business/geoChecksService.ts @@ -1,7 +1,7 @@ import type { Monitor, GeoCheck } from "@/types/index.js"; -import type { GeoCheckResult } from "@/types/geoCheck.js"; +import type { GeoCheckResult, GeoContinent } from "@/types/geoCheck.js"; import { Types } from "mongoose"; -import type { IGeoChecksRepository } from "@/repositories/index.js"; +import type { FlatGeoChecksQueryResult, IGeoChecksRepository } from "@/repositories/index.js"; import type { IMonitorsRepository } from "@/repositories/index.js"; import type { IGlobalPingService } from "@/service/infrastructure/globalPingService.js"; import type { ILogger } from "@/utils/logger.js"; @@ -13,7 +13,15 @@ export interface IGeoChecksService { readonly serviceName: string; buildGeoCheck(monitor: Monitor): Promise; createGeoChecks(geoChecks: GeoCheck[]): Promise; - getGeoChecksByMonitor(args: { monitorId: string; query: any; teamId: string }): Promise; + getGeoChecksByMonitor(args: { + monitorId: string; + teamId: string; + sortOrder: string; + dateRange: string; + page?: number; + rowsPerPage?: number; + continent?: GeoContinent | GeoContinent[]; + }): Promise; } export class GeoChecksService implements IGeoChecksService { @@ -139,7 +147,23 @@ export class GeoChecksService implements IGeoChecksService { return this.geoChecksRepository.createGeoChecks(geoChecks); }; - getGeoChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { + getGeoChecksByMonitor = async ({ + monitorId, + teamId, + sortOrder, + dateRange, + page, + rowsPerPage, + continent, + }: { + monitorId: string; + teamId: string; + sortOrder: string; + dateRange: string; + page?: number; + rowsPerPage?: number; + continent: GeoContinent | GeoContinent[]; + }) => { if (!monitorId) { throw new AppError({ message: "No monitor ID in request", @@ -167,10 +191,9 @@ export class GeoChecksService implements IGeoChecksService { }); } - const { sortOrder, dateRange, page, rowsPerPage, continent } = query; const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined; - const parsedPage = page ? parseInt(page) : page; - const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; + const parsedPage = page || 0; + const parsedRowsPerPage = rowsPerPage || 5; const result = await this.geoChecksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, continents); return result; }; diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 710336016..2070a8937 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -2,7 +2,6 @@ const SERVICE_NAME = "incidentService"; import type { Monitor } from "@/types/monitor.js"; import type { MonitorStatusResponse } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; -import { ParseBoolean } from "@/utils/utils.js"; import { getDateForRange } from "@/utils/dataUtils.js"; import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; import type { Incident, IncidentSummary, User } from "@/types/index.js"; @@ -22,13 +21,13 @@ export interface IIncidentService { teamId: string, sortOrder: string, dateRange: string, - page: string, - rowsPerPage: string, - status: string, - monitorId: string, - resolutionType: string + page: number, + rowsPerPage: number, + status: boolean | undefined, + monitorId: string | undefined, + resolutionType: string | undefined ): Promise<{ incidents: Incident[]; count: number }>; - getIncidentSummary(teamId: string, limit?: string): Promise; + getIncidentSummary(teamId: string, limit?: number): Promise; getIncidentById(incidentId: string, teamId: string): Promise<{ incident: Incident; monitor: Monitor; user: User | null }>; } @@ -96,17 +95,14 @@ export class IncidentService implements IIncidentService { } } - if (decision.shouldResolveIncident) { - 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); + if (!decision.shouldResolveIncident || !activeIncident) { + return null; } - return null; + activeIncident.status = false; + activeIncident.endTime = Date.now().toString(); + activeIncident.resolutionType = "automatic"; + return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident); }; private buildThresholdBreachMessage(monitor: Monitor, monitorStatusResponse?: MonitorStatusResponse): string { @@ -164,13 +160,13 @@ export class IncidentService implements IIncidentService { }); return resolvedIncident; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ service: SERVICE_NAME, method: "resolveIncident", - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", details: { id: incidentId }, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -180,11 +176,11 @@ export class IncidentService implements IIncidentService { teamId: string, sortOrder: string, dateRange: string, - page: string, - rowsPerPage: string, - status: string, - monitorId: string, - resolutionType: string + page: number, + rowsPerPage: number, + status: boolean | undefined, + monitorId: string | undefined, + resolutionType: string | undefined ) => { try { if (!teamId) { @@ -193,9 +189,8 @@ export class IncidentService implements IIncidentService { const startDate = getDateForRange(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 parsedPage = page ?? 0; + const parsedRowsPerPage = rowsPerPage ?? 20; const incidents = await this.incidentsRepository.findByTeamId( teamId, @@ -203,43 +198,43 @@ export class IncidentService implements IIncidentService { parsedPage, parsedRowsPerPage, sortOrder, - parsedStatus, + status, monitorId, resolutionType ); - const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType); + const count = await this.incidentsRepository.countByTeamId(teamId, startDate, status, monitorId, resolutionType); return { incidents, count }; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ service: SERVICE_NAME, method: "getIncidentsByTeam", - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", details: { teamId }, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } }; - getIncidentSummary = async (teamId: string, limit?: string) => { + getIncidentSummary = async (teamId: string, limit?: number) => { 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 parsedLimit = limit ?? 10; const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit); return summary; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ service: SERVICE_NAME, method: "getIncidentSummary", - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", details: { teamId }, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -254,13 +249,13 @@ export class IncidentService implements IIncidentService { user = await this.usersRepository.findById(incident.resolvedBy); } return { incident, monitor, user }; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ service: SERVICE_NAME, method: "getIncidentById", - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", details: { incidentId }, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index 9d1cbf59d..7e2b7de52 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -2,6 +2,8 @@ import type { Invite, UserRole } from "@/types/index.js"; import { canManageRole } from "@/types/user.js"; import type { IInvitesRepository } from "@/repositories/index.js"; import { AppError } from "@/utils/AppError.js"; +import { ISettingsService } from "../system/settingsService.js"; +import { IEmailService } from "../infrastructure/emailService.js"; const SERVICE_NAME = "inviteService"; @@ -14,8 +16,8 @@ export interface IInviteService { export class InviteService implements IInviteService { static SERVICE_NAME = SERVICE_NAME; - private settingsService: any; - private emailService: any; + private settingsService: ISettingsService; + private emailService: IEmailService; private invitesRepository: IInvitesRepository; constructor({ @@ -24,8 +26,8 @@ export class InviteService implements IInviteService { emailService, }: { invitesRepository: IInvitesRepository; - settingsService: any; - emailService: any; + settingsService: ISettingsService; + emailService: IEmailService; }) { this.invitesRepository = invitesRepository; this.settingsService = settingsService; @@ -57,8 +59,16 @@ export class InviteService implements IInviteService { return inviteToken; }; - sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial; firstName: any; userRoles: UserRole[] }) => { + sendInviteEmail = async ({ invite, firstName, userRoles }: { invite: Partial; firstName: string; userRoles: UserRole[] }) => { const inviteRoles = invite.role ?? []; + if (!invite.email) { + throw new AppError({ + message: "Invite email is required to send an invite", + service: SERVICE_NAME, + method: "sendInviteEmail", + status: 400, + }); + } for (const targetRole of inviteRoles) { const canManage = userRoles.some((actorRole) => canManageRole(actorRole, targetRole)); @@ -66,7 +76,7 @@ export class InviteService implements IInviteService { throw new AppError({ message: "You do not have permission to create this invite", service: SERVICE_NAME, - method: "getInviteToken", + method: "sendInviteEmail", status: 403, }); } @@ -79,6 +89,16 @@ export class InviteService implements IInviteService { name: firstName, link: `${clientHost}/register/${inviteToken.token}`, }); + + if (!html) { + throw new AppError({ + message: "Failed to build invite e-mail... Please verify your settings.", + service: SERVICE_NAME, + method: "sendInviteEmail", + status: 500, + }); + } + const result = await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html); if (!result) { throw new AppError({ diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index abf513eb5..50278d215 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -1,16 +1,29 @@ import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; -import type { MaintenanceWindow } from "@/types/index.js"; -import { ParseBoolean } from "@/utils/utils.js"; +import type { DurationUnit, MaintenanceWindow } from "@/types/index.js"; import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "maintenanceWindowService"; export interface IMaintenanceWindowService { - createMaintenanceWindow(params: { teamId: string; body: Partial & { monitors: string[] } }): Promise; + createMaintenanceWindow(params: { + teamId: string; + monitorIDs: string[]; + name: string; + active: boolean; + duration: number; + durationUnit: DurationUnit; + repeat: number; + start: string; + end: string; + }): Promise; getMaintenanceWindowById(params: { id: string; teamId: string }): Promise; getMaintenanceWindowsByTeamId(params: { teamId: string; - query: Record; + active?: boolean; + page?: number; + rowsPerPage?: number; + field?: string; + order?: string; }): Promise<{ maintenanceWindows: MaintenanceWindow[]; maintenanceWindowCount: number }>; getMaintenanceWindowsByMonitorId(params: { monitorId: string; teamId: string }): Promise; deleteMaintenanceWindow(params: { id: string; teamId: string }): Promise; @@ -37,9 +50,28 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return MaintenanceWindowService.SERVICE_NAME; } - createMaintenanceWindow = async ({ teamId, body }: { teamId: string; body: any }) => { - const monitorIds = body.monitors; - const monitors = await this.monitorsRepository.findByIds(monitorIds); + createMaintenanceWindow = async ({ + teamId, + monitorIDs, + name, + active, + duration, + durationUnit, + repeat, + start, + end, + }: { + teamId: string; + monitorIDs: string[]; + name: string; + active: boolean; + duration: number; + durationUnit: DurationUnit; + repeat: number; + start: string; + end: string; + }) => { + const monitors = await this.monitorsRepository.findByIds(monitorIDs); const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId); @@ -52,17 +84,17 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { }); } - const dbTransactions = monitorIds.map((monitorId: string) => { + const dbTransactions = monitorIDs.map((monitorId: string) => { return this.maintenanceWindowsRepository.create({ teamId, monitorId, - name: body.name, - active: body.active ? body.active : true, - duration: body.duration, - durationUnit: body.durationUnit, - repeat: body.repeat, - start: body.start, - end: body.end, + name: name, + active: active, + duration: duration, + durationUnit: durationUnit, + repeat: repeat, + start: start, + end: end, }); }); await Promise.all(dbTransactions); @@ -72,14 +104,25 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return await this.maintenanceWindowsRepository.findById(id, teamId); }; - getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => { - let { active, page, rowsPerPage, field, order } = query || {}; + getMaintenanceWindowsByTeamId = async ({ + teamId, + active, + page, + rowsPerPage, + field, + order, + }: { + teamId: string; + active?: boolean; + page?: number; + rowsPerPage?: number; + field?: string; + order?: string; + }) => { + page = page ?? 0; + rowsPerPage = rowsPerPage ?? 10; - 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 maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, page, rowsPerPage, field, order, active); const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active); return { maintenanceWindows, maintenanceWindowCount }; }; @@ -92,7 +135,7 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { return await this.maintenanceWindowsRepository.deleteById(id, teamId); }; - editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => { + editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: Partial }) => { return await this.maintenanceWindowsRepository.updateById(id, teamId, body); }; } diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index a7a30111c..4ab5c9476 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -7,6 +7,7 @@ import type { HardwareDetailsResult, PageSpeedDetailsResult, GamesMap, + GroupedGeoCheckResult, } from "@/types/monitor.js"; import { supportsGeoCheck } from "@/types/monitor.js"; import type { GeoContinent } from "@/types/geoCheck.js"; @@ -18,11 +19,11 @@ import type { IMonitorStatsRepository, IStatusPagesRepository, } from "@/repositories/index.js"; -import fs from "fs"; -import { fileURLToPath } from "url"; -import path from "path"; +import demoMonitorsData from "@/utils/demoMonitors.json" with { type: "json" }; import { AppError } from "@/utils/AppError.js"; +import type { ImportedMonitor } from "@/validation/monitorValidation.js"; import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "MonitorService"; type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; @@ -31,15 +32,20 @@ export interface IMonitorService { readonly serviceName: string; // create - createMonitor(teamId: string, userId: string, body: Monitor): Promise; - createMonitors(monitors: Array, userId: string, teamId: string): Promise; + createMonitor(teamId: string, userId: string, body: Partial): Promise; + createMonitors(monitors: Array): Promise; addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read - getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; + getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; dateRange: string; continents?: GeoContinent[] }): Promise; + getGeoChecksByMonitorId(args: { + teamId: string; + monitorId: string; + dateRange: string; + continents?: GeoContinent[]; + }): Promise; getMonitorById(args: { teamId: string; monitorId: string }): Promise; getMonitorsByTeamId(args: { teamId: string; @@ -60,32 +66,32 @@ export interface IMonitorService { filter?: string; field?: string; order?: "asc" | "desc"; - explain?: boolean; }): Promise; getAllGames(): GamesMap; getGroupsByTeamId(args: { teamId: string }): Promise; // update - editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; + editMonitor(args: { teamId: string; monitorId: string; body: Partial }): Promise; pauseMonitor(args: { teamId: string; monitorId: string }): Promise; // delete deleteMonitor(args: { teamId: string; monitorId: string }): Promise; deleteAllMonitors(args: { teamId: string }): Promise; + // notifications + updateNotifications(args: { teamId: string; monitorIds: string[]; notificationIds: string[]; action: "add" | "remove" | "set" }): Promise; + // other - sendTestEmail(args: { to: string }): Promise; exportMonitorsToJSON(args: { teamId: string }): Promise; - importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: any[] }): Promise<{ imported: number; errors: string[] }>; + importMonitorsFromJSON(args: { teamId: string; userId: string; monitors: ImportedMonitor[] }): Promise<{ imported: number; errors: string[] }>; } export class MonitorService implements IMonitorService { static SERVICE_NAME = SERVICE_NAME; private jobQueue: ISuperSimpleQueue; - private emailService: any; - private logger: any; - private games: any; + private logger: ILogger; + private games: GamesMap; private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private geoChecksRepository: IGeoChecksRepository; @@ -95,7 +101,6 @@ export class MonitorService implements IMonitorService { constructor({ jobQueue, - emailService, logger, games, monitorsRepository, @@ -106,9 +111,8 @@ export class MonitorService implements IMonitorService { incidentsRepository, }: { jobQueue: ISuperSimpleQueue; - emailService: any; - logger: any; - games: any; + logger: ILogger; + games: GamesMap; monitorsRepository: IMonitorsRepository; checksRepository: IChecksRepository; geoChecksRepository: IGeoChecksRepository; @@ -117,7 +121,6 @@ export class MonitorService implements IMonitorService { incidentsRepository: IIncidentsRepository; }) { this.jobQueue = jobQueue; - this.emailService = emailService; this.logger = logger; this.games = games; this.monitorsRepository = monitorsRepository; @@ -166,9 +169,9 @@ export class MonitorService implements IMonitorService { this.jobQueue.addJob(monitor.id, monitor); }; - createMonitors = async (monitors: Array, userId: string, teamId: string): Promise => { + createMonitors = async (monitors: Array): Promise => { const createdMonitors = await this.monitorsRepository.createMonitors(monitors); - if (!monitors || monitors.length === 0) { + if (!createdMonitors || createdMonitors.length === 0) { throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createMonitors" }); } @@ -176,24 +179,17 @@ export class MonitorService implements IMonitorService { 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.createMonitors(monitors); + addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise => { + const monitors = demoMonitorsData.map((monitor) => ({ + userId, + teamId, + name: monitor.name, + description: monitor.name, + type: "http" as const, + url: monitor.url, + interval: 60000, + })); + const demoMonitors = await this.monitorsRepository.createMonitors(monitors as unknown as Monitor[]); await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor))); return demoMonitors; @@ -203,18 +199,16 @@ export class MonitorService implements IMonitorService { 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 rangeKey = dateRange as DateRangeKey; const { start, end } = this.getDateRange(rangeKey); const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { type: monitor.type, @@ -263,7 +257,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 }); } - const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const rangeKey = dateRange as DateRangeKey; const { start, end } = this.getDateRange(rangeKey); const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { type: monitor.type, @@ -305,7 +299,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 }); } - const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const rangeKey = dateRange as DateRangeKey; const { start, end } = this.getDateRange(rangeKey); const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { type: monitor.type, @@ -336,7 +330,7 @@ export class MonitorService implements IMonitorService { monitorId: string; dateRange: string; continents?: GeoContinent[]; - }): Promise => { + }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -346,7 +340,7 @@ export class MonitorService implements IMonitorService { return { groupedGeoChecks: [] }; } - const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const rangeKey = dateRange as DateRangeKey; const { start, end } = this.getDateRange(rangeKey); const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange( monitor.id, @@ -360,8 +354,7 @@ export class MonitorService implements IMonitorService { }; getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - const monitor = await this.monitorsRepository.findById(monitorId, teamId); - return monitor; + return await this.monitorsRepository.findById(monitorId, teamId); }; getMonitorsByTeamId = async ({ @@ -373,11 +366,7 @@ export class MonitorService implements IMonitorService { type?: MonitorType | MonitorType[]; filter?: string; }): Promise => { - const monitors = await this.monitorsRepository.findByTeamId(teamId, { - type, - filter, - }); - return monitors; + return await this.monitorsRepository.findByTeamId(teamId, { type, filter }); }; getMonitorsWithChecksByTeamId = async ({ @@ -389,7 +378,6 @@ export class MonitorService implements IMonitorService { filter, field, order, - explain, }: { teamId: string; limit?: number; @@ -399,7 +387,6 @@ export class MonitorService implements IMonitorService { 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 }); @@ -434,59 +421,84 @@ export class MonitorService implements IMonitorService { }; getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { - const groups = await this.monitorsRepository.findGroupsByTeamId(teamId); - return groups; + return await this.monitorsRepository.findGroupsByTeamId(teamId); }; - editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { + editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Partial }) => { const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); await this.jobQueue.updateJob(editedMonitor); return editedMonitor; }; + updateNotifications = async ({ + teamId, + monitorIds, + notificationIds, + action, + }: { + teamId: string; + monitorIds: string[]; + notificationIds: string[]; + action: "add" | "remove" | "set"; + }): Promise => { + const modifiedCount = await this.monitorsRepository.updateNotifications(teamId, monitorIds, notificationIds, action); + + // If notifications were updated, we should update the jobs in the queue + if (modifiedCount > 0) { + const monitors = await this.monitorsRepository.findByIds(monitorIds); + await Promise.all(monitors.map((monitor) => this.jobQueue.updateJob(monitor))); + } + + return modifiedCount; + }; + 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); + if (monitor.isActive) { + await this.jobQueue.resumeJob(monitor); + } else { + 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) => { + await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { this.logger.warn({ message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`, service: SERVICE_NAME, - stack: err.stack, + stack: err instanceof Error ? err.stack : undefined, }); }); - await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: any) => { + await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { this.logger.warn({ message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`, service: SERVICE_NAME, - stack: err.stack, + stack: err instanceof Error ? err.stack : undefined, }); }); - await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: any) => { + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: unknown) => { this.logger.warn({ message: `Error removing monitor ${monitor.id} with name ${monitor.name} from status pages`, service: SERVICE_NAME, - stack: err.stack, + stack: err instanceof Error ? err.stack : undefined, }); }); - await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: any) => { + await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId).catch((err: unknown) => { this.logger.warn({ message: `Error deleting incidents for monitor ${monitor.id} with name ${monitor.name}`, service: SERVICE_NAME, - stack: err.stack, + stack: err instanceof Error ? err.stack : undefined, }); }); - await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: any) => { + await this.geoChecksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { this.logger.warn({ message: `Error deleting geo checks for monitor ${monitor.id} with name ${monitor.name}`, service: SERVICE_NAME, - stack: err.stack, + stack: err instanceof Error ? err.stack : undefined, }); }); @@ -504,12 +516,12 @@ export class MonitorService implements IMonitorService { await this.geoChecksRepository.deleteByMonitorId(monitor.id); await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); await this.monitorStatsRepository.deleteByMonitorId(monitor.id); - } catch (error: any) { + } catch (error: unknown) { this.logger.warn({ message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, service: SERVICE_NAME, method: "deleteAllMonitors", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); } }) @@ -517,21 +529,7 @@ export class MonitorService implements IMonitorService { 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 => { + exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); if (!monitors || monitors.length === 0) { @@ -548,33 +546,22 @@ export class MonitorService implements IMonitorService { }: { teamId: string; userId: string; - monitors: any[]; + monitors: ImportedMonitor[]; }): Promise<{ imported: number; errors: string[] }> => { const errors: string[] = []; - const cleanedMonitors = monitors.map((monitor) => { - const cleanData = { ...monitor }; - delete cleanData.id; - delete cleanData._id; - delete cleanData.createdAt; - delete cleanData.updatedAt; - delete cleanData.recentChecks; - // Monitors must belong to current team - cleanData.teamId = teamId; - return cleanData; - }); + const cleanedMonitors: Monitor[] = monitors.map((monitor) => ({ + ...monitor, + id: "", + teamId, + userId, + recentChecks: [], + createdAt: "", + updatedAt: "", + })); - const createdMonitors = await this.createMonitors(cleanedMonitors, userId, teamId); + const createdMonitors = await this.createMonitors(cleanedMonitors); - if (!createdMonitors || createdMonitors.length === 0) { - throw new AppError({ - message: "Failed to import any monitors. Please check the file format and try again.", - service: SERVICE_NAME, - method: "importMonitorsFromJSON", - status: 400, - }); - } - - return { imported: createdMonitors.length, errors }; + return { imported: createdMonitors!.length, errors }; }; } diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 48f3a51cf..7acca5742 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -12,24 +12,31 @@ import bcrypt from "bcryptjs"; import { AppError } from "@/utils/AppError.js"; import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; import { IEmailService } from "@/service/infrastructure/emailService.js"; -import { ISettingsService } from "@/service/system/settingsService.js"; +import { EnvConfig, ISettingsService } from "@/service/system/settingsService.js"; import { ILogger } from "@/utils/logger.js"; import jwt from "jsonwebtoken"; +import crypto from "crypto"; +type CryptoType = typeof crypto; type JwtType = typeof jwt; const SERVICE_NAME = "userService"; export interface IUserService { - issueToken(payload: Partial, appSettings: { jwtTTL?: string; jwtSecret?: string }): string; - registerUser(user: Partial, inviteToken: string, file: unknown): Promise<{ user: User; token: string }>; - createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: unknown): Promise; + issueToken(payload: Partial, appSettings: EnvConfig): string; + registerUser(user: Partial, inviteToken: string, file: Express.Multer.File | null): Promise<{ user: User; token: string }>; + createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise; loginUser(email: string, password: string): Promise<{ user: User; token: string }>; - editUser(updates: Partial, file: unknown, currentUser: User): Promise; + editUser( + updates: Partial, + file: Express.Multer.File | null, + currentUserId: string, + currentUserEmail: string + ): Promise; checkSuperadminExists(): Promise; requestRecovery(email: string): Promise; validateRecovery(recoveryToken: string): Promise; resetPassword(password: string, recoveryToken: string): Promise<{ user: User; token: string }>; - deleteUser(user: User): Promise; - deleteUserById(actor: User, targetUserId: string): Promise; + deleteUser(params: { userId: string; teamId: string; roles: UserRole[] }): Promise; + deleteUserById(params: { actorId: string; actorTeamId: string; actorRoles: UserRole[]; targetUserId: string }): Promise; getAllUsers(): Promise; getUserById(roles: UserRole[], userId: string): Promise; editUserById(userId: string, patch: Partial): Promise; @@ -49,7 +56,7 @@ export class UserService implements IUserService { private logger: ILogger; private jwt: JwtType; private jobQueue: ISuperSimpleQueue; - private crypto: any; + private crypto: CryptoType; private monitorsRepository: IMonitorsRepository; private usersRepository: IUsersRepository; private invitesRepository: IInvitesRepository; @@ -71,7 +78,7 @@ export class UserService implements IUserService { settingsRepository, teamsRepository, }: { - crypto: any; + crypto: CryptoType; emailService: IEmailService; settingsService: ISettingsService; logger: ILogger; @@ -102,14 +109,11 @@ export class UserService implements IUserService { 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 }); + issueToken = (payload: Partial, appSettings: EnvConfig) => { + return this.jwt.sign(payload, appSettings.jwtSecret, { expiresIn: appSettings.jwtTTL }); }; - registerUser = async (user: Partial, inviteToken: string, file: any) => { + registerUser = async (user: Partial, inviteToken: string, file: Express.Multer.File | null) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.usersRepository.findSuperAdmin(); @@ -136,7 +140,7 @@ export class UserService implements IUserService { user.password = this.hashPassword(user.password); } - const newUser = await this.usersRepository.create(user, file); + const newUser = await this.usersRepository.create(user as User, file); this.logger.debug({ message: "New user created", @@ -179,7 +183,7 @@ export class UserService implements IUserService { return { user: newUser, token }; }; - createUser = async (userData: Partial, teamId: string, actorRoles: UserRole[], file: any) => { + createUser = async (userData: Partial, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null) => { // Validate that the creator can assign the requested roles const targetRoles = userData.role ?? []; for (const targetRole of targetRoles) { @@ -200,7 +204,7 @@ export class UserService implements IUserService { userData.password = this.hashPassword(userData.password); } - const newUser = await this.usersRepository.create(userData, file); + const newUser = await this.usersRepository.create(userData as User, file); this.logger.debug({ message: "New user created by superadmin", @@ -239,16 +243,17 @@ export class UserService implements IUserService { return { user: userWithoutPassword, token }; }; - editUser = async (updates: Partial, file: any, currentUser: any) => { + editUser = async ( + updates: Partial, + file: Express.Multer.File | null, + currentUserId: string, + currentUserEmail: string + ) => { // 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 (updates.password && updates.newPassword) { + updates.email = currentUserEmail; + const user = await this.usersRepository.findByEmail(currentUserEmail); + 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) { @@ -259,7 +264,7 @@ export class UserService implements IUserService { delete updates.newPassword; } - return await this.usersRepository.updateById(currentUser.id, updates, file); + return await this.usersRepository.updateById(currentUserId, updates, file); }; checkSuperadminExists = async () => { @@ -273,8 +278,8 @@ export class UserService implements IUserService { 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 settings = this.settingsService.getSettings(); + const url = `${settings.clientHost}/set-new-password/${recoveryToken.token}`; const html = await this.emailService.buildEmail("passwordResetTemplate", { name, @@ -319,53 +324,39 @@ export class UserService implements IUserService { 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; + deleteUser = async ({ userId, teamId, roles }: { userId: string; teamId: string; roles: UserRole[] }) => { 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 - if (res && res.length > 0) { - await Promise.all( - res.map(async (monitor) => { - await this.jobQueue.deleteJob(monitor.id); - }) - ); + const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); + if (monitors) { + await Promise.all(monitors.map((monitor) => this.jobQueue.deleteJob(monitor))); } } // 6. Delete the user by id await this.usersRepository.deleteById(userId); }; - deleteUserById = async (actor: User, targetUserId: string) => { - if (actor.id === targetUserId) { + deleteUserById = async ({ + actorId, + actorRoles, + actorTeamId, + targetUserId, + }: { + actorId: string; + actorTeamId: string; + actorRoles: UserRole[]; + targetUserId: string; + }) => { + if (actorId === targetUserId) { throw new AppError({ message: "Cannot delete your own account from here", service: SERVICE_NAME, method: "deleteUserById", status: 400 }); } const targetUser = await this.usersRepository.findById(targetUserId); - if (targetUser.teamId !== actor.teamId) { + if (targetUser.teamId !== actorTeamId) { throw new AppError({ message: "User is not on your team", service: SERVICE_NAME, method: "deleteUserById", status: 403 }); } @@ -373,7 +364,6 @@ export class UserService implements IUserService { throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUserById", status: 400 }); } - const actorRoles = actor.role; const targetRoles = targetUser.role; // Check actor can manage all of target's roles @@ -392,7 +382,7 @@ export class UserService implements IUserService { await this.usersRepository.deleteById(targetUserId); this.logger.info({ - message: `User ${targetUserId} deleted by ${actor.id}`, + message: `User ${targetUserId} deleted by ${actorId}`, service: SERVICE_NAME, method: "deleteUserById", }); @@ -402,20 +392,18 @@ export class UserService implements IUserService { return await this.usersRepository.findAll(); }; - getUserById = async (roles: any, userId: any) => { + getUserById = async (roles: UserRole[], userId: string) => { if (!roles.includes("superadmin") && !roles.includes("admin")) { throw new AppError({ message: "Insufficient permissions", service: SERVICE_NAME, status: 403 }); } - const user = await this.usersRepository.findById(userId); - - return user; + return await this.usersRepository.findById(userId); }; - editUserById = async (userId: any, patch: Partial) => { + editUserById = async (userId: string, patch: Partial) => { await this.usersRepository.updateById(userId, patch, null); }; - setPasswordByUserId = async (userId: any, password: string) => { + setPasswordByUserId = async (userId: string, password: string) => { const hashedPassword = this.hashPassword(password); const updatedUser = await this.usersRepository.updateById(userId, { password: hashedPassword }, null); return updatedUser; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 464866e09..5db8afc4f 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -27,7 +27,11 @@ export * from "@/service/infrastructure/notificationProviders/INotificationProvi export * from "@/service/infrastructure/notificationProviders/matrix.js"; export * from "@/service/infrastructure/notificationProviders/pagerduty.js"; export * from "@/service/infrastructure/notificationProviders/slack.js"; +export * from "@/service/infrastructure/notificationProviders/teams.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/telegram.js"; +export * from "@/service/infrastructure/notificationProviders/pushover.js"; +export * from "@/service/infrastructure/notificationProviders/twilio.js"; // System services export * from "@/service/system/settingsService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index a96e7855b..7260c82c7 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -2,7 +2,7 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; import Scheduler from "super-simple-scheduler"; import { ISuperSimpleQueueHelper } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import { supportsGeoCheck } from "@/types/monitor.js"; +import { Monitor, MonitorType, supportsGeoCheck } from "@/types/monitor.js"; const SERVICE_NAME = "JobQueue"; type QueueJobFailure = { @@ -42,16 +42,15 @@ type QueueJobSummary = { 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; + addJob(monitorId: string, monitor: Monitor): Promise; + deleteJob(monitor: Monitor): Promise; + pauseJob(monitor: Monitor): Promise; + resumeJob(monitor: Monitor): Promise; + updateJob(monitor: Monitor): Promise; shutdown(): Promise; getMetrics(): Promise; getJobs(): Promise; flushQueues(): Promise<{ success: boolean }>; - obliterate(): Promise; } export class SuperSimpleQueue implements ISuperSimpleQueue { @@ -73,25 +72,95 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { return SuperSimpleQueue.SERVICE_NAME; } + private registerListeners = () => { + this.scheduler.on("scheduler:start", () => { + this.logger.info({ + message: "Scheduler started", + service: SERVICE_NAME, + }); + }); + + this.scheduler.on("scheduler:stop", () => { + this.logger.info({ + message: "Scheduler stopped", + service: SERVICE_NAME, + }); + }); + + this.scheduler.on("scheduler:error", (error) => { + this.logger.error({ + message: `Scheduler error: ${error instanceof Error ? error.message : String(error)}`, + service: SERVICE_NAME, + stack: error instanceof Error ? error.stack : undefined, + }); + }); + + this.scheduler.on("job:abort", (job, reason) => { + this.logger.warn({ + message: `${job.id} aborted: ${reason}`, + service: SERVICE_NAME, + }); + }); + + this.scheduler.on("job:attempt", (job, attempt) => { + this.logger.debug({ + message: `${job.id} attempt ${attempt}`, + service: SERVICE_NAME, + }); + }); + + this.scheduler.on("job:complete", (job) => { + this.logger.debug({ + message: `${job.id} completed successfully`, + service: SERVICE_NAME, + }); + }); + + this.scheduler.on("job:exhausted", (job, error) => { + this.logger.error({ + message: `${job.id} exhausted all retries: ${error instanceof Error ? error.message : String(error)}`, + service: SERVICE_NAME, + stack: error instanceof Error ? error.stack : undefined, + }); + }); + + this.scheduler.on("job:fail", (job, error, attempt) => { + this.logger.warn({ + message: `${job.id} failed on attempt ${attempt}: ${error instanceof Error ? error.message : String(error)}`, + service: SERVICE_NAME, + stack: error instanceof Error ? error.stack : undefined, + }); + }); + + this.scheduler.on("job:start", (job) => { + this.logger.debug({ + message: `${job.id} started`, + service: SERVICE_NAME, + }); + }); + }; + static async create(logger: ILogger, helper: ISuperSimpleQueueHelper, 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.registerListeners(); - this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); - this.scheduler.addTemplate("geo-check-job", this.helper.getGeoCheckJob()); + this.scheduler.start(); + this.scheduler.addTemplate("monitor-job", this.helper.getHeartbeatJob()); + this.scheduler.addTemplate("geo-check-job", this.helper.getHeartbeatGeoJob()); this.scheduler.addTemplate("cleanup-orphaned", this.helper.getCleanupOrphanedJob()); + this.scheduler.addTemplate("cleanup-retention-job", this.helper.getCleanupRetentionJob()); const monitors = await this.monitorsRepository.findAll(); if (!monitors) { return true; @@ -104,20 +173,20 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { } this.scheduler.addJob({ id: "cleanup-orphaned", template: "cleanup-orphaned", active: true }); + this.scheduler.addJob({ id: "cleanup-retention", template: "cleanup-retention-job", active: true, repeat: 24 * 60 * 60 * 1000 }); return true; - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: "Failed to initialize SuperSimpleQueue", + message: `Failed to initialize SuperSimpleQueue: ${error instanceof Error ? error.message : String(error)}`, service: SERVICE_NAME, method: "init", - details: error, }); return false; } }; - addJob = async (monitorId: string, monitor: any) => { + addJob = async (monitorId: string, monitor: Monitor) => { this.scheduler.addJob({ id: monitorId, template: "monitor-job", @@ -143,17 +212,20 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { } }; - deleteJob = async (monitor: any) => { + deleteJob = async (monitor: Monitor) => { this.scheduler.removeJob(monitor.id); - this.scheduler.removeJob(`${monitor.id}-geo`); + const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`); + if (geoJob) await this.scheduler.removeJob(`${monitor.id}-geo`); }; - pauseJob = async (monitor: any) => { + pauseJob = async (monitor: Monitor) => { const result = await this.scheduler.pauseJob(monitor.id); if (result === false) { throw new Error("Failed to pause monitor"); } - await this.scheduler.pauseJob(`${monitor.id}-geo`); + const geoJob = await this.scheduler.getJob(`${monitor.id}-geo`); + if (geoJob) await this.scheduler.removeJob(`${monitor.id}-geo`); + this.logger.debug({ message: `Paused monitor ${monitor.id}`, service: SERVICE_NAME, @@ -161,7 +233,7 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); }; - resumeJob = async (monitor: any) => { + resumeJob = async (monitor: Monitor) => { const result = await this.scheduler.resumeJob(monitor.id); if (result === false) { throw new Error("Failed to resume monitor"); @@ -176,31 +248,35 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { }); }; - updateJob = async (monitor: any) => { - this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); - - // Handle geo check job lifecycle + private syncGeoJob = async (monitor: Monitor) => { const geoJobId = `${monitor.id}-geo`; - if (monitor.geoCheckEnabled && supportsGeoCheck(monitor.type)) { - // Check if geo job exists - const existingGeoJob = await this.scheduler.getJob(geoJobId); - if (existingGeoJob) { - // Update existing geo job - this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor }); - } else { - // Create new geo job - this.scheduler.addJob({ - id: geoJobId, - template: "geo-check-job", - repeat: monitor.geoCheckInterval, - active: monitor.isActive, - data: monitor, - }); - } - } else { - // Remove geo job if disabled or monitor type changed - this.scheduler.removeJob(geoJobId); + const existingGeoJob = await this.scheduler.getJob(geoJobId); + + // If geoChecks have been disabled, or the monitor type doesn't support them, remove + if (!monitor.geoCheckEnabled || !supportsGeoCheck(monitor.type)) { + if (existingGeoJob) this.scheduler.removeJob(geoJobId); + return; } + + // If the job exists, update it + if (existingGeoJob) { + this.scheduler.updateJob(geoJobId, { repeat: monitor.geoCheckInterval, active: monitor.isActive, data: monitor }); + return; + } + + // Otherwise, create it + this.scheduler.addJob({ + id: geoJobId, + template: "geo-check-job", + repeat: monitor.geoCheckInterval, + active: monitor.isActive, + data: monitor, + }); + }; + + updateJob = async (monitor: Monitor) => { + this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); + await this.syncGeoJob(monitor); }; shutdown = async () => { @@ -229,8 +305,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { if (failCount > 0) { acc.jobsWithFailures.push({ monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, + monitorUrl: job.data?.url || null, + monitorType: job.data?.type || null, failedAt: job.lastFailedAt ?? null, failCount, failReason: job.lastFailReason ?? null, @@ -244,8 +320,8 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { failingJobs: 0, jobsWithFailures: [] as Array<{ monitorId: string | number; - monitorUrl: any; - monitorType: any; + monitorUrl: string; + monitorType: MonitorType; failedAt: number | null; failCount: number; failReason: string | null; @@ -262,9 +338,9 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { return jobs.map((job) => { return { monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - monitorInterval: job?.data?.interval || null, + 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, @@ -286,12 +362,4 @@ export class SuperSimpleQueue implements ISuperSimpleQueue { success: Boolean(stopRes && flushRes && initRes), }; }; - - obliterate = async () => { - this.logger.warn({ - message: "obliterate method not implemented", - service: SERVICE_NAME, - method: "obliterate", - }); - }; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index eef0a75be..956d19c98 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -2,8 +2,16 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { supportsGeoCheck } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService, INotificationsService, IStatusService, IncidentService, type IGeoChecksService } from "@/service/index.js"; -import type { StatusChangeResult } from "@/types/index.js"; +import { + ICheckService, + INetworkService, + INotificationsService, + ISettingsService, + IStatusService, + IncidentService, + type IGeoChecksService, +} from "@/service/index.js"; +import { CHECK_TTL_SENTINEL, type MaintenanceWindow, type StatusChangeResult } from "@/types/index.js"; import { IMaintenanceWindowsRepository, IMonitorsRepository, @@ -18,9 +26,10 @@ import { IBufferService } from "@/service/index.js"; export interface ISuperSimpleQueueHelper { readonly serviceName: string; - getMonitorJob(): (monitor: Monitor) => Promise; + getHeartbeatJob(): (monitor: Monitor) => Promise; + getHeartbeatGeoJob(): (monitor: Monitor) => Promise; getCleanupOrphanedJob(): () => Promise; - getGeoCheckJob(): (monitor: Monitor) => Promise; + getCleanupRetentionJob(): () => Promise; isInMaintenanceWindow(monitorId: string, teamId: string): Promise; } @@ -41,11 +50,12 @@ export interface MonitorActionDecision { export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; - private logger: any; + private logger: ILogger; private networkService: INetworkService; private statusService: IStatusService; private notificationsService: INotificationsService; - private checkService: any; + private checkService: ICheckService; + private settingsService: ISettingsService; private buffer: IBufferService; private incidentService: IncidentService; private maintenanceWindowsRepository: IMaintenanceWindowsRepository; @@ -62,7 +72,8 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { networkService: INetworkService, statusService: IStatusService, notificationsService: INotificationsService, - checkService: any, + checkService: ICheckService, + settingsService: ISettingsService, buffer: IBufferService, incidentService: IncidentService, maintenanceWindowsRepository: IMaintenanceWindowsRepository, @@ -78,6 +89,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.networkService = networkService; this.statusService = statusService; this.checkService = checkService; + this.settingsService = settingsService; this.buffer = buffer; this.notificationsService = notificationsService; this.incidentService = incidentService; @@ -95,7 +107,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return SuperSimpleQueueHelper.SERVICE_NAME; } - getMonitorJob = () => { + getHeartbeatJob = () => { return async (monitor: Monitor) => { try { const monitorId = monitor.id; @@ -126,7 +138,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { } // Step 3. Build check - const check = await this.checkService.buildCheck(status); + const check = this.checkService.buildCheck(status); if (!check) { this.logger.warn({ message: `No check could be built for monitor ${monitorId}`, @@ -146,33 +158,33 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { // Step 6. Handle notifications (best effort, continue even in event of failure, don't wait) if (decision.shouldSendNotification) { - this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: any) => { + this.notificationsService.handleNotifications(statusChangeResult.monitor, status, decision).catch((error: unknown) => { this.logger.error({ - message: error.message, + message: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); }); } - // Step 7. Handle incidents (best effort, don't wait) - this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status).catch((error: any) => { + // Step 7. Handle incidents + try { + await this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code, decision, status); + } catch (error: unknown) { this.logger.warn({ - message: error.message, + message: `Error handling incident for job ${monitor.id}: ${error instanceof Error ? error.message : "Unknown error"}`, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error handling incident for job ${monitor.id}: ${error.message}`, - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); - }); - } catch (error: any) { + } + } catch (error: unknown) { this.logger.warn({ - message: error.message, - service: error.service || SERVICE_NAME, - method: error.method || "getMonitorJob", - stack: error.stack, + message: error instanceof Error ? error.message : "Unknown error", + service: SERVICE_NAME, + method: "getMonitorJob", + stack: error instanceof Error ? error.stack : undefined, }); throw error; } @@ -258,19 +270,19 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { service: SERVICE_NAME, method: "getCleanupOrphanedJob", }); - } catch (error: any) { + } catch (error: unknown) { this.logger.warn({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, method: "getCleanupOrphanedJob", - stack: error.stack, + stack: error instanceof Error ? error.stack : undefined, }); throw error; } }; }; - getGeoCheckJob = () => { + getHeartbeatGeoJob = () => { return async (monitor: Monitor) => { try { const monitorId = monitor.id; @@ -278,7 +290,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { // Step 1: Validate monitor eligibility if (!monitorId) { - throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getGeoCheckJob" }); + throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getHeartbeatGeoJob" }); } if (!monitor.geoCheckEnabled) { @@ -288,7 +300,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Monitor ${monitorId} type does not support geo checks, skipping`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -297,7 +309,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.warn({ message: `No geo check locations configured for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -308,7 +320,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Monitor ${monitorId} is in maintenance window, skipping geo check`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -319,7 +331,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.warn({ message: `No geo check could be built for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); return; } @@ -330,14 +342,14 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { this.logger.debug({ message: `Geo check job executed for monitor ${monitorId}`, service: SERVICE_NAME, - method: "getGeoCheckJob", + method: "getHeartbeatGeoJob", }); - } catch (error: any) { + } catch (error: unknown) { this.logger.error({ - message: error.message, + message: error instanceof Error ? error.message : "Unknown error", service: SERVICE_NAME, - method: "getGeoCheckJob", - stack: error.stack, + method: "getHeartbeatGeoJob", + stack: error instanceof Error ? error.stack : undefined, }); // Don't throw - geo check failures shouldn't crash the job scheduler } @@ -347,7 +359,7 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { 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) => { + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: boolean, window: MaintenanceWindow) => { if (window.active) { const start = new Date(window.start); const end = new Date(window.end); @@ -367,13 +379,47 @@ export class SuperSimpleQueueHelper implements ISuperSimpleQueueHelper { return true; } } - return false; + return acc; } return acc; }, false); return maintenanceWindowIsActive; } + getCleanupRetentionJob = () => { + return async () => { + try { + const settings = await this.settingsService.getDBSettings(); + + const checkTTL = settings.checkTTL; // Check TTL is in DAYS, not MS + + if (checkTTL === CHECK_TTL_SENTINEL) { + this.logger.info({ + message: `Check TTL is set to unlimited, skipping cleanup`, + service: SERVICE_NAME, + method: "getCleanupRetentionJob", + }); + return; + } + const checkTTLInMs = checkTTL * 24 * 60 * 60 * 1000; + const cutoffDate = new Date(Date.now() - checkTTLInMs); + const deleteCount = await this.checkService.deleteOlderThan(cutoffDate); + this.logger.info({ + message: `Deleted ${deleteCount} checks older than ${cutoffDate.toISOString()}`, + service: SERVICE_NAME, + method: "getCleanupRetentionJob", + }); + } catch (error: unknown) { + this.logger.error({ + message: error instanceof Error ? error.message : "Unknown error", + service: SERVICE_NAME, + method: "getCleanupRetentionJob", + stack: error instanceof Error ? error.stack : undefined, + }); + } + }; + }; + private evaluateMonitorAction(statusChangeResult: StatusChangeResult): MonitorActionDecision { const { monitor, statusChanged, prevStatus } = statusChangeResult; diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 229c5e7d4..07be446c6 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -9,7 +9,6 @@ const SERVICE_NAME = "BufferService"; export interface IBufferService { addToBuffer(check: Check): void; addGeoCheckToBuffer(geoCheck: GeoCheck): void; - removeCheckFromBuffer(check: Check): boolean; scheduleNextFlush(): void; flushBuffer(): Promise; flushGeoBuffer(): Promise; @@ -72,44 +71,6 @@ export class BufferService implements IBufferService { } } - 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.metadata.monitorId?.toString() === checkToRemove.metadata.monitorId && - check.metadata.teamId?.toString() === checkToRemove.metadata.teamId && - check.metadata.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: unknown) { - this.logger.error({ - message: error instanceof Error ? error.message : "Unknown error", - service: this.SERVICE_NAME, - method: "removeCheckFromBuffer", - stack: error instanceof Error ? error.stack : undefined, - }); - return false; - } - } - scheduleNextFlush() { if (this.bufferTimer) { clearTimeout(this.bufferTimer); diff --git a/server/src/service/infrastructure/emailService.ts b/server/src/service/infrastructure/emailService.ts index f536f3f18..6cf8caf4f 100755 --- a/server/src/service/infrastructure/emailService.ts +++ b/server/src/service/infrastructure/emailService.ts @@ -35,7 +35,6 @@ export class EmailService implements IEmailService { private logger: ILogger; private transporter: ReturnType | null = null; private templateLookup: Record) => string) | undefined>; - private loadTemplate: (templateName: string) => ((context: Record) => string) | undefined; constructor( settingsService: ISettingsService, @@ -54,7 +53,6 @@ export class EmailService implements IEmailService { this.nodemailer = nodemailer; this.logger = logger; this.templateLookup = {}; - this.loadTemplate = () => undefined; this.init(); } @@ -63,7 +61,7 @@ export class EmailService implements IEmailService { } init = () => { - this.loadTemplate = (templateName) => { + const loadTemplate = (templateName: string) => { try { const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`); const templateContent = this.fs.readFileSync(templatePath, "utf8"); @@ -79,12 +77,12 @@ export class EmailService implements IEmailService { }; this.templateLookup = { - welcomeEmailTemplate: this.loadTemplate("welcomeEmail"), - employeeActivationTemplate: this.loadTemplate("employeeActivation"), - noIncidentsThisWeekTemplate: this.loadTemplate("noIncidentsThisWeek"), - passwordResetTemplate: this.loadTemplate("passwordReset"), - testEmailTemplate: this.loadTemplate("testEmailTemplate"), - unifiedNotificationTemplate: this.loadTemplate("unifiedNotification"), + welcomeEmailTemplate: loadTemplate("welcomeEmail"), + employeeActivationTemplate: loadTemplate("employeeActivation"), + noIncidentsThisWeekTemplate: loadTemplate("noIncidentsThisWeek"), + passwordResetTemplate: loadTemplate("passwordReset"), + testEmailTemplate: loadTemplate("testEmailTemplate"), + unifiedNotificationTemplate: loadTemplate("unifiedNotification"), }; }; @@ -167,7 +165,7 @@ export class EmailService implements IEmailService { subject: subject, html: html, }); - return info?.messageId; + return info.messageId; } catch (error: unknown) { this.logger.error({ message: error instanceof Error ? error.message : "Unknown error", diff --git a/server/src/service/infrastructure/network/AdvancedMatcher.ts b/server/src/service/infrastructure/network/AdvancedMatcher.ts index 57bf7022f..d6350ef63 100644 --- a/server/src/service/infrastructure/network/AdvancedMatcher.ts +++ b/server/src/service/infrastructure/network/AdvancedMatcher.ts @@ -2,7 +2,11 @@ import { Monitor } from "@/types/monitor.js"; import jmespath from "jmespath"; type JmesPath = typeof jmespath; -export class AdvancedMatcher { +export interface IAdvancedMatcher { + validate(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: T | undefined }; +} + +export class AdvancedMatcher implements IAdvancedMatcher { constructor(private jmespath: JmesPath) {} private compare(actual: unknown, expected: string, method?: string): boolean { @@ -12,7 +16,7 @@ export class AdvancedMatcher { return String(actual) === expected; // Default } - validate(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: any } { + validate(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: T | undefined } { const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor; if (!useAdvancedMatching) return { ok: true, message: "Success" }; @@ -35,10 +39,10 @@ export class AdvancedMatcher { }; } - const isFalsey = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null; + const isFalsy = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null; return { - ok: !isFalsey, - message: !isFalsey ? "Success" : "Extracted value is falsey", + ok: !isFalsy, + message: !isFalsy ? "Success" : "Extracted value is falsy", extracted: dataToValidate, }; } diff --git a/server/src/service/infrastructure/network/DockerProvider.ts b/server/src/service/infrastructure/network/DockerProvider.ts index 8e84b5dc6..1d4b98818 100644 --- a/server/src/service/infrastructure/network/DockerProvider.ts +++ b/server/src/service/infrastructure/network/DockerProvider.ts @@ -90,7 +90,8 @@ export class DockerProvider implements IStatusProvider { if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID"); if (matchTypes.length > 1) { - const message = `Ambiguous container match for "${containerInput}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`; + const matchUsed = exactIdMatch ? "exact ID" : "exact name"; + const message = `Ambiguous container match for "${containerInput}". Matched by: ${matchTypes.join(", ")}. Using ${matchUsed} match.`; this.logger.warn({ message, @@ -138,11 +139,24 @@ export class DockerProvider implements IStatusProvider { }; } + if (!response) { + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: NETWORK_ERROR, + message: "No response from Docker container inspect", + responseTime, + payload: null, + }; + } + return { monitorId: monitor.id, teamId: monitor.teamId, type: monitor.type, - status: response?.State?.Status === "running", + status: response.State?.Status === "running", code: 200, message: "Docker container status fetched successfully", responseTime, diff --git a/server/src/service/infrastructure/network/GrpcProvider.ts b/server/src/service/infrastructure/network/GrpcProvider.ts index 66684ba0c..14a063bca 100644 --- a/server/src/service/infrastructure/network/GrpcProvider.ts +++ b/server/src/service/infrastructure/network/GrpcProvider.ts @@ -60,7 +60,7 @@ export class GrpcProvider implements IStatusProvider { throw new AppError({ message: "Monitor port is required", service: SERVICE_NAME, method: "handle" }); } - const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0]; + const host = url.replace(/^https?:\/\//, "").split(/[/?#:]/)[0]; const target = `${host}:${port}`; const currentFilePath = fileURLToPath(import.meta.url); diff --git a/server/src/service/infrastructure/network/HttpProvider.ts b/server/src/service/infrastructure/network/HttpProvider.ts index 42fa39a40..bbc0934f8 100644 --- a/server/src/service/infrastructure/network/HttpProvider.ts +++ b/server/src/service/infrastructure/network/HttpProvider.ts @@ -1,5 +1,5 @@ import { type Got, HTTPError, RequestError } from "got"; -import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; +import { IAdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js"; import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js"; import { HttpStatusPayload } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js"; @@ -13,9 +13,9 @@ export class HttpProvider implements IStatusProvider { constructor( private got: Got, - private advancedMatcher: AdvancedMatcher + private advancedMatcher: IAdvancedMatcher ) { - const cacheable = new CacheableLookup(); + const cacheable = new CacheableLookup({ maxTtl: 300, errorTtl: 30 }); this.got = got.extend({ dnsCache: cacheable, timeout: { diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index e475bc24b..ca85d12ac 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,4 +1,4 @@ -import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +import type { Monitor, MonitorStatusResponse, MonitorPayloadMap, MonitorType } from "@/types/index.js"; import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js"; @@ -8,7 +8,7 @@ const SERVICE_NAME = "NetworkService"; export interface INetworkService { readonly serviceName: string; - requestStatus(monitor: Monitor): Promise>; + requestStatus(monitor: Monitor & { type: T }): Promise>; requestWebhook( type: string, url: string, @@ -43,12 +43,12 @@ export class NetworkService implements INetworkService { } // Main entry point - async requestStatus(monitor: Monitor) { + async requestStatus(monitor: Monitor & { type: T }): Promise> { const provider = this.providers.find((p) => p.supports(monitor.type)); if (!provider) { - return this.handleUnsupportedType(monitor.type); + return this.handleUnsupportedType(monitor.type) as Promise>; } - return provider.handle(monitor); + return provider.handle(monitor) as Promise>; } private async handleUnsupportedType(type: string): Promise { @@ -118,7 +118,7 @@ export class NetworkService implements INetworkService { }, }); - if (response?.data?.status !== "success") return false; + if (response.data?.status !== "success") return false; return true; } catch (err: unknown) { const originalMessage = err instanceof Error ? err.message : String(err); diff --git a/server/src/service/infrastructure/notificationMessageBuilder.ts b/server/src/service/infrastructure/notificationMessageBuilder.ts index 3d16acf19..934163b2a 100644 --- a/server/src/service/infrastructure/notificationMessageBuilder.ts +++ b/server/src/service/infrastructure/notificationMessageBuilder.ts @@ -145,9 +145,9 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent { - const title = `Threshold Breach: ${monitor.name}`; - const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`; - const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`]; + const title = `Threshold Exceeded: ${monitor.name}`; + const summary = `Monitor "${monitor.name}" has exceeded one or more thresholds.`; + const details = [`URL: ${monitor.url}`, `Status: Threshold exceeded`, `Type: ${monitor.type}`]; const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse); diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 87056ef7a..b5c886408 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,7 +1,30 @@ import type { Notification } from "@/types/index.js"; import type { NotificationMessage } from "@/types/notificationMessage.js"; +import { ILogger } from "@/utils/logger.js"; +import type { OptionsOfJSONResponseBody } from "got"; +export const DEFAULT_NOTIFICATION_TIMEOUT_MS = 30_000; export interface INotificationProvider { sendMessage: (notification: Notification, message: NotificationMessage) => Promise; - sendTestAlert(notification: Notification): Promise; + sendTestAlert(notification: Partial): Promise; +} + +export abstract class NotificationProvider implements INotificationProvider { + protected readonly logger: ILogger; + protected readonly timeoutMs: number; + + constructor(logger: ILogger, timeoutMs: number = DEFAULT_NOTIFICATION_TIMEOUT_MS) { + this.logger = logger; + this.timeoutMs = timeoutMs; + } + + protected gotRequestOptions(): Pick { + return { + timeout: { request: this.timeoutMs }, + retry: { limit: 0 }, + }; + } + + abstract sendMessage(notification: Notification, message: NotificationMessage): Promise; + abstract sendTestAlert(notification: Partial): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index e630e4e86..942e0b578 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,19 +1,12 @@ const SERVICE_NAME = "DiscordProvider"; import type { AlertDiscordPayload, DiscordEmbedField, Notification } from "@/types/index.js"; -import { INotificationProvider } from "@/service/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import type { NotificationMessage, NotificationSeverity } from "@/types/notificationMessage.js"; import got from "got"; -import { ILogger } from "@/utils/logger.js"; -export class DiscordProvider implements INotificationProvider { - private logger: ILogger; - - constructor(logger: ILogger) { - this.logger = logger; - } - - sendTestAlert = async (notification: Notification) => { +export class DiscordProvider extends NotificationProvider { + sendTestAlert = async (notification: Partial) => { if (!notification.address) { return false; } @@ -23,6 +16,7 @@ export class DiscordProvider implements INotificationProvider { headers: { "Content-Type": "application/json", }, + ...this.gotRequestOptions(), }); return true; } catch (error) { @@ -55,6 +49,7 @@ export class DiscordProvider implements INotificationProvider { headers: { "Content-Type": "application/json", }, + ...this.gotRequestOptions(), }); return true; } catch (error) { diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index c14d59a85..0890eac7f 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,20 +1,19 @@ const SERVICE_NAME = "EmailProvider"; import type { Notification } from "@/types/index.js"; -import { INotificationProvider } from "@/service/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; import { buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { ILogger } from "@/utils/logger.js"; import { IEmailService } from "@/service/infrastructure/emailService.js"; -export class EmailProvider implements INotificationProvider { +export class EmailProvider extends NotificationProvider { private emailService: IEmailService; - private logger: ILogger; constructor(emailService: IEmailService, logger: ILogger) { + super(logger); this.emailService = emailService; - this.logger = logger; } - async sendTestAlert(notification: Notification): Promise { + async sendTestAlert(notification: Partial): Promise { const subject = "Test notification"; const html = await buildTestEmail(this.emailService); @@ -84,7 +83,7 @@ export class EmailProvider implements INotificationProvider { case "monitor_up": return `Monitor ${message.monitor.name} is back up`; case "threshold_breach": - return `Monitor ${message.monitor.name} threshold breached`; + return `Monitor ${message.monitor.name} threshold exceeded`; case "threshold_resolved": return `Monitor ${message.monitor.name} thresholds resolved`; default: diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index ce53215bd..f8a093ab7 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -1,80 +1,63 @@ const SERVICE_NAME = "MatrixProvider"; import got from "got"; -import type { INotificationProvider } from "@/service/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; import type { AlertMatrixPayload, Notification } from "@/types/index.js"; import type { NotificationMessage } from "@/types/notificationMessage.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; -import { ILogger } from "@/utils/logger.js"; +import { randomUUID } from "crypto"; -export class MatrixProvider implements INotificationProvider { - private logger: ILogger; - - constructor(logger: ILogger) { - this.logger = logger; - } - - sendTestAlert = async (notification: Notification) => { +export class MatrixProvider extends NotificationProvider { + sendTestAlert = async (notification: Partial) => { 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; - } + return await this.sendMessageWithBody(notification, body); }; sendMessage = async (notification: Notification, message: NotificationMessage): Promise => { const { homeserverUrl, accessToken, roomId } = notification; - if (!homeserverUrl || !accessToken || !roomId) { return false; } const { plainText, htmlText } = this.buildMatrixMessage(message); - - const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; const body = { msgtype: "m.text", body: plainText, format: "org.matrix.custom.html", formatted_body: htmlText, }; + return this.sendMessageWithBody(notification, body); + }; + + private sendMessageWithBody = async (notification: Partial, body: unknown): Promise => { + const { homeserverUrl, accessToken, roomId } = notification; + const txnId = randomUUID(); + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message/${txnId}`; try { - await got.post(url, { + await got.put(url, { json: body, headers: { "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, }, + ...this.gotRequestOptions(), }); return true; } catch (error) { const err = error as Error; this.logger.warn({ - message: "Matrix notification failed", + message: `Matrix notification failed : ${err.message}`, service: SERVICE_NAME, method: "sendMessage", - stack: err?.stack, + stack: err.stack, }); return false; } diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index b15717878..0d2fa75be 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -1,20 +1,13 @@ const SERVICE_NAME = "PagerDutyProvider"; import got from "got"; import type { Notification } from "@/types/index.js"; -import { INotificationProvider } from "@/service/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; import type { NotificationMessage } from "@/types/notificationMessage.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; -import { ILogger } from "@/utils/logger.js"; import { AlertPagerDutyPayload } from "@/types/index.js"; -export class PagerDutyProvider implements INotificationProvider { - private logger: ILogger; - - constructor(logger: ILogger) { - this.logger = logger; - } - - async sendTestAlert(notification: Notification): Promise { +export class PagerDutyProvider extends NotificationProvider { + async sendTestAlert(notification: Partial): Promise { try { await got.post("https://events.pagerduty.com/v2/enqueue", { json: { @@ -28,6 +21,7 @@ export class PagerDutyProvider implements INotificationProvider { }, }, responseType: "json", + ...this.gotRequestOptions(), }); return true; } catch (error) { @@ -59,6 +53,7 @@ export class PagerDutyProvider implements INotificationProvider { const payload = this.buildPagerDutyPayload(notification, message); await got.post("https://events.pagerduty.com/v2/enqueue", { json: payload, + ...this.gotRequestOptions(), }); this.logger.info({ message: "[NEW] PagerDuty notification sent via sendMessage", diff --git a/server/src/service/infrastructure/notificationProviders/pushover.ts b/server/src/service/infrastructure/notificationProviders/pushover.ts new file mode 100644 index 000000000..5d001892d --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/pushover.ts @@ -0,0 +1,109 @@ +const SERVICE_NAME = "PushoverProvider"; +import type { Notification } from "@/types/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +import type { NotificationMessage } from "@/types/notificationMessage.js"; +import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; + +export class PushoverProvider extends NotificationProvider { + async sendTestAlert(notification: Partial): Promise { + if (!notification.address || !notification.accessToken) { + return false; + } + + try { + await got.post("https://api.pushover.net/1/messages.json", { + form: { + token: notification.accessToken, + user: notification.address, + message: getTestMessage(), + title: "Checkmate Test Notification", + }, + ...this.gotRequestOptions(), + }); + return true; + } catch (error) { + const errMsg = error instanceof Error ? error.message : "unknown error"; + const errStack = error instanceof Error ? error.stack : undefined; + this.logger.warn({ + message: "Pushover test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: errStack, + details: { error: errMsg }, + }); + return false; + } + } + + async sendMessage(notification: Notification, message: NotificationMessage): Promise { + if (!notification.address || !notification.accessToken) { + return false; + } + + const text = this.buildPushoverText(message); + + try { + await got.post("https://api.pushover.net/1/messages.json", { + form: { + token: notification.accessToken, + user: notification.address, + message: text, + title: message.content.title, + }, + ...this.gotRequestOptions(), + }); + + this.logger.info({ + message: "Pushover notification sent", + service: SERVICE_NAME, + method: "sendMessage", + }); + return true; + } catch (error) { + const errMsg = error instanceof Error ? error.message : "unknown error"; + const errStack = error instanceof Error ? error.stack : undefined; + this.logger.warn({ + message: "Pushover alert failed", + service: SERVICE_NAME, + method: "sendMessage", + stack: errStack, + details: { error: errMsg }, + }); + return false; + } + } + + private buildPushoverText(message: NotificationMessage): string { + const lines: string[] = []; + + lines.push(message.content.summary); + lines.push(""); + lines.push("Monitor Details:"); + lines.push(`• Name: ${message.monitor.name}`); + lines.push(`• URL: ${message.monitor.url}`); + lines.push(`• Type: ${message.monitor.type}`); + lines.push(`• Status: ${message.monitor.status}`); + + if (message.content.details && message.content.details.length > 0) { + lines.push(""); + lines.push("Additional Information:"); + message.content.details.forEach((detail) => lines.push(`• ${detail}`)); + } + + if (message.content.thresholds && message.content.thresholds.length > 0) { + lines.push(""); + lines.push("Threshold Breaches:"); + message.content.thresholds.forEach((breach) => { + lines.push(`• ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`); + }); + } + + if (message.content.incident) { + lines.push(""); + lines.push(`View Incident: ${message.clientHost}/incidents/${message.monitor.id}`); + } + + return lines.join("\n"); + } +} diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index e13362669..1ab046f7f 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,19 +1,12 @@ const SERVICE_NAME = "SlackProvider"; import type { Notification } from "@/types/index.js"; -import { INotificationProvider } from "@/service/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; import type { NotificationMessage } from "@/types/notificationMessage.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got, { HTTPError } from "got"; -import { ILogger } from "@/utils/logger.js"; -export class SlackProvider implements INotificationProvider { - private logger: ILogger; - - constructor(logger: ILogger) { - this.logger = logger; - } - - async sendTestAlert(notification: Notification): Promise { +export class SlackProvider extends NotificationProvider { + async sendTestAlert(notification: Partial): Promise { if (!notification.address) { return false; } @@ -24,6 +17,7 @@ export class SlackProvider implements INotificationProvider { headers: { "Content-Type": "application/json", }, + ...this.gotRequestOptions(), }); return true; } catch (error) { @@ -54,6 +48,7 @@ export class SlackProvider implements INotificationProvider { headers: { "Content-Type": "application/json", }, + ...this.gotRequestOptions(), }); this.logger.info({ message: "[NEW] Slack notification sent via sendMessage", diff --git a/server/src/service/infrastructure/notificationProviders/teams.ts b/server/src/service/infrastructure/notificationProviders/teams.ts new file mode 100644 index 000000000..f98724295 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/teams.ts @@ -0,0 +1,269 @@ +const SERVICE_NAME = "TeamsProvider"; +import type { Notification } from "@/types/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +import type { NotificationMessage } from "@/types/notificationMessage.js"; +import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; +import got, { HTTPError } from "got"; + +// Types for Adaptive Card elements +type TextBlock = { + type: "TextBlock"; + text: string; + weight?: "Bolder" | "Normal" | "Lighter"; + size?: "Small" | "Medium" | "Large" | "ExtraLarge"; + color?: "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention"; + wrap?: boolean; + spacing?: "None" | "Small" | "Medium" | "Large" | "ExtraLarge" | "Auto"; + isSubtle?: boolean; +}; + +type ColumnSet = { + type: "ColumnSet"; + separator?: boolean; + spacing?: "None" | "Small" | "Medium" | "Large" | "ExtraLarge" | "Auto"; + columns: unknown[]; +}; + +type Fact = { + title: string; + value: string; +}; + +type FactSet = { + type: "FactSet"; + facts: Fact[]; +}; + +type Action = { + type: string; + title: string; + url?: string; +}; + +type AdaptiveCard = { + type: "AdaptiveCard"; + $schema: "http://adaptivecards.io/schemas/adaptive-card.json"; + version: "1.4"; + body: (TextBlock | ColumnSet | FactSet)[]; + actions?: Action[]; +}; + +type TeamsMessage = { + type: "message"; + attachments: Array<{ + contentType: "application/vnd.microsoft.card.adaptive"; + contentUrl: null; + content: AdaptiveCard; + }>; +}; + +export class TeamsProvider extends NotificationProvider { + async sendTestAlert(notification: Partial): Promise { + if (!notification.address) { + return false; + } + + try { + const payload = this.wrapAdaptiveCard({ + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.4", + body: [ + { + type: "TextBlock", + text: getTestMessage(), + weight: "Bolder", + size: "Medium", + }, + ], + }); + + await got.post(notification.address, { + json: payload, + headers: { + "Content-Type": "application/json", + }, + ...this.gotRequestOptions(), + }); + return true; + } catch (error) { + const err = error as HTTPError; + this.logger.warn({ + message: "Teams test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); + return false; + } + } + + async sendMessage(notification: Notification, message: NotificationMessage): Promise { + if (!notification.address) { + return false; + } + + const payload = this.wrapAdaptiveCard(this.buildAdaptiveCard(message)); + + try { + await got.post(notification.address, { + json: payload, + headers: { + "Content-Type": "application/json", + }, + ...this.gotRequestOptions(), + }); + this.logger.info({ + message: "Teams notification sent via sendMessage", + service: SERVICE_NAME, + method: "sendMessage", + }); + return true; + } catch (error) { + const err = error as HTTPError; + this.logger.warn({ + message: "Teams alert failed via sendMessage", + service: SERVICE_NAME, + method: "sendMessage", + stack: err?.stack, + }); + return false; + } + } + + /** + * Wrap an Adaptive Card in the Teams webhook envelope format + */ + private wrapAdaptiveCard(card: AdaptiveCard): TeamsMessage { + return { + type: "message", + attachments: [ + { + contentType: "application/vnd.microsoft.card.adaptive", + contentUrl: null, + content: card, + }, + ], + }; + } + + /** + * Build an Adaptive Card from NotificationMessage + */ + private buildAdaptiveCard(message: NotificationMessage): AdaptiveCard { + const colorMap: Record = { + critical: "attention", + warning: "warning", + success: "good", + info: "accent", + }; + const color = (colorMap[message.severity] || "default") as "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention" | undefined; + + const body: (TextBlock | ColumnSet | FactSet)[] = []; + + // Header with colored status indicator + body.push({ + type: "TextBlock", + text: message.content.title, + weight: "Bolder", + size: "Large", + color, + wrap: true, + }); + + // Summary + body.push({ + type: "TextBlock", + text: message.content.summary, + wrap: true, + spacing: "Small", + }); + + // Separator + body.push({ + type: "ColumnSet", + separator: true, + spacing: "Medium", + columns: [], + }); + + // Monitor details as a FactSet + body.push({ + type: "FactSet", + facts: [ + { title: "Name", value: message.monitor.name }, + { title: "Type", value: message.monitor.type }, + { title: "Status", value: message.monitor.status }, + { title: "URL", value: message.monitor.url }, + ], + }); + + // Threshold breaches + if (message.content.thresholds && message.content.thresholds.length > 0) { + body.push({ + type: "TextBlock", + text: "**Threshold Breaches**", + weight: "Bolder", + spacing: "Medium", + wrap: true, + }); + + for (const breach of message.content.thresholds) { + body.push({ + type: "TextBlock", + text: `• **${breach.metric.toUpperCase()}**: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`, + wrap: true, + spacing: "Small", + }); + } + } + + // Additional details + if (message.content.details && message.content.details.length > 0) { + body.push({ + type: "TextBlock", + text: "**Additional Information**", + weight: "Bolder", + spacing: "Medium", + wrap: true, + }); + + for (const detail of message.content.details) { + body.push({ + type: "TextBlock", + text: `• ${detail}`, + wrap: true, + spacing: "Small", + }); + } + } + + // Timestamp footer + body.push({ + type: "TextBlock", + text: `Checkmate | ${new Date(message.content.timestamp).toUTCString()}`, + size: "Small", + isSubtle: true, + spacing: "Medium", + wrap: true, + }); + + // Actions (incident link) + const actions: Action[] = []; + if (message.content.incident) { + actions.push({ + type: "Action.OpenUrl", + title: "View Incident", + url: `${message.clientHost}/incidents/${message.content.incident.id}`, + }); + } + + return { + type: "AdaptiveCard", + $schema: "http://adaptivecards.io/schemas/adaptive-card.json", + version: "1.4", + body, + ...(actions.length > 0 ? { actions } : {}), + }; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/telegram.ts b/server/src/service/infrastructure/notificationProviders/telegram.ts new file mode 100644 index 000000000..9d6968fdd --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/telegram.ts @@ -0,0 +1,109 @@ +const SERVICE_NAME = "TelegramProvider"; +import type { Notification } from "@/types/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +import type { NotificationMessage } from "@/types/notificationMessage.js"; +import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; + +export class TelegramProvider extends NotificationProvider { + async sendTestAlert(notification: Partial): Promise { + if (!notification.address || !notification.accessToken) { + return false; + } + + try { + await got.post(`https://api.telegram.org/bot${notification.accessToken}/sendMessage`, { + json: { + chat_id: notification.address, + text: getTestMessage(), + parse_mode: "HTML", + }, + ...this.gotRequestOptions(), + }); + return true; + } catch (error) { + const errMsg = error instanceof Error ? error.message : "unknown error"; + const errStack = error instanceof Error ? error.stack : undefined; + this.logger.warn({ + message: "Telegram test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: errStack, + details: { error: errMsg }, + }); + return false; + } + } + + async sendMessage(notification: Notification, message: NotificationMessage): Promise { + if (!notification.address || !notification.accessToken) { + return false; + } + + const text = this.buildTelegramText(message); + + try { + await got.post(`https://api.telegram.org/bot${notification.accessToken}/sendMessage`, { + json: { + chat_id: notification.address, + text, + parse_mode: "HTML", + }, + ...this.gotRequestOptions(), + }); + + this.logger.info({ + message: "Telegram notification sent", + service: SERVICE_NAME, + method: "sendMessage", + }); + return true; + } catch (error) { + const errMsg = error instanceof Error ? error.message : "unknown error"; + const errStack = error instanceof Error ? error.stack : undefined; + this.logger.warn({ + message: "Telegram alert failed", + service: SERVICE_NAME, + method: "sendMessage", + stack: errStack, + details: { error: errMsg }, + }); + return false; + } + } + + private buildTelegramText(message: NotificationMessage): string { + const lines: string[] = []; + + lines.push(`${message.content.title}`); + lines.push(message.content.summary); + lines.push(""); + + lines.push("Monitor Details:"); + lines.push(`• Name: ${message.monitor.name}`); + lines.push(`• URL: ${message.monitor.url}`); + lines.push(`• Type: ${message.monitor.type}`); + lines.push(`• Status: ${message.monitor.status}`); + + if (message.content.details && message.content.details.length > 0) { + lines.push(""); + lines.push("Additional Information:"); + message.content.details.forEach((detail) => lines.push(`• ${detail}`)); + } + + if (message.content.thresholds && message.content.thresholds.length > 0) { + lines.push(""); + lines.push("Threshold Breaches:"); + message.content.thresholds.forEach((breach) => { + lines.push(`• ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`); + }); + } + + if (message.content.incident) { + lines.push(""); + lines.push(`View Incident`); + } + + return lines.join("\n"); + } +} diff --git a/server/src/service/infrastructure/notificationProviders/twilio.ts b/server/src/service/infrastructure/notificationProviders/twilio.ts new file mode 100644 index 000000000..a871fe5cb --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/twilio.ts @@ -0,0 +1,96 @@ +const SERVICE_NAME = "TwilioProvider"; +import type { Notification } from "@/types/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +import type { NotificationMessage } from "@/types/notificationMessage.js"; +import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; + +export class TwilioProvider extends NotificationProvider { + async sendTestAlert(notification: Partial): Promise { + if (!notification.accountSid || !notification.accessToken || !notification.phone || !notification.twilioPhoneNumber) { + return false; + } + + try { + await got.post(`https://api.twilio.com/2010-04-01/Accounts/${notification.accountSid}/Messages.json`, { + form: { + To: notification.phone, + From: notification.twilioPhoneNumber, + Body: getTestMessage(), + }, + username: notification.accountSid, + password: notification.accessToken, + ...this.gotRequestOptions(), + }); + return true; + } catch (error) { + const errMsg = error instanceof Error ? error.message : "unknown error"; + const errStack = error instanceof Error ? error.stack : undefined; + this.logger.warn({ + message: "Twilio test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: errStack, + details: { error: errMsg }, + }); + return false; + } + } + + async sendMessage(notification: Notification, message: NotificationMessage): Promise { + if (!notification.accountSid || !notification.accessToken || !notification.phone || !notification.twilioPhoneNumber) { + return false; + } + + const text = this.buildSmsText(message); + + try { + await got.post(`https://api.twilio.com/2010-04-01/Accounts/${notification.accountSid}/Messages.json`, { + form: { + To: notification.phone, + From: notification.twilioPhoneNumber, + Body: text, + }, + username: notification.accountSid, + password: notification.accessToken, + ...this.gotRequestOptions(), + }); + + this.logger.info({ + message: "Twilio SMS notification sent", + service: SERVICE_NAME, + method: "sendMessage", + }); + return true; + } catch (error) { + const errMsg = error instanceof Error ? error.message : "unknown error"; + const errStack = error instanceof Error ? error.stack : undefined; + this.logger.warn({ + message: "Twilio SMS alert failed", + service: SERVICE_NAME, + method: "sendMessage", + stack: errStack, + details: { error: errMsg }, + }); + return false; + } + } + + private buildSmsText(message: NotificationMessage): string { + const lines: string[] = []; + + lines.push(message.content.title); + lines.push(message.content.summary); + lines.push(""); + lines.push(`URL: ${message.monitor.url}`); + lines.push(`Status: ${message.monitor.status}`); + + if (message.content.thresholds && message.content.thresholds.length > 0) { + message.content.thresholds.forEach((breach) => { + lines.push(`${breach.metric.toUpperCase()}: ${breach.formattedValue}`); + }); + } + + return lines.join("\n"); + } +} diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 6a00f3f1e..292650f91 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,18 +1,11 @@ const SERVICE_NAME = "WebhookProvider"; import type { Notification } from "@/types/index.js"; -import { INotificationProvider } from "@/service/index.js"; +import { NotificationProvider } from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; import type { NotificationMessage } from "@/types/notificationMessage.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; -import { ILogger } from "@/utils/logger.js"; - -export class WebhookProvider implements INotificationProvider { - private logger: ILogger; - - constructor(logger: ILogger) { - this.logger = logger; - } +export class WebhookProvider extends NotificationProvider { sendMessage = async (notification: Notification, message: NotificationMessage): Promise => { if (!notification.address) { return false; @@ -27,6 +20,7 @@ export class WebhookProvider implements INotificationProvider { headers: { "Content-Type": "application/json", }, + ...this.gotRequestOptions(), }); this.logger.info({ message: "Webhook notification sent", @@ -98,7 +92,7 @@ export class WebhookProvider implements INotificationProvider { }; } - sendTestAlert = async (notification: Notification) => { + sendTestAlert = async (notification: Partial) => { if (!notification.address) { return false; } @@ -108,6 +102,7 @@ export class WebhookProvider implements INotificationProvider { headers: { "Content-Type": "application/json", }, + ...this.gotRequestOptions(), }); return true; } catch (error) { diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 1da308678..90e9d637a 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -8,14 +8,14 @@ import { ILogger } from "@/utils/logger.js"; import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js"; export interface INotificationsService { - createNotification: (notificationData: Partial) => Promise; + createNotification: (notificationData: Partial, userId: string, teamId: string) => 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, decision: MonitorActionDecision) => Promise; - sendTestNotification: (notification: Notification) => Promise; + sendTestNotification: (notification: Partial) => Promise; testAllNotifications: (notificationIds: string[]) => Promise; } @@ -32,6 +32,10 @@ export class NotificationsService implements INotificationsService { private discordProvider: INotificationProvider; private pagerDutyProvider: INotificationProvider; private matrixProvider: INotificationProvider; + private teamsProvider: INotificationProvider; + private telegramProvider: INotificationProvider; + private pushoverProvider: INotificationProvider; + private twilioProvider: INotificationProvider; private logger: ILogger; private settingsService: ISettingsService; private notificationMessageBuilder: INotificationMessageBuilder; @@ -45,6 +49,10 @@ export class NotificationsService implements INotificationsService { discordProvider: INotificationProvider, pagerDutyProvider: INotificationProvider, matrixProvider: INotificationProvider, + teamsProvider: INotificationProvider, + telegramProvider: INotificationProvider, + pushoverProvider: INotificationProvider, + twilioProvider: INotificationProvider, settingsService: ISettingsService, logger: ILogger, notificationMessageBuilder: INotificationMessageBuilder @@ -57,6 +65,10 @@ export class NotificationsService implements INotificationsService { this.discordProvider = discordProvider; this.pagerDutyProvider = pagerDutyProvider; this.matrixProvider = matrixProvider; + this.teamsProvider = teamsProvider; + this.telegramProvider = telegramProvider; + this.pushoverProvider = pushoverProvider; + this.twilioProvider = twilioProvider; this.settingsService = settingsService; this.logger = logger; this.notificationMessageBuilder = notificationMessageBuilder; @@ -92,6 +104,14 @@ export class NotificationsService implements INotificationsService { return await this.discordProvider.sendMessage!(notification, notificationMessage); case "email": return await this.emailProvider.sendMessage!(notification, notificationMessage); + case "teams": + return await this.teamsProvider.sendMessage!(notification, notificationMessage); + case "telegram": + return await this.telegramProvider.sendMessage!(notification, notificationMessage); + case "pushover": + return await this.pushoverProvider.sendMessage!(notification, notificationMessage); + case "twilio": + return await this.twilioProvider.sendMessage!(notification, notificationMessage); default: this.logger.warn({ message: `Unknown notification type: ${notification.type}`, @@ -136,7 +156,7 @@ export class NotificationsService implements INotificationsService { return await this.sendNotifications(monitor, monitorStatusResponse, decision); }; - sendTestNotification = async (notification: Notification) => { + sendTestNotification = async (notification: Partial) => { switch (notification.type) { case "email": return await this.emailProvider.sendTestAlert(notification); @@ -150,6 +170,14 @@ export class NotificationsService implements INotificationsService { return await this.matrixProvider.sendTestAlert(notification); case "webhook": return await this.webhookProvider.sendTestAlert(notification); + case "teams": + return await this.teamsProvider.sendTestAlert(notification); + case "telegram": + return await this.telegramProvider.sendTestAlert(notification); + case "pushover": + return await this.pushoverProvider.sendTestAlert(notification); + case "twilio": + return await this.twilioProvider.sendTestAlert(notification); default: return false; } @@ -167,7 +195,9 @@ export class NotificationsService implements INotificationsService { return true; }; - createNotification = async (notificationData: Partial): Promise => { + createNotification = async (notificationData: Partial, userId: string, teamId: string): Promise => { + notificationData.userId = userId; + notificationData.teamId = teamId; return await this.notificationsRepository.create(notificationData); }; @@ -184,8 +214,8 @@ export class NotificationsService implements INotificationsService { }; deleteById = async (id: string, teamId: string): Promise => { - const deleted = await this.notificationsRepository.deleteById(id, teamId); await this.monitorsRepository.removeNotificationFromMonitors(id); + const deleted = await this.notificationsRepository.deleteById(id, teamId); return deleted; }; } diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index badcf78ec..a21fa293f 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -14,13 +14,19 @@ import type { GameStatusPayload, GrpcStatusPayload, CheckSnapshot, - MonitorStats, CheckDiskInfo, } from "@/types/index.js"; import { AppError } from "@/utils/AppError.js"; import { ILogger } from "@/utils/logger.js"; import { IBufferService } from "./bufferService.js"; +import type { HardwareStatusMetrics } from "@/types/network.js"; const SERVICE_NAME = "StatusService"; +const MAX_RECENT_CHECKS = 25; +const HARDWARE_ALERT_COUNTER_START = 5; +const HARDWARE_METRIC_KEYS = ["cpu", "memory", "disk", "temp"] as const; +type HardwareMetricKey = (typeof HARDWARE_METRIC_KEYS)[number]; +type HardwareBreaches = Record; +type HardwareCounters = Record; export interface IStatusService { updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse): Promise; @@ -68,103 +74,11 @@ export class StatusService implements IStatusService { async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) { try { - const monitorId = monitor.id; - const { responseTime, status } = networkResponse; - let existingStats: MonitorStats | null = null; - existingStats = await this.monitorStatsRepository - .findByMonitorId(monitorId) - .then((result) => result) - .catch(() => { - this.logger.debug({ - service: SERVICE_NAME, - method: "updateRunningStats", - message: `No existing stats found for monitor ${monitorId}, initializing new stats.`, - }); - return null; - }); - - let stats: Omit; - - if (!existingStats) { - // Initialize new stats - stats = { - avgResponseTime: 0, - maxResponseTime: 0, - totalChecks: 0, - totalUpChecks: 0, - totalDownChecks: 0, - uptimePercentage: 0, - lastResponseTime: 0, - lastCheckTimestamp: 0, - }; - } else { - // Use existing stats (omit id, monitorId, createdAt, updatedAt) - stats = { - avgResponseTime: existingStats.avgResponseTime, - maxResponseTime: existingStats.maxResponseTime, - totalChecks: existingStats.totalChecks, - totalUpChecks: existingStats.totalUpChecks, - totalDownChecks: existingStats.totalDownChecks, - uptimePercentage: existingStats.uptimePercentage, - lastResponseTime: existingStats.lastResponseTime, - lastCheckTimestamp: existingStats.lastCheckTimestamp, - timeOfLastFailure: existingStats.timeOfLastFailure, - }; - } - - // Update stats - stats.totalChecks++; - - // Last response time - stats.lastResponseTime = responseTime ?? 0; - - // Max response time - if (responseTime && responseTime > stats.maxResponseTime) { - stats.maxResponseTime = 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 - 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(); - - // Create or update - if (!existingStats) { - await this.monitorStatsRepository.create({ monitorId, ...stats }); - } else { - await this.monitorStatsRepository.updateByMonitorId(monitorId, stats); - } - + await this.monitorStatsRepository.updateByMonitorId(monitor.id, { + status: networkResponse.status === true, + responseTime: networkResponse.responseTime ?? 0, + now: Date.now(), + }); return true; } catch (error: unknown) { this.logger.error({ @@ -177,6 +91,115 @@ export class StatusService implements IStatusService { } } + private tryUpdateRunningStats = async (monitor: Monitor, statusResponse: MonitorStatusResponse) => { + const statsOk = await this.updateRunningStats(monitor, statusResponse); + if (!statsOk) { + this.logger.warn({ + service: SERVICE_NAME, + method: "updateMonitorStatus", + message: `Stats update failed for monitor ${monitor.id}`, + }); + } + }; + + private toCheckSnapshot = (check: Check): CheckSnapshot => { + return { + id: check.id, + status: check.status, + responseTime: check.responseTime, + timings: check.timings, + statusCode: check.statusCode, + message: check.message, + 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, + createdAt: check.createdAt, + }; + }; + + private computeReachability = ( + currentStatus: MonitorStatus, + window: Array, + threshold: number + ): { nextStatus: "up" | "down"; transitioned: boolean } => { + const failures = window.filter((status) => status === false).length; + const failureRate = (failures / window.length) * 100; + + if (failureRate >= threshold && currentStatus !== "down") { + return { nextStatus: "down", transitioned: true }; + } + + if (failureRate < threshold && currentStatus === "down") { + return { nextStatus: "up", transitioned: true }; + } + return { nextStatus: "up", transitioned: false }; + }; + + private computeHardwareStatus = (params: { + currentStatus: MonitorStatus; + reachabilityDown: boolean; + metrics: HardwareStatusMetrics; + thresholds: { cpu: number; memory: number; disk: number; temp: number }; + counters: HardwareCounters; + }): { + nextStatus: MonitorStatus; + transitioned: boolean; + breaches: HardwareBreaches; + nextCounters: HardwareCounters; + } => { + const { metrics, thresholds, counters, currentStatus, reachabilityDown } = params; + + const cpuUsage = metrics.cpu?.usage_percent ?? -1; + const memoryUsage = metrics.memory?.usage_percent ?? -1; + const temps = metrics.cpu?.temperature ?? []; + + const breaches: HardwareBreaches = { + cpu: cpuUsage !== -1 && cpuUsage > thresholds.cpu / 100, + memory: memoryUsage !== -1 && memoryUsage > thresholds.memory / 100, + disk: metrics.disk + ? metrics.disk.some((d: CheckDiskInfo) => d != null && typeof d.usage_percent === "number" && d.usage_percent > thresholds.disk / 100) + : false, + temp: temps.some((temp: number) => temp > thresholds.temp), + }; + + // Update counters: decrement (floored at 0) if breached, reset to start otherwise. + const nextCounters = { ...counters }; + for (const key of HARDWARE_METRIC_KEYS) { + nextCounters[key] = breaches[key] ? Math.max(0, counters[key] - 1) : HARDWARE_ALERT_COUNTER_START; + } + + // Status transition: reachability "down" takes precedence; hardware can only drive + // transitions into "breached" and back out to "up". Any other case leaves status untouched. + let nextStatus: MonitorStatus = currentStatus; + let transitioned = false; + + if (!reachabilityDown) { + // A counter can only reach zero via the decrement path, which only runs when that + // metric is currently breaching — so anyCounterZero already implies anyBreached. + const anyCounterZero = HARDWARE_METRIC_KEYS.some((k) => nextCounters[k] === 0); + const allNormal = HARDWARE_METRIC_KEYS.every((k) => !breaches[k]); + + if (anyCounterZero && currentStatus !== "breached") { + nextStatus = "breached"; + transitioned = true; + } else if (allNormal && currentStatus === "breached") { + nextStatus = "up"; + transitioned = true; + } + } + + return { nextStatus, transitioned, breaches, nextCounters }; + }; + updateMonitorStatus = async ( statusResponse: MonitorStatusResponse< | PingStatusPayload @@ -196,50 +219,33 @@ export class StatusService implements IStatusService { const monitor = await this.monitorsRepository.findById(monitorId, teamId); // Update running stats - this.updateRunningStats(monitor, statusResponse); - - monitor.statusWindow = monitor.statusWindow || []; - monitor.statusWindow.push(status); - while (monitor.statusWindow.length > monitor.statusWindowSize) { - monitor.statusWindow.shift(); - } - - const checkSnapshot: CheckSnapshot = { - id: check.id, - status: check.status, - responseTime: check.responseTime, - timings: check.timings, - statusCode: check.statusCode, - message: check.message, - 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, - createdAt: check.createdAt, - }; - monitor.recentChecks = monitor.recentChecks || []; - monitor.recentChecks.push(checkSnapshot); - const maxRecentChecks = 25; - while (monitor.recentChecks.length > maxRecentChecks) { - monitor.recentChecks.shift(); - } + await this.tryUpdateRunningStats(monitor, statusResponse); const prevStatus = monitor.status; - let newStatus: MonitorStatus = status === true ? "up" : "down"; - let statusChanged = false; + const checkSnapshot = this.toCheckSnapshot(check); + + // Project the window as it will look after updating DB + // This is done because we need the updated status window to compute new status, but we don't + // want an an extra DB write just to get the window. + const projectedWindow = [...(monitor.statusWindow || []), check.status].slice(-monitor.statusWindowSize); + + // Build the status patch — computed against the projected window + const patch: Partial = {}; + + // Not enough data points yet + if (projectedWindow.length < monitor.statusWindowSize) { + patch.status = status === true ? "up" : "down"; + + const updated = await this.monitorsRepository.updateStatusWindowAndChecks( + monitor.id, + monitor.teamId, + check.status, + checkSnapshot, + monitor.statusWindowSize, + MAX_RECENT_CHECKS, + patch + ); - // Return early if not enough data points - if (monitor.statusWindow.length < monitor.statusWindowSize) { - monitor.status = newStatus; - const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { monitor: updated, statusChanged: false, @@ -249,106 +255,70 @@ export class StatusService implements IStatusService { }; } - // 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 !== "down") { - newStatus = "down"; - statusChanged = true; + // With a full window, a single raw check must not change UNLESS we are initializing. + // Otherwise, only the sliding-window threshold can trigger a transition. + let newStatus: MonitorStatus; + if (monitor.status === "initializing") { + newStatus = status === true ? "up" : "down"; + } else { + newStatus = monitor.status; } - // If the failure rate is below the threshold and the monitor is down, recover: - else if (failureRate < monitor.statusWindowThreshold && monitor.status === "down") { - newStatus = "up"; + + let statusChanged = false; + + // First evaluate reachability-based status changes, which apply to all monitor types + // and take precedence over hardware breaches. + const reachabilityResult = this.computeReachability(newStatus, projectedWindow, monitor.statusWindowThreshold); + if (reachabilityResult.transitioned) { + newStatus = reachabilityResult.nextStatus; statusChanged = true; } - // Evaluate hardware threshold breaches (only for hardware monitors) - let thresholdBreaches: { cpu: boolean; memory: boolean; disk: boolean; temp: boolean } | undefined; - if (monitor.type === "hardware" && statusResponse.payload) { - const payload = statusResponse.payload as HardwareStatusPayload; - const metrics = payload?.data; + // Evaluate hardware threshold breaches (only for hardware monitors with metrics payload) + let thresholdBreaches: HardwareBreaches | undefined; + const hardwarePayload = statusResponse.payload as HardwareStatusPayload | undefined; + if (monitor.type === "hardware" && hardwarePayload?.data) { + const hardware = this.computeHardwareStatus({ + currentStatus: newStatus, + reachabilityDown: newStatus === "down", + metrics: hardwarePayload.data, + thresholds: { + cpu: monitor.cpuAlertThreshold, + memory: monitor.memoryAlertThreshold, + disk: monitor.diskAlertThreshold, + temp: monitor.tempAlertThreshold, + }, + counters: { + cpu: monitor.cpuAlertCounter, + memory: monitor.memoryAlertCounter, + disk: monitor.diskAlertCounter, + temp: monitor.tempAlertCounter, + }, + }); - if (metrics) { - // Evaluate threshold breaches - const cpuUsage = metrics.cpu?.usage_percent ?? -1; - const cpuBreach = cpuUsage !== -1 && cpuUsage > monitor.cpuAlertThreshold / 100; - - const memoryUsage = metrics.memory?.usage_percent ?? -1; - const memoryBreach = memoryUsage !== -1 && memoryUsage > monitor.memoryAlertThreshold / 100; - - const diskBreach = - metrics.disk?.some((d: CheckDiskInfo) => typeof d?.usage_percent === "number" && d.usage_percent > monitor.diskAlertThreshold / 100) ?? - false; - - const temps = metrics.cpu?.temperature ?? []; - const tempBreach = temps.some((temp: number) => temp > monitor.tempAlertThreshold); - - thresholdBreaches = { - cpu: cpuBreach, - memory: memoryBreach, - disk: diskBreach, - temp: tempBreach, - }; - - // Update counters: decrement if breached, reset to 5 if not breached - if (cpuBreach) { - monitor.cpuAlertCounter = Math.max(0, monitor.cpuAlertCounter - 1); - } else { - monitor.cpuAlertCounter = 5; - } - - if (memoryBreach) { - monitor.memoryAlertCounter = Math.max(0, monitor.memoryAlertCounter - 1); - } else { - monitor.memoryAlertCounter = 5; - } - - if (diskBreach) { - monitor.diskAlertCounter = Math.max(0, monitor.diskAlertCounter - 1); - } else { - monitor.diskAlertCounter = 5; - } - - if (tempBreach) { - monitor.tempAlertCounter = Math.max(0, monitor.tempAlertCounter - 1); - } else { - monitor.tempAlertCounter = 5; - } - - // Check if any counter has reached zero (initial breach) - const anyCounterZero = - monitor.cpuAlertCounter === 0 || monitor.memoryAlertCounter === 0 || monitor.diskAlertCounter === 0 || monitor.tempAlertCounter === 0; - - const anyThresholdBreached = cpuBreach || memoryBreach || diskBreach || tempBreach; - const allThresholdsNormal = !cpuBreach && !memoryBreach && !diskBreach && !tempBreach; - - // Update monitor status based on threshold breach state - if (newStatus !== "down") { - // Don't override "down" status - service unreachable takes precedence - // Check current monitor status, not newStatus for comparison - if (anyCounterZero && anyThresholdBreached && monitor.status !== "breached") { - // Initial breach: counter hit zero, change status to breached - newStatus = "breached"; - statusChanged = true; - } else if (anyCounterZero && anyThresholdBreached && monitor.status === "breached") { - // Already breached, keep status but don't mark as changed - newStatus = "breached"; - // statusChanged remains false - } else if (allThresholdsNormal && monitor.status === "breached") { - // All thresholds returned to normal, recover from breached state - newStatus = "up"; - statusChanged = true; - } - } + patch.cpuAlertCounter = hardware.nextCounters.cpu; + patch.memoryAlertCounter = hardware.nextCounters.memory; + patch.diskAlertCounter = hardware.nextCounters.disk; + patch.tempAlertCounter = hardware.nextCounters.temp; + thresholdBreaches = hardware.breaches; + if (hardware.transitioned) { + newStatus = hardware.nextStatus; + statusChanged = true; } } - // Apply the final status - monitor.status = newStatus; + patch.status = newStatus; - const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); + // Single atomic write: push arrays + set status/counters + const updated = await this.monitorsRepository.updateStatusWindowAndChecks( + monitor.id, + monitor.teamId, + check.status, + checkSnapshot, + monitor.statusWindowSize, + MAX_RECENT_CHECKS, + patch + ); return { monitor: updated, @@ -360,34 +330,10 @@ export class StatusService implements IStatusService { }; } catch (error: unknown) { throw new AppError({ - message: `Failed to update monitor status: ${error instanceof Error ? error.message : "Unknown error"}`, - details: `Error updating status for monitor with id ${check.metadata.monitorId}`, + message: `Failed to update monitor with id ${check.metadata.monitorId} with status: ${error instanceof Error ? error.message : "Unknown error"}`, service: SERVICE_NAME, method: "updateMonitorStatus", }); } }; - - 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: unknown) { - this.logger.error({ - message: error instanceof Error ? error.message : "Unknown error", - service: SERVICE_NAME, - method: "insertCheck", - details: { msg: `Error inserting check for monitor: ${check?.metadata.monitorId}` }, - stack: error instanceof Error ? error.stack : undefined, - }); - } - }; } diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 1d75c0a36..6638bc493 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -1,17 +1,18 @@ import { ISettingsRepository } from "@/repositories/index.js"; -import { Settings } from "@/types/index.js"; +import { type DbType, Settings, SettingsUpdate } from "@/types/index.js"; import { AppError } from "@/utils/AppError.js"; import { ValidatedEnv } from "@/validation/envValidation.js"; - +import type { StringValue } from "ms"; const SERVICE_NAME = "SettingsService"; export type EnvConfig = { jwtSecret: string; - jwtTTL: string; + jwtTTL: StringValue; nodeEnv: string; logLevel: string; clientHost: string; dbConnectionString: string; + dbType: DbType; }; export interface ISettingsService { @@ -19,25 +20,30 @@ export interface ISettingsService { loadSettings(): EnvConfig; getSettings(): EnvConfig; getDBSettings(): Promise; + updateDbSettings(newSettings: SettingsUpdate): Promise; } export class SettingsService implements ISettingsService { static SERVICE_NAME = SERVICE_NAME; private settings: EnvConfig; - private settingsRepository: ISettingsRepository; + private settingsRepository: ISettingsRepository | null = null; - constructor(settingsRepository: ISettingsRepository, env: ValidatedEnv) { - this.settingsRepository = settingsRepository; + constructor(env: ValidatedEnv) { this.settings = { jwtSecret: env.JWT_SECRET, - jwtTTL: env.TOKEN_TTL, + jwtTTL: env.TOKEN_TTL as StringValue, nodeEnv: env.NODE_ENV, logLevel: env.LOG_LEVEL, clientHost: env.CLIENT_HOST, dbConnectionString: env.DB_CONNECTION_STRING, + dbType: env.DB_TYPE, }; } + setRepository(settingsRepository: ISettingsRepository) { + this.settingsRepository = settingsRepository; + } + get serviceName() { return SettingsService.SERVICE_NAME; } @@ -47,24 +53,33 @@ export class SettingsService implements ISettingsService { } 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); + private getRepository(): ISettingsRepository { + if (!this.settingsRepository) { + throw new AppError({ + message: "Settings repository not initialized. Call setRepository() after DB connect.", + status: 500, + service: SERVICE_NAME, + }); + } + return this.settingsRepository; + } + + updateDbSettings = async (newSettings: SettingsUpdate) => { + return await this.getRepository().update(newSettings); }; getDBSettings = async () => { + const repo = this.getRepository(); // Remove any old settings - await this.settingsRepository.deleteLegacy(); + await repo.deleteLegacy(); - let settings = await this.settingsRepository.findSingleton(); + let settings = await repo.findSingleton(); if (settings === null) { - await this.settingsRepository.create({}); - settings = await this.settingsRepository.findSingleton(); + await repo.create({}); + settings = await repo.findSingleton(); } if (!settings) { diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 6c4c26445..f4c64f926 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -1,5 +1,8 @@ import type { MonitorType } from "@/types/index.js"; import type { Response } from "got"; + +export const CHECK_TTL_SENTINEL = 366; + export type GotTimings = Response["timings"]; export interface CheckMetadata { @@ -98,9 +101,6 @@ export interface Check { timings?: GotTimings; statusCode: number; message: string; - ack: boolean; - ackAt?: string | null; - expiry: string; cpu?: CheckCpuInfo; memory?: CheckMemoryInfo; disk?: CheckDiskInfo[]; @@ -113,7 +113,6 @@ export interface Check { seo?: number; performance?: number; audits?: CheckAudits; - __v: number; createdAt: string; updatedAt: string; } @@ -205,4 +204,4 @@ export type NormalizedUptimeCheck = T & { originalAvgResponseTime: number; }; -export type CheckSnapshot = Omit; +export type CheckSnapshot = Omit; diff --git a/server/src/types/dlqItem.ts b/server/src/types/dlqItem.ts new file mode 100644 index 000000000..da882ace5 --- /dev/null +++ b/server/src/types/dlqItem.ts @@ -0,0 +1,20 @@ +export const DLQItemTypes = ["notification", "incident_create", "incident_resolve"] as const; +export type DLQItemType = (typeof DLQItemTypes)[number]; + +export const DLQItemStatuses = ["pending", "retrying", "failed"] as const; +export type DLQItemStatus = (typeof DLQItemStatuses)[number]; + +export interface DLQItem { + id: string; + type: DLQItemType; + status: DLQItemStatus; + payload: Record; + monitorId: string; + teamId: string; + retryCount: number; + maxRetries: number; + lastError: string; + nextRetryAt: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index e5b7bf59d..89de348e2 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -14,3 +14,4 @@ export * from "@/types/incident.js"; export * from "@/types/email.js"; export * from "@/types/team.js"; export * from "@/types/maintenanceWindow.js"; +export * from "@/types/dlqItem.js"; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 89740162b..f29ce75d7 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,6 +1,6 @@ import type { CheckSnapshot } from "@/types/check.js"; export type { CheckSnapshot } from "@/types/check.js"; -import type { GeoContinent } from "@/types/geoCheck.js"; +import type { GeoContinent, GroupedGeoCheck } from "@/types/geoCheck.js"; export type { GeoContinent } from "@/types/geoCheck.js"; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"] as const; @@ -12,7 +12,8 @@ export const supportsGeoCheck = (type: MonitorType): boolean => GeoCheckSupporte export const MonitorStatuses = ["up", "down", "paused", "initializing", "maintenance", "breached"] as const; export type MonitorStatus = (typeof MonitorStatuses)[number]; -export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; +export const MonitorMatchMethods = ["equal", "include", "regex"] as const; +export type MonitorMatchMethod = (typeof MonitorMatchMethods)[number] | ""; export interface Monitor { id: string; @@ -73,6 +74,10 @@ export interface MonitorsWithChecksByTeamIdResult { monitors: Monitor[]; } +export interface GroupedGeoCheckResult { + groupedGeoChecks: GroupedGeoCheck[]; +} + export interface UptimeDetailsResult { monitorData: { monitor: Monitor; diff --git a/server/src/types/monitorStats.ts b/server/src/types/monitorStats.ts index 97c3cb28c..a3ac914cf 100644 --- a/server/src/types/monitorStats.ts +++ b/server/src/types/monitorStats.ts @@ -1,3 +1,9 @@ +export interface CheckResultInput { + status: boolean; + responseTime: number; + now: number; +} + export interface MonitorStats { id: string; monitorId: string; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 07e7c3736..6c7172724 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -126,7 +126,7 @@ export interface MonitorPayloadMap { game: GameStatusPayload; grpc: GrpcStatusPayload; websocket: WebSocketStatusPayload; - default: unknown; + unknown: unknown; } export type StatusChangeResult = { diff --git a/server/src/types/notification.ts b/server/src/types/notification.ts index 669c840ca..9d5b914c0 100644 --- a/server/src/types/notification.ts +++ b/server/src/types/notification.ts @@ -1,4 +1,15 @@ -export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const; +export const NotificationChannels = [ + "email", + "slack", + "discord", + "webhook", + "pager_duty", + "matrix", + "teams", + "telegram", + "pushover", + "twilio", +] as const; export type NotificationChannel = (typeof NotificationChannels)[number]; export interface Notification { @@ -12,6 +23,8 @@ export interface Notification { homeserverUrl?: string; roomId?: string; accessToken?: string; + accountSid?: string; + twilioPhoneNumber?: string; createdAt: string; updatedAt: string; } diff --git a/server/src/types/settings.ts b/server/src/types/settings.ts index 108cdcb72..eb8452866 100644 --- a/server/src/types/settings.ts +++ b/server/src/types/settings.ts @@ -1,3 +1,6 @@ +export const DbTypes = ["mongodb"] as const; +export type DbType = (typeof DbTypes)[number]; + export interface SettingsThresholds { cpu?: number; memory?: number; @@ -5,6 +8,10 @@ export interface SettingsThresholds { temperature?: number; } +export type SettingsUpdate = { + [K in keyof Settings]?: Settings[K] | null; +}; + export interface Settings { id: string; checkTTL: number; diff --git a/server/src/types/ssl-checker.d.ts b/server/src/types/ssl-checker.d.ts new file mode 100644 index 000000000..bcbc39858 --- /dev/null +++ b/server/src/types/ssl-checker.d.ts @@ -0,0 +1,24 @@ +declare module "ssl-checker" { + import { Agent } from "https"; + + export interface SSLDetails { + daysRemaining: number; + valid: boolean; + validFrom: string; + validTo: string; + validFor: string[]; + } + + export interface SSLOptions { + method?: "GET" | "HEAD"; + port?: number; + agent?: Agent; + rejectUnauthorized?: boolean; + validateSubjectAltName?: boolean; + } + + function sslChecker(hostname: string, options?: SSLOptions): Promise; + + // Change 'export default' to 'export =' to allow it to be callable + export = sslChecker; +} diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts index b11cf53d6..0329f4e6f 100644 --- a/server/src/types/statusPage.ts +++ b/server/src/types/statusPage.ts @@ -1,4 +1,4 @@ -export const StatusPageTypes = ["uptime"] as const; +export const StatusPageTypes = ["uptime", "infrastructure"] as const; export type StatusPageType = (typeof StatusPageTypes)[number]; export interface StatusPageLogo { @@ -15,7 +15,7 @@ export interface StatusPage { id: string; userId: string; teamId: string; - type: StatusPageType; + type: StatusPageType[]; companyName: string; url: string; timezone?: string; @@ -28,6 +28,7 @@ export interface StatusPage { showCharts: boolean; showUptimePercentage: boolean; showAdminLoginLink: boolean; + showInfrastructure: boolean; customCSS: string; createdAt: string; updatedAt: string; diff --git a/server/src/utils/AppError.ts b/server/src/utils/AppError.ts index c19f726f2..4c8887a9c 100644 --- a/server/src/utils/AppError.ts +++ b/server/src/utils/AppError.ts @@ -1,18 +1,18 @@ export interface AppErrorConfig { - message: string; + message?: string; status?: number; - service?: string | null; - method?: string | null; - details?: unknown; + service?: string; + method?: string; + details?: Record | undefined; } export class AppError extends Error { - private status: number; - private service: string | null; - private method: string | null; - private details: unknown; + readonly status: number; + readonly service: string; + readonly method: string; + readonly details: Record | undefined; - constructor({ message, status = 500, service = null, method = null, details = null }: AppErrorConfig) { + constructor({ message, status = 500, service = "unknownService", method = "unknownMethod", details = undefined }: AppErrorConfig) { super(message); this.status = status; this.service = service; diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index 3c86c911e..f6d877a35 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -45,7 +45,7 @@ export const NormalizeData = (checks: T[], rangeMin: const min = calculatePercentile(checks, 0); const max = calculatePercentile(checks, 95); const normalizedChecks = checks.map((check) => { - const originalResponseTime = check.responseTime; + const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0; let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min); normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); @@ -59,7 +59,8 @@ export const NormalizeData = (checks: T[], rangeMin: return normalizedChecks; } else { return checks.map((check) => { - return { ...check, originalResponseTime: check.responseTime }; + const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0; + return { ...check, originalResponseTime: originalResponseTime }; }); } }; diff --git a/server/src/validation/authValidation.ts b/server/src/validation/authValidation.ts index 045868c40..d8f592cee 100644 --- a/server/src/validation/authValidation.ts +++ b/server/src/validation/authValidation.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { passwordPattern, nameValidation, lowercaseEmailValidation } from "./shared.js"; +import { UserRoles } from "@/types/user.js"; //**************************************** // Auth Validations @@ -41,7 +42,7 @@ export const newPasswordValidation = z.object({ export const inviteBodyValidation = z.object({ email: z.email("Must be a valid email address"), - role: z.array(z.string()).min(1, "At least one role is required"), + role: z.array(z.enum(UserRoles)).min(1, "At least one role is required"), teamId: z.string().min(1, "Team ID is required"), }); diff --git a/server/src/validation/checkValidation.ts b/server/src/validation/checkValidation.ts index d4f75f3ac..e511118e6 100644 --- a/server/src/validation/checkValidation.ts +++ b/server/src/validation/checkValidation.ts @@ -12,33 +12,31 @@ export const getChecksParamValidation = z.object({ export const getChecksQueryValidation = z.object({ type: z.enum(["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"]).optional(), - sortOrder: z.enum(["asc", "desc"]).optional(), + sortOrder: z.enum(["asc", "desc"]), limit: z.coerce.number().optional(), - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), filter: z.enum(["all", "up", "down", "resolve"]).optional(), ack: booleanCoercion.optional(), - page: z.coerce.number().optional(), - rowsPerPage: z.coerce.number().optional(), + page: z.coerce.number(), + rowsPerPage: z.coerce.number(), status: booleanCoercion.optional(), continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), }); export const getTeamChecksQueryValidation = z.object({ - sortOrder: z.enum(["asc", "desc"]).optional(), + sortOrder: z.enum(["asc", "desc"]), limit: z.coerce.number().optional(), - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), filter: z.enum(["all", "up", "down", "resolve"]).optional(), ack: booleanCoercion.optional(), - page: z.coerce.number().optional(), - rowsPerPage: z.coerce.number().optional(), + page: z.coerce.number(), + rowsPerPage: z.coerce.number(), }); export const deleteChecksParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); -export const deleteChecksByTeamIdParamValidation = z.object({}); - -export const updateChecksTTLBodyValidation = z.object({ - ttl: z.number().min(1, "TTL is required"), +export const getChecksSummaryByTeamIdQueryValidation = z.object({ + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), }); diff --git a/server/src/validation/envValidation.ts b/server/src/validation/envValidation.ts index 2b0e36fcf..447748516 100644 --- a/server/src/validation/envValidation.ts +++ b/server/src/validation/envValidation.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { DbTypes } from "@/types/settings.js"; const envSchema = z.object({ // Server Configuration @@ -8,6 +9,7 @@ const envSchema = z.object({ // Database DB_CONNECTION_STRING: z.string().min(1, "Database connection string is required"), + DB_TYPE: z.enum(DbTypes).default("mongodb"), // JWT Authentication JWT_SECRET: z.string().min(1, "JWT_SECRET is required"), diff --git a/server/src/validation/incidentValidation.ts b/server/src/validation/incidentValidation.ts new file mode 100644 index 000000000..2047eefa5 --- /dev/null +++ b/server/src/validation/incidentValidation.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; +import { booleanCoercion } from "./shared.js"; + +//**************************************** +// Incident Validations +//**************************************** + +export const getIncidentsByTeamQueryValidation = z.object({ + sortOrder: z.enum(["asc", "desc"]), + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).default("all"), + page: z.coerce.number().int().min(0), + rowsPerPage: z.coerce.number().int().min(1), + status: booleanCoercion.optional(), + monitorId: z.string().optional(), + resolutionType: z.enum(["manual", "automatic"]).optional(), +}); + +export const getIncidentSummaryQueryValidation = z.object({ + limit: z.coerce.number().int().min(1).optional(), +}); diff --git a/server/src/validation/index.ts b/server/src/validation/index.ts index 7439f043d..cc1d33372 100644 --- a/server/src/validation/index.ts +++ b/server/src/validation/index.ts @@ -20,3 +20,4 @@ export * from "./settingsValidation.js"; export * from "./statusPageValidation.js"; export * from "./notificationValidation.js"; export * from "./userValidation.js"; +export * from "./incidentValidation.js"; diff --git a/server/src/validation/maintenanceWindowValidation.ts b/server/src/validation/maintenanceWindowValidation.ts index e09ac2989..bee06e7bc 100644 --- a/server/src/validation/maintenanceWindowValidation.ts +++ b/server/src/validation/maintenanceWindowValidation.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; +const dateToString = z.coerce.date().transform((d) => d.toISOString()); + //**************************************** // Maintenance Window Validations //**************************************** @@ -11,10 +13,10 @@ export const createMaintenanceWindowBodyValidation = z.object({ active: z.boolean().optional(), duration: z.number().min(1, "Duration is required"), durationUnit: z.enum(["seconds", "minutes", "hours", "days"]), - start: z.coerce.date(), - end: z.coerce.date(), + start: dateToString, + end: dateToString, repeat: z.number().min(0, "Repeat must be a non-negative number"), - expiry: z.coerce.date().optional(), + expiry: dateToString.optional(), }); export const getMaintenanceWindowByIdParamValidation = z.object({ @@ -45,10 +47,10 @@ export const editMaintenanceByIdWindowBodyValidation = z.object({ active: z.boolean().optional(), name: z.string().optional(), repeat: z.number().optional(), - start: z.coerce.date().optional(), - end: z.coerce.date().optional(), - expiry: z.coerce.date().optional(), - monitors: z.array(z.unknown()).optional(), + start: dateToString.optional(), + end: dateToString.optional(), + expiry: dateToString.optional(), + monitors: z.array(z.string()).optional(), duration: z.number().optional(), durationUnit: z.enum(["seconds", "minutes", "hours", "days"]).optional(), }); diff --git a/server/src/validation/monitorValidation.ts b/server/src/validation/monitorValidation.ts index 75ffccd19..df000ecef 100644 --- a/server/src/validation/monitorValidation.ts +++ b/server/src/validation/monitorValidation.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; import { GeoContinents } from "@/types/geoCheck.js"; +import { MonitorMatchMethods, MonitorTypes } from "@/types/monitor.js"; export const getMonitorByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), @@ -13,7 +14,7 @@ export const getMonitorByIdQueryValidation = z.object({ dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), numToDisplay: z.coerce.number().optional(), normalize: booleanCoercion.optional(), - continent: z.enum(GeoContinents).optional(), + continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), }); export const getMonitorsByTeamIdParamValidation = z.object({}); @@ -25,14 +26,14 @@ export const getMonitorsByTeamIdQueryValidation = z.object({ z.array(z.enum(["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"])), ]) .optional(), - filter: z.union([z.string(), z.literal(""), z.null()]).optional(), + filter: z.union([z.string(), z.literal("")]).optional(), }); export const getMonitorsWithChecksQueryValidation = z.object({ limit: z.coerce.number().int().min(1).max(100).optional(), page: z.coerce.number().int().min(0).optional(), rowsPerPage: z.coerce.number().int().min(1).max(100).optional(), - filter: z.union([z.string(), z.literal(""), z.null()]).optional(), + filter: z.union([z.string(), z.literal("")]).optional(), field: z.string().optional(), order: z.enum(["asc", "desc"]).optional(), type: z @@ -51,8 +52,8 @@ export const getCertificateParamValidation = z.object({ export const createMonitorBodyValidation = z.object({ _id: z.string().optional(), name: z.string().min(1, "Name is required"), - description: z.union([z.string(), z.null(), z.literal("")]).optional(), - type: z.string().min(1, "Type is required"), + description: z.union([z.string(), z.literal("")]).optional(), + type: z.enum(MonitorTypes, "Invalid monitor type"), statusWindowSize: z.number().min(1).max(20).default(5), statusWindowThreshold: z.number().min(1).max(100).default(60), url: z.string().min(1, "URL is required"), @@ -69,7 +70,7 @@ export const createMonitorBodyValidation = z.object({ secret: z.string().optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), - matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), gameId: z.union([z.string(), z.literal("")]).optional(), grpcServiceName: z.union([z.string(), z.literal("")]).default(""), selectedDisks: z.array(z.string()).optional(), @@ -81,11 +82,11 @@ export const createMonitorBodyValidation = z.object({ export const editMonitorBodyValidation = z.object({ name: z.string().optional(), - type: z.string().optional(), + type: z.enum(MonitorTypes).optional(), url: z.string().optional(), statusWindowSize: z.number().min(1).max(20).default(5), statusWindowThreshold: z.number().min(1).max(100).default(60), - description: z.union([z.string(), z.null(), z.literal("")]).optional(), + description: z.union([z.string(), z.literal("")]).optional(), interval: z.number().optional(), notifications: z.array(z.string()).optional(), secret: z.string().optional(), @@ -93,7 +94,7 @@ export const editMonitorBodyValidation = z.object({ useAdvancedMatching: z.boolean().optional(), jsonPath: z.union([z.string(), z.literal("")]).optional(), expectedValue: z.union([z.string(), z.literal("")]).optional(), - matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), port: z.number().min(1).max(65535).optional(), cpuAlertThreshold: z.number().optional(), memoryAlertThreshold: z.number().optional(), @@ -112,6 +113,63 @@ export const pauseMonitorParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); +export const getUptimeDetailsByIdParamValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + +export const getUptimeDetailsByIdQueryValidation = z.object({ + dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), + normalize: booleanCoercion.optional(), +}); + +const importedMonitorSchema = z.object({ + id: z.string().optional(), + userId: z.string().optional(), + teamId: z.string().optional(), + name: z.string().min(1, "Name is required"), + description: z.union([z.string(), z.literal("")]).optional(), + status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).default("initializing"), + statusWindow: z.array(z.boolean()).default([]), + statusWindowSize: z.number().min(1).max(20).default(5), + statusWindowThreshold: z.number().min(1).max(100).default(60), + type: z.enum(MonitorTypes, "Invalid monitor type"), + ignoreTlsErrors: z.boolean().default(false), + useAdvancedMatching: z.boolean().default(false), + jsonPath: z.union([z.string(), z.literal("")]).optional(), + expectedValue: z.union([z.string(), z.literal("")]).optional(), + matchMethod: z.union([z.enum(MonitorMatchMethods), z.literal("")]).optional(), + url: z.string().min(1, "URL is required"), + port: z.number().optional(), + isActive: z.boolean().default(true), + interval: z.number().default(60000), + uptimePercentage: z.number().optional(), + notifications: z.array(z.string()).default([]), + secret: z.string().optional(), + cpuAlertThreshold: z.number().default(100), + cpuAlertCounter: z.number().default(5), + memoryAlertThreshold: z.number().default(100), + memoryAlertCounter: z.number().default(5), + diskAlertThreshold: z.number().default(100), + diskAlertCounter: z.number().default(5), + tempAlertThreshold: z.number().default(100), + tempAlertCounter: z.number().default(5), + selectedDisks: z.array(z.string()).default([]), + gameId: z.union([z.string(), z.literal("")]).optional(), + grpcServiceName: z.union([z.string(), z.literal("")]).default(""), + group: z.union([z.string().max(50).trim(), z.null()]).default(null), + geoCheckEnabled: z.boolean().default(false), + geoCheckLocations: z.array(z.enum(GeoContinents)).default([]), + geoCheckInterval: z.number().min(300000).default(300000), + createdAt: z.string().optional(), + updatedAt: z.string().optional(), +}); + +export const importMonitorsBodyValidation = z.object({ + monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"), +}); + +export type ImportedMonitor = z.output; + export const getHardwareDetailsByIdParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), }); diff --git a/server/src/validation/notificationValidation.ts b/server/src/validation/notificationValidation.ts index d974d873f..cf7478610 100644 --- a/server/src/validation/notificationValidation.ts +++ b/server/src/validation/notificationValidation.ts @@ -18,7 +18,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("webhook"), - address: z.string().url("Please enter a valid Webhook URL"), + address: z.url({ message: "Please enter a valid Webhook URL" }), homeserverUrl: z.union([z.string(), z.literal("")]).optional(), roomId: z.union([z.string(), z.literal("")]).optional(), accessToken: z.union([z.string(), z.literal("")]).optional(), @@ -27,7 +27,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("slack"), - address: z.string().url("Please enter a valid Webhook URL"), + address: z.url({ message: "Please enter a valid Webhook URL" }), homeserverUrl: z.union([z.string(), z.literal("")]).optional(), roomId: z.union([z.string(), z.literal("")]).optional(), accessToken: z.union([z.string(), z.literal("")]).optional(), @@ -36,7 +36,7 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ z.object({ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("discord"), - address: z.string().url("Please enter a valid Webhook URL"), + address: z.url({ message: "Please enter a valid Webhook URL" }), homeserverUrl: z.union([z.string(), z.literal("")]).optional(), roomId: z.union([z.string(), z.literal("")]).optional(), accessToken: z.union([z.string(), z.literal("")]).optional(), @@ -55,12 +55,57 @@ export const createNotificationBodyValidation = z.discriminatedUnion("type", [ notificationName: z.string().min(1, "Notification name is required"), type: z.literal("matrix"), address: z.union([z.string(), z.literal("")]).optional(), - homeserverUrl: z.string().url("Please enter a valid Homeserver URL"), + homeserverUrl: z.url({ message: "Please enter a valid Homeserver URL" }), roomId: z.string().min(1, "Room ID is required"), accessToken: z.string().min(1, "Access Token is required"), }), + // Teams notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("teams"), + address: z.url({ message: "Please enter a valid Webhook URL" }), + }), + // Telegram notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("telegram"), + address: z.string().min(1, "Chat ID is required"), + accessToken: z.string().min(1, "Bot token is required"), + }), + // Pushover notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("pushover"), + address: z.string().min(1, "User key is required"), + accessToken: z.string().min(1, "App token is required"), + }), + // Twilio SMS notification + z.object({ + notificationName: z.string().min(1, "Notification name is required"), + type: z.literal("twilio"), + accountSid: z.string().min(1, "Account SID is required"), + accessToken: z.string().min(1, "Auth token is required"), + phone: z.string().min(1, "Recipient phone number is required"), + twilioPhoneNumber: z.string().min(1, "Twilio phone number is required"), + }), ]); +export const testNotificationBodyValidation = createNotificationBodyValidation; + +export const deleteNotificationParamValidation = z.object({ + id: z.string().min(1, "Notification ID is required"), +}); +export const getNotificationByIdParamValidation = z.object({ + id: z.string().min(1, "Notification ID is required"), +}); +export const editNotificationParamValidation = z.object({ + id: z.string().min(1, "Notification ID is required"), +}); + +export const testAllNotificationsBodyValidation = z.object({ + monitorId: z.string().min(1, "Monitor ID is required"), +}); + export const sendTestEmailBodyValidation = z.object({ to: z.string().min(1, "To field is required"), systemEmailHost: z.string().optional(), @@ -76,3 +121,20 @@ export const sendTestEmailBodyValidation = z.object({ systemEmailRejectUnauthorized: z.boolean().optional(), systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(), }); + +export const updateNotificationsValidation = z + .object({ + monitorIds: z.array(z.string()).min(1, "At least one monitor ID is required").max(100, "Cannot update more than 100 monitors at once"), + notificationIds: z.array(z.string()).max(100, "Cannot specify more than 100 notification IDs at once"), + action: z.enum(["add", "remove", "set"] as const), + }) + .refine( + (data) => { + if (data.action !== "set" && data.notificationIds.length === 0) return false; + return true; + }, + { + message: "Notification IDs cannot be empty unless action is 'set'", + path: ["notificationIds"], + } + ); diff --git a/server/src/validation/settingsValidation.ts b/server/src/validation/settingsValidation.ts index ee2a2ee87..f2c4e4c94 100644 --- a/server/src/validation/settingsValidation.ts +++ b/server/src/validation/settingsValidation.ts @@ -1,3 +1,4 @@ +import { CHECK_TTL_SENTINEL } from "@/types/check.js"; import { z } from "zod"; //**************************************** @@ -6,18 +7,17 @@ import { z } from "zod"; export const updateAppSettingsBodyValidation = z .object({ - checkTTL: z.union([z.number(), z.literal("")]).optional(), - systemEmailPort: z.union([z.number(), z.literal("")]).optional(), - - pagespeedApiKey: z.union([z.string(), z.literal("")]).optional(), - language: z.union([z.string(), z.literal("")]).optional(), - timezone: z.union([z.string(), z.literal("")]).optional(), - systemEmailHost: z.union([z.string(), z.literal("")]).optional(), - systemEmailAddress: z.union([z.string(), z.literal("")]).optional(), - systemEmailPassword: z.union([z.string(), z.literal("")]).optional(), - systemEmailUser: z.union([z.string(), z.literal("")]).optional(), - systemEmailConnectionHost: z.union([z.string(), z.literal("")]).optional(), - systemEmailTLSServername: z.union([z.string(), z.literal("")]).optional(), + checkTTL: z.number().int().min(1).max(CHECK_TTL_SENTINEL).optional(), + systemEmailPort: z.number().nullable().optional(), + pagespeedApiKey: z.string().nullable().optional(), + language: z.string().optional(), + timezone: z.string().optional(), + systemEmailHost: z.string().nullable().optional(), + systemEmailAddress: z.string().nullable().optional(), + systemEmailPassword: z.string().nullable().optional(), + systemEmailUser: z.string().nullable().optional(), + systemEmailConnectionHost: z.string().nullable().optional(), + systemEmailTLSServername: z.string().nullable().optional(), showURL: z.boolean().optional(), systemEmailSecure: z.boolean().optional(), @@ -28,10 +28,10 @@ export const updateAppSettingsBodyValidation = z globalThresholds: z .object({ - cpu: z.union([z.number().min(1).max(100), z.literal("")]).optional(), - memory: z.union([z.number().min(1).max(100), z.literal("")]).optional(), - disk: z.union([z.number().min(1).max(100), z.literal("")]).optional(), - temperature: z.union([z.number().min(1).max(150), z.literal("")]).optional(), + cpu: z.number().min(1).max(100).optional(), + memory: z.number().min(1).max(100).optional(), + disk: z.number().min(1).max(100).optional(), + temperature: z.number().min(1).max(150).optional(), }) .optional(), }) diff --git a/server/src/validation/statusPageValidation.ts b/server/src/validation/statusPageValidation.ts index b4b5e270c..8b2ab3f20 100644 --- a/server/src/validation/statusPageValidation.ts +++ b/server/src/validation/statusPageValidation.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; +import { StatusPageTypes } from "@/types/statusPage.js"; //**************************************** // Status Page Validations @@ -10,13 +11,13 @@ export const getStatusPageParamValidation = z.object({ }); export const getStatusPageQueryValidation = z.object({ - type: z.literal("uptime"), + type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])), timeFrame: z.coerce.number().optional(), }); export const createStatusPageBodyValidation = z .object({ - type: z.literal("uptime"), + type: z.union([z.enum(StatusPageTypes), z.array(z.enum(StatusPageTypes))]).transform((val) => (Array.isArray(val) ? val : [val])), companyName: z.string().min(1, "Company name is required"), url: z.string().regex(/^[a-zA-Z0-9_-]+$/, { message: "URL can only contain letters, numbers, underscores, and hyphens", @@ -30,6 +31,7 @@ export const createStatusPageBodyValidation = z showCharts: booleanCoercion.optional(), showUptimePercentage: booleanCoercion, showAdminLoginLink: booleanCoercion.optional(), + showInfrastructure: booleanCoercion.optional(), removeLogo: z.union([z.literal("true"), z.literal("false")]).optional(), }) .strip(); diff --git a/server/test/helpers/InMemoryIncidentsRepository.ts b/server/test/helpers/InMemoryIncidentsRepository.ts new file mode 100644 index 000000000..f76236f89 --- /dev/null +++ b/server/test/helpers/InMemoryIncidentsRepository.ts @@ -0,0 +1,138 @@ +import type { IIncidentsRepository } from "../../src/repositories/incidents/IIncidentsRepository.ts"; +import type { Incident, IncidentSummary } from "../../src/types/index.ts"; +import { randomUUID } from "crypto"; + +export class InMemoryIncidentsRepository implements IIncidentsRepository { + private incidents: Incident[] = []; + + async create(incident: Partial): Promise { + const now = new Date().toISOString(); + const full: Incident = { + id: randomUUID(), + monitorId: incident.monitorId ?? "", + teamId: incident.teamId ?? "", + startTime: incident.startTime ?? now, + endTime: incident.endTime ?? null, + status: incident.status ?? true, + message: incident.message ?? null, + statusCode: incident.statusCode ?? null, + resolutionType: incident.resolutionType ?? null, + resolvedBy: incident.resolvedBy ?? null, + resolvedByEmail: incident.resolvedByEmail ?? null, + comment: incident.comment ?? null, + createdAt: now, + updatedAt: now, + }; + this.incidents.push(full); + return { ...full }; + } + + async findById(incidentId: string, teamId: string): Promise { + const incident = this.incidents.find((i) => i.id === incidentId && i.teamId === teamId); + if (!incident) { + throw new Error(`Incident ${incidentId} not found`); + } + return { ...incident }; + } + + async findActiveByIncidentId(incidentId: string, teamId: string): Promise { + const incident = this.incidents.find((i) => i.id === incidentId && i.teamId === teamId && i.status === true); + return incident ? { ...incident } : null; + } + + async findActiveByMonitorId(monitorId: string, teamId: string): Promise { + const incident = this.incidents.find((i) => i.monitorId === monitorId && i.teamId === teamId && i.status === true); + return incident ? { ...incident } : null; + } + + async findByTeamId( + teamId: string, + startDate: Date | undefined, + page: number, + rowsPerPage: number, + sortOrder?: string, + status?: boolean, + monitorId?: string, + resolutionType?: string + ): Promise { + let results = this.incidents.filter((i) => i.teamId === teamId); + if (startDate) { + results = results.filter((i) => new Date(i.startTime) >= startDate); + } + if (status !== undefined) { + results = results.filter((i) => i.status === status); + } + if (monitorId) { + results = results.filter((i) => i.monitorId === monitorId); + } + if (resolutionType) { + results = results.filter((i) => i.resolutionType === resolutionType); + } + results.sort((a, b) => { + const cmp = new Date(a.startTime).getTime() - new Date(b.startTime).getTime(); + return sortOrder === "desc" ? -cmp : cmp; + }); + return results.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map((i) => ({ ...i })); + } + + async findSummaryByTeamId(_teamId: string, _limit?: number): Promise { + throw new Error("Not implemented"); + } + + async countByTeamId(teamId: string, startDate: Date | undefined, status?: boolean, monitorId?: string, resolutionType?: string): Promise { + let results = this.incidents.filter((i) => i.teamId === teamId); + if (startDate) { + results = results.filter((i) => new Date(i.startTime) >= startDate); + } + if (status !== undefined) { + results = results.filter((i) => i.status === status); + } + if (monitorId) { + results = results.filter((i) => i.monitorId === monitorId); + } + if (resolutionType) { + results = results.filter((i) => i.resolutionType === resolutionType); + } + return results.length; + } + + async updateById(incidentId: string, teamId: string, updateData: Partial): Promise { + const index = this.incidents.findIndex((i) => i.id === incidentId && i.teamId === teamId); + if (index === -1) { + throw new Error(`Incident ${incidentId} not found`); + } + const updated: Incident = { + ...this.incidents[index], + ...updateData, + id: this.incidents[index].id, + monitorId: this.incidents[index].monitorId, + teamId: this.incidents[index].teamId, + createdAt: this.incidents[index].createdAt, + updatedAt: new Date().toISOString(), + }; + this.incidents[index] = updated; + return { ...updated }; + } + + async deleteByMonitorId(monitorId: string, teamId: string): Promise { + const before = this.incidents.length; + this.incidents = this.incidents.filter((i) => !(i.monitorId === monitorId && i.teamId === teamId)); + return before - this.incidents.length; + } + + async deleteByMonitorIdsNotIn(monitorIds: string[]): Promise { + const before = this.incidents.length; + this.incidents = this.incidents.filter((i) => monitorIds.includes(i.monitorId)); + return before - this.incidents.length; + } + + // Test helpers — not part of the interface + + getAll(): Incident[] { + return this.incidents.map((i) => ({ ...i })); + } + + clear(): void { + this.incidents = []; + } +} diff --git a/server/test/helpers/InMemoryMonitorsRepository.ts b/server/test/helpers/InMemoryMonitorsRepository.ts new file mode 100644 index 000000000..a151ee41e --- /dev/null +++ b/server/test/helpers/InMemoryMonitorsRepository.ts @@ -0,0 +1,147 @@ +import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "../../src/repositories/monitors/IMonitorsRepository.ts"; +import type { Monitor, MonitorsSummary, CheckSnapshot } from "../../src/types/index.ts"; + +export class InMemoryMonitorsRepository implements IMonitorsRepository { + private monitors: Monitor[] = []; + + async create(monitor: Monitor, _teamId: string, _userId: string): Promise { + this.monitors.push({ ...monitor }); + return { ...monitor }; + } + + async createMonitors(monitors: Monitor[]): Promise { + const created = monitors.map((m) => ({ ...m })); + this.monitors.push(...created); + return created; + } + + async findById(monitorId: string, teamId: string): Promise { + const monitor = this.monitors.find((m) => m.id === monitorId && m.teamId === teamId); + if (!monitor) { + throw new Error(`Monitor ${monitorId} not found`); + } + return { ...monitor }; + } + + async findAll(): Promise { + return this.monitors.map((m) => ({ ...m })); + } + + async findByTeamId(_teamId: string, _config: TeamQueryConfig): Promise { + return this.monitors.filter((m) => m.teamId === _teamId).map((m) => ({ ...m })); + } + + async findByIds(monitorIds: string[]): Promise { + return this.monitors.filter((m) => monitorIds.includes(m.id)).map((m) => ({ ...m })); + } + + async findByIdsWithChecks(monitorIds: string[], _checksCount?: number): Promise { + return this.findByIds(monitorIds); + } + + async updateById(monitorId: string, teamId: string, updates: Partial): Promise { + const index = this.monitors.findIndex((m) => m.id === monitorId && m.teamId === teamId); + if (index === -1) { + throw new Error(`Monitor ${monitorId} not found`); + } + const updated = { ...this.monitors[index], ...updates, id: this.monitors[index].id, teamId: this.monitors[index].teamId }; + this.monitors[index] = updated; + return { ...updated }; + } + + async updateStatusWindowAndChecks( + monitorId: string, + teamId: string, + status: boolean, + checkSnapshot: CheckSnapshot, + windowSize: number, + maxRecentChecks: number, + statusPatch?: Partial + ): Promise { + const index = this.monitors.findIndex((m) => m.id === monitorId && m.teamId === teamId); + if (index === -1) { + throw new Error(`Monitor ${monitorId} not found`); + } + const monitor = this.monitors[index]; + monitor.statusWindow = monitor.statusWindow || []; + monitor.statusWindow.push(status); + while (monitor.statusWindow.length > windowSize) { + monitor.statusWindow.shift(); + } + monitor.recentChecks = monitor.recentChecks || []; + monitor.recentChecks.push(checkSnapshot); + while (monitor.recentChecks.length > maxRecentChecks) { + monitor.recentChecks.shift(); + } + if (statusPatch) { + Object.assign(monitor, statusPatch); + } + this.monitors[index] = monitor; + return { ...monitor }; + } + + async togglePauseById(monitorId: string, teamId: string): Promise { + const monitor = await this.findById(monitorId, teamId); + const newStatus = monitor.status === "paused" ? "up" : "paused"; + return this.updateById(monitorId, teamId, { status: newStatus }); + } + + async deleteById(monitorId: string, teamId: string): Promise { + const index = this.monitors.findIndex((m) => m.id === monitorId && m.teamId === teamId); + if (index === -1) { + throw new Error(`Monitor ${monitorId} not found`); + } + const [deleted] = this.monitors.splice(index, 1); + return { ...deleted }; + } + + async deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }> { + const toDelete = this.monitors.filter((m) => m.teamId === teamId); + this.monitors = this.monitors.filter((m) => m.teamId !== teamId); + return { monitors: toDelete, deletedCount: toDelete.length }; + } + + async findMonitorCountByTeamIdAndType(teamId: string, _config: TeamQueryConfig): Promise { + return this.monitors.filter((m) => m.teamId === teamId).length; + } + + async findMonitorsSummaryByTeamId(_teamId: string, _config?: SummaryConfig): Promise { + throw new Error("Not implemented"); + } + + async findGroupsByTeamId(_teamId: string): Promise { + throw new Error("Not implemented"); + } + + async removeNotificationFromMonitors(_notificationId: string): Promise { + throw new Error("Not implemented"); + } + + async updateNotifications(_teamId: string, _monitorIds: string[], _notificationIds: string[], _action: "add" | "remove" | "set"): Promise { + throw new Error("Not implemented"); + } + + async deleteByTeamIdsNotIn(teamIds: string[]): Promise { + const before = this.monitors.length; + this.monitors = this.monitors.filter((m) => teamIds.includes(m.teamId)); + return before - this.monitors.length; + } + + async findAllMonitorIds(): Promise { + return this.monitors.map((m) => m.id); + } + + // Test helpers + + seed(monitor: Monitor): void { + this.monitors.push({ ...monitor }); + } + + getAll(): Monitor[] { + return this.monitors.map((m) => ({ ...m })); + } + + clear(): void { + this.monitors = []; + } +} diff --git a/server/test/helpers/createMockLogger.ts b/server/test/helpers/createMockLogger.ts new file mode 100644 index 000000000..96f6fc7bd --- /dev/null +++ b/server/test/helpers/createMockLogger.ts @@ -0,0 +1,12 @@ +import { jest } from "@jest/globals"; + +export const createMockLogger = () => ({ + serviceName: "test", + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + cacheLog: jest.fn(), + getLogs: jest.fn().mockReturnValue([]), + buildLogEntry: jest.fn().mockReturnValue({ level: "info", timestamp: new Date().toISOString() }), +}); diff --git a/server/test/helpers/heartbeatTestHarness.ts b/server/test/helpers/heartbeatTestHarness.ts new file mode 100644 index 000000000..ec0c3714e --- /dev/null +++ b/server/test/helpers/heartbeatTestHarness.ts @@ -0,0 +1,179 @@ +import { jest } from "@jest/globals"; +import { SuperSimpleQueueHelper } from "../../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; +import { StatusService } from "../../src/service/infrastructure/statusService.ts"; +import { IncidentService } from "../../src/service/business/incidentService.ts"; +import { InMemoryMonitorsRepository } from "./InMemoryMonitorsRepository.ts"; +import { InMemoryIncidentsRepository } from "./InMemoryIncidentsRepository.ts"; +import { createMockLogger } from "./createMockLogger.ts"; +import type { Monitor } from "../../src/types/monitor.ts"; +import type { MonitorStatusResponse } from "../../src/types/network.ts"; +import type { Check } from "../../src/types/check.ts"; +import type { MaintenanceWindow } from "../../src/types/maintenanceWindow.ts"; + +let checkCounter = 0; + +export const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + name: "Test Monitor", + type: "http", + url: "https://example.com", + status: "up", + // Pre-fill window so threshold evaluation runs immediately. + // Without this, the warm-up path sets monitor.status directly + // from each check, bypassing the threshold logic. + statusWindow: [true, true, true, true, true], + statusWindowSize: 5, + statusWindowThreshold: 60, + recentChecks: [], + notifications: [], + ...overrides, + }) as Monitor; + +export const makeStatusResponse = (status: boolean, code: number): MonitorStatusResponse => ({ + monitorId: "mon-1", + teamId: "team-1", + type: "http", + status, + code, + message: status ? "OK" : "Service Unavailable", + responseTime: status ? 150 : 0, +}); + +export const makeCheck = (status: boolean, code: number): Check => { + const now = new Date().toISOString(); + return { + id: `check-${++checkCounter}`, + metadata: { monitorId: "mon-1", teamId: "team-1", type: "http" }, + status, + statusCode: code, + responseTime: status ? 150 : 0, + message: status ? "OK" : "Service Unavailable", + createdAt: now, + updatedAt: now, + }; +}; + +const createStubMonitorStatsRepo = () => ({ + findByMonitorId: jest.fn().mockRejectedValue(new Error("no stats")), + create: jest.fn().mockResolvedValue({}), + updateByMonitorId: jest.fn().mockResolvedValue({}), + deleteByMonitorId: jest.fn(), + deleteByMonitorIds: jest.fn(), + deleteByMonitorIdsNotIn: jest.fn(), +}); + +const createStubChecksRepo = () => ({ + create: jest.fn(), + createChecks: jest.fn(), + findByMonitorId: jest.fn(), + findByTeamId: jest.fn(), + findLatestByMonitorIds: jest.fn(), + findByDateRangeAndMonitorId: jest.fn(), + findSummaryByTeamId: jest.fn(), + deleteByMonitorId: jest.fn(), + deleteByTeamId: jest.fn(), + deleteByMonitorIdsNotIn: jest.fn(), + deleteOlderThan: jest.fn(), +}); + +export interface HeartbeatTestHarness { + monitorsRepo: InMemoryMonitorsRepository; + incidentsRepo: InMemoryIncidentsRepository; + statusService: StatusService; + incidentService: IncidentService; + notificationsService: { handleNotifications: jest.Mock }; + networkService: { requestStatus: jest.Mock }; + bufferStub: { addToBuffer: jest.Mock; addGeoCheckToBuffer: jest.Mock; scheduleNextFlush: jest.Mock }; + maintenanceWindowsRepo: { findByMonitorId: jest.Mock }; + messageBuilder: { extractThresholdBreaches: jest.Mock }; + heartbeatJob: (monitor: Monitor) => Promise; + setNextResponse: (status: boolean, code: number) => void; + setNextResponseFull: (response: MonitorStatusResponse) => void; +} + +export function createHeartbeatTestHarness(): HeartbeatTestHarness { + checkCounter = 0; + + const monitorsRepo = new InMemoryMonitorsRepository(); + const incidentsRepo = new InMemoryIncidentsRepository(); + const logger = createMockLogger() as any; + const bufferStub = { addToBuffer: jest.fn(), addGeoCheckToBuffer: jest.fn(), scheduleNextFlush: jest.fn() }; + + const statusService = new StatusService( + logger, + bufferStub as any, + monitorsRepo as any, + createStubMonitorStatsRepo() as any, + createStubChecksRepo() as any + ); + + const messageBuilder = { extractThresholdBreaches: jest.fn().mockReturnValue([]) }; + const incidentService = new IncidentService(logger, incidentsRepo, monitorsRepo as any, { findById: jest.fn() } as any, messageBuilder as any); + + const notificationsService = { handleNotifications: jest.fn().mockResolvedValue(true) }; + + let nextResponse: MonitorStatusResponse | null = null; + let nextStatus = true; + let nextCode = 200; + const networkService = { + requestStatus: jest.fn().mockImplementation(() => { + if (nextResponse) { + return Promise.resolve(nextResponse); + } + return Promise.resolve(makeStatusResponse(nextStatus, nextCode)); + }), + }; + const checkService = { + buildCheck: jest.fn().mockImplementation((response: MonitorStatusResponse) => { + return makeCheck(response.status, response.code); + }), + }; + + const setNextResponse = (status: boolean, code: number) => { + nextResponse = null; + nextStatus = status; + nextCode = code; + }; + + const setNextResponseFull = (response: MonitorStatusResponse) => { + nextResponse = response; + }; + + const maintenanceWindowsRepo = { findByMonitorId: jest.fn().mockResolvedValue([]) }; + + const helper = new SuperSimpleQueueHelper( + logger, + networkService as any, + statusService as any, + notificationsService as any, + checkService as any, + { getDBSettings: jest.fn() } as any, + bufferStub as any, + incidentService as any, + maintenanceWindowsRepo as any, + monitorsRepo as any, + { findAllTeamIds: jest.fn() } as any, + createStubMonitorStatsRepo() as any, + createStubChecksRepo() as any, + incidentsRepo as any, + { buildGeoCheck: jest.fn() } as any, + { deleteByMonitorIdsNotIn: jest.fn() } as any + ); + + return { + monitorsRepo, + incidentsRepo, + statusService, + incidentService, + notificationsService, + networkService, + bufferStub, + maintenanceWindowsRepo, + messageBuilder, + heartbeatJob: helper.getHeartbeatJob(), + setNextResponse, + setNextResponseFull, + }; +} diff --git a/server/test/helpers/notificationMessage.ts b/server/test/helpers/notificationMessage.ts new file mode 100644 index 000000000..c25644c81 --- /dev/null +++ b/server/test/helpers/notificationMessage.ts @@ -0,0 +1,51 @@ +import type { Notification } from "../../src/types/index.ts"; +import type { NotificationMessage } from "../../src/types/notificationMessage.ts"; + +export const makeNotification = (overrides?: Partial): Notification => + ({ + address: "https://hooks.example.com/webhook", + accessToken: "token-abc", + homeserverUrl: "https://matrix.example.com", + roomId: "!room:example.com", + ...overrides, + }) as Notification; + +export const makeMessage = (overrides?: Partial): NotificationMessage => ({ + type: "monitor_down", + severity: "critical", + monitor: { id: "mon-1", name: "Test Monitor", url: "https://example.com", type: "http", status: "down" }, + content: { + title: "Monitor Down: Test Monitor", + summary: 'Monitor "Test Monitor" is currently down.', + details: ["URL: https://example.com", "Status: Down"], + timestamp: new Date("2025-01-01T00:00:00Z"), + }, + clientHost: "https://app.example.com", + metadata: { teamId: "team-1", notificationReason: "status_change" }, + ...overrides, +}); + +export const makeMessageWithThresholds = (): NotificationMessage => + makeMessage({ + type: "threshold_breach", + severity: "warning", + monitor: { id: "mon-1", name: "Infra Server", url: "https://infra.example.com", type: "hardware", status: "up" }, + content: { + title: "Threshold Exceeded: Infra Server", + summary: "Thresholds exceeded.", + details: ["URL: https://infra.example.com"], + thresholds: [ + { metric: "cpu", currentValue: 90, threshold: 80, unit: "%", formattedValue: "90.0%" }, + { metric: "memory", currentValue: 85, threshold: 70, unit: "%", formattedValue: "85.0%" }, + ], + timestamp: new Date("2025-01-01T00:00:00Z"), + }, + }); + +export const makeMessageWithIncident = (): NotificationMessage => + makeMessage({ + content: { + ...makeMessage().content, + incident: { id: "inc-1", url: "https://app.example.com/incidents/inc-1", createdAt: new Date("2025-01-01T00:00:00Z") }, + }, + }); diff --git a/server/test/helpers/notificationProviderContract.ts b/server/test/helpers/notificationProviderContract.ts new file mode 100644 index 000000000..f95048f24 --- /dev/null +++ b/server/test/helpers/notificationProviderContract.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "@jest/globals"; +import type { INotificationProvider } from "../../src/service/infrastructure/notificationProviders/INotificationProvider.ts"; +import type { Notification } from "../../src/types/index.ts"; +import type { NotificationMessage } from "../../src/types/notificationMessage.ts"; + +const makeMessage = (): NotificationMessage => ({ + type: "monitor_down", + severity: "critical", + monitor: { id: "mon-1", name: "Test", url: "https://example.com", type: "http", status: "down" }, + content: { + title: "Monitor Down: Test", + summary: "Test is down", + details: ["URL: https://example.com"], + timestamp: new Date("2025-01-01T00:00:00Z"), + }, + clientHost: "https://app.example.com", + metadata: { teamId: "team-1", notificationReason: "status_change" }, +}); + +export const testNotificationProviderContract = ( + name: string, + opts: { + create: () => INotificationProvider; + makeNotification: () => Partial; + } +) => { + describe(`INotificationProvider contract: ${name}`, () => { + it("sendTestAlert returns a boolean", async () => { + const provider = opts.create(); + const result = await provider.sendTestAlert(opts.makeNotification()); + expect(typeof result).toBe("boolean"); + }); + + it("sendMessage returns a boolean", async () => { + const provider = opts.create(); + const result = await provider.sendMessage(opts.makeNotification() as Notification, makeMessage()); + expect(typeof result).toBe("boolean"); + }); + }); +}; diff --git a/server/test/helpers/statusProviderContract.ts b/server/test/helpers/statusProviderContract.ts new file mode 100644 index 000000000..6f7c89466 --- /dev/null +++ b/server/test/helpers/statusProviderContract.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from "@jest/globals"; +import type { IStatusProvider } from "../../src/service/infrastructure/network/IStatusProvider.ts"; +import type { Monitor } from "../../src/types/index.ts"; + +/** + * Shared contract tests for all IStatusProvider implementations. + * Tests the interface guarantees that every provider must satisfy. + */ +export const testStatusProviderContract = ( + name: string, + opts: { + create: () => IStatusProvider; + supportedType: string; + unsupportedType: string; + makeMonitor: () => Monitor; + } +) => { + describe(`IStatusProvider contract: ${name}`, () => { + it("has a readonly type property matching the supported type", () => { + const provider = opts.create(); + expect(provider.type).toBe(opts.supportedType); + }); + + it("supports() returns true for its own type", () => { + const provider = opts.create(); + expect(provider.supports(opts.supportedType as any)).toBe(true); + }); + + it("supports() returns false for a different type", () => { + const provider = opts.create(); + expect(provider.supports(opts.unsupportedType as any)).toBe(false); + }); + + it("handle() returns an object with required MonitorStatusResponse fields", async () => { + const provider = opts.create(); + try { + const result = await provider.handle(opts.makeMonitor()); + expect(result).toEqual( + expect.objectContaining({ + monitorId: expect.anything(), + teamId: expect.anything(), + type: expect.anything(), + status: expect.any(Boolean), + code: expect.any(Number), + message: expect.any(String), + }) + ); + } catch (err: any) { + // Some providers throw AppError on failure — that's also valid behavior + expect(err).toBeDefined(); + } + }); + }); +}; diff --git a/server/test/integration/heartbeatJobDownDetection.test.ts b/server/test/integration/heartbeatJobDownDetection.test.ts new file mode 100644 index 000000000..71628aabe --- /dev/null +++ b/server/test/integration/heartbeatJobDownDetection.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it, beforeEach } from "@jest/globals"; +import { createHeartbeatTestHarness, makeMonitor, type HeartbeatTestHarness } from "../helpers/heartbeatTestHarness.ts"; + +describe("Heartbeat job: down detection", () => { + let h: HeartbeatTestHarness; + + beforeEach(() => { + h = createHeartbeatTestHarness(); + }); + + it("transitions monitor to down and creates incident after threshold failures", async () => { + // Monitor starts with full passing window: [true, true, true, true, true] + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // 2 failures: window = [true, true, true, false, false] = 40% < 60% + h.setNextResponse(false, 503); + await h.heartbeatJob(monitor); + await h.heartbeatJob(monitor); + + let storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("up"); + expect(h.incidentsRepo.getAll()).toHaveLength(0); + + // 3rd failure: window = [true, true, false, false, false] = 60% >= 60% + await h.heartbeatJob(monitor); + + storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("down"); + + // Incident should have been created + const incidents = h.incidentsRepo.getAll(); + expect(incidents).toHaveLength(1); + expect(incidents[0].monitorId).toBe("mon-1"); + expect(incidents[0].status).toBe(true); + expect(incidents[0].statusCode).toBe(503); + + // Notification should have been triggered + expect(h.notificationsService.handleNotifications).toHaveBeenCalled(); + }); + + it("does not create duplicate incidents on continued failures", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + h.setNextResponse(false, 500); + + // 3 failures trigger down, 4 more continue failing + for (let i = 0; i < 7; i++) { + await h.heartbeatJob(monitor); + } + + // Only one incident despite 7 heartbeats + expect(h.incidentsRepo.getAll()).toHaveLength(1); + }); + + it("stays up when failures are below threshold", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // 2 failures: window = [true, true, true, false, false] = 40% < 60% + h.setNextResponse(false, 500); + await h.heartbeatJob(monitor); + await h.heartbeatJob(monitor); + + // 3 passes: window = [false, false, true, true, true] = 40% < 60% + h.setNextResponse(true, 200); + await h.heartbeatJob(monitor); + await h.heartbeatJob(monitor); + await h.heartbeatJob(monitor); + + const storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("up"); + expect(h.incidentsRepo.getAll()).toHaveLength(0); + // Regression guard for #3438: a sub-threshold blip must produce NO notification + // dispatches. Before PR #3505, a failing check below threshold silently wrote + // status="down" and the next passing check fired a spurious "Recovered" event. + expect(h.notificationsService.handleNotifications).not.toHaveBeenCalled(); + }); + + it("fires zero notifications on a sub-threshold blip (regression #3438)", async () => { + // Dedicated end-to-end guard for the #3438 bug. Distinct from "stays up when + // failures are below threshold" because it pins the notification-dispatch + // contract independently of status/incident assertions, so a future refactor + // that reintroduces the silent-down-write can't sneak past by happening to + // leave status and incidents in the right shape. + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // Failing check below threshold (window [t,t,t,t,f], 20%) + h.setNextResponse(false, 503); + await h.heartbeatJob(monitor); + + // Passing check — pre-fix, this is where the spurious "Recovered" fired + h.setNextResponse(true, 200); + await h.heartbeatJob(monitor); + + expect((await h.monitorsRepo.findById("mon-1", "team-1")).status).toBe("up"); + expect(h.incidentsRepo.getAll()).toHaveLength(0); + expect(h.notificationsService.handleNotifications).not.toHaveBeenCalled(); + }); +}); diff --git a/server/test/integration/heartbeatJobMaintenance.test.ts b/server/test/integration/heartbeatJobMaintenance.test.ts new file mode 100644 index 000000000..663c2e61e --- /dev/null +++ b/server/test/integration/heartbeatJobMaintenance.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it, beforeEach } from "@jest/globals"; +import { createHeartbeatTestHarness, makeMonitor, type HeartbeatTestHarness } from "../helpers/heartbeatTestHarness.ts"; +import type { MaintenanceWindow } from "../../src/types/maintenanceWindow.ts"; + +const makeMaintenanceWindow = (overrides?: Partial): MaintenanceWindow => { + const now = new Date(); + const start = new Date(now.getTime() - 60 * 60 * 1000); // 1 hour ago + const end = new Date(now.getTime() + 60 * 60 * 1000); // 1 hour from now + return { + id: "mw-1", + monitorId: "mon-1", + teamId: "team-1", + active: true, + name: "Test Maintenance", + duration: 2, + durationUnit: "hours", + repeat: 0, + start: start.toISOString(), + end: end.toISOString(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, + }; +}; + +describe("Heartbeat job: maintenance windows", () => { + let h: HeartbeatTestHarness; + + beforeEach(() => { + h = createHeartbeatTestHarness(); + }); + + it("skips checks and sets monitor to maintenance when window is active", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // Configure an active maintenance window covering the current time + h.maintenanceWindowsRepo.findByMonitorId.mockResolvedValue([makeMaintenanceWindow()]); + + await h.heartbeatJob(monitor); + + // Monitor status should be set to maintenance + const storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("maintenance"); + + // No network request should have been made + expect(h.networkService.requestStatus).not.toHaveBeenCalled(); + + // No check should have been buffered + expect(h.bufferStub.addToBuffer).not.toHaveBeenCalled(); + + // No incident should have been created + expect(h.incidentsRepo.getAll()).toHaveLength(0); + + // No notification should have been sent + expect(h.notificationsService.handleNotifications).not.toHaveBeenCalled(); + }); + + it("does not update status if monitor is already in maintenance", async () => { + const monitor = makeMonitor({ status: "maintenance" }); + h.monitorsRepo.seed(monitor); + + h.maintenanceWindowsRepo.findByMonitorId.mockResolvedValue([makeMaintenanceWindow()]); + + await h.heartbeatJob(monitor); + + // findById reads the stored monitor; updateById should not have been called + // since status was already "maintenance" + const storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("maintenance"); + expect(h.networkService.requestStatus).not.toHaveBeenCalled(); + }); + + it("resumes normal checks when maintenance window expires", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // First heartbeat: inside maintenance window + h.maintenanceWindowsRepo.findByMonitorId.mockResolvedValue([makeMaintenanceWindow()]); + await h.heartbeatJob(monitor); + + let storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("maintenance"); + + // Second heartbeat: maintenance window expired (no windows returned) + h.maintenanceWindowsRepo.findByMonitorId.mockResolvedValue([]); + h.setNextResponse(true, 200); + await h.heartbeatJob(monitor); + + // Network request should have been made this time + expect(h.networkService.requestStatus).toHaveBeenCalledTimes(1); + + // Check should have been buffered + expect(h.bufferStub.addToBuffer).toHaveBeenCalledTimes(1); + }); + + it("skips checks when window is inactive (active: false)", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // Inactive maintenance window — should be ignored + h.maintenanceWindowsRepo.findByMonitorId.mockResolvedValue([makeMaintenanceWindow({ active: false })]); + + h.setNextResponse(true, 200); + await h.heartbeatJob(monitor); + + // Normal check should proceed since window is inactive + expect(h.networkService.requestStatus).toHaveBeenCalledTimes(1); + expect(h.bufferStub.addToBuffer).toHaveBeenCalledTimes(1); + }); + + it("skips checks when window is in the past (expired)", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // Window ended an hour ago + const pastEnd = new Date(Date.now() - 60 * 60 * 1000); + const pastStart = new Date(pastEnd.getTime() - 2 * 60 * 60 * 1000); + h.maintenanceWindowsRepo.findByMonitorId.mockResolvedValue([ + makeMaintenanceWindow({ start: pastStart.toISOString(), end: pastEnd.toISOString() }), + ]); + + h.setNextResponse(true, 200); + await h.heartbeatJob(monitor); + + // Window is in the past — normal check should proceed + expect(h.networkService.requestStatus).toHaveBeenCalledTimes(1); + }); +}); diff --git a/server/test/integration/heartbeatJobNotificationFailure.test.ts b/server/test/integration/heartbeatJobNotificationFailure.test.ts new file mode 100644 index 000000000..bb231aa71 --- /dev/null +++ b/server/test/integration/heartbeatJobNotificationFailure.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, beforeEach } from "@jest/globals"; +import { createHeartbeatTestHarness, makeMonitor, type HeartbeatTestHarness } from "../helpers/heartbeatTestHarness.ts"; + +describe("Heartbeat job: notification failure isolation", () => { + let h: HeartbeatTestHarness; + + beforeEach(() => { + h = createHeartbeatTestHarness(); + }); + + it("creates incident even when notification dispatch throws", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // Make notifications throw + h.notificationsService.handleNotifications.mockRejectedValue(new Error("SMTP connection refused")); + + // Drive monitor down + h.setNextResponse(false, 503); + for (let i = 0; i < 3; i++) { + await h.heartbeatJob(monitor); + } + + // Incident should still have been created + const incidents = h.incidentsRepo.getAll(); + expect(incidents).toHaveLength(1); + expect(incidents[0].status).toBe(true); + expect(incidents[0].statusCode).toBe(503); + + // Monitor should still be marked down + const storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("down"); + }); + + it("auto-resolves incident even when recovery notification throws", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // Drive monitor down (notifications succeed here) + h.setNextResponse(false, 503); + for (let i = 0; i < 3; i++) { + await h.heartbeatJob(monitor); + } + expect(h.incidentsRepo.getAll()[0].status).toBe(true); + + // Now make notifications throw for the recovery + h.notificationsService.handleNotifications.mockRejectedValue(new Error("Slack API timeout")); + + // Recover + h.setNextResponse(true, 200); + for (let i = 0; i < 5; i++) { + await h.heartbeatJob(monitor); + } + + // Incident should still be resolved despite notification failure + const incidents = h.incidentsRepo.getAll(); + expect(incidents).toHaveLength(1); + expect(incidents[0].status).toBe(false); + expect(incidents[0].resolutionType).toBe("automatic"); + }); +}); diff --git a/server/test/integration/heartbeatJobRecovery.test.ts b/server/test/integration/heartbeatJobRecovery.test.ts new file mode 100644 index 000000000..10b6a32ca --- /dev/null +++ b/server/test/integration/heartbeatJobRecovery.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it, beforeEach } from "@jest/globals"; +import { createHeartbeatTestHarness, makeMonitor, type HeartbeatTestHarness } from "../helpers/heartbeatTestHarness.ts"; + +describe("Heartbeat job: recovery", () => { + let h: HeartbeatTestHarness; + + beforeEach(() => { + h = createHeartbeatTestHarness(); + }); + + it("recovers monitor and auto-resolves incident when checks pass", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // 3 failures to trigger down (60% threshold) + h.setNextResponse(false, 503); + for (let i = 0; i < 3; i++) { + await h.heartbeatJob(monitor); + } + expect(h.incidentsRepo.getAll()).toHaveLength(1); + expect(h.incidentsRepo.getAll()[0].status).toBe(true); + + // Send passing checks to push failure rate below threshold + // Window after 3 fails: [false, false, false, false, false] (prior trues shifted out) + // Need enough passes to drop below 60% + h.setNextResponse(true, 200); + for (let i = 0; i < 5; i++) { + await h.heartbeatJob(monitor); + } + + // Monitor should be back up + const storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("up"); + + // Incident should be auto-resolved + const incidents = h.incidentsRepo.getAll(); + expect(incidents).toHaveLength(1); + expect(incidents[0].status).toBe(false); + expect(incidents[0].resolutionType).toBe("automatic"); + expect(incidents[0].endTime).not.toBeNull(); + }); + + it("creates a new incident after recovery if monitor goes down again", async () => { + const monitor = makeMonitor(); + h.monitorsRepo.seed(monitor); + + // First outage: 3 failures to trigger (window shifts from all-true) + h.setNextResponse(false, 503); + for (let i = 0; i < 3; i++) { + await h.heartbeatJob(monitor); + } + + // Recovery: enough passes to drop below threshold + h.setNextResponse(true, 200); + for (let i = 0; i < 5; i++) { + await h.heartbeatJob(monitor); + } + + // Second outage: 3 more failures to trigger again + h.setNextResponse(false, 502); + for (let i = 0; i < 3; i++) { + await h.heartbeatJob(monitor); + } + + const incidents = h.incidentsRepo.getAll(); + expect(incidents).toHaveLength(2); + + // First incident resolved + expect(incidents[0].status).toBe(false); + expect(incidents[0].resolutionType).toBe("automatic"); + expect(incidents[0].statusCode).toBe(503); + + // Second incident active + expect(incidents[1].status).toBe(true); + expect(incidents[1].statusCode).toBe(502); + }); +}); diff --git a/server/test/integration/heartbeatJobThresholdBreach.test.ts b/server/test/integration/heartbeatJobThresholdBreach.test.ts new file mode 100644 index 000000000..ccb4065fa --- /dev/null +++ b/server/test/integration/heartbeatJobThresholdBreach.test.ts @@ -0,0 +1,204 @@ +import { describe, expect, it, beforeEach } from "@jest/globals"; +import { createHeartbeatTestHarness, makeMonitor, type HeartbeatTestHarness } from "../helpers/heartbeatTestHarness.ts"; +import type { MonitorStatusResponse } from "../../src/types/network.ts"; +import type { HardwareStatusPayload } from "../../src/types/network.ts"; + +const makeHardwareResponse = (cpuUsage: number): MonitorStatusResponse => ({ + monitorId: "mon-1", + teamId: "team-1", + type: "hardware", + status: true, + code: 200, + message: "OK", + responseTime: 50, + payload: { + data: { + cpu: { usage_percent: cpuUsage, temperature: [] }, + memory: { usage_percent: 0.3 }, + disk: [{ usage_percent: 0.2 }], + }, + }, +}); + +describe("Heartbeat job: hardware threshold breach", () => { + let h: HeartbeatTestHarness; + + beforeEach(() => { + h = createHeartbeatTestHarness(); + }); + + it("transitions to breached and creates incident after alert counter hits zero", async () => { + // Hardware monitor with CPU threshold at 80% (stored as 80, compared as 80/100 = 0.8) + // Alert counters start at 5, decrement each breach, trigger at 0 + const monitor = makeMonitor({ + type: "hardware", + status: "up", + cpuAlertThreshold: 80, + cpuAlertCounter: 5, + memoryAlertThreshold: 100, + memoryAlertCounter: 5, + diskAlertThreshold: 100, + diskAlertCounter: 5, + tempAlertThreshold: 100, + tempAlertCounter: 5, + selectedDisks: [], + }); + h.monitorsRepo.seed(monitor); + + // CPU at 90% (0.9) exceeds threshold of 80% (0.8) + h.setNextResponseFull(makeHardwareResponse(0.9)); + + // Configure message builder for when incident is created + h.messageBuilder.extractThresholdBreaches.mockReturnValue([{ metric: "cpu", formattedValue: "90%", threshold: 80, unit: "%" }]); + + // 4 heartbeats: counter goes 5→4→3→2→1, status still "up" + for (let i = 0; i < 4; i++) { + await h.heartbeatJob(monitor); + } + + let storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("up"); + expect(storedMonitor.cpuAlertCounter).toBe(1); + expect(h.incidentsRepo.getAll()).toHaveLength(0); + + // 5th heartbeat: counter hits 0, status transitions to "breached" + await h.heartbeatJob(monitor); + + storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("breached"); + expect(storedMonitor.cpuAlertCounter).toBe(0); + + // Incident created with statusCode 9999 and threshold message + const incidents = h.incidentsRepo.getAll(); + expect(incidents).toHaveLength(1); + expect(incidents[0].statusCode).toBe(9999); + expect(incidents[0].message).toBe("CPU: 90% (threshold: 80%)"); + expect(incidents[0].status).toBe(true); + + // Notification should have been triggered + expect(h.notificationsService.handleNotifications).toHaveBeenCalled(); + }); + + it("recovers from breached to up when metrics return to normal", async () => { + // Start with a monitor already in breached state with counter at 0 + const monitor = makeMonitor({ + type: "hardware", + status: "breached", + cpuAlertThreshold: 80, + cpuAlertCounter: 0, + memoryAlertThreshold: 100, + memoryAlertCounter: 5, + diskAlertThreshold: 100, + diskAlertCounter: 5, + tempAlertThreshold: 100, + tempAlertCounter: 5, + selectedDisks: [], + }); + h.monitorsRepo.seed(monitor); + + // Also seed an active incident for this monitor + await h.incidentsRepo.create({ + monitorId: "mon-1", + teamId: "team-1", + status: true, + statusCode: 9999, + message: "CPU: 90% (threshold: 80%)", + }); + + // CPU at 50% (0.5) — below threshold of 80% (0.8) + h.setNextResponseFull(makeHardwareResponse(0.5)); + + await h.heartbeatJob(monitor); + + // Monitor should recover to "up" + const storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(storedMonitor.status).toBe("up"); + // Counter resets to 5 when not breaching + expect(storedMonitor.cpuAlertCounter).toBe(5); + + // Incident should be auto-resolved + const incidents = h.incidentsRepo.getAll(); + expect(incidents).toHaveLength(1); + expect(incidents[0].status).toBe(false); + expect(incidents[0].resolutionType).toBe("automatic"); + }); + + it("transitions breached hardware monitor to down when reachability also fails", async () => { + // A hardware monitor currently in "breached" state starts failing its reachability + // checks enough to cross the statusWindow threshold. "down" must take precedence + // over "breached" — when both conditions are true, the user wants the escalated + // alert. Unit-covered in statusService.test.ts via computeReachability; this is + // the end-to-end counterpart that exercises the full heartbeat path including + // incident-lifecycle handling. + const monitor = makeMonitor({ + type: "hardware", + status: "breached", + cpuAlertThreshold: 80, + cpuAlertCounter: 0, + memoryAlertThreshold: 100, + memoryAlertCounter: 5, + diskAlertThreshold: 100, + diskAlertCounter: 5, + tempAlertThreshold: 100, + tempAlertCounter: 5, + selectedDisks: [], + // Window is fully healthy — the monitor is breached from hardware metrics, + // not from reachability. The test drives the window from clean → failing. + statusWindow: [true, true, true, true, true], + }); + h.monitorsRepo.seed(monitor); + + // Seed an active breached incident matching the initial state. + await h.incidentsRepo.create({ + monitorId: "mon-1", + teamId: "team-1", + status: true, + statusCode: 9999, + message: "CPU: 90% (threshold: 80%)", + }); + + // Drive reachability failure: 3 consecutive failing heartbeats. + // Window evolves: [t,t,t,t,t] → [t,t,t,t,f] (20%) → [t,t,t,f,f] (40%) → [t,t,f,f,f] (60%) + // At 60% >= threshold, and current status "breached" !== "down", reachability fires. + // Reachability sends HTTP-shaped responses with no hardware payload, so the + // hardware block is correctly skipped for these checks. + h.setNextResponse(false, 503); + for (let i = 0; i < 3; i++) { + await h.heartbeatJob(monitor); + } + + const stored = await h.monitorsRepo.findById("mon-1", "team-1"); + expect(stored.status).toBe("down"); + + // Notification dispatch must have fired for the transition. + expect(h.notificationsService.handleNotifications).toHaveBeenCalled(); + }); + + it("does not create duplicate incidents while remaining breached", async () => { + const monitor = makeMonitor({ + type: "hardware", + status: "up", + cpuAlertThreshold: 80, + cpuAlertCounter: 5, + memoryAlertThreshold: 100, + memoryAlertCounter: 5, + diskAlertThreshold: 100, + diskAlertCounter: 5, + tempAlertThreshold: 100, + tempAlertCounter: 5, + selectedDisks: [], + }); + h.monitorsRepo.seed(monitor); + + h.setNextResponseFull(makeHardwareResponse(0.9)); + h.messageBuilder.extractThresholdBreaches.mockReturnValue([{ metric: "cpu", formattedValue: "90%", threshold: 80, unit: "%" }]); + + // 5 heartbeats to trigger breach, then 5 more while still breaching + for (let i = 0; i < 10; i++) { + await h.heartbeatJob(monitor); + } + + // Only one incident despite 10 heartbeats + expect(h.incidentsRepo.getAll()).toHaveLength(1); + }); +}); diff --git a/server/test/integration/incidentLifecycle.test.ts b/server/test/integration/incidentLifecycle.test.ts new file mode 100644 index 000000000..d2429a812 --- /dev/null +++ b/server/test/integration/incidentLifecycle.test.ts @@ -0,0 +1,216 @@ +import { describe, expect, it, jest, beforeEach } from "@jest/globals"; +import { IncidentService } from "../../src/service/business/incidentService.ts"; +import { InMemoryIncidentsRepository } from "../helpers/InMemoryIncidentsRepository.ts"; +import { createMockLogger } from "../helpers/createMockLogger.ts"; +import type { IMonitorsRepository, IUsersRepository } from "../../src/repositories/index.ts"; +import type { INotificationMessageBuilder } from "../../src/service/infrastructure/notificationMessageBuilder.ts"; +import type { Monitor } from "../../src/types/monitor.ts"; +import type { MonitorActionDecision } from "../../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + name: "Test Monitor", + type: "http", + url: "https://example.com", + status: "down", + ...overrides, + }) as Monitor; + +const makeDecision = (overrides?: Partial): MonitorActionDecision => ({ + shouldCreateIncident: false, + shouldResolveIncident: false, + shouldSendNotification: false, + incidentReason: null, + notificationReason: null, + ...overrides, +}); + +// ── Test suite ─────────────────────────────────────────────────────────────── + +describe("Incident lifecycle (integration)", () => { + let repo: InMemoryIncidentsRepository; + let service: IncidentService; + let monitorsRepo: jest.Mocked; + let usersRepo: jest.Mocked; + let messageBuilder: jest.Mocked; + + beforeEach(() => { + repo = new InMemoryIncidentsRepository(); + monitorsRepo = { findById: jest.fn() } as unknown as jest.Mocked; + usersRepo = { findById: jest.fn() } as unknown as jest.Mocked; + messageBuilder = { extractThresholdBreaches: jest.fn() } as unknown as jest.Mocked; + service = new IncidentService(createMockLogger() as any, repo, monitorsRepo, usersRepo, messageBuilder); + }); + + // ── Creation ───────────────────────────────────────────────────────────── + + it("creates an incident when monitor goes down", async () => { + const monitor = makeMonitor({ status: "down" }); + const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); + + const incident = await service.handleIncident(monitor, 503, decision); + + expect(incident).not.toBeNull(); + expect(incident!.monitorId).toBe("mon-1"); + expect(incident!.teamId).toBe("team-1"); + expect(incident!.status).toBe(true); + expect(incident!.statusCode).toBe(503); + expect(incident!.endTime).toBeNull(); + expect(incident!.resolutionType).toBeNull(); + + const stored = repo.getAll(); + expect(stored).toHaveLength(1); + expect(stored[0].id).toBe(incident!.id); + }); + + // ── Idempotency ────────────────────────────────────────────────────────── + + it("does not create a duplicate incident for the same monitor", async () => { + const monitor = makeMonitor({ status: "down" }); + const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); + + const first = await service.handleIncident(monitor, 503, decision); + const second = await service.handleIncident(monitor, 503, decision); + + expect(second!.id).toBe(first!.id); + expect(repo.getAll()).toHaveLength(1); + }); + + // ── Auto-resolve ───────────────────────────────────────────────────────── + + it("auto-resolves an incident when monitor recovers", async () => { + const monitor = makeMonitor({ status: "down" }); + const createDecision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); + const created = await service.handleIncident(monitor, 503, createDecision); + + const resolveDecision = makeDecision({ shouldResolveIncident: true }); + const resolved = await service.handleIncident(monitor, 200, resolveDecision); + + expect(resolved).not.toBeNull(); + expect(resolved!.id).toBe(created!.id); + expect(resolved!.status).toBe(false); + expect(resolved!.endTime).toBeDefined(); + expect(resolved!.resolutionType).toBe("automatic"); + }); + + // ── Resolve with nothing active ────────────────────────────────────────── + + it("returns null when resolving with no active incident", async () => { + const monitor = makeMonitor({ status: "up" }); + const decision = makeDecision({ shouldResolveIncident: true }); + + const result = await service.handleIncident(monitor, 200, decision); + + expect(result).toBeNull(); + }); + + // ── Manual resolution ──────────────────────────────────────────────────── + + it("manually resolves an active incident with comment", async () => { + const monitor = makeMonitor({ status: "down" }); + const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); + const created = await service.handleIncident(monitor, 500, decision); + + const resolved = await service.resolveIncident(created!.id, "user-1", "team-1", "Root cause identified", "user@test.com"); + + expect(resolved.status).toBe(false); + expect(resolved.resolutionType).toBe("manual"); + expect(resolved.resolvedBy).toBe("user-1"); + expect(resolved.resolvedByEmail).toBe("user@test.com"); + expect(resolved.comment).toBe("Root cause identified"); + expect(resolved.endTime).toBeDefined(); + }); + + it("throws when manually resolving an already-resolved incident", async () => { + const monitor = makeMonitor({ status: "down" }); + const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); + const created = await service.handleIncident(monitor, 500, decision); + + await service.resolveIncident(created!.id, "user-1", "team-1"); + + await expect(service.resolveIncident(created!.id, "user-1", "team-1")).rejects.toThrow("Incident not found"); + }); + + // ── Threshold breach ───────────────────────────────────────────────────── + + it("creates a threshold breach incident with statusCode 9999 and descriptive message", async () => { + const monitor = makeMonitor({ status: "breached", type: "hardware" }); + (messageBuilder.extractThresholdBreaches as jest.Mock).mockReturnValue([ + { metric: "cpu", formattedValue: "92%", threshold: 80, unit: "%" }, + { metric: "memory", formattedValue: "88%", threshold: 85, unit: "%" }, + ]); + const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "threshold_breach" }); + + const incident = await service.handleIncident(monitor, 200, decision, { monitorId: "mon-1" } as any); + + expect(incident!.statusCode).toBe(9999); + expect(incident!.message).toBe("CPU: 92% (threshold: 80%), MEMORY: 88% (threshold: 85%)"); + }); + + // ── No action ──────────────────────────────────────────────────────────── + + it("returns null when no action is needed", async () => { + const monitor = makeMonitor({ status: "up" }); + const decision = makeDecision(); + + const result = await service.handleIncident(monitor, 200, decision); + + expect(result).toBeNull(); + expect(repo.getAll()).toHaveLength(0); + }); + + // ── Full lifecycle ─────────────────────────────────────────────────────── + + it("handles a complete lifecycle: create -> manual resolve -> new incident -> auto-resolve", async () => { + const monitor = makeMonitor({ status: "down" }); + + // First outage + const first = await service.handleIncident(monitor, 503, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" })); + expect(first!.status).toBe(true); + + // Manually resolved + await service.resolveIncident(first!.id, "user-1", "team-1", "Restarted server"); + + // Second outage — new incident since previous was resolved + const second = await service.handleIncident(monitor, 502, makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" })); + expect(second!.id).not.toBe(first!.id); + expect(second!.statusCode).toBe(502); + + // Auto-resolve + const resolved = await service.handleIncident(monitor, 200, makeDecision({ shouldResolveIncident: true })); + expect(resolved!.id).toBe(second!.id); + expect(resolved!.status).toBe(false); + expect(resolved!.resolutionType).toBe("automatic"); + + // Both incidents stored, both resolved + const all = repo.getAll(); + expect(all).toHaveLength(2); + expect(all.every((i) => i.status === false)).toBe(true); + }); + + // ── Cross-monitor isolation ────────────────────────────────────────────── + + it("incidents for different monitors do not interfere", async () => { + const monitorA = makeMonitor({ id: "mon-a", status: "down" }); + const monitorB = makeMonitor({ id: "mon-b", status: "down" }); + const decision = makeDecision({ shouldCreateIncident: true, incidentReason: "status_down" }); + + const incidentA = await service.handleIncident(monitorA, 500, decision); + const incidentB = await service.handleIncident(monitorB, 502, decision); + + expect(incidentA!.id).not.toBe(incidentB!.id); + expect(repo.getAll()).toHaveLength(2); + + // Resolving monitor A does not affect monitor B + const resolved = await service.handleIncident(monitorA, 200, makeDecision({ shouldResolveIncident: true })); + expect(resolved!.id).toBe(incidentA!.id); + + const bStillActive = await repo.findActiveByMonitorId("mon-b", "team-1"); + expect(bStillActive).not.toBeNull(); + expect(bStillActive!.status).toBe(true); + }); +}); diff --git a/server/test/monitorService.test.ts b/server/test/monitorService.test.ts deleted file mode 100644 index 32708a42e..000000000 --- a/server/test/monitorService.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -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 deleted file mode 100644 index 3b179bd91..000000000 --- a/server/test/superSimpleQueueHelper.test.ts +++ /dev/null @@ -1,101 +0,0 @@ -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/test/unit/providers/network/advancedMatcher.test.ts b/server/test/unit/providers/network/advancedMatcher.test.ts new file mode 100644 index 000000000..e0b2c773c --- /dev/null +++ b/server/test/unit/providers/network/advancedMatcher.test.ts @@ -0,0 +1,228 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { AdvancedMatcher } from "../../../../src/service/infrastructure/network/AdvancedMatcher.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const createMockJmespath = () => ({ + search: jest.fn((data: any, expr: string) => { + // Simple dot-path evaluation for tests + return expr.split(".").reduce((acc: any, key: string) => acc?.[key], data); + }), +}); + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + useAdvancedMatching: false, + jsonPath: undefined, + matchMethod: undefined, + expectedValue: undefined, + ...overrides, + }) as Monitor; + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("AdvancedMatcher", () => { + describe("validate", () => { + it("returns ok when advanced matching is disabled", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate({ data: "test" }, makeMonitor({ useAdvancedMatching: false })); + + expect(result).toEqual({ ok: true, message: "Success" }); + }); + + // ── jsonPath extraction ────────────────────────────────────────── + + describe("jsonPath extraction", () => { + it("extracts value using jsonPath", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate({ status: "healthy" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "status" })); + + expect(result).toEqual({ ok: true, message: "Success", extracted: "healthy" }); + }); + + it("returns error when jsonPath evaluation throws", () => { + const jmespath = { + search: jest.fn().mockImplementation(() => { + throw new Error("bad expression"); + }), + }; + const matcher = new AdvancedMatcher(jmespath as any); + + const result = matcher.validate({ data: "test" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "invalid[" })); + + expect(result).toEqual({ ok: false, message: "Error evaluating JSON path" }); + }); + }); + + // ── expectedValue matching ─────────────────────────────────────── + + describe("expectedValue with matchMethod", () => { + it("matches with 'equal' method", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { status: "ok" }, + makeMonitor({ useAdvancedMatching: true, jsonPath: "status", matchMethod: "equal", expectedValue: "ok" }) + ); + + expect(result.ok).toBe(true); + }); + + it("fails with 'equal' method on mismatch", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { status: "error" }, + makeMonitor({ useAdvancedMatching: true, jsonPath: "status", matchMethod: "equal", expectedValue: "ok" }) + ); + + expect(result.ok).toBe(false); + expect(result.message).toBe("Expected value did not match"); + }); + + it("matches with 'include' method", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { msg: "server is healthy" }, + makeMonitor({ useAdvancedMatching: true, jsonPath: "msg", matchMethod: "include", expectedValue: "healthy" }) + ); + + expect(result.ok).toBe(true); + }); + + it("fails with 'include' method when substring not found", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { msg: "server is down" }, + makeMonitor({ useAdvancedMatching: true, jsonPath: "msg", matchMethod: "include", expectedValue: "healthy" }) + ); + + expect(result.ok).toBe(false); + }); + + it("matches with 'regex' method", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { code: "200" }, + makeMonitor({ useAdvancedMatching: true, jsonPath: "code", matchMethod: "regex", expectedValue: "^2\\d{2}$" }) + ); + + expect(result.ok).toBe(true); + }); + + it("fails with 'regex' method on mismatch", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { code: "500" }, + makeMonitor({ useAdvancedMatching: true, jsonPath: "code", matchMethod: "regex", expectedValue: "^2\\d{2}$" }) + ); + + expect(result.ok).toBe(false); + }); + + it("defaults to 'equal' comparison when matchMethod is undefined", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate({ val: "exact" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "val", expectedValue: "exact" })); + + expect(result.ok).toBe(true); + }); + + it("defaults to 'equal' comparison for unknown matchMethod", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { val: "exact" }, + makeMonitor({ useAdvancedMatching: true, jsonPath: "val", matchMethod: "unknown" as any, expectedValue: "exact" }) + ); + + expect(result.ok).toBe(true); + }); + + it("compares entire payload when jsonPath is not set", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + "hello", + makeMonitor({ useAdvancedMatching: true, jsonPath: undefined, matchMethod: "equal", expectedValue: "hello" }) + ); + + expect(result.ok).toBe(true); + expect(result.extracted).toBe("hello"); + }); + + it("includes extracted value in result", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { nested: { value: "42" } }, + makeMonitor({ useAdvancedMatching: true, jsonPath: "nested.value", matchMethod: "equal", expectedValue: "42" }) + ); + + expect(result.extracted).toBe("42"); + }); + }); + + // ── falsy value check (no expectedValue) ──────────────────────── + + describe("falsy value check (no expectedValue)", () => { + it("returns ok for truthy extracted value", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate({ status: "up" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "status" })); + + expect(result.ok).toBe(true); + expect(result.extracted).toBe("up"); + }); + + it("returns not ok for false", () => { + const jmespath = { search: jest.fn().mockReturnValue(false) }; + const matcher = new AdvancedMatcher(jmespath as any); + + const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "missing" })); + + expect(result.ok).toBe(false); + expect(result.message).toBe("Extracted value is falsy"); + }); + + it("returns not ok for string 'false'", () => { + const jmespath = { search: jest.fn().mockReturnValue("false") }; + const matcher = new AdvancedMatcher(jmespath as any); + + const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" })); + + expect(result.ok).toBe(false); + }); + + it("returns not ok for undefined", () => { + const jmespath = { search: jest.fn().mockReturnValue(undefined) }; + const matcher = new AdvancedMatcher(jmespath as any); + + const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" })); + + expect(result.ok).toBe(false); + }); + + it("returns not ok for null", () => { + const jmespath = { search: jest.fn().mockReturnValue(null) }; + const matcher = new AdvancedMatcher(jmespath as any); + + const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" })); + + expect(result.ok).toBe(false); + }); + + it("returns ok for zero (not in falsy list)", () => { + const jmespath = { search: jest.fn().mockReturnValue(0) }; + const matcher = new AdvancedMatcher(jmespath as any); + + const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" })); + + expect(result.ok).toBe(true); + }); + + it("returns ok for empty string (not in falsy list)", () => { + const jmespath = { search: jest.fn().mockReturnValue("") }; + const matcher = new AdvancedMatcher(jmespath as any); + + const result = matcher.validate({}, makeMonitor({ useAdvancedMatching: true, jsonPath: "field" })); + + expect(result.ok).toBe(true); + }); + }); + }); +}); diff --git a/server/test/unit/providers/network/dockerProvider.test.ts b/server/test/unit/providers/network/dockerProvider.test.ts new file mode 100644 index 000000000..0f3dd68f6 --- /dev/null +++ b/server/test/unit/providers/network/dockerProvider.test.ts @@ -0,0 +1,343 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { DockerProvider } from "../../../../src/service/infrastructure/network/DockerProvider.ts"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import { createMockLogger } from "../../../helpers/createMockLogger.ts"; +import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "docker", + url: "my-container", + ...overrides, + }) as Monitor; + +const makeContainer = (overrides?: Record) => ({ + Id: "abc123def456abc123def456abc123def456abc123def456abc123def456abcd", + Names: ["/my-container"], + State: "running", + ...overrides, +}); + +const createMockDocker = (containers: any[] = [makeContainer()], inspectResult?: any) => { + const inspect = jest.fn().mockResolvedValue(inspectResult ?? { State: { Status: "running" } }); + const getContainer = jest.fn().mockReturnValue({ inspect }); + + const instance = { + listContainers: jest.fn().mockResolvedValue(containers), + getContainer, + }; + + const DockerLib = jest.fn().mockReturnValue(instance) as any; + + return { DockerLib, instance, inspect, getContainer }; +}; + +const createProvider = (opts?: { containers?: any[]; inspectResult?: any }) => { + const logger = createMockLogger(); + const { DockerLib, instance, inspect, getContainer } = createMockDocker(opts?.containers, opts?.inspectResult); + const provider = new DockerProvider(logger as any, DockerLib); + return { provider, logger, docker: instance, inspect, getContainer }; +}; + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("DockerProvider", { + create: () => createProvider().provider, + supportedType: "docker", + unsupportedType: "http", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("DockerProvider", () => { + // ── Container matching ─────────────────────────────────────────────── + + describe("container matching", () => { + it("matches by exact container name", async () => { + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ url: "my-container" })); + + expect(result.status).toBe(true); + expect(result.code).toBe(200); + }); + + it("matches by exact full ID (64 chars)", async () => { + const fullId = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd"; + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ url: fullId })); + + expect(result.status).toBe(true); + }); + + it("matches by partial ID prefix", async () => { + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ url: "abc123" })); + + expect(result.status).toBe(true); + }); + + it("strips leading slashes from input", async () => { + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ url: "///my-container" })); + + expect(result.status).toBe(true); + }); + + it("matches case-insensitively", async () => { + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ url: "MY-CONTAINER" })); + + expect(result.status).toBe(true); + }); + + it("returns 404 when no container matches", async () => { + const { provider, logger } = createProvider(); + + const result = await provider.handle(makeMonitor({ url: "nonexistent" })); + + expect(result.status).toBe(false); + expect(result.code).toBe(404); + expect(result.message).toBe("Docker container not found"); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("No container found"), + }) + ); + }); + }); + + // ── Ambiguity detection ────────────────────────────────────────────── + + describe("ambiguity detection", () => { + it("returns error when input matches both exact name and partial ID", async () => { + // Container whose name matches the input AND whose ID starts with the input + const container = makeContainer({ + Id: "my-containerabc123def456abc123def456abc123def456abc123def456abcd", + Names: ["/my-container"], + }); + const { provider, logger } = createProvider({ containers: [container] }); + + const result = await provider.handle(makeMonitor({ url: "my-container" })); + + expect(result.status).toBe(false); + expect(result.code).toBe(NETWORK_ERROR); + expect(result.message).toContain("Ambiguous"); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("Ambiguous"), + }) + ); + }); + + it("reports 'exact ID' in ambiguity message when exact ID and name both match", async () => { + const fullId = "abc123def456abc123def456abc123def456abc123def456abc123def456abcd"; + const container = makeContainer({ + Id: fullId, + Names: ["/" + fullId], + }); + const { provider } = createProvider({ containers: [container] }); + + const result = await provider.handle(makeMonitor({ url: fullId })); + + expect(result.message).toContain("Using exact ID"); + }); + + it("reports 'exact name' in ambiguity message when name and partial ID match", async () => { + const container = makeContainer({ + Id: "my-containerabc123def456abc123def456abc123def456abc123def456abcd", + Names: ["/my-container"], + }); + const { provider } = createProvider({ containers: [container] }); + + const result = await provider.handle(makeMonitor({ url: "my-container" })); + + expect(result.message).toContain("Using exact name"); + }); + }); + + // ── Container inspection ───────────────────────────────────────────── + + describe("container inspection", () => { + it("returns running status for running container", async () => { + const { provider } = createProvider({ inspectResult: { State: { Status: "running" } } }); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(true); + expect(result.message).toBe("Docker container status fetched successfully"); + }); + + it("returns not-running status for stopped container", async () => { + const { provider } = createProvider({ inspectResult: { State: { Status: "exited" } } }); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + }); + + it("returns NETWORK_ERROR when inspect response is null", async () => { + const logger = createMockLogger(); + const inspect = jest.fn().mockResolvedValue(null); + const getContainer = jest.fn().mockReturnValue({ inspect }); + const instance = { listContainers: jest.fn().mockResolvedValue([makeContainer()]), getContainer }; + const DockerLib = jest.fn().mockReturnValue(instance) as any; + const provider = new DockerProvider(logger as any, DockerLib); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + expect(result.code).toBe(NETWORK_ERROR); + expect(result.message).toBe("No response from Docker container inspect"); + }); + + it("returns false status when inspect response has no State", async () => { + const { provider } = createProvider({ inspectResult: {} }); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + }); + + it("returns false status when inspect response has State but no Status", async () => { + const { provider } = createProvider({ inspectResult: { State: {} } }); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + }); + + it("handles inspect error with Docker-specific error", async () => { + const { provider } = createProvider(); + // Make inspect throw a Docker error + const dockerErr = Object.assign(new Error("container not found"), { + statusCode: 404, + reason: "no such container", + json: { message: "Container not found" }, + }); + provider["docker"] = { + listContainers: jest.fn().mockResolvedValue([makeContainer()]), + getContainer: jest.fn().mockReturnValue({ + inspect: jest.fn().mockRejectedValue(dockerErr), + }), + } as any; + + // Need to re-assign; simpler to use the createProvider mock + const { provider: p2 } = createProvider(); + // Override getContainer to return failing inspect + p2["docker"].getContainer = jest.fn().mockReturnValue({ + inspect: jest.fn().mockRejectedValue(dockerErr), + }); + + const result = await p2.handle(makeMonitor()); + + expect(result.status).toBe(false); + expect(result.code).toBe(404); + expect(result.message).toBe("Container not found"); + }); + + it("handles inspect error with Docker error using reason fallback", async () => { + const { provider } = createProvider(); + const dockerErr = Object.assign(new Error("error"), { + statusCode: 500, + reason: "internal error", + json: {}, + }); + provider["docker"].getContainer = jest.fn().mockReturnValue({ + inspect: jest.fn().mockRejectedValue(dockerErr), + }); + + const result = await provider.handle(makeMonitor()); + + expect(result.message).toBe("internal error"); + }); + + it("handles inspect error with Docker error using Error message fallback", async () => { + const { provider } = createProvider(); + const dockerErr = Object.assign(new Error("base error"), { + statusCode: 503, + reason: undefined, + json: undefined, + }); + provider["docker"].getContainer = jest.fn().mockReturnValue({ + inspect: jest.fn().mockRejectedValue(dockerErr), + }); + + const result = await provider.handle(makeMonitor()); + + expect(result.message).toBe("base error"); + }); + + it("handles inspect error with NETWORK_ERROR when no statusCode", async () => { + const { provider } = createProvider(); + const dockerErr = Object.assign(new Error("unknown"), { + statusCode: undefined, + }); + provider["docker"].getContainer = jest.fn().mockReturnValue({ + inspect: jest.fn().mockRejectedValue(dockerErr), + }); + + const result = await provider.handle(makeMonitor()); + + expect(result.code).toBe(NETWORK_ERROR); + }); + + it("handles inspect error with standard Error (not Docker error)", async () => { + const { provider } = createProvider(); + provider["docker"].getContainer = jest.fn().mockReturnValue({ + inspect: jest.fn().mockRejectedValue(new Error("ECONNREFUSED")), + }); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + expect(result.message).toBe("ECONNREFUSED"); + }); + + it("handles inspect error with non-Error non-Docker thrown value", async () => { + const { provider } = createProvider(); + provider["docker"].getContainer = jest.fn().mockReturnValue({ + inspect: jest.fn().mockRejectedValue("string error"), + }); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + expect(result.message).toBe("Failed to fetch Docker container information"); + }); + }); + + // ── Outer catch ────────────────────────────────────────────────────── + + describe("outer error handling", () => { + it("throws AppError when containerInput is missing", async () => { + const { provider } = createProvider(); + + await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("Container name or ID is required for Docker monitor"); + }); + + it("throws AppError when listContainers throws", async () => { + const { provider } = createProvider(); + provider["docker"].listContainers = jest.fn().mockRejectedValue(new Error("Docker daemon unavailable")); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Docker daemon unavailable"); + }); + + it("throws AppError with default message for non-Error thrown values", async () => { + const { provider } = createProvider(); + provider["docker"].listContainers = jest.fn().mockRejectedValue(null); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing Docker request"); + }); + }); +}); diff --git a/server/test/unit/providers/network/gameProvider.test.ts b/server/test/unit/providers/network/gameProvider.test.ts new file mode 100644 index 000000000..d34e99621 --- /dev/null +++ b/server/test/unit/providers/network/gameProvider.test.ts @@ -0,0 +1,191 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { GameProvider } from "../../../../src/service/infrastructure/network/GameProvider.ts"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import { createMockLogger } from "../../../helpers/createMockLogger.ts"; +import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "game", + url: "play.example.com", + port: 25565, + gameId: "minecraft", + ...overrides, + }) as Monitor; + +const createMockGameDig = (state?: Record | null) => ({ + query: jest.fn().mockResolvedValue( + state ?? { + name: "My Server", + map: "world", + players: [], + ping: 45, + } + ), +}); + +const createProvider = (gameDig?: any) => { + const logger = createMockLogger(); + const dig = gameDig ?? createMockGameDig(); + const provider = new GameProvider(logger as any, dig as any); + return { provider, logger, gameDig: dig }; +}; + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("GameProvider", { + create: () => createProvider().provider, + supportedType: "game", + unsupportedType: "http", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("GameProvider", () => { + it("returns success with server state", async () => { + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + teamId: "team-1", + type: "game", + status: true, + code: 200, + message: "Success", + responseTime: 45, + }) + ); + }); + + it("queries with correct host, port, and gameId", async () => { + const { provider, gameDig } = createProvider(); + + await provider.handle(makeMonitor({ url: "https://play.example.com/path", port: 27015, gameId: "csgo" })); + + expect(gameDig.query).toHaveBeenCalledWith({ + type: "csgo", + host: "play.example.com", + port: 27015, + }); + }); + + it("strips protocol and path from url for host", async () => { + const { provider, gameDig } = createProvider(); + + await provider.handle(makeMonitor({ url: "http://game.server.com:8080/info" })); + + expect(gameDig.query).toHaveBeenCalledWith(expect.objectContaining({ host: "game.server.com" })); + }); + + it("defaults gameId to 'unknown' when not set", async () => { + const { provider, gameDig } = createProvider(); + + await provider.handle(makeMonitor({ gameId: undefined })); + + expect(gameDig.query).toHaveBeenCalledWith(expect.objectContaining({ type: "unknown" })); + }); + + it("defaults port to 0 when not set", async () => { + const { provider, gameDig } = createProvider(); + + await provider.handle(makeMonitor({ port: undefined })); + + expect(gameDig.query).toHaveBeenCalledWith(expect.objectContaining({ port: 0 })); + }); + + it("defaults responseTime to 0 when ping is undefined", async () => { + const { provider } = createProvider(createMockGameDig({ ping: undefined })); + + const result = await provider.handle(makeMonitor()); + + expect(result.responseTime).toBe(0); + }); + + it("returns failure when query resolves to undefined (caught error)", async () => { + const gameDig = { query: jest.fn().mockRejectedValue(new Error("Server offline")) }; + // GameProvider catches the error internally via .catch() + const { provider, logger } = createProvider(gameDig); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: NETWORK_ERROR, + message: "No response from game server", + responseTime: 0, + }) + ); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + message: "Server offline", + method: "handle", + }) + ); + }); + + it("logs non-Error thrown values from query", async () => { + const gameDig = { query: jest.fn().mockRejectedValue("string error") }; + const { provider, logger } = createProvider(gameDig); + + await provider.handle(makeMonitor()); + + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "string error" })); + }); + + it("uses empty string as host when url is undefined", async () => { + const { provider, gameDig } = createProvider(); + + await provider.handle(makeMonitor({ url: undefined })); + + expect(gameDig.query).toHaveBeenCalledWith(expect.objectContaining({ host: "" })); + }); + + it("throws AppError with default message when error stringifies to empty", async () => { + const gameDig = { + query: jest.fn().mockReturnValue({ + catch: () => { + throw new Error(""); + }, + }), + }; + const { provider } = createProvider(gameDig); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing game server check"); + }); + + it("throws AppError with default message for non-Error that stringifies to empty", async () => { + const gameDig = { + query: jest.fn().mockReturnValue({ + catch: () => { + throw ""; + }, + }), + }; + const { provider } = createProvider(gameDig); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing game server check"); + }); + + it("throws AppError with Error message in outer catch", async () => { + const gameDig = { + query: jest.fn().mockReturnValue({ + catch: () => { + throw new Error("unexpected"); + }, + }), + }; + const { provider } = createProvider(gameDig); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("unexpected"); + }); +}); diff --git a/server/test/unit/providers/network/grpcProvider.test.ts b/server/test/unit/providers/network/grpcProvider.test.ts new file mode 100644 index 000000000..efa2e7992 --- /dev/null +++ b/server/test/unit/providers/network/grpcProvider.test.ts @@ -0,0 +1,378 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { GrpcProvider } from "../../../../src/service/infrastructure/network/GrpcProvider.ts"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "grpc", + url: "grpc.example.com", + port: 50051, + grpcServiceName: "my.Service", + ignoreTlsErrors: false, + ...overrides, + }) as Monitor; + +const createMockClient = (behavior: "serving" | "not-serving" | "error" | "grpc-error" = "serving") => { + const close = jest.fn(); + const Check = jest.fn((_req: any, _opts: any, cb: Function) => { + if (behavior === "serving") { + cb(null, { status: "SERVING" }); + } else if (behavior === "not-serving") { + cb(null, { status: "NOT_SERVING" }); + } else if (behavior === "error") { + cb({ code: 14, details: "Connection refused", message: "unavailable" }); + } else if (behavior === "grpc-error") { + cb({ code: 2, message: "Unknown error" }); + } + }); + return { Check, close }; +}; + +const createMockGrpc = (client?: ReturnType) => { + const c = client ?? createMockClient(); + const Health = jest.fn().mockReturnValue(c); + + return { + grpc: { + loadPackageDefinition: jest.fn().mockReturnValue({ + grpc: { health: { v1: { Health } } }, + }), + credentials: { + createInsecure: jest.fn().mockReturnValue("insecure-creds"), + createSsl: jest.fn().mockReturnValue("ssl-creds"), + }, + }, + protoLoader: { + loadSync: jest.fn().mockReturnValue({}), + }, + Health, + client: c, + }; +}; + +const createProvider = (opts?: { behavior?: "serving" | "not-serving" | "error" | "grpc-error" }) => { + const client = createMockClient(opts?.behavior ?? "serving"); + const mocks = createMockGrpc(client); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + return { provider, ...mocks, client }; +}; + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("GrpcProvider", { + create: () => createProvider().provider, + supportedType: "grpc", + unsupportedType: "http", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("GrpcProvider", () => { + // ── Success paths ──────────────────────────────────────────────────── + + describe("success responses", () => { + it("returns healthy status for SERVING response", async () => { + const { provider } = createProvider({ behavior: "serving" }); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + teamId: "team-1", + type: "grpc", + status: true, + code: 200, + message: expect.stringContaining("SERVING"), + }) + ); + expect(result.payload).toEqual( + expect.objectContaining({ + grpcStatusCode: 0, + grpcStatusName: "OK", + serviceName: "my.Service", + servingStatus: "SERVING", + }) + ); + }); + + it("returns unhealthy status for NOT_SERVING response", async () => { + const { provider } = createProvider({ behavior: "not-serving" }); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + expect(result.code).toBe(5000); + expect(result.message).toContain("NOT_SERVING"); + }); + + it("defaults to UNKNOWN when response has no status", async () => { + const client = { Check: jest.fn((_r: any, _o: any, cb: Function) => cb(null, {})), close: jest.fn() }; + const mocks = createMockGrpc(client as any); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.payload).toEqual(expect.objectContaining({ servingStatus: "UNKNOWN" })); + }); + + it("defaults to UNKNOWN when response is undefined", async () => { + const client = { Check: jest.fn((_r: any, _o: any, cb: Function) => cb(null, undefined)), close: jest.fn() }; + const mocks = createMockGrpc(client as any); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.payload).toEqual(expect.objectContaining({ servingStatus: "UNKNOWN" })); + }); + }); + + // ── gRPC errors ────────────────────────────────────────────────────── + + describe("gRPC errors", () => { + it("returns failure with gRPC error details", async () => { + const { provider } = createProvider({ behavior: "error" }); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + expect(result.code).toBe(14); + expect(result.message).toBe("Connection refused"); + expect(result.payload).toEqual( + expect.objectContaining({ + grpcStatusCode: 14, + grpcStatusName: "UNAVAILABLE", + servingStatus: "UNKNOWN", + }) + ); + }); + + it("falls back to message when details is missing", async () => { + const client = { + Check: jest.fn((_r: any, _o: any, cb: Function) => cb({ code: 2, message: "Unknown error" })), + close: jest.fn(), + }; + const mocks = createMockGrpc(client as any); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.message).toBe("Unknown error"); + }); + + it("uses default message and code when error has no code/details/message", async () => { + const client = { + Check: jest.fn((_r: any, _o: any, cb: Function) => cb({})), + close: jest.fn(), + }; + const mocks = createMockGrpc(client as any); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.code).toBe(5000); + expect(result.message).toBe("gRPC error"); + expect(result.payload).toEqual(expect.objectContaining({ grpcStatusCode: -1, grpcStatusName: "UNKNOWN" })); + }); + + it("defaults grpcCode to 5000 when error has no grpcCode", async () => { + const client = { + Check: jest.fn((_r: any, _o: any, cb: Function) => { + const err = new Error("test") as any; + // no grpcPayload or grpcCode + cb(err); + }), + close: jest.fn(), + }; + const mocks = createMockGrpc(client as any); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.code).toBe(5000); + }); + + it("includes grpcPayload in error result", async () => { + const { provider } = createProvider({ behavior: "error" }); + + const result = await provider.handle(makeMonitor()); + + expect(result.payload).toEqual( + expect.objectContaining({ + grpcStatusCode: 14, + grpcStatusName: "UNAVAILABLE", + }) + ); + }); + + it("defaults code, message, and payload when error lacks gRPC properties", async () => { + // client.Check throws synchronously — timeRequest catches a plain object with no gRPC props + const client = { + Check: jest.fn(() => { + throw { unexpected: true }; + }), + close: jest.fn(), + }; + const mocks = createMockGrpc(client as any); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.code).toBe(5000); + expect(result.message).toBe("gRPC health check failed"); + expect(result.payload).toBeNull(); + }); + }); + + // ── Credentials ────────────────────────────────────────────────────── + + describe("credentials", () => { + it("uses insecure credentials by default", async () => { + const { provider, grpc } = createProvider(); + + await provider.handle(makeMonitor({ ignoreTlsErrors: false })); + + expect(grpc.credentials.createInsecure).toHaveBeenCalled(); + }); + + it("uses SSL credentials with checkServerIdentity override when ignoreTlsErrors is true", async () => { + const { provider, grpc } = createProvider(); + + await provider.handle(makeMonitor({ ignoreTlsErrors: true })); + + expect(grpc.credentials.createSsl).toHaveBeenCalledWith(null, null, null, { + checkServerIdentity: expect.any(Function), + }); + + // Verify checkServerIdentity returns undefined + const opts = (grpc.credentials.createSsl as jest.Mock).mock.calls[0][3]; + expect(opts.checkServerIdentity()).toBeUndefined(); + }); + }); + + // ── URL parsing ────────────────────────────────────────────────────── + + describe("URL and target construction", () => { + it("strips protocol from URL", async () => { + const { provider, Health } = createProvider(); + + await provider.handle(makeMonitor({ url: "https://grpc.example.com", port: 443 })); + + expect(Health).toHaveBeenCalledWith("grpc.example.com:443", expect.anything()); + }); + + it("uses grpcServiceName in Check request", async () => { + const { provider, client } = createProvider(); + + await provider.handle(makeMonitor({ grpcServiceName: "health.v1" })); + + expect(client.Check).toHaveBeenCalledWith( + { service: "health.v1" }, + expect.objectContaining({ deadline: expect.any(Date) }), + expect.any(Function) + ); + }); + + it("defaults grpcServiceName to empty string when not set", async () => { + const { provider, client } = createProvider(); + + await provider.handle(makeMonitor({ grpcServiceName: undefined })); + + expect(client.Check).toHaveBeenCalledWith({ service: "" }, expect.anything(), expect.any(Function)); + }); + }); + + // ── Validation ─────────────────────────────────────────────────────── + + describe("validation", () => { + it("throws AppError when url is missing", async () => { + const { provider } = createProvider(); + + await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("Monitor host is required"); + }); + + it("throws AppError when port is missing", async () => { + const { provider } = createProvider(); + + await expect(provider.handle(makeMonitor({ port: undefined }))).rejects.toThrow("Monitor port is required"); + }); + }); + + // ── Outer catch ────────────────────────────────────────────────────── + + describe("outer error handling", () => { + it("throws AppError when protoLoader.loadSync throws", async () => { + const mocks = createMockGrpc(); + mocks.protoLoader.loadSync = jest.fn().mockImplementation(() => { + throw new Error("Proto file not found"); + }); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Proto file not found"); + }); + + it("throws AppError with default message when Error has empty message", async () => { + const mocks = createMockGrpc(); + mocks.protoLoader.loadSync = jest.fn().mockImplementation(() => { + throw new Error(""); + }); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing gRPC health check"); + }); + + it("throws AppError with stringified message for non-Error thrown values", async () => { + const mocks = createMockGrpc(); + mocks.protoLoader.loadSync = jest.fn().mockImplementation(() => { + throw "proto load failed"; + }); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("proto load failed"); + }); + }); + + // ── getGrpcStatusName ──────────────────────────────────────────────── + + describe("gRPC status name mapping", () => { + const statusCodes: [number, string][] = [ + [0, "OK"], + [1, "CANCELLED"], + [2, "UNKNOWN"], + [3, "INVALID_ARGUMENT"], + [4, "DEADLINE_EXCEEDED"], + [5, "NOT_FOUND"], + [6, "ALREADY_EXISTS"], + [7, "PERMISSION_DENIED"], + [8, "RESOURCE_EXHAUSTED"], + [9, "FAILED_PRECONDITION"], + [10, "ABORTED"], + [11, "OUT_OF_RANGE"], + [12, "UNIMPLEMENTED"], + [13, "INTERNAL"], + [14, "UNAVAILABLE"], + [15, "DATA_LOSS"], + [16, "UNAUTHENTICATED"], + [99, "UNKNOWN"], + ]; + + it.each(statusCodes)("maps gRPC code %i to %s", async (code, expectedName) => { + const client = { + Check: jest.fn((_r: any, _o: any, cb: Function) => cb({ code, details: "test" })), + close: jest.fn(), + }; + const mocks = createMockGrpc(client as any); + const provider = new GrpcProvider(mocks.grpc as any, mocks.protoLoader as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.payload).toEqual(expect.objectContaining({ grpcStatusName: expectedName })); + }); + }); +}); diff --git a/server/test/unit/providers/network/hardwareProvider.test.ts b/server/test/unit/providers/network/hardwareProvider.test.ts new file mode 100644 index 000000000..2f51454a0 --- /dev/null +++ b/server/test/unit/providers/network/hardwareProvider.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { HardwareProvider } from "../../../../src/service/infrastructure/network/HardwareProvider.ts"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import type { HttpProvider } from "../../../../src/service/infrastructure/network/HttpProvider.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "hardware", + url: "https://capture.example.com", + ...overrides, + }) as Monitor; + +const createMockHttpProvider = () => + ({ + handle: jest.fn().mockResolvedValue({ + monitorId: "mon-1", + teamId: "team-1", + type: "hardware", + status: true, + code: 200, + message: "OK", + responseTime: 50, + payload: { data: { cpu: { usage_percent: 0.5 } } }, + }), + }) as unknown as jest.Mocked; + +const createProvider = () => { + const httpProvider = createMockHttpProvider(); + const provider = new HardwareProvider(httpProvider); + return { provider, httpProvider }; +}; + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("HardwareProvider", { + create: () => createProvider().provider, + supportedType: "hardware", + unsupportedType: "http", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("HardwareProvider", () => { + it("delegates to httpProvider.handle", async () => { + const { provider, httpProvider } = createProvider(); + const monitor = makeMonitor(); + + await provider.handle(monitor); + + expect(httpProvider.handle).toHaveBeenCalledWith(monitor); + }); + + it("returns the response from httpProvider", async () => { + const { provider } = createProvider(); + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + status: true, + code: 200, + }) + ); + }); + + it("throws AppError when url is missing", async () => { + const { provider } = createProvider(); + + await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for Hardware monitor"); + }); + + it("throws AppError when httpProvider.handle throws", async () => { + const { provider, httpProvider } = createProvider(); + (httpProvider.handle as jest.Mock).mockRejectedValue(new Error("connection failed")); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("connection failed"); + }); + + it("throws AppError with default message for non-Error thrown values", async () => { + const { provider, httpProvider } = createProvider(); + (httpProvider.handle as jest.Mock).mockRejectedValue("string error"); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing Hardware request"); + }); +}); diff --git a/server/test/unit/providers/network/httpProvider.test.ts b/server/test/unit/providers/network/httpProvider.test.ts new file mode 100644 index 000000000..1b814d7b2 --- /dev/null +++ b/server/test/unit/providers/network/httpProvider.test.ts @@ -0,0 +1,348 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Mocks ──────────────────────────────────────────────────────────────────── + +jest.unstable_mockModule("cacheable-lookup", () => ({ + default: jest.fn().mockImplementation(() => ({})), +})); + +const mockGot = jest.fn(); +// got instance returned by got.extend() +mockGot.mockImplementation(() => Promise.resolve()); +(mockGot as any).extend = jest.fn().mockReturnValue(mockGot); + +jest.unstable_mockModule("got", () => ({ + type: { Got: {} }, + HTTPError: class HTTPError extends Error { + response: any; + timings: any; + constructor(msg: string, response?: any, timings?: any) { + super(msg); + this.name = "HTTPError"; + this.response = response; + this.timings = timings; + } + }, + RequestError: class RequestError extends Error { + response: any; + timings: any; + constructor(msg: string, response?: any, timings?: any) { + super(msg); + this.name = "RequestError"; + this.response = response; + this.timings = timings; + } + }, +})); + +const { HttpProvider } = await import("../../../../src/service/infrastructure/network/HttpProvider.ts"); +const gotModule = await import("got"); +const { HTTPError, RequestError } = gotModule; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "http", + url: "https://example.com", + secret: undefined, + jsonPath: undefined, + ignoreTlsErrors: false, + useAdvancedMatching: false, + matchMethod: undefined, + expectedValue: undefined, + ...overrides, + }) as Monitor; + +const createMockMatcher = (result?: { ok: boolean; message: string; extracted?: unknown }) => ({ + validate: jest.fn().mockReturnValue(result ?? { ok: true, message: "Success" }), +}); + +const makeGotResponse = (overrides?: Record) => ({ + ok: true, + statusCode: 200, + statusMessage: "OK", + headers: { "content-type": "text/html" }, + body: "", + timings: { phases: { total: 100 } }, + ...overrides, +}); + +const createProvider = (matcher?: any) => { + const advancedMatcher = matcher ?? createMockMatcher(); + const provider = new HttpProvider(mockGot as any, advancedMatcher); + return { provider, advancedMatcher }; +}; + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("HttpProvider", { + create: () => { + mockGot.mockResolvedValue(makeGotResponse()); + return createProvider().provider; + }, + supportedType: "http", + unsupportedType: "ping", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("HttpProvider", () => { + // ── Success paths ──────────────────────────────────────────────────── + + describe("success responses", () => { + it("returns success for a standard HTML response", async () => { + mockGot.mockResolvedValue(makeGotResponse()); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + teamId: "team-1", + type: "http", + status: true, + code: 200, + message: "OK", + responseTime: 100, + }) + ); + }); + + it("parses JSON body when content-type is application/json", async () => { + mockGot.mockResolvedValue( + makeGotResponse({ + headers: { "content-type": "application/json" }, + body: '{"status":"ok"}', + }) + ); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result.payload).toEqual({ status: "ok" }); + }); + + it("returns raw body when JSON parsing fails", async () => { + mockGot.mockResolvedValue( + makeGotResponse({ + headers: { "content-type": "application/json" }, + body: "not-json", + }) + ); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result.payload).toBe("not-json"); + }); + + it("returns raw body for non-JSON content", async () => { + mockGot.mockResolvedValue(makeGotResponse({ body: "test" })); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result.payload).toBe("test"); + }); + + it("passes Authorization header when secret is set", async () => { + mockGot.mockResolvedValue(makeGotResponse()); + const { provider } = createProvider(); + + await provider.handle(makeMonitor({ secret: "my-token" })); + + expect(mockGot).toHaveBeenCalledWith( + "https://example.com", + expect.objectContaining({ + headers: { Authorization: "Bearer my-token" }, + }) + ); + }); + + it("passes undefined headers when no secret", async () => { + mockGot.mockResolvedValue(makeGotResponse()); + const { provider } = createProvider(); + + await provider.handle(makeMonitor({ secret: undefined })); + + expect(mockGot).toHaveBeenCalledWith( + "https://example.com", + expect.objectContaining({ + headers: undefined, + }) + ); + }); + + it("defaults responseTime to 0 when timings.phases.total is undefined", async () => { + mockGot.mockResolvedValue(makeGotResponse({ timings: { phases: { total: undefined } } })); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result.responseTime).toBe(0); + }); + + it("defaults statusMessage to 'OK' when undefined", async () => { + mockGot.mockResolvedValue(makeGotResponse({ statusMessage: undefined })); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result.message).toBe("OK"); + }); + + it("uses empty string for content-type when header is missing", async () => { + mockGot.mockResolvedValue(makeGotResponse({ headers: {} })); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + // Should treat as non-JSON + expect(result.payload).toBe(""); + }); + }); + + // ── jsonPath + non-JSON response ───────────────────────────────────── + + describe("jsonPath validation", () => { + it("returns failure when jsonPath is set but response is not JSON", async () => { + mockGot.mockResolvedValue(makeGotResponse({ headers: { "content-type": "text/html" } })); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ jsonPath: "status" })); + + expect(result.status).toBe(false); + expect(result.message).toBe("Response is not JSON"); + }); + + it("defaults responseTime to 0 in non-JSON jsonPath response when total is undefined", async () => { + mockGot.mockResolvedValue( + makeGotResponse({ + headers: { "content-type": "text/html" }, + timings: { phases: { total: undefined } }, + }) + ); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor({ jsonPath: "status" })); + + expect(result.responseTime).toBe(0); + }); + }); + + // ── AdvancedMatcher integration ────────────────────────────────────── + + describe("advanced matching", () => { + it("uses matcher result for status and message", async () => { + mockGot.mockResolvedValue(makeGotResponse()); + const matcher = createMockMatcher({ ok: false, message: "Mismatch", extracted: "value" }); + const { provider } = createProvider(matcher); + + const result = await provider.handle(makeMonitor({ useAdvancedMatching: true })); + + expect(result.status).toBe(false); + expect(result.message).toBe("Mismatch"); + expect(result.extracted).toBe("value"); + }); + + it("sets status to false when response.ok is false even if matcher passes", async () => { + mockGot.mockResolvedValue(makeGotResponse({ ok: false, statusCode: 301 })); + const matcher = createMockMatcher({ ok: true, message: "Success" }); + const { provider } = createProvider(matcher); + + const result = await provider.handle(makeMonitor()); + + // status = response.ok && matchResult.ok + expect(result.status).toBe(false); + }); + }); + + // ── Error handling ─────────────────────────────────────────────────── + + describe("error handling", () => { + it("handles HTTPError with response and timings", async () => { + const err = new HTTPError("Not Found"); + (err as any).response = { statusCode: 404 }; + (err as any).timings = { phases: { total: 50 } }; + mockGot.mockRejectedValue(err); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: 404, + message: "Not Found", + responseTime: 50, + }) + ); + }); + + it("handles RequestError with response and timings", async () => { + const err = new RequestError("ECONNREFUSED"); + (err as any).response = { statusCode: undefined }; + (err as any).timings = { phases: { total: undefined } }; + mockGot.mockRejectedValue(err); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result.code).toBe(NETWORK_ERROR); + expect(result.responseTime).toBe(0); + }); + + it("handles HTTPError without response (defaults to NETWORK_ERROR)", async () => { + const err = new HTTPError("Timeout"); + (err as any).response = undefined; + (err as any).timings = undefined; + mockGot.mockRejectedValue(err); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result.code).toBe(NETWORK_ERROR); + expect(result.responseTime).toBe(0); + }); + + it("handles generic Error (non-HTTPError/RequestError)", async () => { + mockGot.mockRejectedValue(new Error("DNS lookup failed")); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: NETWORK_ERROR, + message: "DNS lookup failed", + responseTime: 0, + }) + ); + }); + + it("handles non-Error thrown values", async () => { + mockGot.mockRejectedValue("string error"); + const { provider } = createProvider(); + + const result = await provider.handle(makeMonitor()); + + expect(result.message).toBe("string error"); + expect(result.code).toBe(NETWORK_ERROR); + }); + + it("throws when url is missing", async () => { + const { provider } = createProvider(); + + await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for HTTP monitor"); + }); + }); +}); diff --git a/server/test/unit/providers/network/pageSpeedProvider.test.ts b/server/test/unit/providers/network/pageSpeedProvider.test.ts new file mode 100644 index 000000000..c461b0ad2 --- /dev/null +++ b/server/test/unit/providers/network/pageSpeedProvider.test.ts @@ -0,0 +1,165 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { PageSpeedProvider } from "../../../../src/service/infrastructure/network/PageSpeedProvider.ts"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import { createMockLogger } from "../../../helpers/createMockLogger.ts"; +import type { HttpProvider } from "../../../../src/service/infrastructure/network/HttpProvider.ts"; +import type { ISettingsService } from "../../../../src/service/system/settingsService.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "pagespeed", + url: "https://example.com", + ...overrides, + }) as Monitor; + +const createMockHttpProvider = () => + ({ + handle: jest.fn().mockResolvedValue({ + monitorId: "mon-1", + teamId: "team-1", + type: "pagespeed", + status: true, + code: 200, + message: "OK", + responseTime: 2000, + payload: { lighthouseResult: {} }, + }), + }) as unknown as jest.Mocked; + +const createMockSettingsService = (apiKey?: string) => + ({ + getDBSettings: jest.fn().mockResolvedValue({ pagespeedApiKey: apiKey }), + }) as unknown as jest.Mocked; + +const createProvider = (opts?: { apiKey?: string }) => { + const logger = createMockLogger(); + const httpProvider = createMockHttpProvider(); + const settingsService = createMockSettingsService(opts?.apiKey); + const provider = new PageSpeedProvider(httpProvider, settingsService, logger as any); + return { provider, httpProvider, settingsService, logger }; +}; + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("PageSpeedProvider", { + create: () => createProvider({ apiKey: "test-key" }).provider, + supportedType: "pagespeed", + unsupportedType: "http", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("PageSpeedProvider", () => { + it("delegates to httpProvider with PageSpeed API URL", async () => { + const { provider, httpProvider } = createProvider({ apiKey: "my-key" }); + + await provider.handle(makeMonitor({ url: "https://example.com" })); + + expect(httpProvider.handle).toHaveBeenCalledWith( + expect.objectContaining({ + url: expect.stringContaining("pagespeedonline.googleapis.com"), + }) + ); + }); + + it("includes API key in URL when available", async () => { + const { provider, httpProvider } = createProvider({ apiKey: "my-key" }); + + await provider.handle(makeMonitor()); + + const calledUrl = (httpProvider.handle as jest.Mock).mock.calls[0][0].url; + expect(calledUrl).toContain("key=my-key"); + }); + + it("omits API key and logs warning when not configured", async () => { + const { provider, httpProvider, logger } = createProvider({ apiKey: undefined }); + + await provider.handle(makeMonitor()); + + const calledUrl = (httpProvider.handle as jest.Mock).mock.calls[0][0].url; + expect(calledUrl).not.toContain("key="); + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("API key not found"), + service: "PageSpeedProvider", + }) + ); + }); + + it("encodes the monitor URL in the PageSpeed API URL", async () => { + const { provider, httpProvider } = createProvider({ apiKey: "key" }); + + await provider.handle(makeMonitor({ url: "https://example.com/path?q=1" })); + + const calledUrl = (httpProvider.handle as jest.Mock).mock.calls[0][0].url; + expect(calledUrl).toContain(encodeURIComponent("https://example.com/path?q=1")); + }); + + it("includes all PageSpeed categories in the URL", async () => { + const { provider, httpProvider } = createProvider({ apiKey: "key" }); + + await provider.handle(makeMonitor()); + + const calledUrl = (httpProvider.handle as jest.Mock).mock.calls[0][0].url; + expect(calledUrl).toContain("category=seo"); + expect(calledUrl).toContain("category=accessibility"); + expect(calledUrl).toContain("category=best-practices"); + expect(calledUrl).toContain("category=performance"); + }); + + it("passes all monitor properties through to httpProvider", async () => { + const { provider, httpProvider } = createProvider({ apiKey: "key" }); + const monitor = makeMonitor({ id: "mon-5", teamId: "team-9" }); + + await provider.handle(monitor); + + expect(httpProvider.handle).toHaveBeenCalledWith(expect.objectContaining({ id: "mon-5", teamId: "team-9" })); + }); + + it("throws AppError when url is missing", async () => { + const { provider } = createProvider(); + + await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for PageSpeed monitor"); + }); + + it("throws AppError when httpProvider.handle throws", async () => { + const { provider, httpProvider } = createProvider({ apiKey: "key" }); + (httpProvider.handle as jest.Mock).mockRejectedValue(new Error("timeout")); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("timeout"); + }); + + it("throws AppError with default message for non-Error thrown values", async () => { + const { provider, httpProvider } = createProvider({ apiKey: "key" }); + (httpProvider.handle as jest.Mock).mockRejectedValue("string error"); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Error performing PageSpeed request"); + }); + + it("handles null apiKey from DB settings", async () => { + const { provider, logger } = createProvider({ apiKey: null as any }); + + await provider.handle(makeMonitor()); + + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("API key not found") })); + }); + + it("handles null dbSettings response", async () => { + const logger = createMockLogger(); + const httpProvider = createMockHttpProvider(); + const settingsService = { + getDBSettings: jest.fn().mockResolvedValue(null), + } as unknown as jest.Mocked; + const provider = new PageSpeedProvider(httpProvider, settingsService, logger as any); + + await provider.handle(makeMonitor()); + + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: expect.stringContaining("API key not found") })); + }); +}); diff --git a/server/test/unit/providers/network/pingProvider.test.ts b/server/test/unit/providers/network/pingProvider.test.ts new file mode 100644 index 000000000..2f38eef00 --- /dev/null +++ b/server/test/unit/providers/network/pingProvider.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { PingProvider } from "../../../../src/service/infrastructure/network/PingProvider.ts"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "ping", + url: "example.com", + ...overrides, + }) as Monitor; + +const createMockPing = (result?: Partial<{ alive: boolean; time: number | string }>) => ({ + promise: { + probe: jest.fn().mockResolvedValue({ + alive: true, + time: 25, + ...result, + }), + }, +}); + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("PingProvider", { + create: () => new PingProvider(createMockPing() as any), + supportedType: "ping", + unsupportedType: "http", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("PingProvider", () => { + it("returns success when host is alive", async () => { + const provider = new PingProvider(createMockPing({ alive: true, time: 30 }) as any); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + teamId: "team-1", + type: "ping", + status: true, + code: 200, + message: "Success", + responseTime: 30, + }) + ); + }); + + it("returns failure when host is not alive", async () => { + const provider = new PingProvider(createMockPing({ alive: false, time: 0 }) as any); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: 5000, + message: "Ping failed", + }) + ); + }); + + it("parses string time values", async () => { + const provider = new PingProvider(createMockPing({ alive: true, time: "42.5" }) as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.responseTime).toBe(42.5); + }); + + it("defaults responseTime to 0 for non-numeric time", async () => { + const provider = new PingProvider(createMockPing({ alive: true, time: "unknown" }) as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.responseTime).toBe(0); + }); + + it("defaults status to false when alive is undefined", async () => { + const provider = new PingProvider(createMockPing({ alive: undefined as any }) as any); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + }); + + it("sanitizes URL by stripping protocol and path", async () => { + const mockPing = createMockPing(); + const provider = new PingProvider(mockPing as any); + + await provider.handle(makeMonitor({ url: "https://example.com/path?query=1" })); + + expect(mockPing.promise.probe).toHaveBeenCalledWith("example.com"); + }); + + it("sanitizes URL by stripping port", async () => { + const mockPing = createMockPing(); + const provider = new PingProvider(mockPing as any); + + await provider.handle(makeMonitor({ url: "example.com:8080" })); + + expect(mockPing.promise.probe).toHaveBeenCalledWith("example.com"); + }); + + it("throws AppError when url is missing", async () => { + const provider = new PingProvider(createMockPing() as any); + + await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for ping monitor"); + }); + + it("throws AppError when ping.probe rejects", async () => { + const mockPing = { promise: { probe: jest.fn().mockRejectedValue(new Error("Host unreachable")) } }; + const provider = new PingProvider(mockPing as any); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("Host unreachable"); + }); + + it("throws AppError when probe returns no response", async () => { + const mockPing = { promise: { probe: jest.fn().mockResolvedValue(null) } }; + const provider = new PingProvider(mockPing as any); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("No response from ping"); + }); + + it("throws AppError with stringified message for non-Error throws", async () => { + const mockPing = { promise: { probe: jest.fn().mockRejectedValue(42) } }; + const provider = new PingProvider(mockPing as any); + + await expect(provider.handle(makeMonitor())).rejects.toThrow("42"); + }); +}); diff --git a/server/test/unit/providers/network/portProvider.test.ts b/server/test/unit/providers/network/portProvider.test.ts new file mode 100644 index 000000000..cbf64af16 --- /dev/null +++ b/server/test/unit/providers/network/portProvider.test.ts @@ -0,0 +1,270 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { PortProvider } from "../../../../src/service/infrastructure/network/PortProvider.ts"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "port", + url: "example.com", + port: 443, + ...overrides, + }) as Monitor; + +const createMockNet = (behavior: "connect" | "timeout" | "error" = "connect") => { + const socket = { + setTimeout: jest.fn(), + on: jest.fn(), + end: jest.fn(), + destroy: jest.fn(), + }; + + const createConnection = jest.fn((opts: any, onConnect: () => void) => { + if (behavior === "connect") { + // Simulate immediate connection + process.nextTick(onConnect); + } else if (behavior === "timeout") { + process.nextTick(() => { + const timeoutCb = socket.on.mock.calls.find((c: any) => c[0] === "timeout"); + // Simulate via setTimeout handler + const setTimeoutCb = socket.setTimeout.mock.calls[0]; + if (setTimeoutCb) { + // The timeout event handler is registered via socket.on("timeout", ...) + } + }); + } else if (behavior === "error") { + process.nextTick(() => { + const errorCb = socket.on.mock.calls.find((c: any) => c[0] === "error"); + if (errorCb) { + errorCb[1](new Error("ECONNREFUSED")); + } + }); + } + return socket; + }); + + return { createConnection, __socket: socket } as any; +}; + +const createSuccessNet = () => { + const socket = { + setTimeout: jest.fn(), + on: jest.fn(), + end: jest.fn(), + destroy: jest.fn(), + }; + return { + createConnection: jest.fn((_opts: any, onConnect: () => void) => { + process.nextTick(onConnect); + return socket; + }), + } as any; +}; + +const createErrorNet = (error: Error = new Error("ECONNREFUSED")) => { + const socket = { + setTimeout: jest.fn(), + on: jest.fn((event: string, cb: Function) => { + if (event === "error") { + process.nextTick(() => cb(error)); + } + }), + end: jest.fn(), + destroy: jest.fn(), + }; + return { + createConnection: jest.fn(() => socket), + } as any; +}; + +const createTimeoutNet = () => { + const socket = { + setTimeout: jest.fn(), + on: jest.fn((event: string, cb: Function) => { + if (event === "timeout") { + process.nextTick(() => cb()); + } + }), + end: jest.fn(), + destroy: jest.fn(), + }; + return { + createConnection: jest.fn(() => socket), + } as any; +}; + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("PortProvider", { + create: () => new PortProvider(createSuccessNet()), + supportedType: "port", + unsupportedType: "http", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("PortProvider", () => { + it("returns success when port connection succeeds", async () => { + const provider = new PortProvider(createSuccessNet()); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + teamId: "team-1", + type: "port", + status: true, + code: 200, + message: "Port check successful", + payload: { success: true }, + }) + ); + expect(result.responseTime).toBeGreaterThanOrEqual(0); + }); + + it("returns failure when connection errors", async () => { + const provider = new PortProvider(createErrorNet()); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: NETWORK_ERROR, + message: "ECONNREFUSED", + payload: { success: false }, + }) + ); + }); + + it("returns failure with generic message for non-Error connection error", async () => { + const net = createErrorNet(); + // Override the on handler to throw a non-Error + const socket = { + setTimeout: jest.fn(), + on: jest.fn((event: string, cb: Function) => { + if (event === "error") { + process.nextTick(() => cb("string error")); + } + }), + end: jest.fn(), + destroy: jest.fn(), + }; + net.createConnection = jest.fn(() => socket); + + const provider = new PortProvider(net); + const result = await provider.handle(makeMonitor()); + + expect(result.message).toBe("Port check failed"); + }); + + it("returns failure when connection times out", async () => { + const provider = new PortProvider(createTimeoutNet()); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: NETWORK_ERROR, + message: "Connection timeout", + }) + ); + }); + + it("throws AppError when url is missing", async () => { + const provider = new PortProvider(createSuccessNet()); + + await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL and port are required for port monitoring"); + }); + + it("throws AppError when port is missing", async () => { + const provider = new PortProvider(createSuccessNet()); + + await expect(provider.handle(makeMonitor({ port: undefined }))).rejects.toThrow("URL and port are required for port monitoring"); + }); + + it("returns failure with generic message when timeRequest catches non-Error", async () => { + const socket = { + setTimeout: jest.fn(), + on: jest.fn((event: string, cb: Function) => { + if (event === "error") { + process.nextTick(() => cb({ notAnError: true })); + } + }), + end: jest.fn(), + destroy: jest.fn(), + }; + const net = { createConnection: jest.fn(() => socket) } as any; + const provider = new PortProvider(net); + + const result = await provider.handle(makeMonitor()); + + expect(result.status).toBe(false); + expect(result.message).toBe("Port check failed"); + }); + + it("throws AppError wrapping the original Error message", async () => { + const provider = new PortProvider(createSuccessNet()); + + try { + await provider.handle(makeMonitor({ url: "" })); + expect.unreachable("should have thrown"); + } catch (err: any) { + expect(err.service).toBe("PortProvider"); + expect(err.method).toBe("handle"); + } + }); + + it("throws AppError when an unexpected error occurs in outer try", async () => { + const provider = new PortProvider(createSuccessNet()); + const monitor = makeMonitor(); + let calls = 0; + Object.defineProperty(monitor, "url", { + get() { + calls++; + if (calls === 1) throw new Error("getter exploded"); + return "example.com"; + }, + }); + + await expect(provider.handle(monitor)).rejects.toThrow("getter exploded"); + }); + + it("throws AppError with fallback message when Error has empty message", async () => { + const provider = new PortProvider(createSuccessNet()); + const monitor = makeMonitor(); + let calls = 0; + Object.defineProperty(monitor, "url", { + get() { + calls++; + if (calls === 1) throw new Error(""); + return "example.com"; + }, + }); + + await expect(provider.handle(monitor)).rejects.toThrow("Error performing port check"); + }); + + it("throws AppError with stringified message for non-Error thrown values", async () => { + const provider = new PortProvider(createSuccessNet()); + const monitor = makeMonitor(); + let calls = 0; + Object.defineProperty(monitor, "url", { + get() { + calls++; + if (calls === 1) throw 42; + return "example.com"; + }, + }); + + await expect(provider.handle(monitor)).rejects.toThrow("42"); + }); +}); diff --git a/server/test/unit/providers/network/utils.test.ts b/server/test/unit/providers/network/utils.test.ts new file mode 100644 index 000000000..56d8e7f9c --- /dev/null +++ b/server/test/unit/providers/network/utils.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "@jest/globals"; +import { timeRequest, NETWORK_ERROR, PING_ERROR } from "../../../../src/service/infrastructure/network/utils.ts"; + +describe("network utils", () => { + describe("timeRequest", () => { + it("returns response and responseTime on success", async () => { + const result = await timeRequest(async () => "hello"); + + expect(result.response).toBe("hello"); + expect(result.responseTime).toBeGreaterThanOrEqual(0); + expect(result.error).toBeNull(); + }); + + it("returns error and responseTime on failure", async () => { + const err = new Error("boom"); + const result = await timeRequest(async () => { + throw err; + }); + + expect(result.response).toBeNull(); + expect(result.responseTime).toBeGreaterThanOrEqual(0); + expect(result.error).toBe(err); + }); + + it("measures elapsed time", async () => { + const result = await timeRequest(async () => { + await new Promise((r) => setTimeout(r, 50)); + return "done"; + }); + + expect(result.responseTime).toBeGreaterThanOrEqual(40); + }); + }); + + describe("constants", () => { + it("NETWORK_ERROR is 5000", () => { + expect(NETWORK_ERROR).toBe(5000); + }); + + it("PING_ERROR is 5001", () => { + expect(PING_ERROR).toBe(5001); + }); + }); +}); diff --git a/server/test/unit/providers/network/webSocketProvider.test.ts b/server/test/unit/providers/network/webSocketProvider.test.ts new file mode 100644 index 000000000..73e4416a1 --- /dev/null +++ b/server/test/unit/providers/network/webSocketProvider.test.ts @@ -0,0 +1,194 @@ +import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals"; +import { WebSocketProvider } from "../../../../src/service/infrastructure/network/WebSocketProvider.ts"; +import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; +import { NETWORK_ERROR } from "../../../../src/service/infrastructure/network/utils.ts"; +import type { Monitor } from "../../../../src/types/index.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeMonitor = (overrides?: Partial): Monitor => + ({ + id: "mon-1", + teamId: "team-1", + type: "websocket", + url: "wss://example.com/ws", + ignoreTlsErrors: false, + ...overrides, + }) as Monitor; + +const createMockWS = (behavior: "open" | "error" = "open") => { + return jest.fn().mockImplementation((_url: string, _opts: any) => { + const handlers: Record = {}; + const ws = { + on: jest.fn((event: string, cb: Function) => { + handlers[event] = cb; + if (behavior === "open" && event === "open") { + process.nextTick(() => cb()); + } + if (behavior === "error" && event === "error") { + process.nextTick(() => cb(new Error("Connection failed"))); + } + }), + close: jest.fn(), + }; + return ws; + }) as any; +}; + +// ── Contract ───────────────────────────────────────────────────────────────── + +testStatusProviderContract("WebSocketProvider", { + create: () => new WebSocketProvider(createMockWS("open")), + supportedType: "websocket", + unsupportedType: "http", + makeMonitor: () => makeMonitor(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("WebSocketProvider", () => { + it("returns success when connection opens", async () => { + const provider = new WebSocketProvider(createMockWS("open")); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + monitorId: "mon-1", + teamId: "team-1", + type: "websocket", + status: true, + code: 200, + message: "WebSocket check successful", + payload: { connected: true }, + }) + ); + }); + + it("returns failure when connection errors", async () => { + const provider = new WebSocketProvider(createMockWS("error")); + + const result = await provider.handle(makeMonitor()); + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: NETWORK_ERROR, + message: "Connection failed", + payload: { connected: false }, + }) + ); + }); + + it("returns failure with generic message for non-Error connection error", async () => { + const WS = jest.fn().mockImplementation(() => ({ + on: jest.fn((event: string, cb: Function) => { + if (event === "error") { + process.nextTick(() => cb("string error")); + } + }), + close: jest.fn(), + })) as any; + + const provider = new WebSocketProvider(WS); + const result = await provider.handle(makeMonitor()); + + expect(result.message).toBe("WebSocket check failed"); + }); + + it("passes rejectUnauthorized: false when ignoreTlsErrors is true", async () => { + const WS = jest.fn().mockImplementation((_url: string, opts: any) => { + expect(opts.rejectUnauthorized).toBe(false); + return { + on: jest.fn((event: string, cb: Function) => { + if (event === "open") process.nextTick(() => cb()); + }), + close: jest.fn(), + }; + }) as any; + + const provider = new WebSocketProvider(WS); + await provider.handle(makeMonitor({ ignoreTlsErrors: true })); + + expect(WS).toHaveBeenCalled(); + }); + + it("throws AppError when url is missing", async () => { + const provider = new WebSocketProvider(createMockWS("open")); + + await expect(provider.handle(makeMonitor({ url: "" }))).rejects.toThrow("URL is required for WebSocket monitoring"); + }); + + it("throws AppError when an unexpected error occurs in outer try", async () => { + const provider = new WebSocketProvider(createMockWS("open")); + const monitor = makeMonitor(); + let calls = 0; + Object.defineProperty(monitor, "url", { + get() { + calls++; + if (calls === 1) throw new Error("getter failed"); + return "wss://example.com"; + }, + }); + + await expect(provider.handle(monitor)).rejects.toThrow("getter failed"); + }); + + it("throws AppError with fallback message when Error has empty message", async () => { + const provider = new WebSocketProvider(createMockWS("open")); + const monitor = makeMonitor(); + let calls = 0; + Object.defineProperty(monitor, "url", { + get() { + calls++; + if (calls === 1) throw new Error(""); + return "wss://example.com"; + }, + }); + + await expect(provider.handle(monitor)).rejects.toThrow("Error performing WebSocket check"); + }); + + it("throws AppError with stringified message for non-Error thrown values", async () => { + const provider = new WebSocketProvider(createMockWS("open")); + const monitor = makeMonitor(); + let calls = 0; + Object.defineProperty(monitor, "url", { + get() { + calls++; + if (calls === 1) throw 99; + return "wss://example.com"; + }, + }); + + await expect(provider.handle(monitor)).rejects.toThrow("99"); + }); + + describe("connection timeout", () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + it("returns failure on connection timeout", async () => { + // WS that never fires open or error + const WS = jest.fn().mockImplementation(() => ({ + on: jest.fn(), + close: jest.fn(), + })) as any; + + const provider = new WebSocketProvider(WS); + const promise = provider.handle(makeMonitor()); + + await jest.advanceTimersByTimeAsync(10000); + + const result = await promise; + + expect(result).toEqual( + expect.objectContaining({ + status: false, + code: NETWORK_ERROR, + message: "WebSocket connection timeout", + }) + ); + }); + }); +}); diff --git a/server/test/unit/providers/notifications/discordProvider.test.ts b/server/test/unit/providers/notifications/discordProvider.test.ts new file mode 100644 index 000000000..28c30ed22 --- /dev/null +++ b/server/test/unit/providers/notifications/discordProvider.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import { createMockLogger } from "../../../helpers/createMockLogger.ts"; +import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts"; +import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts"; + +const mockGotPost = jest.fn().mockResolvedValue({}); +jest.unstable_mockModule("got", () => ({ default: { post: mockGotPost } })); + +const { DiscordProvider } = await import("../../../../src/service/infrastructure/notificationProviders/discord.ts"); + +const createProvider = () => { + const logger = createMockLogger(); + const provider = new DiscordProvider(logger as any); + return { provider, logger }; +}; + +testNotificationProviderContract("DiscordProvider", { + create: () => { + mockGotPost.mockResolvedValue({}); + return createProvider().provider; + }, + makeNotification: () => makeNotification(), +}); + +describe("DiscordProvider", () => { + beforeEach(() => mockGotPost.mockReset().mockResolvedValue({})); + + describe("sendTestAlert", () => { + it("sends test message and returns true", async () => { + const { provider } = createProvider(); + const result = await provider.sendTestAlert(makeNotification()); + expect(result).toBe(true); + expect(mockGotPost).toHaveBeenCalledWith( + "https://hooks.example.com/webhook", + expect.objectContaining({ json: expect.objectContaining({ content: expect.any(String) }) }) + ); + }); + + it("returns false when address is missing", async () => { + const { provider } = createProvider(); + expect(await provider.sendTestAlert(makeNotification({ address: "" }))).toBe(false); + }); + + it("returns false and logs on error", async () => { + mockGotPost.mockRejectedValue(new Error("network")); + const { provider, logger } = createProvider(); + expect(await provider.sendTestAlert(makeNotification())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendTestAlert" })); + }); + + it("handles undefined thrown values", async () => { + mockGotPost.mockRejectedValue(undefined); + const { provider, logger } = createProvider(); + expect(await provider.sendTestAlert(makeNotification())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined })); + }); + }); + + describe("sendMessage", () => { + it("sends embed and returns true", async () => { + const { provider } = createProvider(); + const result = await provider.sendMessage(makeNotification() as any, makeMessage()); + expect(result).toBe(true); + expect(mockGotPost).toHaveBeenCalledWith( + "https://hooks.example.com/webhook", + expect.objectContaining({ json: expect.objectContaining({ embeds: expect.any(Array) }) }) + ); + }); + + it("returns false when address is missing", async () => { + const { provider } = createProvider(); + expect(await provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false); + }); + + it("returns false and logs on error", async () => { + mockGotPost.mockRejectedValue(new Error("fail")); + const { provider, logger } = createProvider(); + expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ method: "sendMessage" })); + }); + + it("handles undefined thrown values in sendMessage", async () => { + mockGotPost.mockRejectedValue(undefined); + const { provider, logger } = createProvider(); + expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined })); + }); + + it("includes threshold breaches in embed fields", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds()); + const payload = mockGotPost.mock.calls[0][1].json; + const fields = payload.embeds[0].fields; + expect(fields.some((f: any) => f.name === "Threshold Breaches")).toBe(true); + }); + + it("includes details in embed fields", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessage()); + const fields = mockGotPost.mock.calls[0][1].json.embeds[0].fields; + expect(fields.some((f: any) => f.name === "Details")).toBe(true); + }); + + it("maps all severity levels to colors", async () => { + const { provider } = createProvider(); + for (const severity of ["critical", "warning", "success", "info"] as const) { + mockGotPost.mockClear(); + await provider.sendMessage(makeNotification() as any, makeMessage({ severity })); + const color = mockGotPost.mock.calls[0][1].json.embeds[0].color; + expect(color).toBeGreaterThan(0); + } + }); + + it("uses info color for unknown severity", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessage({ severity: "unknown" as any })); + const color = mockGotPost.mock.calls[0][1].json.embeds[0].color; + expect(color).toBe(0x3b82f6); + }); + + it("omits threshold fields when not present", async () => { + const { provider } = createProvider(); + const msg = makeMessage(); + msg.content.thresholds = undefined; + msg.content.details = undefined; + await provider.sendMessage(makeNotification() as any, msg); + const fields = mockGotPost.mock.calls[0][1].json.embeds[0].fields; + expect(fields.some((f: any) => f.name === "Threshold Breaches")).toBe(false); + expect(fields.some((f: any) => f.name === "Details")).toBe(false); + }); + }); +}); diff --git a/server/test/unit/providers/notifications/emailProvider.test.ts b/server/test/unit/providers/notifications/emailProvider.test.ts new file mode 100644 index 000000000..e0dc8b676 --- /dev/null +++ b/server/test/unit/providers/notifications/emailProvider.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it, jest, beforeEach } from "@jest/globals"; +import { EmailProvider } from "../../../../src/service/infrastructure/notificationProviders/email.ts"; +import { createMockLogger } from "../../../helpers/createMockLogger.ts"; +import { makeNotification, makeMessage } from "../../../helpers/notificationMessage.ts"; +import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts"; +import type { IEmailService } from "../../../../src/service/infrastructure/emailService.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const createMockEmailService = () => + ({ + buildEmail: jest.fn().mockResolvedValue("email"), + sendEmail: jest.fn().mockResolvedValue("msg-123"), + }) as unknown as jest.Mocked; + +const createProvider = () => { + const logger = createMockLogger(); + const emailService = createMockEmailService(); + const provider = new EmailProvider(emailService, logger as any); + return { provider, logger, emailService }; +}; + +testNotificationProviderContract("EmailProvider", { + create: () => createProvider().provider, + makeNotification: () => makeNotification(), +}); + +// ── Tests ──────────────────────────────────────────────────────────────────── + +describe("EmailProvider", () => { + describe("sendTestAlert", () => { + it("builds test email and sends it", async () => { + const { provider, emailService } = createProvider(); + const result = await provider.sendTestAlert(makeNotification()); + expect(result).toBe(true); + expect(emailService.buildEmail).toHaveBeenCalledWith("testEmailTemplate", { testName: "Monitoring System" }); + expect(emailService.sendEmail).toHaveBeenCalledWith("https://hooks.example.com/webhook", "Test notification", "email"); + }); + + it("returns false when address is missing", async () => { + const { provider, logger } = createProvider(); + expect(await provider.sendTestAlert(makeNotification({ address: "" }))).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Missing address" })); + }); + + it("returns false when buildEmail returns undefined", async () => { + const { provider, emailService, logger } = createProvider(); + (emailService.buildEmail as jest.Mock).mockResolvedValue(undefined); + expect(await provider.sendTestAlert(makeNotification())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Failed to build test email content" })); + }); + + it("returns false when sendEmail returns falsy", async () => { + const { provider, emailService, logger } = createProvider(); + (emailService.sendEmail as jest.Mock).mockResolvedValue(false); + expect(await provider.sendTestAlert(makeNotification())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Email test alert failed" })); + }); + }); + + describe("sendMessage", () => { + it("builds email from message and sends it", async () => { + const { provider, emailService } = createProvider(); + const result = await provider.sendMessage(makeNotification() as any, makeMessage()); + expect(result).toBe(true); + expect(emailService.buildEmail).toHaveBeenCalledWith( + "unifiedNotificationTemplate", + expect.objectContaining({ title: "Monitor Down: Test Monitor" }) + ); + expect(emailService.sendEmail).toHaveBeenCalled(); + }); + + it("returns false when address is missing", async () => { + const { provider } = createProvider(); + expect(await provider.sendMessage(makeNotification({ address: "" }) as any, makeMessage())).toBe(false); + }); + + it("returns false when buildEmail returns undefined", async () => { + const { provider, emailService, logger } = createProvider(); + (emailService.buildEmail as jest.Mock).mockResolvedValue(undefined); + expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Failed to build email content" })); + }); + + it("returns false when sendEmail returns falsy", async () => { + const { provider, emailService, logger } = createProvider(); + (emailService.sendEmail as jest.Mock).mockResolvedValue(undefined); + expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ message: "Email notification failed" })); + }); + + it("builds correct subject for monitor_down", async () => { + const { provider, emailService } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessage({ type: "monitor_down" })); + expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Monitor Test Monitor is down", expect.anything()); + }); + + it("builds correct subject for monitor_up", async () => { + const { provider, emailService } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessage({ type: "monitor_up" })); + expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Monitor Test Monitor is back up", expect.anything()); + }); + + it("builds correct subject for threshold_breach", async () => { + const { provider, emailService } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessage({ type: "threshold_breach" })); + expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Monitor Test Monitor threshold exceeded", expect.anything()); + }); + + it("builds correct subject for threshold_resolved", async () => { + const { provider, emailService } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessage({ type: "threshold_resolved" })); + expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Monitor Test Monitor thresholds resolved", expect.anything()); + }); + + it("builds default subject for unknown type", async () => { + const { provider, emailService } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessage({ type: "test" })); + expect(emailService.sendEmail).toHaveBeenCalledWith(expect.anything(), "Alert: Test Monitor", expect.anything()); + }); + + it("passes incident url in context when present", async () => { + const { provider, emailService } = createProvider(); + const msg = makeMessage(); + msg.content.incident = { id: "inc-1", url: "https://app.example.com/incidents/inc-1", createdAt: new Date() }; + await provider.sendMessage(makeNotification() as any, msg); + expect(emailService.buildEmail).toHaveBeenCalledWith( + "unifiedNotificationTemplate", + expect.objectContaining({ incidentUrl: "https://app.example.com/incidents/inc-1" }) + ); + }); + + it("maps all severity levels to colors", async () => { + const { provider, emailService } = createProvider(); + const severityColors: Record = { critical: "red", warning: "#f59e0b", info: "#3b82f6", success: "green" }; + for (const [severity, color] of Object.entries(severityColors)) { + (emailService.buildEmail as jest.Mock).mockClear(); + await provider.sendMessage(makeNotification() as any, makeMessage({ severity: severity as any })); + expect(emailService.buildEmail).toHaveBeenCalledWith("unifiedNotificationTemplate", expect.objectContaining({ headerColor: color })); + } + }); + + it("uses default color for unknown severity", async () => { + const { provider, emailService } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessage({ severity: "unknown" as any })); + expect(emailService.buildEmail).toHaveBeenCalledWith("unifiedNotificationTemplate", expect.objectContaining({ headerColor: "#3b82f6" })); + }); + }); +}); diff --git a/server/test/unit/providers/notifications/matrixProvider.test.ts b/server/test/unit/providers/notifications/matrixProvider.test.ts new file mode 100644 index 000000000..f6ad36692 --- /dev/null +++ b/server/test/unit/providers/notifications/matrixProvider.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it, jest, beforeEach } from "@jest/globals"; +import { createMockLogger } from "../../../helpers/createMockLogger.ts"; +import { makeNotification, makeMessage, makeMessageWithThresholds, makeMessageWithIncident } from "../../../helpers/notificationMessage.ts"; +import { testNotificationProviderContract } from "../../../helpers/notificationProviderContract.ts"; + +const mockGotPut = jest.fn().mockResolvedValue({}); +jest.unstable_mockModule("got", () => ({ default: { put: mockGotPut } })); +jest.unstable_mockModule("crypto", () => ({ randomUUID: () => "test-uuid-1234" })); + +const { MatrixProvider } = await import("../../../../src/service/infrastructure/notificationProviders/matrix.ts"); + +const createProvider = () => { + const logger = createMockLogger(); + return { provider: new MatrixProvider(logger as any), logger }; +}; + +testNotificationProviderContract("MatrixProvider", { + create: () => { + mockGotPut.mockResolvedValue({}); + return createProvider().provider; + }, + makeNotification: () => makeNotification(), +}); + +describe("MatrixProvider", () => { + beforeEach(() => mockGotPut.mockReset().mockResolvedValue({})); + + describe("sendTestAlert", () => { + it("sends to Matrix API and returns true", async () => { + expect(await createProvider().provider.sendTestAlert(makeNotification())).toBe(true); + expect(mockGotPut).toHaveBeenCalledWith( + expect.stringContaining("matrix.example.com/_matrix/client/v3/rooms/!room:example.com/send/m.room.message/test-uuid-1234"), + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "Bearer token-abc" }), + }) + ); + }); + + it("returns false when homeserverUrl is missing", async () => { + expect(await createProvider().provider.sendTestAlert(makeNotification({ homeserverUrl: undefined }))).toBe(false); + }); + + it("returns false when accessToken is missing", async () => { + expect(await createProvider().provider.sendTestAlert(makeNotification({ accessToken: undefined }))).toBe(false); + }); + + it("returns false when roomId is missing", async () => { + expect(await createProvider().provider.sendTestAlert(makeNotification({ roomId: undefined }))).toBe(false); + }); + + it("returns false and logs on error", async () => { + mockGotPut.mockRejectedValue(new Error("fail")); + const { provider, logger } = createProvider(); + expect(await provider.sendTestAlert(makeNotification())).toBe(false); + expect(logger.warn).toHaveBeenCalled(); + }); + + it("handles non-Error thrown values", async () => { + mockGotPut.mockRejectedValue({ message: "custom obj" }); + const { provider, logger } = createProvider(); + expect(await provider.sendTestAlert(makeNotification())).toBe(false); + expect(logger.warn).toHaveBeenCalledWith(expect.objectContaining({ stack: undefined })); + }); + }); + + describe("sendMessage", () => { + it("sends formatted HTML and plain text", async () => { + const { provider } = createProvider(); + expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(true); + const body = mockGotPut.mock.calls[0][1].json; + expect(body.msgtype).toBe("m.text"); + expect(body.format).toBe("org.matrix.custom.html"); + expect(body.body).toContain("Monitor Down"); + expect(body.formatted_body).toContain(" { + expect(await createProvider().provider.sendMessage(makeNotification({ homeserverUrl: undefined }) as any, makeMessage())).toBe(false); + }); + + it("returns false and logs on error", async () => { + mockGotPut.mockRejectedValue(new Error("fail")); + const { provider, logger } = createProvider(); + expect(await provider.sendMessage(makeNotification() as any, makeMessage())).toBe(false); + expect(logger.warn).toHaveBeenCalled(); + }); + + it("includes threshold breaches", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithThresholds()); + const body = mockGotPut.mock.calls[0][1].json; + expect(body.body).toContain("CPU"); + expect(body.formatted_body).toContain("CPU"); + }); + + it("includes incident link", async () => { + const { provider } = createProvider(); + await provider.sendMessage(makeNotification() as any, makeMessageWithIncident()); + const body = mockGotPut.mock.calls[0][1].json; + expect(body.body).toContain("View Incident"); + expect(body.formatted_body).toContain("View Incident"); + }); + + it("omits optional sections when not present", async () => { + const { provider } = createProvider(); + const msg = makeMessage(); + msg.content.thresholds = undefined; + msg.content.details = undefined; + msg.content.incident = undefined; + await provider.sendMessage(makeNotification() as any, msg); + const body = mockGotPut.mock.calls[0][1].json; + expect(body.body).not.toContain("Threshold"); + expect(body.body).not.toContain("Additional"); + expect(body.body).not.toContain("View Incident"); + }); + + it("escapes HTML special characters", async () => { + const { provider } = createProvider(); + const msg = makeMessage(); + msg.monitor.name = ''; + await provider.sendMessage(makeNotification() as any, msg); + const html = mockGotPut.mock.calls[0][1].json.formatted_body; + expect(html).not.toContain("