Compare commits

...
Author SHA1 Message Date
cihatata df091fc3b7 refactor: replace static string service getters with dynamic get() method 2025-09-01 09:06:40 +03:00
Alexander Holliday c953ec6152 Merge pull request #2889 from bluewave-labs/fix/package-lock
regenerate package-lock
2025-08-30 19:31:02 -07:00
Alex Holliday a433f10240 regenerate package-lock 2025-08-30 19:30:24 -07:00
Alexander Holliday b73b97b5e9 Merge pull request #2887 from bluewave-labs/feat/add-maintenance-window-description
Update maintenance window description
2025-08-30 19:01:45 -07:00
gorkem-bwl 0607e0b228 refactor: simplify maintenance window description per feedback
- Remove redundant words 'active' and 'completely'
- Remove technical details about specific check types
- Keep the description concise while maintaining clarity
- Maintain all key information about maintenance behavior
2025-08-30 21:57:17 -04:00
gorkem-bwl 3bd4f67ec7 docs: add comprehensive maintenance window behavior description
- Update maintenance window description to clearly explain what happens during maintenance
- Clarify that ALL monitoring types are suspended (pings, HTTP, port checks, etc.)
- Explain that no notifications or alerts will be triggered
- Describe status page behavior (maintenance indicator shown)
- Note that monitoring resumes automatically when maintenance ends
- Clarify that maintenance periods don't affect uptime calculations
- Provide users with complete understanding of maintenance window impact
2025-08-30 21:31:44 -04:00
Alexander Holliday 0101da731f Merge pull request #2884 from bluewave-labs/fix/bump-version
bump version
2025-08-30 16:23:03 -07:00
Alex Holliday 68592acd39 bump version 2025-08-30 16:22:16 -07:00
Alexander Holliday 32b54ddf11 Merge pull request #2882 from bluewave-labs/feat/migration
add migration for statusWindowThreshold
2025-08-30 16:20:23 -07:00
Alex Holliday f8a0741e6c <= -> < 2025-08-30 16:19:17 -07:00
Alex Holliday 652d231771 add migration for statusWindowThreshold 2025-08-30 16:07:03 -07:00
Alexander Holliday 1c3c5f4fec Merge pull request #2881 from bluewave-labs/fix/queue
fix: queue
2025-08-30 11:30:38 -07:00
Alex Holliday e7006a4a44 fix default value 2025-08-30 11:28:54 -07:00
Alex Holliday 0f81d54f25 remove success toast 2025-08-30 11:18:05 -07:00
Alex Holliday f3121c24c6 add interval to queue table 2025-08-30 11:17:34 -07:00
Alex Holliday 7abd0f2cd4 add root level gitignore 2025-08-30 11:13:53 -07:00
Alex Holliday 46b342005a update monitor interval and data 2025-08-30 11:11:58 -07:00
Alex Holliday 4e0349bd58 bump queue 2025-08-30 11:11:19 -07:00
Alexander Holliday 30a569233b Merge pull request #2873 from bluewave-labs/feat/docker-container-name-monitoring
feat: Enable Docker container monitoring by name in addition to ID
2025-08-29 09:33:45 -07:00
Alex Holliday 44ba6b4d0f early returns 2025-08-29 09:22:02 -07:00
Alexander Holliday 9065070eb2 Merge pull request #2871 from bluewave-labs/feat/incident-tooltip
feat:incident tooltip
2025-08-29 09:07:35 -07:00
gorkem-bwl ba757b9aa4 style: fix prettier formatting in validation.js 2025-08-29 00:58:03 -04:00
gorkem-bwlandClaude c445463c0c refactor: improve Docker container matching with deterministic priority-based logic
Address code review feedback to eliminate ambiguity issues and improve reliability:

**Backend Improvements:**
- Implement priority-based matching: exact ID → exact name → partial ID
- Add input normalization (strip leading slashes, case-insensitive)
- Add ambiguity detection with warning logs for debugging
- Maintain full backwards compatibility with existing monitors

**Frontend Improvements:**
- Update validation regex to support leading slashes (/container-name)
- Improve error message clarity for user guidance

**Resolves Ambiguity Issues:**
- Container name "abc" vs Container ID "abc123..." now deterministic
- Multiple containers with similar names handled predictably
- Case-insensitive matching for better user experience

**Priority Order:**
1. Exact full ID match (64-char) - highest priority
2. Exact container name match - case-insensitive
3. Partial ID match - backwards compatibility fallback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 00:53:40 -04:00
gorkem-bwlandClaude 4693177e4b feat: add Docker container monitoring by name
- Update Docker monitoring to support both container names and IDs
- Maintain backward compatibility with existing container ID monitoring
- Add logic to find containers by name (with or without "/" prefix)
- Update frontend UI labels and placeholders to reflect name/ID support
- Update instructions to clarify both name and ID are supported

Resolves user request: "It would be great if we could monitor docker containers by name and not just ID - given that the ID changes when containers are taken down for any reason"

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 00:27:01 -04:00
Alex Holliday 30b135eba3 add tooltip to incident table 2025-08-28 12:21:34 -07:00
Alex Holliday bda48e07f1 add optional tooltip 2025-08-28 12:21:23 -07:00
Alexander Holliday 0ad759a384 Merge pull request #2865 from bluewave-labs/fix/status-page-deletion-error
Fix status page deletion error
2025-08-27 14:49:45 -07:00
gorkem-bwl d3d20b29c3 Fix status page deletion error - missing module reference
- Fix deleteStatusPage method call to use statusPageModule
- Change this.db.deleteStatusPage to this.db.statusPageModule.deleteStatusPage
- Ensures consistency with other controller methods
- Resolves "this.db.deleteStatusPage is not a function" error

Fixes bug where status page deletion failed due to incorrect database module reference.
2025-08-27 17:47:11 -04:00
Alexander Holliday fbb570c0d5 Merge pull request #2862 from bluewave-labs/fix/advanced-matching
fix: advanced matching
2025-08-26 14:56:20 -07:00
Alex Holliday 2318082bb8 fix advanced matching 2025-08-26 14:54:24 -07:00
Alexander Holliday 841251d09e Merge pull request #2857 from bluewave-labs/fix/model-types
fix/model types
2025-08-25 15:05:41 -07:00
Alexander Holliday a4dd8f70f0 Merge branch 'develop' into fix/model-types 2025-08-25 15:05:30 -07:00
Alex Holliday 9d0d1329b6 bump version 2025-08-25 15:04:44 -07:00
Alex Holliday 7a929777b4 fix model types 2025-08-25 15:03:31 -07:00
Alexander Holliday 4f7847275e Merge pull request #2854 from bluewave-labs/feat/bump-version
bump version
2025-08-25 14:37:21 -07:00
Alex Holliday 9a5ddb22b3 bump version 2025-08-25 14:37:00 -07:00
Alexander Holliday b1322cbec3 Merge pull request #2853 from bluewave-labs/fix/string-service-path
fix: string service path, resolves #2847, resolves #2848
2025-08-25 14:32:21 -07:00
Alex Holliday 637bc84136 fix string service path 2025-08-25 14:30:41 -07:00
Alexander Holliday 3147f02b6a Merge pull request #2843 from bluewave-labs/fix/notification-channel-validation
fix notification validation
2025-08-22 11:34:38 -07:00
Alex Holliday dabff74946 fix notification validation 2025-08-22 11:33:14 -07:00
Gorkem Cetin (BWL) de99577b9d Merge pull request #2838 from bluewave-labs/disabledbydefault
Fix Settings Page Select Component Value Handling for URL Visibility Option
2025-08-22 10:38:16 -04:00
Alexander Holliday 1846432601 Merge pull request #2839 from bluewave-labs/fix/docker-compose-arm
fix: update arm dist docker compose
2025-08-21 22:39:03 -07:00
Alex Holliday 6f307dc822 update arm dist docker compose 2025-08-21 22:37:59 -07:00
imjainish f43210e29e Address PR Comments - Format 2025-08-21 23:13:50 -04:00
imjainish 50b9be4eca Address PR Comments 2025-08-21 23:05:50 -04:00
imjainish 1a38222974 Fix Disabled Field in Settings 2025-08-21 22:39:41 -04:00
Alexander Holliday cb9cd0a43c Merge pull request #2829 from bluewave-labs/fix-timestamps-format
fix: add missing tooltip date format for 'recent' timeframe in charts
2025-08-21 11:39:13 -07:00
Alexander Holliday df790a92be Merge pull request #2836 from bluewave-labs/fix/bump-version
bump version
2025-08-21 10:13:13 -07:00
Alex Holliday e1f9242b34 bump version 2025-08-21 10:12:44 -07:00
Alexander Holliday 9d2a52e639 Merge pull request #2833 from bluewave-labs/translation-update-77
🌐 Updated Translations from POEditor
2025-08-21 09:25:38 -07:00
GitHub Action 5cbca9457e feat: translations updated from POEditor 2025-08-21 16:22:53 +00:00
karenvicent 75a2e0deee Merge branch 'develop' into fix-timestamps-format 2025-08-20 18:16:50 -04:00
karenvicent b857137f1a fix data format in uptime and infrastructure charts 2025-08-20 18:14:42 -04:00
Alexander Holliday 40141cdcb0 Merge pull request #2828 from bluewave-labs/fix/version-bump
Fix/version bump
2025-08-20 14:10:16 -07:00
Alex Holliday c5c1d6903e 3.1 2025-08-20 14:09:41 -07:00
Alex Holliday f15c8188b9 bump again 2025-08-20 14:07:30 -07:00
Alexander Holliday 32c811ec5a Merge pull request #2827 from bluewave-labs/fix/version-bump
fix: bump version
2025-08-20 14:04:34 -07:00
Alex Holliday 4febc56c30 bump version 2025-08-20 14:04:02 -07:00
Alexander Holliday f22ee4a68b Merge pull request #2824 from bluewave-labs/fix/remove-log
remove console statement
2025-08-20 14:01:57 -07:00
Alex Holliday 87bec0399f remove console statement 2025-08-20 14:01:32 -07:00
Alexander Holliday 1f29255dd1 Merge pull request #2821 from bluewave-labs/refactor/create-maintenance-window
refactor create maintenance window page
2025-08-20 13:29:56 -07:00
karenvicent 4a11638182 refactor create maintenance window page 2025-08-20 12:51:24 -04:00
Alexander Holliday 10743372f8 Merge pull request #2813 from bluewave-labs/feat/n-of-m-frontend
feat/n of m frontend
2025-08-19 08:08:52 -07:00
Alex Holliday d4b11bbb76 extract strings, sort json 2025-08-18 15:46:41 -07:00
Alex Holliday ced2417c98 add incident config 2025-08-18 15:34:30 -07:00
Alex Holliday 27415aeca6 fix undefined bug, updated server validation 2025-08-18 15:34:15 -07:00
Alexander Holliday 9bd2e336a6 Merge pull request #2811 from bluewave-labs/feat/alerts-n-of-m
feat: alerts on  `n` of `m`, resolves #2366
2025-08-18 14:16:35 -07:00
Alex Holliday d5778ec173 update stauts service to use n of m status changes 2025-08-18 13:40:56 -07:00
Alex Holliday 74e80f086a udpate monitor model 2025-08-18 13:40:22 -07:00
Alexander Holliday b1c6191fa1 Merge pull request #2809 from bluewave-labs/fix/fe/minor-changes
Fix: Minor Changes
2025-08-18 12:56:00 -07:00
Alexander Holliday 8e0245c844 Merge pull request #2807 from bluewave-labs/feat/simplify-checks
feat: unify check models
2025-08-18 12:54:45 -07:00
Br0wnHammer 613fbf8c39 Fix: Minor Changes 2025-08-19 01:11:56 +05:30
Alex Holliday 1788cda5c2 let -> const 2025-08-18 11:27:41 -07:00
Alex Holliday 155adf549e add backend changes 2025-08-18 10:55:12 -07:00
Alex Holliday 5463542532 unify check models 2025-08-18 10:52:00 -07:00
Alexander Holliday f5a25bc0e5 Merge pull request #2806 from bluewave-labs/fix/pager_duty
fix: update pager_duty validation, resolves #2805
2025-08-18 09:17:22 -07:00
Alex Holliday 993339d0d1 update pager_duty validation 2025-08-17 20:21:18 -07:00
Alexander Holliday bc4fe897cf Merge pull request #2804 from bluewave-labs/refactor-create-infrastruture
Refactor create infrastruture
2025-08-17 15:16:28 -07:00
Alexander Holliday c773bdaabd Merge pull request #2802 from bluewave-labs/feat/network-service-refactor
feat: network service refactor
2025-08-17 15:08:40 -07:00
Alex Holliday 9f9a2be1d8 fix missing URL 2025-08-17 15:08:12 -07:00
karenvicent 1e4b0c2f20 Merge branch 'develop' into refactor-create-infrastruture 2025-08-17 15:28:49 -04:00
karenvicent 56025f6d56 refactor create infraestructure monitor page 2025-08-17 15:28:06 -04:00
Alexander Holliday 5188b90f32 Merge pull request #2800 from mattstein/fix/pagespeed-validation
Fix: resolve erroneous PageSpeed settings validation
2025-08-15 17:47:30 -07:00
Matt Stein 7178f537a8 Update with changes from code review. 2025-08-15 16:40:21 -07:00
Alex Holliday 45ac589975 refactor network service 2025-08-15 16:04:43 -07:00
Alex Holliday 9925e10ffc add timings to model 2025-08-15 16:03:12 -07:00
Alex Holliday e3fde8d564 add got 2025-08-15 16:02:52 -07:00
Matt Stein 8aee01e008 Fix PageSpeed settings validation. 2025-08-15 14:34:42 -07:00
Alexander Holliday cd203ff067 Merge pull request #2794 from Owaiseimdad/Network-patch-fixes-part-1
Fixed the selector and also null points for networks
2025-08-15 13:12:04 -07:00
Alexander Holliday 254cb6d80f Merge pull request #2788 from bluewave-labs/fix/filter-header-alignment
Fix: Improve vertical alignment of text and plus icons in filter headers
2025-08-15 12:21:50 -07:00
Alexander Holliday 4ae3bb5780 Merge pull request #2795 from bluewave-labs/hotfix/path
fix path, remove redis form build script
2025-08-15 09:18:18 -07:00
Alex Holliday b8aa1ef765 fix path, remove redis form build script 2025-08-15 09:17:33 -07:00
gorkem-bwl 5e5daef49a Move vertical alignment styling from component to theme level
- Move MuiSelect vertical alignment from FilterHeader component to globalTheme.js
- Ensures consistent vertical alignment for all Select components application-wide
- Removes component-specific styling in favor of theme-level consistency
- Addresses reviewer feedback to make this a theme-level change

Components already with custom Select styling will continue to work as local styles override theme styles.
2025-08-15 03:38:22 -04:00
Owaise 88c478c533 Formatting is done. 2025-08-15 11:37:38 +05:30
Owaise 1ecec115e4 Fixed the selector and also null points for networks. 2025-08-15 11:28:22 +05:30
Alexander Holliday 8ff597eedb Merge pull request #2791 from bluewave-labs/fix/remove-logs-descriptions
Fix: Remove description text and dividers from Logs sections
2025-08-14 20:36:44 -07:00
Alexander Holliday 69f74488e5 Merge pull request #2790 from bluewave-labs/fix/logs-default-tab
Fix: Set Server Logs as default tab on Logs page
2025-08-14 20:34:15 -07:00
gorkem-bwl ded9a27f42 fix: remove excess spacing after removing description text
Removed top margin (mt={theme.spacing(10)}) and adjusted sticky position
from the log level selector since it was creating unwanted space between
the tab bar and the 'Log level' dropdown after removing the description
text and divider.
2025-08-14 22:22:05 -04:00
gorkem-bwl 43284ae625 fix: remove description text and dividers from Logs sections
Removed description strings and divider lines from:
- Server logs: 'System logs - last 1000 lines' and divider
- Queue metrics: queue description and divider

This cleans up the UI by removing redundant descriptive text
that users can infer from the tab names and content context.
2025-08-14 22:19:54 -04:00
gorkem-bwl f7b1ef9741 fix: set Server Logs as default tab on Logs page
Changed default tab from Diagnostics (index 2) to Server Logs (index 0)
when users navigate to the Logs section from the sidebar. This makes
Server Logs the active tab by default, which is more intuitive since
it's the first tab in the list.
2025-08-14 22:11:47 -04:00
gorkem-bwl a6d445818f fix: improve vertical alignment of text and plus icons in filter headers
Added flex alignment styles to MuiSelect-select class to ensure proper vertical
alignment between text labels and plus icons in Type, Status, and State filter
buttons on the Uptime page.
2025-08-14 20:15:56 -04:00
79 changed files with 4691 additions and 5081 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.vscode
+4 -622
View File
@@ -491,262 +491,6 @@
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
@@ -763,134 +507,6 @@
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@@ -2063,123 +1679,6 @@
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
"integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz",
"integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz",
"integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz",
"integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz",
"integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz",
"integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz",
"integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz",
"integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz",
"integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
@@ -2193,71 +1692,6 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz",
"integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz",
"integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz",
"integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz",
"integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz",
"integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
@@ -2284,45 +1718,6 @@
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz",
"integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz",
"integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz",
"integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -4342,14 +3737,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -4363,20 +3759,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -1,5 +1,6 @@
export const tooltipDateFormatLookup = (dateRange) => {
const dateFormatLookup = {
recent: "ddd. MMMM D, YYYY, hh:mm A",
day: "ddd. MMMM D, YYYY, hh:mm A",
week: "ddd. MMMM D, YYYY, hh:mm A",
month: "ddd. MMMM D, YYYY",
@@ -1,7 +1,6 @@
import { useTheme } from "@emotion/react";
import Box from "@mui/material/Box";
import Background from "../../assets/Images/background-grid.svg?react";
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
import OutputAnimation from "../../assets/Animations/output.gif";
import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif";
import { useSelector } from "react-redux";
@@ -13,7 +12,7 @@ const FallbackBackground = () => {
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
Background="transparent"
background="transparent"
alt="Loading animation"
sx={{
zIndex: 1,
+1 -1
View File
@@ -74,7 +74,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
<Typography
variant="body1"
sx={{
color: theme.palette.text.secondary,
color: theme.palette.primary.contrastTextTertiary,
fontSize: "0.938rem",
lineHeight: 1.5,
mb: 1,
+48 -16
View File
@@ -7,6 +7,7 @@ import {
TableHead,
TableRow,
} from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import SkeletonLayout from "./skeleton";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
@@ -41,6 +42,7 @@ const DataTable = ({
data = [],
config = {
emptyView: "No data",
tooltipContent: null,
onRowClick: () => {},
},
}) => {
@@ -101,24 +103,54 @@ const DataTable = ({
data.map((row) => {
const key = row.id || row._id || Math.random();
return (
<TableRow
<Tooltip
key={key}
sx={config?.rowSX ?? {}}
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
followCursor
enterDelay={500}
enterNextDelay={500}
title={
typeof config.tooltipContent === "function"
? config.tooltipContent(row)
: config.tooltipContent
}
slotProps={{
tooltip: {
sx: {
background: "unset",
},
},
popper: {
modifiers: [
{
name: "offset",
options: {
offset: ({ popper }) => {
return [popper.width / 2 + 20, -popper.height / 8];
},
},
},
],
},
}}
>
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
key={header.id}
onClick={header.onClick ? (e) => header.onClick(e, row) : null}
sx={header.getCellSx ? header.getCellSx(row) : {}}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
<TableRow
sx={config?.rowSX ?? {}}
onClick={config?.onRowClick ? () => config.onRowClick(row) : null}
>
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
key={header.id}
onClick={header.onClick ? (e) => header.onClick(e, row) : null}
sx={header.getCellSx ? header.getCellSx(row) : {}}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
</Tooltip>
);
})
)}
-3
View File
@@ -15,9 +15,6 @@ const useFetchLogs = () => {
setIsLoading(true);
const response = await networkService.getLogs();
setLogs(response.data.data);
createToast({
body: t("logsPage.toast.fetchLogsSuccess"),
});
} catch (error) {
setError(error);
createToast({
+2
View File
@@ -369,6 +369,8 @@ const useUpdateMonitor = () => {
setIsLoading(true);
const updatedFields = {
name: monitor.name,
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
description: monitor.description,
interval: monitor.interval,
notifications: monitor.notifications,
@@ -1,5 +1,10 @@
//Components
import Table from "../../../../Components/Table";
import Stack from "@mui/material/Stack";
import DataTable from "../../../../Components/Table";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableRow from "@mui/material/TableRow";
import TableCell from "@mui/material/TableCell";
import TableSkeleton from "../../../../Components/Table/skeleton";
import Pagination from "../../../../Components/Table/TablePagination";
import { StatusLabel } from "../../../../Components/Label";
@@ -16,7 +21,68 @@ import { useTranslation } from "react-i18next";
import { useFetchChecksTeam } from "../../../../Hooks/checkHooks";
import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks";
import { useResolveIncident } from "../../../../Hooks/checkHooks";
import { Button, Typography } from "@mui/material";
import { Button, Typography, useTheme } from "@mui/material";
import { lighten } from "@mui/material/styles";
const GetTooltip = (row) => {
const theme = useTheme();
const phases = row?.timings?.phases;
const phaseKeyFormattingMap = {
firstByte: "first byte",
};
return (
<Stack
backgroundColor={lighten(theme.palette.primary.main, 0.1)}
border={`1px solid ${theme.palette.primary.lowContrast}`}
borderRadius={theme.shape.borderRadius}
py={theme.spacing(2)}
px={theme.spacing(4)}
>
<Typography
variant="body2"
color={theme.palette.primary.contrastText}
>{`Status code: ${row?.statusCode}`}</Typography>
<Typography
variant="body2"
color={theme.palette.primary.contrastText}
>{`Response time: ${row?.responseTime} ms`}</Typography>
{phases && (
<>
<Typography
variant="body2"
color={theme.palette.primary.contrastText}
>{`Request timing: `}</Typography>
<Table
size="small"
sx={{ ml: theme.spacing(2), mt: theme.spacing(2) }}
>
<TableBody>
{Object.keys(phases)?.map((phaseKey) => (
<TableRow key={phaseKey}>
<TableCell sx={{ border: "none", p: 0 }}>
<Typography
variant="body2"
color="success"
>
{`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`}
</Typography>
</TableCell>
<TableCell sx={{ border: "none", p: 0 }}>
<Typography
color={theme.palette.primary.contrastText}
variant="body2"
>{`${phases[phaseKey]} ms`}</Typography>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
)}
</Stack>
);
};
const IncidentTable = ({
isLoading,
@@ -167,9 +233,10 @@ const IncidentTable = ({
return (
<>
<Table
<DataTable
headers={headers}
data={checks}
config={{ tooltipContent: GetTooltip }}
/>
<Pagination
paginationLabel={t("incidentsTablePaginationLabel")}
@@ -0,0 +1,87 @@
import ConfigBox from "../../../../Components/ConfigBox";
import { Box, Stack, Typography } from "@mui/material";
import { CustomThreshold } from "../Components/CustomThreshold";
import { capitalizeFirstLetter } from "../../../../Utils/stringUtils";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const CustomAlertsSection = ({
errors,
onChange,
infrastructureMonitor,
handleCheckboxChange,
}) => {
const theme = useTheme();
const { t } = useTranslation();
const METRICS = ["cpu", "memory", "disk", "temperature"];
const METRIC_PREFIX = "usage_";
const hasAlertError = (errors) => {
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
};
const getAlertError = (errors) => {
const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX));
return errorKey ? errors[errorKey] : null;
};
return (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("infrastructureCustomizeAlerts")}
</Typography>
<Typography component="p">
{t("infrastructureAlertNotificationDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu" ? capitalizeFirstLetter(metric) : metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
onFieldChange={onChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{getAlertError(errors)}
</Typography>
)}
</Stack>
</ConfigBox>
);
};
CustomAlertsSection.propTypes = {
errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
infrastructureMonitor: PropTypes.object.isRequired,
handleCheckboxChange: PropTypes.func.isRequired,
};
export default CustomAlertsSection;
@@ -0,0 +1,78 @@
import { useState } from "react";
import { Box, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import Dialog from "../../../../Components/Dialog";
import PropTypes from "prop-types";
const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => {
const theme = useTheme();
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
return (
<Box
alignSelf="flex-end"
ml="auto"
>
<Button
onClick={handlePause}
loading={isBusy}
variant="contained"
color="secondary"
sx={{
pl: theme.spacing(4),
pr: theme.spacing(6),
"& svg": {
mr: theme.spacing(2),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 0.1,
},
},
}}
>
{monitor?.isActive ? (
<>
<PauseCircleOutlineIcon />
{t("pause")}
</>
) : (
<>
<PlayCircleOutlineRoundedIcon />
{t("resume")}
</>
)}
</Button>
<Button
loading={isBusy}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ ml: theme.spacing(6) }}
>
{t("remove")}
</Button>
<Dialog
open={isOpen}
theme={theme}
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
/>
</Box>
);
};
MonitorActionButtons.propTypes = {
monitor: PropTypes.object.isRequired,
isBusy: PropTypes.bool.isRequired,
handlePause: PropTypes.func.isRequired,
handleRemove: PropTypes.func.isRequired,
};
export default MonitorActionButtons;
@@ -0,0 +1,73 @@
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import PulseDot from "../../../../Components/Animated/PulseDot";
import PropTypes from "prop-types";
const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => {
const theme = useTheme();
const { t } = useTranslation();
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
return (
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={pagespeedStatusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: { offset: [0, -8] },
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="monitorUrl"
>
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
ml={theme.spacing(6)}
mt={theme.spacing(1)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: theme.spacing(2),
height: theme.spacing(2),
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: theme.spacing(-5),
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{t("editing")}
</Typography>
</Stack>
);
};
MonitorStatusHeader.propTypes = {
monitor: PropTypes.object.isRequired,
infrastructureMonitor: PropTypes.object.isRequired,
};
export default MonitorStatusHeader;
@@ -0,0 +1,90 @@
import { useState } from "react";
const useInfrastructureMonitorForm = () => {
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
notifications: [],
notify_email: false,
interval: 0.25,
cpu: false,
usage_cpu: "",
memory: false,
usage_memory: "",
disk: false,
usage_disk: "",
temperature: false,
usage_temperature: "",
secret: "",
});
const onChangeForm = (name, value) => {
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: value,
});
};
const handleCheckboxChange = (event) => {
setInfrastructureMonitor({
...infrastructureMonitor,
[event.target.name]: event.target.checked,
});
};
const initializeInfrastructureMonitorForCreate = (globalSettings) => {
const gt = globalSettings?.data?.settings?.globalThresholds || {};
setInfrastructureMonitor({
url: "",
name: "",
notifications: [],
interval: 0.25,
cpu: gt.cpu !== undefined,
usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "",
memory: gt.memory !== undefined,
usage_memory: gt.memory !== undefined ? gt.memory.toString() : "",
disk: gt.disk !== undefined,
usage_disk: gt.disk !== undefined ? gt.disk.toString() : "",
temperature: gt.temperature !== undefined,
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
secret: "",
});
};
const initializeInfrastructureMonitorForUpdate = (monitor) => {
const MS_PER_MINUTE = 60000;
const { thresholds = {} } = monitor;
setInfrastructureMonitor({
url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "",
notifications: monitor.notifications || [],
interval: monitor.interval / MS_PER_MINUTE,
cpu: thresholds.usage_cpu !== undefined,
usage_cpu:
thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "",
memory: thresholds.usage_memory !== undefined,
usage_memory:
thresholds.usage_memory !== undefined
? (thresholds.usage_memory * 100).toString()
: "",
disk: thresholds.usage_disk !== undefined,
usage_disk:
thresholds.usage_disk !== undefined
? (thresholds.usage_disk * 100).toString()
: "",
temperature: thresholds.usage_temperature !== undefined,
usage_temperature:
thresholds.usage_temperature !== undefined
? (thresholds.usage_temperature * 100).toString()
: "",
secret: monitor.secret || "",
});
};
return {
infrastructureMonitor,
setInfrastructureMonitor,
onChangeForm,
handleCheckboxChange,
initializeInfrastructureMonitorForCreate,
initializeInfrastructureMonitorForUpdate,
};
};
export default useInfrastructureMonitorForm;
@@ -0,0 +1,80 @@
import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks";
const useInfrastructureSubmit = () => {
const [createMonitor, isCreating] = useCreateMonitor();
const [updateMonitor, isUpdating] = useUpdateMonitor();
const buildForm = (infrastructureMonitor, https) => {
const MS_PER_MINUTE = 60000;
let form = {
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
name:
infrastructureMonitor.name === ""
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu }
: {}),
memory: infrastructureMonitor.memory,
...(infrastructureMonitor.memory
? { usage_memory: infrastructureMonitor.usage_memory }
: {}),
disk: infrastructureMonitor.disk,
...(infrastructureMonitor.disk
? { usage_disk: infrastructureMonitor.usage_disk }
: {}),
temperature: infrastructureMonitor.temperature,
...(infrastructureMonitor.temperature
? { usage_temperature: infrastructureMonitor.usage_temperature }
: {}),
secret: infrastructureMonitor.secret,
};
return form;
};
const submitInfrastructureForm = async (
infrastructureMonitor,
form,
isCreate,
monitorId
) => {
const {
cpu,
usage_cpu,
memory,
usage_memory,
disk,
usage_disk,
temperature,
usage_temperature,
...rest
} = form;
const thresholds = {
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
...(memory ? { usage_memory: usage_memory / 100 } : {}),
...(disk ? { usage_disk: usage_disk / 100 } : {}),
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
};
const finalForm = {
...(isCreate ? {} : { _id: monitorId }),
...rest,
description: form.name,
type: "hardware",
notifications: infrastructureMonitor.notifications,
thresholds,
};
// Handle create or update
isCreate
? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" })
: await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" });
};
return {
buildForm,
submitInfrastructureForm,
isCreating,
isUpdating,
};
};
export default useInfrastructureSubmit;
@@ -0,0 +1,37 @@
import { useState } from "react";
import { infrastructureMonitorValidation } from "../../../../Validation/validation";
import { createToast } from "../../../../Utils/toastUtils";
const useValidateInfrastructureForm = () => {
const [errors, setErrors] = useState({});
const validateField = (name, value) => {
const { error } = infrastructureMonitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const validateForm = (form) => {
const { error } = infrastructureMonitorValidation.validate(form, {
abortEarly: false,
});
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
console.log(newErrors);
setErrors(newErrors);
createToast({ body: "Please check the form for errors." });
return error;
}
return null;
};
return { errors, validateField, validateForm };
};
export default useValidateInfrastructureForm;
+41 -351
View File
@@ -1,40 +1,33 @@
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import ConfigBox from "../../../Components/ConfigBox";
import Dialog from "../../../Components/Dialog";
import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
import Link from "../../../Components/Link";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import PulseDot from "../../../Components/Animated/PulseDot";
import Select from "../../../Components/Inputs/Select";
import TextInput from "../../../Components/Inputs/TextInput";
import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material";
import { CustomThreshold } from "./Components/CustomThreshold";
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import MonitorStatusHeader from "./Components/MonitorStatusHeader";
import MonitorActionButtons from "./Components/MonitorActionButtons";
import CustomAlertsSection from "./Components/CustomAlertsSection";
// Utils
import NotificationsConfig from "../../../Components/NotificationConfig";
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import {
useCreateMonitor,
useDeleteMonitor,
useFetchGlobalSettings,
useFetchHardwareMonitorById,
usePauseMonitor,
useUpdateMonitor,
} from "../../../Hooks/monitorHooks";
import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm";
import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm";
import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit";
const CreateInfrastructureMonitor = () => {
const { user } = useSelector((state) => state.auth);
const { monitorId } = useParams();
const isCreate = typeof monitorId === "undefined";
@@ -42,39 +35,29 @@ const CreateInfrastructureMonitor = () => {
const { t } = useTranslation();
// State
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [infrastructureMonitor, setInfrastructureMonitor] = useState({
url: "",
name: "",
notifications: [],
notify_email: false,
interval: 0.25,
cpu: false,
usage_cpu: "",
memory: false,
usage_memory: "",
disk: false,
usage_disk: "",
temperature: false,
usage_temperature: "",
secret: "",
});
// Fetch monitor details if editing
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
const [monitor, isLoading] = useFetchHardwareMonitorById({
monitorId,
updateTrigger,
});
const [createMonitor, isCreating] = useCreateMonitor();
const [deleteMonitor, isDeleting] = useDeleteMonitor();
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId();
const [pauseMonitor, isPausing] = usePauseMonitor();
const [updateMonitor, isUpdating] = useUpdateMonitor();
const {
infrastructureMonitor,
setInfrastructureMonitor,
onChangeForm,
handleCheckboxChange,
initializeInfrastructureMonitorForCreate,
initializeInfrastructureMonitorForUpdate,
} = useInfrastructureMonitorForm();
const { errors, validateField, validateForm } = useValidateInfrastructureForm();
const { buildForm, submitInfrastructureForm, isCreating, isUpdating } =
useInfrastructureSubmit();
const FREQUENCIES = [
{ _id: 0.25, name: t("time.fifteenSeconds") },
@@ -93,157 +76,27 @@ const CreateInfrastructureMonitor = () => {
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` },
]),
];
const METRICS = ["cpu", "memory", "disk", "temperature"];
const METRIC_PREFIX = "usage_";
const MS_PER_MINUTE = 60000;
const hasAlertError = (errors) => {
return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0;
};
const getAlertError = (errors) => {
const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX));
return errorKey ? errors[errorKey] : null;
};
// Populate form fields if editing
useEffect(() => {
if (isCreate) {
if (globalSettingsLoading) return;
const gt = globalSettings?.data?.settings?.globalThresholds || {};
setHttps(false);
setInfrastructureMonitor({
url: "",
name: "",
notifications: [],
interval: 0.25,
cpu: gt.cpu !== undefined,
usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "",
memory: gt.memory !== undefined,
usage_memory: gt.memory !== undefined ? gt.memory.toString() : "",
disk: gt.disk !== undefined,
usage_disk: gt.disk !== undefined ? gt.disk.toString() : "",
temperature: gt.temperature !== undefined,
usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "",
secret: "",
});
initializeInfrastructureMonitorForCreate(globalSettings);
} else if (monitor) {
const { thresholds = {} } = monitor;
setHttps(monitor.url.startsWith("https"));
setInfrastructureMonitor({
url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "",
notifications: monitor.notifications || [],
interval: monitor.interval / MS_PER_MINUTE,
cpu: thresholds.usage_cpu !== undefined,
usage_cpu:
thresholds.usage_cpu !== undefined
? (thresholds.usage_cpu * 100).toString()
: "",
memory: thresholds.usage_memory !== undefined,
usage_memory:
thresholds.usage_memory !== undefined
? (thresholds.usage_memory * 100).toString()
: "",
disk: thresholds.usage_disk !== undefined,
usage_disk:
thresholds.usage_disk !== undefined
? (thresholds.usage_disk * 100).toString()
: "",
temperature: thresholds.usage_temperature !== undefined,
usage_temperature:
thresholds.usage_temperature !== undefined
? (thresholds.usage_temperature * 100).toString()
: "",
secret: monitor.secret || "",
});
initializeInfrastructureMonitorForUpdate(monitor);
}
}, [isCreate, monitor, globalSettings, globalSettingsLoading]);
// Handlers
const onSubmit = async (event) => {
event.preventDefault();
// Build the form
let form = {
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
name:
infrastructureMonitor.name === ""
? infrastructureMonitor.url
: infrastructureMonitor.name,
interval: infrastructureMonitor.interval * MS_PER_MINUTE,
cpu: infrastructureMonitor.cpu,
...(infrastructureMonitor.cpu
? { usage_cpu: infrastructureMonitor.usage_cpu }
: {}),
memory: infrastructureMonitor.memory,
...(infrastructureMonitor.memory
? { usage_memory: infrastructureMonitor.usage_memory }
: {}),
disk: infrastructureMonitor.disk,
...(infrastructureMonitor.disk
? { usage_disk: infrastructureMonitor.usage_disk }
: {}),
temperature: infrastructureMonitor.temperature,
...(infrastructureMonitor.temperature
? { usage_temperature: infrastructureMonitor.usage_temperature }
: {}),
secret: infrastructureMonitor.secret,
};
const { error } = infrastructureMonitorValidation.validate(form, {
abortEarly: false,
});
const form = buildForm(infrastructureMonitor, https);
const error = validateForm(form);
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
console.log(newErrors);
setErrors(newErrors);
createToast({ body: "Please check the form for errors." });
return;
}
// Build the thresholds for the form
const {
cpu,
usage_cpu,
memory,
usage_memory,
disk,
usage_disk,
temperature,
usage_temperature,
...rest
} = form;
const thresholds = {
...(cpu ? { usage_cpu: usage_cpu / 100 } : {}),
...(memory ? { usage_memory: usage_memory / 100 } : {}),
...(disk ? { usage_disk: usage_disk / 100 } : {}),
...(temperature ? { usage_temperature: usage_temperature / 100 } : {}),
};
form = {
...(isCreate ? {} : { _id: monitorId }),
...rest,
description: form.name,
type: "hardware",
notifications: infrastructureMonitor.notifications,
thresholds,
};
// Handle create or update
isCreate
? await createMonitor({ monitor: form, redirect: "/infrastructure" })
: await updateMonitor({ monitor: form, redirect: "/infrastructure" });
submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId);
};
const triggerUpdate = () => {
@@ -252,28 +105,8 @@ const CreateInfrastructureMonitor = () => {
const onChange = (event) => {
const { value, name } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: value,
});
const { error } = infrastructureMonitorValidation.validate(
{ [name]: value },
{ abortEarly: false }
);
setErrors((prev) => ({
...prev,
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
const handleCheckboxChange = (event) => {
const { name } = event.target;
const { checked } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
[name]: checked,
});
onChangeForm(name, value);
validateField(name, value);
};
const handlePause = async () => {
@@ -336,105 +169,19 @@ const CreateInfrastructureMonitor = () => {
)}
</Typography>
{!isCreate && (
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={pagespeedStatusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: { offset: [0, -8] },
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="monitorUrl"
>
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
ml={theme.spacing(6)}
mt={theme.spacing(1)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: theme.spacing(2),
height: theme.spacing(2),
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: theme.spacing(-5),
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{t("editing")}
</Typography>
</Stack>
<MonitorStatusHeader
monitor={monitor}
infrastructureMonitor={infrastructureMonitor}
/>
)}
</Box>
{!isCreate && (
<Box
alignSelf="flex-end"
ml="auto"
>
<Button
onClick={handlePause}
loading={isBusy}
variant="contained"
color="secondary"
sx={{
pl: theme.spacing(4),
pr: theme.spacing(6),
"& svg": {
mr: theme.spacing(2),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 0.1,
},
},
}}
>
{monitor?.isActive ? (
<>
<PauseCircleOutlineIcon />
{t("pause")}
</>
) : (
<>
<PlayCircleOutlineRoundedIcon />
{t("resume")}
</>
)}
</Button>
<Button
loading={isBusy}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ ml: theme.spacing(6) }}
>
{t("remove")}
</Button>
</Box>
<MonitorActionButtons
monitor={monitor}
isBusy={isBusy}
handlePause={handlePause}
handleRemove={handleRemove}
/>
)}
</Stack>
<ConfigBox>
@@ -534,58 +281,12 @@ const CreateInfrastructureMonitor = () => {
setNotifications={infrastructureMonitor.notifications}
/>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("infrastructureCustomizeAlerts")}
</Typography>
<Typography component="p">
{t("infrastructureAlertNotificationDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu"
? capitalizeFirstLetter(metric)
: metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
onFieldChange={onChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{/* Error text */}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{getAlertError(errors)}
</Typography>
)}
</Stack>
</ConfigBox>
<CustomAlertsSection
errors={errors}
onChange={onChange}
infrastructureMonitor={infrastructureMonitor}
handleCheckboxChange={handleCheckboxChange}
/>
<ConfigBox>
<Box>
<Typography
@@ -620,17 +321,6 @@ const CreateInfrastructureMonitor = () => {
</Button>
</Stack>
</Stack>
{!isCreate && (
<Dialog
open={isOpen}
theme={theme}
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
/>
)}
</Box>
);
};
@@ -1,8 +1,9 @@
import PropTypes from "prop-types";
import { useState, useEffect } from "react";
import { FormControl, InputLabel, Select, MenuItem, Box } from "@mui/material";
import { Box } from "@mui/material";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import Select from "../../../../../Components/Inputs/Select";
import NetworkStatBoxes from "./NetworkStatBoxes";
import NetworkCharts from "./NetworkCharts";
import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader";
@@ -63,27 +64,17 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => {
gap={theme.spacing(4)}
>
{availableInterfaces.length > 0 && (
<FormControl
variant="outlined"
size="small"
<Select
name="networkInterface"
label={t("networkInterface")}
value={selectedInterface}
onChange={(e) => setSelectedInterface(e.target.value)}
items={availableInterfaces.map((interfaceName) => ({
_id: interfaceName,
name: interfaceName,
}))}
sx={{ minWidth: 200 }}
>
<InputLabel>{t("networkInterface")}</InputLabel>
<Select
value={selectedInterface}
onChange={(e) => setSelectedInterface(e.target.value)}
label={t("networkInterface")}
>
{availableInterfaces.map((interfaceName) => (
<MenuItem
key={interfaceName}
value={interfaceName}
>
{interfaceName}
</MenuItem>
))}
</Select>
</FormControl>
/>
)}
<MonitorTimeFrameHeader
isLoading={isLoading}
@@ -121,6 +121,7 @@ const InfrastructureDetails = () => {
<AreaChartBoxes
shouldRender={!isLoading}
monitor={monitor}
dateRange={dateRange}
/>
</>
)}
+1 -21
View File
@@ -51,34 +51,14 @@ const Logs = () => {
];
return (
<Stack gap={theme.spacing(4)}>
<Box
sx={{
position: "sticky",
top: theme.spacing(17),
backdropFilter: "blur(10px)",
paddingY: theme.spacing(5),
paddingLeft: theme.spacing(6),
}}
>
<Typography variant="h2">{t("logsPage.description")}</Typography>
</Box>
<Divider
color={theme.palette.accent.main}
sx={{
position: "sticky",
top: theme.spacing(33),
backdropFilter: "blur(10px)",
}}
/>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
mt={theme.spacing(10)}
sx={{
position: "sticky",
top: theme.spacing(34),
top: theme.spacing(17),
backdropFilter: "blur(10px)",
paddingTop: theme.spacing(4),
paddingLeft: theme.spacing(6),
@@ -46,6 +46,11 @@ const JobTable = ({ jobs = [] }) => {
content: t("queuePage.jobTable.urlHeader"),
render: (row) => row.monitorUrl,
},
{
id: "interval",
content: t("queuePage.jobTable.intervalHeader"),
render: (row) => `${row.monitorInterval} ms`,
},
{
id: "type",
content: t("queuePage.jobTable.typeHeader"),
-2
View File
@@ -29,8 +29,6 @@ const QueueDetails = () => {
return (
<Stack gap={theme.spacing(4)}>
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
<Divider color={theme.palette.accent.main} />
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(10)}
+1 -1
View File
@@ -15,7 +15,7 @@ const Logs = () => {
const theme = useTheme();
// Local state
const [value, setValue] = useState(2);
const [value, setValue] = useState(0);
// Handlers
const handleChange = (event, newValue) => {
@@ -0,0 +1,38 @@
import Select from "../../../../../Components/Inputs/Select";
import PropTypes from "prop-types";
const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => {
const getValueById = (config, id) => {
const item = config.find((config) => config._id === id);
return item ? (item.value ? item.value : item.name) : null;
};
const getIdByValue = (config, name) => {
const item = config.find((config) => {
if (config.value) {
return config.value === name;
} else {
return config.name === name;
}
});
return item ? item._id : null;
};
return (
<Select
value={getIdByValue(configSelection, valueSelect)}
onChange={(event) => {
const newValue = getValueById(configSelection, event.target.value);
onChange(newValue);
}}
items={configSelection}
{...props}
/>
);
};
ConfigSelect.propTypes = {
configSelection: PropTypes.array.isRequired,
valueSelect: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
export default ConfigSelect;
@@ -0,0 +1,64 @@
import { networkService } from "../../../../main";
import dayjs from "dayjs";
import {
MS_PER_SECOND,
MS_PER_MINUTE,
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../../Utils/timeUtils";
const useMaintenanceActions = () => {
const MS_LOOKUP = {
seconds: MS_PER_SECOND,
minutes: MS_PER_MINUTE,
hours: MS_PER_HOUR,
days: MS_PER_DAY,
weeks: MS_PER_WEEK,
};
const REPEAT_LOOKUP = {
none: 0,
daily: MS_PER_DAY,
weekly: MS_PER_DAY * 7,
};
const handleSubmitForm = async (maintenanceWindowId, form) => {
const start = dayjs(form.startDate)
.set("hour", form.startTime.hour())
.set("minute", form.startTime.minute());
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
const durationInMs = form.duration * MS_MULTIPLIER;
const end = start.add(durationInMs);
const repeat = REPEAT_LOOKUP[form.repeat];
const submit = {
monitors: form.monitors.map((monitor) => monitor._id),
name: form.name,
start: start.toISOString(),
end: end.toISOString(),
repeat,
};
if (repeat === 0) {
submit.expiry = end;
}
const requestConfig = { maintenanceWindow: submit };
if (maintenanceWindowId !== undefined) {
requestConfig.maintenanceWindowId = maintenanceWindowId;
}
const request =
maintenanceWindowId === undefined
? networkService.createMaintenanceWindow(requestConfig)
: networkService.editMaintenanceWindow(requestConfig);
return request;
};
return {
handleSubmitForm,
};
};
export default useMaintenanceActions;
@@ -0,0 +1,74 @@
import { networkService } from "../../../../main";
import dayjs from "dayjs";
import {
MS_PER_SECOND,
MS_PER_MINUTE,
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../../Utils/timeUtils";
const useMaintenanceData = () => {
const REVERSE_REPEAT_LOOKUP = {
0: "none",
[MS_PER_DAY]: "daily",
[MS_PER_WEEK]: "weekly",
};
const getDurationAndUnit = (durationInMs) => {
if (durationInMs % MS_PER_DAY === 0) {
return {
duration: (durationInMs / MS_PER_DAY).toString(),
durationUnit: "days",
};
} else if (durationInMs % MS_PER_HOUR === 0) {
return {
duration: (durationInMs / MS_PER_HOUR).toString(),
durationUnit: "hours",
};
} else if (durationInMs % MS_PER_MINUTE === 0) {
return {
duration: (durationInMs / MS_PER_MINUTE).toString(),
durationUnit: "minutes",
};
} else {
return {
duration: (durationInMs / MS_PER_SECOND).toString(),
durationUnit: "seconds",
};
}
};
const fetchMonitorsMaintenance = async () => {
const response = await networkService.getMonitorsByTeamId({
limit: null,
types: ["http", "ping", "pagespeed", "port"],
});
const fetchedMonitors = response.data.data.monitors;
return fetchedMonitors;
};
const initializeMaintenanceForEdit = async (maintenanceWindowId, monitorList) => {
const res = await networkService.getMaintenanceWindowById({
maintenanceWindowId: maintenanceWindowId,
});
const maintenanceWindow = res.data.data;
const { name, start, end, repeat, monitorId } = maintenanceWindow;
const startTime = dayjs(start);
const endTime = dayjs(end);
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
const monitor = monitorList.find((monitor) => monitor._id === monitorId);
const maintenanceWindowInformation = {
name,
repeat: REVERSE_REPEAT_LOOKUP[repeat],
startDate: startTime,
startTime,
duration,
durationUnit,
monitors: monitor ? [monitor] : [],
};
return maintenanceWindowInformation;
};
return { fetchMonitorsMaintenance, initializeMaintenanceForEdit };
};
export default useMaintenanceData;
@@ -10,71 +10,21 @@ import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import MonitorList from "./Components/MonitorList";
import Checkbox from "../../../Components/Inputs/Checkbox";
import ConfigSelect from "./Components/ConfigSelect";
import useMaintenanceData from "./hooks/useMaintenanceData";
import useMaintenanceActions from "./hooks/useMaintenanceActions";
import dayjs from "dayjs";
import Select from "../../../Components/Inputs/Select";
import TextInput from "../../../Components/Inputs/TextInput";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import CalendarIcon from "../../../assets/icons/calendar.svg?react";
import "./index.css";
import Search from "../../../Components/Inputs/Search";
import { networkService } from "../../../main";
import { logger } from "../../../Utils/Logger";
import {
MS_PER_SECOND,
MS_PER_MINUTE,
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../Utils/timeUtils";
import { useNavigate, useParams } from "react-router-dom";
import { buildErrors, hasValidationErrors } from "../../../Validation/error";
import { useTranslation } from "react-i18next";
const getDurationAndUnit = (durationInMs) => {
if (durationInMs % MS_PER_DAY === 0) {
return {
duration: (durationInMs / MS_PER_DAY).toString(),
durationUnit: "days",
};
} else if (durationInMs % MS_PER_HOUR === 0) {
return {
duration: (durationInMs / MS_PER_HOUR).toString(),
durationUnit: "hours",
};
} else if (durationInMs % MS_PER_MINUTE === 0) {
return {
duration: (durationInMs / MS_PER_MINUTE).toString(),
durationUnit: "minutes",
};
} else {
return {
duration: (durationInMs / MS_PER_SECOND).toString(),
durationUnit: "seconds",
};
}
};
const MS_LOOKUP = {
seconds: MS_PER_SECOND,
minutes: MS_PER_MINUTE,
hours: MS_PER_HOUR,
days: MS_PER_DAY,
weeks: MS_PER_WEEK,
};
const REPEAT_LOOKUP = {
none: 0,
daily: MS_PER_DAY,
weekly: MS_PER_DAY * 7,
};
const REVERSE_REPEAT_LOOKUP = {
0: "none",
[MS_PER_DAY]: "daily",
[MS_PER_WEEK]: "weekly",
};
import dayjs from "dayjs";
const repeatConfig = [
{ _id: 0, name: "Don't repeat", value: "none" },
@@ -96,24 +46,10 @@ const durationConfig = [
},
];
const getValueById = (config, id) => {
const item = config.find((config) => config._id === id);
return item ? (item.value ? item.value : item.name) : null;
};
const getIdByValue = (config, name) => {
const item = config.find((config) => {
if (config.value) {
return config.value === name;
} else {
return config.name === name;
}
});
return item ? item._id : null;
};
const CreateMaintenance = () => {
const { maintenanceWindowId } = useParams();
const { handleSubmitForm } = useMaintenanceActions();
const { fetchMonitorsMaintenance, initializeMaintenanceForEdit } = useMaintenanceData();
const navigate = useNavigate();
const theme = useTheme();
const { t } = useTranslation();
@@ -121,6 +57,7 @@ const CreateMaintenance = () => {
const [monitors, setMonitors] = useState([]);
const [search, setSearch] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({});
const [form, setForm] = useState({
repeat: "none",
startDate: dayjs(),
@@ -130,43 +67,35 @@ const CreateMaintenance = () => {
name: "",
monitors: [],
});
const [errors, setErrors] = useState({});
const handleFormChange = (key, value) => {
setForm((prev) => ({ ...prev, [key]: value }));
const { error } = maintenanceWindowValidation.validate(
{ [key]: value },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
useEffect(() => {
const fetchMonitors = async () => {
setIsLoading(true);
try {
const response = await networkService.getMonitorsByTeamId({
limit: null,
types: ["http", "ping", "pagespeed", "port"],
});
const monitors = response.data.data.monitors;
setMonitors(monitors);
const fetchedMonitors = await fetchMonitorsMaintenance();
setMonitors(fetchedMonitors);
if (maintenanceWindowId === undefined) {
return;
}
const res = await networkService.getMaintenanceWindowById({
maintenanceWindowId: maintenanceWindowId,
});
const maintenanceWindow = res.data.data;
const { name, start, end, repeat, monitorId } = maintenanceWindow;
const startTime = dayjs(start);
const endTime = dayjs(end);
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
const monitor = monitors.find((monitor) => monitor._id === monitorId);
setForm({
...form,
name,
repeat: REVERSE_REPEAT_LOOKUP[repeat],
startDate: startTime,
startTime,
duration,
durationUnit,
monitors: monitor ? [monitor] : [],
});
const maintenanceWindowInformation = await initializeMaintenanceForEdit(
maintenanceWindowId,
fetchedMonitors
);
setForm((prev) => ({
...prev,
...maintenanceWindowInformation,
}));
} catch (error) {
createToast({ body: "Failed to fetch data" });
logger.error("Failed to fetch monitors", error);
@@ -177,90 +106,9 @@ const CreateMaintenance = () => {
fetchMonitors();
}, [user]);
const handleSearch = (value) => {
setSearch(value);
};
const handleSelectMonitors = (monitors) => {
setForm({ ...form, monitors });
const { error } = maintenanceWindowValidation.validate(
{ monitors },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, "monitors", error);
});
};
const handleFormChange = (key, value) => {
setForm({ ...form, [key]: value });
const { error } = maintenanceWindowValidation.validate(
{ [key]: value },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
const handleTimeChange = (key, newTime) => {
setForm({ ...form, [key]: newTime });
const { error } = maintenanceWindowValidation.validate(
{ [key]: newTime },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
const handleMonitorsChange = (selected) => {
setForm((prev) => ({ ...prev, monitors: selected }));
const { error } = maintenanceWindowValidation.validate(
{ monitors: selected },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, "monitors", error);
});
};
const handleSubmit = async () => {
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return;
// Build timestamp for maintenance window from startDate and startTime
const start = dayjs(form.startDate)
.set("hour", form.startTime.hour())
.set("minute", form.startTime.minute());
// Build end timestamp for maintenance window
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
const durationInMs = form.duration * MS_MULTIPLIER;
const end = start.add(durationInMs);
// Get repeat value in milliseconds
const repeat = REPEAT_LOOKUP[form.repeat];
const submit = {
monitors: form.monitors.map((monitor) => monitor._id),
name: form.name,
start: start.toISOString(),
end: end.toISOString(),
repeat,
};
if (repeat === 0) {
submit.expiry = end;
}
const requestConfig = { maintenanceWindow: submit };
if (maintenanceWindowId !== undefined) {
requestConfig.maintenanceWindowId = maintenanceWindowId;
}
const request =
maintenanceWindowId === undefined
? networkService.createMaintenanceWindow(requestConfig)
: networkService.editMaintenanceWindow(requestConfig);
const request = handleSubmitForm(maintenanceWindowId, form);
try {
setIsLoading(true);
await request;
@@ -353,18 +201,13 @@ const CreateMaintenance = () => {
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
<Select
<ConfigSelect
id="repeat"
name="maintenance-repeat"
label={t("maintenanceRepeat")}
value={getIdByValue(repeatConfig, form.repeat)}
onChange={(event) => {
handleFormChange(
"repeat",
getValueById(repeatConfig, event.target.value)
);
}}
items={repeatConfig}
valueSelect={form.repeat}
configSelection={repeatConfig}
onChange={(value) => handleFormChange("repeat", value)}
/>
<Stack>
<LocalizationProvider dateAdapter={AdapterDayjs}>
@@ -437,7 +280,7 @@ const CreateMaintenance = () => {
}}
sx={{}}
onChange={(newDate) => {
handleTimeChange("startDate", newDate);
handleFormChange("startDate", newDate);
}}
error={errors["startDate"]}
/>
@@ -453,7 +296,7 @@ const CreateMaintenance = () => {
>
{t("startTime")}
</Typography>
<Typography>{t("timeZoneInfo")}</Typography>
<Typography> {t("timeZoneInfo")} </Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
@@ -462,7 +305,7 @@ const CreateMaintenance = () => {
label={t("startTime")}
value={form.startTime}
onChange={(newTime) => {
handleTimeChange("startTime", newTime);
handleFormChange("startTime", newTime);
}}
slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } },
@@ -508,16 +351,11 @@ const CreateMaintenance = () => {
error={errors["duration"] ? true : false}
helperText={errors["duration"]}
/>
<Select
<ConfigSelect
id="durationUnit"
value={getIdByValue(durationConfig, form.durationUnit)}
items={durationConfig}
onChange={(event) => {
handleFormChange(
"durationUnit",
getValueById(durationConfig, event.target.value)
);
}}
valueSelect={form.durationUnit}
configSelection={durationConfig}
onChange={(value) => handleFormChange("durationUnit", value)}
error={errors["durationUnit"]}
/>
</Stack>
@@ -545,7 +383,9 @@ const CreateMaintenance = () => {
inputValue={search}
value={form.monitors}
handleInputChange={setSearch}
handleChange={handleMonitorsChange}
handleChange={(selected) => {
handleFormChange("monitors", selected);
}}
error={errors["monitors"]}
/>
<MonitorList
@@ -89,8 +89,6 @@ const CreateNotifications = () => {
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
console.log(JSON.stringify(newErrors));
console.log(JSON.stringify(form, null, 2));
createToast({ body: Object.values(newErrors)[0] });
setErrors(newErrors);
return;
+4 -4
View File
@@ -9,7 +9,7 @@ export const NOTIFICATION_TYPES = [
export const TITLE_MAP = {
email: "createNotifications.emailSettings.title",
slack: "createNotifications.slackSettings.title",
pager_duty: "createNotifications.pagerDutySettings.title",
pager_duty: "createNotifications.pagerdutySettings.title",
webhook: "createNotifications.webhookSettings.title",
discord: "createNotifications.discordSettings.title",
};
@@ -17,7 +17,7 @@ export const TITLE_MAP = {
export const DESCRIPTION_MAP = {
email: "createNotifications.emailSettings.description",
slack: "createNotifications.slackSettings.description",
pager_duty: "createNotifications.pagerDutySettings.description",
pager_duty: "createNotifications.pagerdutySettings.description",
webhook: "createNotifications.webhookSettings.description",
discord: "createNotifications.discordSettings.description",
};
@@ -25,7 +25,7 @@ export const DESCRIPTION_MAP = {
export const LABEL_MAP = {
email: "createNotifications.emailSettings.emailLabel",
slack: "createNotifications.slackSettings.webhookLabel",
pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel",
pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel",
webhook: "createNotifications.webhookSettings.webhookLabel",
discord: "createNotifications.discordSettings.webhookLabel",
};
@@ -33,7 +33,7 @@ export const LABEL_MAP = {
export const PLACEHOLDER_MAP = {
email: "createNotifications.emailSettings.emailPlaceholder",
slack: "createNotifications.slackSettings.webhookPlaceholder",
pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder",
pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder",
webhook: "createNotifications.webhookSettings.webhookPlaceholder",
discord: "createNotifications.discordSettings.webhookPlaceholder",
};
+1 -1
View File
@@ -152,7 +152,7 @@ const PageSpeedSetup = () => {
});
const { error } = monitorValidation.validate(
{ [name]: value },
{ [name]: value, type: monitor.type },
{ abortEarly: false }
);
setErrors((prev) => ({
@@ -29,7 +29,7 @@ const PieChartLegend = ({ audits }) => {
: theme.palette.tertiary.main;
// Find the position where the number ends and the unit begins
const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/);
const match = audit?.displayValue?.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/);
let value;
let unit;
if (match) {
+1 -1
View File
@@ -29,7 +29,7 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
<Select
name="showURL"
label={t("settingsPage.urlSettings.label")}
value={showURL || ""}
value={showURL === true}
onChange={handleChange}
items={[
{ _id: true, name: t("settingsPage.urlSettings.selectEnabled") },
+41 -15
View File
@@ -44,20 +44,6 @@ import {
useFetchMonitorGames,
} from "../../../Hooks/monitorHooks";
/**
* Parses a URL string and returns a URL object.
*
* @param {string} url - The URL string to parse.
* @returns {URL} - The parsed URL object if valid, otherwise an empty string.
*/
const parseUrl = (url) => {
try {
return new URL(url);
} catch (error) {
return null;
}
};
/**
* Create page renders monitor creation or configuration views.
* @component
@@ -69,6 +55,8 @@ const UptimeCreate = ({ isClone = false }) => {
// States
const [monitor, setMonitor] = useState({
type: "http",
statusWindowSize: 5,
statusWindowThreshold: 60,
matchMethod: "equal",
expectedValue: "",
jsonPath: "",
@@ -191,7 +179,10 @@ const UptimeCreate = ({ isClone = false }) => {
? `http${https ? "s" : ""}://` + monitor.url
: monitor.url,
name: monitor.name || monitor.url.substring(0, 50),
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
type: monitor.type,
port:
monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined,
interval: monitor.interval,
@@ -206,6 +197,8 @@ const UptimeCreate = ({ isClone = false }) => {
_id: monitor._id,
url: monitor.url,
name: monitor.name || monitor.url.substring(0, 50),
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
type: monitor.type,
matchMethod: monitor.matchMethod,
expectedValue: monitor.expectedValue,
@@ -299,7 +292,7 @@ const UptimeCreate = ({ isClone = false }) => {
const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing;
const displayInterval = monitor?.interval / MS_PER_MINUTE || 1;
const parsedUrl = parseUrl(monitor?.url);
const parsedUrl = monitor?.url;
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
useEffect(() => {
@@ -616,6 +609,39 @@ const UptimeCreate = ({ isClone = false }) => {
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("createMonitorPage.incidentConfigTitle")}
</Typography>
<Typography component="p">
{t("createMonitorPage.incidentConfigDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="statusWindowSize"
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
type="number"
value={monitor.statusWindowSize}
onChange={onChange}
error={errors["statusWindowSize"] ? true : false}
helperText={errors["statusWindowSize"]}
/>
<TextInput
name="statusWindowThreshold"
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
type="number"
value={monitor.statusWindowThreshold}
onChange={onChange}
error={errors["statusWindowThreshold"] ? true : false}
helperText={errors["statusWindowThreshold"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography
+4
View File
@@ -564,6 +564,10 @@ const baseTheme = (palette) => ({
"& .MuiSelect-icon": {
color: theme.palette.primary.contrastTextSecondary, // Dropdown + color
},
"& .MuiSelect-select": {
display: "flex",
alignItems: "center",
},
"&:hover": {
backgroundColor: theme.palette.primary.main, // Background on hover
},
+49 -17
View File
@@ -115,15 +115,27 @@ const monitorValidation = joi.object({
_id: joi.string(),
userId: joi.string(),
teamId: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5).messages({
"number.base": "Status window size must be a number.",
"number.min": "Status window size must be at least 1.",
"number.max": "Status window size must be at most 20.",
}),
statusWindowThreshold: joi.number().min(1).max(100).default(60).messages({
"number.base": "Incident percentage must be a number.",
"number.min": "Incident percentage must be at least 1.",
"number.max": "Incident percentage must be at most 100.",
}),
url: joi.when("type", {
is: "docker",
then: joi
.string()
.trim()
.regex(/^[a-z0-9]{64}$/)
.regex(
/^(\/+)?([a-zA-Z0-9][a-zA-Z0-9_.-]*[a-zA-Z0-9]|[a-zA-Z0-9]+|[a-f0-9]{12,64})$/
)
.messages({
"string.empty": "This field is required.",
"string.pattern.base": "Please enter a valid 64-character Docker container ID.",
"string.pattern.base": "Please enter a valid container name or ID.",
}),
otherwise: joi
.string()
@@ -150,6 +162,11 @@ const monitorValidation = joi.object({
// can be replaced by a shortest alternative
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
"(?:" +
// Single hostname without dots (like localhost)
"[a-z0-9\\u00a1-\\uffff][a-z0-9\\u00a1-\\uffff_-]{0,62}" +
"|" +
// Domain with dots
"(?:" +
"(?:" +
"[a-z0-9\\u00a1-\\uffff]" +
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
@@ -159,6 +176,7 @@ const monitorValidation = joi.object({
// TLD identifier name, may end with dot
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
")" +
")" +
// port number (optional)
"(?::\\d{2,5})?" +
// resource path (optional)
@@ -439,21 +457,35 @@ const notificationValidation = joi.object({
}),
address: joi.when("type", {
is: "email",
then: joi
.string()
.email({ tlds: { allow: false } })
.required()
.messages({
"string.empty": "E-mail address cannot be empty",
"any.required": "E-mail address is required",
"string.email": "Please enter a valid e-mail address",
}),
otherwise: joi.string().uri().required().messages({
"string.empty": "Webhook URL cannot be empty",
"any.required": "Webhook URL is required",
"string.uri": "Please enter a valid Webhook URL",
}),
switch: [
{
is: "email",
then: joi
.string()
.email({ tlds: { allow: false } })
.required()
.messages({
"string.empty": "E-mail address cannot be empty",
"any.required": "E-mail address is required",
"string.email": "Please enter a valid e-mail address",
}),
},
{
is: "pager_duty",
then: joi.string().required().messages({
"string.empty": "PagerDuty routing key cannot be empty",
"any.required": "PagerDuty routing key is required",
}),
},
{
is: joi.valid("webhook", "slack", "discord"),
then: joi.string().uri().required().messages({
"string.empty": "Webhook URL cannot be empty",
"any.required": "Webhook URL is required",
"string.uri": "Please enter a valid Webhook URL",
}),
},
],
}),
});
+85 -5
View File
@@ -260,7 +260,6 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -569,7 +568,8 @@
"password": {
"incorrect": ""
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -595,7 +595,8 @@
},
"toasts": {
"success": ""
}
},
"welcome": ""
},
"forgotPassword": {
"heading": "",
@@ -847,6 +848,10 @@
"selectDisabled": "",
"selectEnabled": "",
"title": ""
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
@@ -915,7 +920,12 @@
"twentyMinutes": "",
"oneHour": "",
"oneDay": "",
"oneWeek": ""
"oneWeek": "",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
@@ -967,5 +977,75 @@
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+85 -5
View File
@@ -260,7 +260,6 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -569,7 +568,8 @@
"password": {
"incorrect": ""
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -595,7 +595,8 @@
},
"toasts": {
"success": "Vítejte! Váš účet byl úspěšně vytvořen."
}
},
"welcome": ""
},
"forgotPassword": {
"heading": "Zapomenuté heslo",
@@ -847,6 +848,10 @@
"selectDisabled": "",
"selectEnabled": "",
"title": ""
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
@@ -915,7 +920,12 @@
"twentyMinutes": "",
"oneHour": "",
"oneDay": "",
"oneWeek": ""
"oneWeek": "",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
@@ -967,5 +977,75 @@
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+325 -220
View File
@@ -51,14 +51,14 @@
"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": "",
"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": "",
"webhookPlaceholder": "https://discord.com/api/webhooks/...",
"webhookRequired": "Discord webhook URL wird benötigt"
},
"telegram": {
@@ -78,33 +78,37 @@
"urlRequired": "Webhook URL wird benötigt"
},
"testNotificationDevelop": "Testbenachrichtigung 2",
"integrationButton": "",
"testSuccess": "",
"testFailed": "",
"unsupportedType": "",
"networkError": "",
"integrationButton": "Benachrichtigungsintegration",
"testSuccess": "Testbenachrichtigung erfolgreich gesendet!",
"testFailed": "Senden der Testbenachrichtigung fehlgeschlagen",
"unsupportedType": "Nicht unterstützter Benachrichtigungstyp",
"networkError": "Netzwerkfehler aufgetreten",
"fallback": {
"title": "",
"checks": [""],
"actionButton": ""
"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": "",
"createTitle": "",
"createButton": "Benachrichtigungskanal erstellen",
"createTitle": "Benachrichtigungskanal",
"create": {
"success": "",
"failed": ""
"success": "Benachrichtigung erfolgreich erstellt",
"failed": "Benachrichtigung konnte nicht erstellt werden"
},
"fetch": {
"success": "",
"failed": ""
"success": "Benachrichtigungen erfolgreich abgerufen",
"failed": "Abrufen der Benachrichtigungen fehlgeschlagen"
},
"delete": {
"success": "",
"failed": ""
"success": "Benachrichtigung erfolgreich gelöscht",
"failed": "Benachrichtigung konnte nicht gelöscht werden"
},
"edit": {
"success": "",
"failed": ""
"success": "Benachrichtigung erfolgreich aktualisiert",
"failed": "Benachrichtigung konnte nicht aktualisiert werden"
},
"test": {
"success": "Test Benachrichtigung erfolgreich gesendet",
@@ -227,7 +231,7 @@
"createMaintenance": "Erstelle Wartungsfenster",
"editMaintenance": "Ändere Wartungsfenster",
"maintenanceWindowName": "Name des Wartungsfensters",
"friendlyNameInput": "",
"friendlyNameInput": "Freundlicher Name",
"friendlyNamePlaceholder": "Wartung um __ : __ für ___ Minuten",
"maintenanceRepeat": "Wartungsfenster wiederholen",
"maintenance": "Wartungsfenster",
@@ -247,7 +251,7 @@
"edit": "Ändern",
"createA": "Erstelle ein",
"remove": "Löschen",
"maintenanceWindowDescription": "",
"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",
@@ -260,79 +264,78 @@
"notifyEmails": "Benachrichtige auch per E-Mail an mehrere Adressen (kommt bald)",
"seperateEmails": "Du kannst mehrere E-Mails mit einem Komma trennen",
"checkFrequency": "Überprüfungsfrequenz",
"chooseGame": "",
"matchMethod": "",
"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": "",
"shown": "",
"pageSpeedMonitor": "PageSpeed-Monitor",
"shown": "Gezeigt",
"ago": "vor",
"companyName": "",
"pageSpeedDetailsPerformanceReport": "",
"pageSpeedDetailsPerformanceReportCalculator": "",
"checkingEvery": "",
"statusPageCreateSettings": "",
"basicInformation": "",
"statusPageCreateBasicInfoDescription": "",
"statusPageCreateSelectTimeZoneDescription": "",
"statusPageCreateAppearanceDescription": "",
"statusPageCreateSettingsCheckboxLabel": "",
"statusPageCreateBasicInfoStatusPageAddress": "",
"statusPageCreateTabsContent": "",
"statusPageCreateTabsContentDescription": "",
"statusPageCreateTabsContentFeaturesDescription": "",
"showCharts": "",
"showUptimePercentage": "",
"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": "",
"statusPageStatusContactAdmin": "",
"statusPageStatusNotPublic": "",
"statusPageStatusNoPage": "",
"statusPageStatusServiceStatus": "",
"deleteStatusPage": "",
"deleteStatusPageConfirm": "",
"deleteStatusPageDescription": "",
"uptimeCreate": "",
"uptimeCreateJsonPath": "",
"uptimeCreateJsonPathQuery": "",
"maintenanceTableActionMenuDialogTitle": "",
"infrastructureEditYour": "",
"infrastructureEditMonitor": "",
"infrastructureMonitorCreated": "",
"infrastructureMonitorUpdated": "",
"errorInvalidTypeId": "",
"errorInvalidFieldId": "",
"inviteNoTokenFound": "",
"pageSpeedWarning": "",
"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": "",
"ClickUpload": "Zum Hochladen klicken",
"DragandDrop": "drag and drop",
"MaxSize": "Maximalgröße",
"SupportedFormats": "Unterstützte Formate",
"FirstName": "Vorname",
"LastName": "Zuname",
"EmailDescriptionText": "",
"EmailDescriptionText": "Dies ist Ihre aktuelle E-Mail-Adresse – sie kann nicht geändert werden.",
"YourPhoto": "Profilphoto",
"PhotoDescriptionText": "",
"PhotoDescriptionText": "Dieses Foto wird auf Ihrer Profilseite angezeigt.",
"save": "Speichern",
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"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": "",
"selectFileTips": "",
"selectFileDescription": "",
"title": "Massenimport",
"selectFileTips": "Wählen Sie die CSV-Datei zum Hochladen aus",
"selectFileDescription": "Sie können unsere <template>Vorlage</template> oder <sample>Beispiel</sample> herunterladen.",
"selectFile": "Datei auswählen",
"parsingFailed": "",
"parsingFailed": "Parsen fehlgeschlagen",
"uploadSuccess": "Monitore erfolgreich erstellt!",
"validationFailed": "Prüfung fehlgeschlagen",
"noFileSelected": "Keine Datei ausgewählt",
"fallbackPage": "",
"fallbackPage": "Importieren Sie eine Datei, um eine Liste von Servern gleichzeitig hochzuladen",
"invalidFileType": "Ungültiger Dateityp",
"uploadFailed": "Upload fehlgeschlagen"
},
@@ -347,8 +350,8 @@
"createNew": "Neu anlegen",
"greeting": {
"prepend": "Hallo",
"append": "",
"overview": ""
"append": "Der Nachmittag gehört Ihnen – machen wir ihn episch!",
"overview": "Hier ist eine Übersicht Ihrer {{type}}-Monitore."
},
"roles": {
"superAdmin": "Super admin",
@@ -364,7 +367,7 @@
},
"inviteTeamMember": "Team Mirglied einladen",
"inviteNewTeamMember": "Neues Team Mirglied einladen",
"inviteDescription": "",
"inviteDescription": "Wenn Sie ein neues Teammitglied hinzufügen, erhält dieses Zugriff auf alle Monitore.",
"email": "Email",
"selectRole": "Rolle auswählen",
"inviteLink": "Einladungslink",
@@ -381,17 +384,17 @@
},
"monitorState": {
"paused": "Pausiert",
"resumed": "",
"resumed": "Wiederaufgenommen",
"active": "Aktiv"
},
"menu": {
"uptime": "Uptime",
"pagespeed": "",
"infrastructure": "",
"incidents": "",
"pagespeed": "Pagespeed",
"infrastructure": "Infrastruktur",
"incidents": "Vorfälle",
"statusPages": "Statusseite",
"maintenance": "Wartung",
"integrations": "",
"integrations": "Integrationen",
"settings": "Einstellungen",
"support": "Support",
"discussions": "Diskussionen",
@@ -404,12 +407,12 @@
"notifications": "Benachrichtigungen",
"logs": "Logs"
},
"settingsEmailUser": "",
"state": "",
"settingsEmailUser": "E-Mail-Benutzer – Benutzername zur Authentifizierung, überschreibt die E-Mail-Adresse, falls angegeben",
"state": "Zustand",
"statusBreadCrumbsStatusPages": "Statusseite",
"statusBreadCrumbsDetails": "Details",
"commonSaving": "Speichere...",
"navControls": "",
"navControls": "Steuerungen",
"incidentsPageTitle": "Ereignisse",
"passwordPanel": {
"passwordChangedSuccess": "Dein Passwort wurde erfolgreich geändert.",
@@ -430,22 +433,22 @@
"settingsTestEmailUnknownError": "Unbekannter Fehler",
"statusMsg": {
"paused": "Monitoring pausiert",
"up": "",
"down": "",
"pending": ""
"up": "Ihre Site ist online.",
"down": "Ihre Site ist nicht erreichbar.",
"pending": "Ausstehend..."
},
"uptimeGeneralInstructions": {
"http": "",
"ping": "",
"docker": "",
"port": "",
"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 <short_id> 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": ""
},
"common": {
"appName": "Checkmate",
"monitoringAgentName": "Capture",
"buttons": {
"toggleTheme": ""
"toggleTheme": "Schaltet hell und dunkel um"
},
"toasts": {
"networkError": "Netzwerkfehler",
@@ -465,7 +468,7 @@
"placeholder": "jordan.ellis@domain.com",
"errors": {
"empty": "Email adresse eingeben um fortzufahren",
"invalid": ""
"invalid": "Bitte überprüfen Sie die Gültigkeit der eingegebenen E-Mail-Adresse erneut"
}
},
"password": {
@@ -492,7 +495,7 @@
"highlighted": "einen Kleinbuchstaben enthalten"
},
"match": {
"beginning": "",
"beginning": "Passwort und Kennwort bestätigen",
"highlighted": "muß übereinstimmen"
}
},
@@ -544,7 +547,7 @@
},
"role": {
"errors": {
"min": ""
"min": "Mindestens eine Rolle ist erforderlich"
}
}
}
@@ -569,7 +572,8 @@
"password": {
"incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein"
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -577,25 +581,26 @@
"user": "Anmelden"
},
"subheadings": {
"stepOne": "",
"stepOne": "Geben Sie Ihre persönlichen Daten ein",
"stepTwo": "Email eingeben",
"stepThree": "Passwort eingeben"
},
"description": {
"superAdmin": "Super admin account anlegen um zu beginnen",
"user": ""
"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": "",
"termsAndPolicies": "Indem Sie ein Konto erstellen, stimmen Sie unseren <a1>Servicebedingungen</a1> und <a2>Datenschutzrichtlinien</a2> zu.",
"links": {
"login": "Du hast bereits einen Benutzer? <a>Anmelden</a>"
},
"toasts": {
"success": "Willkommen! Benutzer erfolgreich angelegt"
}
},
"welcome": ""
},
"forgotPassword": {
"heading": "Passwort vergessen?",
@@ -624,7 +629,7 @@
"emailNotFound": "Email nicht gefunden.",
"redirect": "Umleitung in <seconds/>...",
"success": "Passwort erfolgreich zurückgesetzt.",
"error": ""
"error": "Kennwort konnte nicht zurückgesetzt werden. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support."
}
}
},
@@ -635,7 +640,7 @@
"stillUnreachable": "Server immer noch nicht erreichbar. Bitte später erneut versuchen."
},
"alertBox": "Server Verbindungsfehler",
"description": "",
"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..."
@@ -643,16 +648,16 @@
}
},
"createNotifications": {
"title": "",
"title": "Benachrichtigungskanal erstellen",
"nameSettings": {
"title": "Name",
"description": "",
"description": "Ein beschreibender Name für Ihre Integration.",
"nameLabel": "Name",
"namePlaceholder": "z.B. Slack Benachrichtigung"
},
"typeSettings": {
"title": "Typ",
"description": "",
"description": "Wählen Sie den Typ des Benachrichtigungskanals aus, den Sie erstellen möchten.",
"typeLabel": "Typ"
},
"emailSettings": {
@@ -665,7 +670,7 @@
"title": "Slack",
"description": "Slack webhook hier konfigurieren",
"webhookLabel": "Slack webhook URL",
"webhookPlaceholder": ""
"webhookPlaceholder": "https://hooks.slack.com/services/..."
},
"pagerdutySettings": {
"title": "PagerDuty",
@@ -685,38 +690,38 @@
"webhookLabel": "Webhook URL",
"webhookPlaceholder": "https://your-server.com/webhook"
},
"testNotification": "",
"dialogDeleteTitle": "",
"dialogDeleteConfirm": ""
"testNotification": "Testbenachrichtigung",
"dialogDeleteTitle": "Möchten Sie diese Benachrichtigung wirklich löschen?",
"dialogDeleteConfirm": "Löschen"
},
"notificationConfig": {
"title": "Benachrichtigungen",
"description": ""
"description": "Wählen Sie die Benachrichtigungskanäle aus, die Sie verwenden möchten"
},
"monitorStatus": {
"checkingEvery": "",
"withCaptureAgent": "",
"checkingEvery": "Überprüfung alle {{interval}}",
"withCaptureAgent": "mit Capture-Agent {{version}}",
"up": "hoch",
"down": "runter",
"paused": "pausiert"
},
"advancedMatching": "",
"advancedMatching": "Erweiterter Abgleich",
"sendTestNotifications": "Test Benachrichtigung senden",
"selectAll": "Alle auswählen",
"showAdminLoginLink": "",
"showAdminLoginLink": "Link „Administrator? Hier anmelden“ auf der Statusseite anzeigen",
"logsPage": {
"title": "Logs",
"description": "",
"description": "Systemprotokolle – letzte 1000 Zeilen",
"tabs": {
"queue": "Warteschlange",
"logs": "Server logs",
"diagnostics": ""
"diagnostics": "Diagnose"
},
"toast": {
"fetchLogsSuccess": ""
"fetchLogsSuccess": "Protokolle erfolgreich abgerufen"
},
"logLevelSelect": {
"title": "",
"title": "Protokollebene",
"values": {
"all": "Alle",
"info": "Info",
@@ -736,7 +741,7 @@
"urlHeader": "URL",
"typeHeader": "Typ",
"activeHeader": "Aktiv",
"lockedAtHeader": "",
"lockedAtHeader": "Gesperrt bei",
"runCountHeader": "Anzahl Läufe",
"failCountHeader": "Anzahl Fehler",
"lastRunHeader": "Letzter Lauf um",
@@ -744,8 +749,8 @@
"lastRunTookHeader": "Letzter Lauf dauerte"
},
"metricsTable": {
"title": "",
"metricHeader": "",
"title": "Warteschlangenmetriken",
"metricHeader": "Metrisch",
"valueHeader": "Wert"
},
"failedJobTable": {
@@ -766,9 +771,9 @@
"title": "Export/Import",
"import": "Monitore importieren",
"export": "Monitore exportieren",
"deleteSuccess": "",
"deleteFailed": "",
"details": ""
"deleteSuccess": "Monitor erfolgreich gelöscht",
"deleteFailed": "Monitor konnte nicht gelöscht werden",
"details": "Details"
},
"settingsPage": {
"aboutSettings": {
@@ -783,43 +788,43 @@
},
"emailSettings": {
"buttonSendTestEmail": "Test-Email senden",
"description": "",
"descriptionTransport": "",
"labelAddress": "",
"labelConnectionHost": "",
"labelHost": "",
"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": "",
"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": "",
"labelRequireTLS": "STARTTLS erzwingen: TLS-Upgrade erforderlich, schlägt fehl, wenn nicht unterstützt",
"labelSecure": "SSL verwenden (empfohlen): Verbindung mit SSL/TLS verschlüsseln",
"labelTLSServername": "",
"labelUser": "",
"linkTransport": "",
"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": ""
"toastEmailRequiredFieldsError": "E-Mail-Adresse, Host, Port und Passwort sind erforderlich"
},
"pageSpeedSettings": {
"description": "",
"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"
},
"saveButtonLabel": "Speichern",
"statsSettings": {
"clearAllStatsButton": "",
"clearAllStatsDescription": "",
"clearAllStatsDialogConfirm": "",
"clearAllStatsDialogDescription": "",
"clearAllStatsDialogTitle": "",
"description": "",
"labelTTL": "",
"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": ""
"title": "Monitorverlauf"
},
"systemResetSettings": {
"buttonRemoveAllMonitors": "Alle Monitore löschen",
@@ -830,142 +835,242 @@
"title": "System zurücksetzen"
},
"timezoneSettings": {
"description": "",
"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": "",
"description": "Wechseln Sie zwischen hellem und dunklem Modus oder ändern Sie die Sprache der Benutzeroberfläche.",
"labelLanguage": "Sprache",
"labelTheme": "",
"title": ""
"labelTheme": "Themenmodus",
"title": "Aussehen"
},
"urlSettings": {
"description": "",
"label": "",
"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": ""
"title": "Überwachen Sie IP/URL auf der Statusseite"
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
"buttonSave": "Speichern"
},
"incidentsOptionsHeaderFilterResolved": "",
"incidentsOptionsHeaderFilterResolved": "Gelöst",
"settingsSave": "Speichern",
"statusPageCreateAppearanceTitle": "",
"statusPageCreateAppearanceTitle": "Aussehen",
"confirmPassword": "Passwort bestätigen",
"monitorHooks": {
"failureAddDemoMonitors": "Demo Monitor konnte nicht hinzugefügt werden",
"successAddDemoMonitors": "Demo Monitor erfolgreich hinzugefügt"
},
"settingsAppearance": "",
"settingsDisplayTimezone": "",
"settingsGeneralSettings": "",
"incidentsOptionsHeaderTotalIncidents": "",
"settingsAppearance": "Aussehen",
"settingsDisplayTimezone": "Zeitzone anzeigen",
"settingsGeneralSettings": "Allgemeine Einstellungen",
"incidentsOptionsHeaderTotalIncidents": "Gesamtanzahl der Vorfälle",
"statusPage": {
"deleteSuccess": "",
"deleteFailed": "",
"createSuccess": "",
"updateSuccess": "",
"generalSettings": "",
"contents": "",
"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": [""],
"title": "",
"actionButton": ""
"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": "",
"incidentsTableResolvedAt": "",
"incidentsTableActionResolve": "",
"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": "",
"failureResolveAll": "",
"failureResolveMonitor": ""
"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": "",
"checkFormError": "Bitte überprüfen Sie das Formular auf Fehler.",
"diagnosticsPage": {
"diagnosticDescription": "",
"statsDescription": "",
"diagnosticDescription": "Systemdiagnose",
"statsDescription": "Systemstatistiken",
"gauges": {
"heapAllocationTitle": "",
"heapAllocationSubtitle": "",
"heapUsageTitle": "",
"heapUsageSubtitle": "",
"heapUtilizationTitle": "",
"heapUtilizationSubtitle": "",
"instantCpuUsageTitle": "",
"instantCpuUsageSubtitle": ""
"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": "",
"uptimeTitle": "",
"usedHeapSizeTitle": "",
"totalHeapSizeTitle": "",
"osMemoryLimitTitle": ""
"eventLoopDelayTitle": "Ereignisschleifenverzögerung",
"uptimeTitle": "Betriebszeit",
"usedHeapSizeTitle": "Verwendete Heap-Größe",
"totalHeapSizeTitle": "Gesamtgröße des Heapspeichers",
"osMemoryLimitTitle": "Betriebssystem-Speicherlimit"
}
},
"pageSpeedLighthouseAPI": "",
"pageSpeedLighthouseAPI": "Verwenden Sie die Lighthouse PageSpeed API, um Ihre Website zu überwachen",
"time": {
"threeMinutes": "",
"fiveMinutes": "",
"tenMinutes": "",
"twentyMinutes": "",
"oneHour": "",
"oneDay": "",
"oneWeek": ""
"threeMinutes": "3 Minuten",
"fiveMinutes": "5 Minuten",
"tenMinutes": "10 Minuten",
"twentyMinutes": "20 Minuten",
"oneHour": "1 Stunde",
"oneDay": "1 Tag",
"oneWeek": "1 Woche",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
"noOptionsFound": "Keine {{unit}} gefunden"
},
"infrastructureMonitor": {
"fallback": {
"checks": [""],
"title": "",
"actionButton": ""
"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": [""],
"title": "",
"actionButton": ""
"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": [""],
"title": "",
"actionButton": ""
"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": [""],
"title": "",
"actionButton": ""
"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": "",
"firstName": "",
"lastName": "",
"role": "",
"save": ""
"email": "E-Mail",
"firstName": "Vorname",
"lastName": "Nachname",
"role": "Rollen",
"save": "Speichern"
},
"table": {
"actionHeader": "",
"roleHeader": ""
"actionHeader": "Aktion",
"roleHeader": "Rolle"
},
"title": "",
"title": "Benutzer bearbeiten",
"toast": {
"successUserUpdate": "",
"validationErrors": ""
"successUserUpdate": "Benutzer erfolgreich aktualisiert",
"validationErrors": "Validierungsfehler"
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"incidentsPageActionResolveMonitor": "Monitor-Vorfälle beheben",
"incidentsPageActionResolveAll": "Alle Vorfälle lösen",
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+885 -878
View File
File diff suppressed because it is too large. Load diff
+85 -5
View File
@@ -260,7 +260,6 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -569,7 +568,8 @@
"password": {
"incorrect": ""
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -595,7 +595,8 @@
},
"toasts": {
"success": ""
}
},
"welcome": ""
},
"forgotPassword": {
"heading": "",
@@ -847,6 +848,10 @@
"selectDisabled": "",
"selectEnabled": "",
"title": ""
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
@@ -915,7 +920,12 @@
"twentyMinutes": "",
"oneHour": "",
"oneDay": "",
"oneWeek": ""
"oneWeek": "",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
@@ -967,5 +977,75 @@
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+85 -5
View File
@@ -260,7 +260,6 @@
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
@@ -569,7 +568,8 @@
"password": {
"incorrect": ""
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -595,7 +595,8 @@
},
"toasts": {
"success": ""
}
},
"welcome": ""
},
"forgotPassword": {
"heading": "",
@@ -847,6 +848,10 @@
"selectDisabled": "",
"selectEnabled": "",
"title": ""
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
@@ -915,7 +920,12 @@
"twentyMinutes": "",
"oneHour": "",
"oneDay": "",
"oneWeek": ""
"oneWeek": "",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
@@ -967,5 +977,75 @@
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+130 -29
View File
@@ -90,7 +90,7 @@
"Informez les ingénieurs lorsque des incidents se produisent",
"Tenez les administrateurs informés des changements apportés au système"
],
"actionButton": ""
"actionButton": "Créez votre premier canal de notification !"
},
"createButton": "Créer un canal de notification",
"createTitle": "Canal de notification",
@@ -264,7 +264,6 @@
"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",
"chooseGame": "",
"matchMethod": "Méthode de rapprochement",
"expectedValue": "Valeur attendue",
"deleteDialogTitle": "Voulez-vous vraiment supprimer ce moniteur ?",
@@ -548,7 +547,7 @@
},
"role": {
"errors": {
"min": ""
"min": "Au moins un rôle est requis"
}
}
}
@@ -573,7 +572,8 @@
"password": {
"incorrect": "Le mot de passe fourni est erroné"
}
}
},
"welcome": "Bon retour sur Checkmate !"
},
"registration": {
"heading": {
@@ -599,7 +599,8 @@
},
"toasts": {
"success": "Bienvenue ! Votre compte a été créé avec succès."
}
},
"welcome": "Bienvenue sur Checkmate !"
},
"forgotPassword": {
"heading": "Mot de passe oublié ?",
@@ -851,6 +852,10 @@
"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": {
@@ -876,9 +881,13 @@
"generalSettings": "Paramètres généraux",
"contents": "Contenus",
"fallback": {
"checks": [""],
"title": "",
"actionButton": ""
"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'",
@@ -919,57 +928,149 @@
"twentyMinutes": "20 minutes",
"oneHour": "1 heure",
"oneDay": "1 jour",
"oneWeek": "1 semaine"
"oneWeek": "1 semaine",
"fourMinutes": "4 minutes",
"oneMinute": "1 minute",
"twoMinutes": "2 minutes",
"fifteenSeconds": "15 secondes",
"thirtySeconds": "30 secondes"
},
"general": {
"noOptionsFound": ""
"noOptionsFound": "Aucune {{unit}} trouvée"
},
"infrastructureMonitor": {
"fallback": {
"checks": [""],
"title": "",
"actionButton": ""
"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": [""],
"title": "",
"actionButton": ""
"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": [""],
"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": [""],
"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": "",
"firstName": "",
"lastName": "",
"role": "",
"save": ""
"email": "Email",
"firstName": "Prénom",
"lastName": "Nom",
"role": "Rôles",
"save": "Enregistrer"
},
"table": {
"actionHeader": "",
"roleHeader": ""
"actionHeader": "Action",
"roleHeader": "Rôle"
},
"title": "",
"title": "Modifier l'utilisateur",
"toast": {
"successUserUpdate": "",
"successUserUpdate": "L'utilisateur à bien été modifié",
"validationErrors": ""
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+85 -5
View File
@@ -264,7 +264,6 @@
"notifyEmails": "複数のアドレスにメールでも通知(近日公開)",
"seperateEmails": "複数のメールはカンマで区切ることができます",
"checkFrequency": "チェック頻度",
"chooseGame": "",
"matchMethod": "マッチ方法",
"expectedValue": "期待値",
"deleteDialogTitle": "本当にこのモニターを削除しますか?",
@@ -573,7 +572,8 @@
"password": {
"incorrect": "入力されたパスワードが記録と一致しません"
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -599,7 +599,8 @@
},
"toasts": {
"success": "ようこそ!アカウントが正常に作成されました。"
}
},
"welcome": ""
},
"forgotPassword": {
"heading": "パスワードを忘れましたか?",
@@ -851,6 +852,10 @@
"selectDisabled": "無効",
"selectEnabled": "有効",
"title": "ステータスページのモニターIP/URL"
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
@@ -919,7 +924,12 @@
"twentyMinutes": "20分",
"oneHour": "1時間",
"oneDay": "1日",
"oneWeek": "1週間"
"oneWeek": "1週間",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
@@ -971,5 +981,75 @@
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+400 -295
View File
@@ -84,31 +84,35 @@
"unsupportedType": "Tipo de notificação não suportado",
"networkError": "Ocorreu um erro de rede",
"fallback": {
"title": "",
"checks": [""],
"actionButton": ""
"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": "",
"createTitle": "",
"createButton": "Criar canal de notificação",
"createTitle": "Canal de notificação",
"create": {
"success": "",
"failed": ""
"success": "Notificação criada com sucesso",
"failed": "Falha ao criar notificação"
},
"fetch": {
"success": "",
"failed": ""
"success": "Notificações obtidas com sucesso",
"failed": "Falha ao buscar notificações"
},
"delete": {
"success": "",
"failed": ""
"success": "Notificação excluída com sucesso",
"failed": "Falha ao excluir a notificação"
},
"edit": {
"success": "",
"failed": ""
"success": "Notificação atualizada com sucesso",
"failed": "Falha ao atualizar a notificação"
},
"test": {
"success": "",
"failed": ""
"success": "Notificação de teste enviada com sucesso",
"failed": "Falha ao enviar notificação de teste"
}
},
"testLocale": "testLocale",
@@ -260,7 +264,6 @@
"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",
"chooseGame": "",
"matchMethod": "Método de correspondência",
"expectedValue": "Valor esperado",
"deleteDialogTitle": "Você realmente deseja excluir este monitor?",
@@ -333,8 +336,8 @@
"validationFailed": "Falha na validação",
"noFileSelected": "Nenhum arquivo selecionado",
"fallbackPage": "Importe um arquivo para enviar uma lista de servidores em massa",
"invalidFileType": "",
"uploadFailed": ""
"invalidFileType": "Tipo de arquivo inválido",
"uploadFailed": "Falha no upload"
},
"DeleteAccountTitle": "Remover conta",
"DeleteAccountButton": "Remover conta",
@@ -401,8 +404,8 @@
"password": "Senha",
"team": "Equipe",
"logOut": "Sair",
"notifications": "",
"logs": ""
"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",
@@ -477,41 +480,41 @@
},
"special": {
"beginning": "Deve conter pelo menos",
"highlighted": ""
"highlighted": "um caractere especial"
},
"number": {
"beginning": "",
"highlighted": ""
"beginning": "Deve ter pelo menos",
"highlighted": "um número"
},
"uppercase": {
"beginning": "",
"highlighted": ""
"beginning": "Deve ter pelo menos",
"highlighted": "uma letra maiúscula"
},
"lowercase": {
"beginning": "",
"highlighted": ""
"beginning": "Deve ter pelo menos",
"highlighted": "uma letra minúscula"
},
"match": {
"beginning": "",
"highlighted": ""
"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": "",
"number": "",
"special": "",
"incorrect": ""
"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": "",
"placeholder": "",
"label": "Confirme a senha",
"placeholder": "Informe a senha novamente para confirmar",
"errors": {
"empty": "",
"different": ""
"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": {
@@ -539,12 +542,12 @@
"fields": {
"password": {
"errors": {
"incorrect": ""
"incorrect": "A senha que você forneceu não corresponde aos nossos registros"
}
},
"role": {
"errors": {
"min": ""
"min": "Pelo menos uma função é necessária"
}
}
}
@@ -558,8 +561,8 @@
"links": {
"forgotPassword": "Esqueceu sua senha?",
"register": "Não tem uma conta?",
"forgotPasswordLink": "",
"registerLink": ""
"forgotPasswordLink": "Redefinir senha",
"registerLink": "Registre-se aqui"
},
"toasts": {
"success": "Bem-vindo de volta! Você está autenticado.",
@@ -567,9 +570,10 @@
},
"errors": {
"password": {
"incorrect": ""
"incorrect": "A senha que você forneceu não corresponde aos nossos registros"
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -577,42 +581,43 @@
"user": "Se inscrever"
},
"subheadings": {
"stepOne": "",
"stepTwo": "",
"stepThree": ""
"stepOne": "Informe seus dados pessoais",
"stepTwo": "Informe seu email",
"stepThree": "Crie sua senha"
},
"description": {
"superAdmin": "",
"user": ""
"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": "",
"user": ""
"superAdmin": "Criar conta de superadministrador",
"user": "Cadastre-se como usuário normal"
},
"termsAndPolicies": "",
"termsAndPolicies": "Ao criar uma conta, você concorda com nossos <a1>Termos de Serviço</a1> e <a2>Política de Privacidade</a2>.",
"links": {
"login": ""
"login": "Já tem uma conta? <a>Entrar</a>"
},
"toasts": {
"success": ""
}
"success": "Bem-vindo! Sua conta foi criada com sucesso."
},
"welcome": ""
},
"forgotPassword": {
"heading": "",
"heading": "Esqueceu sua senha?",
"subheadings": {
"stepOne": "",
"stepTwo": "",
"stepThree": "",
"stepFour": ""
"stepOne": "Não se preocupe, enviaremos instruções de redefinição.",
"stepTwo": "Enviamos um link de redefinição de senha para <email/>",
"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": "",
"resetPassword": ""
"openEmail": "Abra o aplicativo de e-mail",
"resetPassword": "Redefinir senha"
},
"imageAlts": {
"passwordKey": "",
"email": "",
"lock": "",
"passwordKey": "Ícone de chave de senha",
"email": "Ícone de e-mail",
"lock": "Ícone de cadeado",
"passwordConfirm": "Ícone de confirmação de senha"
},
"links": {
@@ -620,352 +625,452 @@
"resend": "Não recebeu o email? <a>Clique para reenviar</a>"
},
"toasts": {
"sent": "",
"emailNotFound": "",
"redirect": "",
"success": "",
"error": ""
"sent": "Instruções enviadas para <email/>.",
"emailNotFound": "E-mail não encontrado.",
"redirect": "Redirecionando em <segundos/>...",
"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."
}
}
},
"errorPages": {
"serverUnreachable": {
"toasts": {
"reconnected": "",
"stillUnreachable": ""
"reconnected": "Reconectado ao servidor com sucesso.",
"stillUnreachable": "O servidor ainda está inacessível. Tente novamente mais tarde."
},
"alertBox": "",
"description": "",
"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": "",
"processing": ""
"default": "Tentar conectar novamente",
"processing": "Conectando..."
}
}
},
"createNotifications": {
"title": "",
"title": "Criar canal de notificação",
"nameSettings": {
"title": "",
"description": "",
"nameLabel": "",
"namePlaceholder": ""
"title": "Nome",
"description": "Um nome descritivo para sua integração.",
"nameLabel": "Nome",
"namePlaceholder": "por exemplo, notificações do Slack"
},
"typeSettings": {
"title": "",
"description": "",
"typeLabel": ""
"title": "Tipo",
"description": "Selecione o tipo de canal de notificação que você deseja criar.",
"typeLabel": "Tipo"
},
"emailSettings": {
"title": "",
"description": "",
"emailLabel": "",
"emailPlaceholder": ""
"title": "E-mail",
"description": "Endereços de e-mail de destino.",
"emailLabel": "Endereço de email",
"emailPlaceholder": "por exemplo john@example.com"
},
"slackSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
"title": "Folga",
"description": "Configure seu webhook do Slack aqui",
"webhookLabel": "URL do webhook do Slack",
"webhookPlaceholder": "https://hooks.slack.com/services/..."
},
"pagerdutySettings": {
"title": "",
"description": "",
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
"title": "PagerDuty",
"description": "Configure sua integração com o PagerDuty aqui",
"integrationKeyLabel": "Chave de integração",
"integrationKeyPlaceholder": "1234567890"
},
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
"title": "Discórdia",
"description": "Configure seu webhook do Discord aqui",
"webhookLabel": "URL do webhook do Discord",
"webhookPlaceholder": "https://seu-servidor.com/webhook"
},
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
"title": "Webhook",
"description": "Configure seu webhook aqui",
"webhookLabel": "URL do webhook",
"webhookPlaceholder": "https://seu-servidor.com/webhook"
},
"testNotification": "",
"dialogDeleteTitle": "",
"dialogDeleteConfirm": ""
"testNotification": "Notificação de teste",
"dialogDeleteTitle": "Tem certeza de que deseja excluir esta notificação?",
"dialogDeleteConfirm": "Excluir"
},
"notificationConfig": {
"title": "",
"description": ""
"title": "Notificações",
"description": "Selecione os canais de notificação que deseja usar"
},
"monitorStatus": {
"checkingEvery": "",
"withCaptureAgent": "",
"up": "",
"down": "",
"paused": ""
"checkingEvery": "Verificando a cada {{intervalo}}",
"withCaptureAgent": "com agente de captura {{versão}}",
"up": "acima",
"down": "abaixo",
"paused": "pausado"
},
"advancedMatching": "",
"sendTestNotifications": "",
"selectAll": "",
"showAdminLoginLink": "",
"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": "",
"description": "",
"title": "Registros",
"description": "Logs do sistema - últimas 1000 linhas",
"tabs": {
"queue": "",
"logs": "",
"diagnostics": ""
"queue": "Fila de tarefas",
"logs": "Logs do servidor",
"diagnostics": "Diagnóstico"
},
"toast": {
"fetchLogsSuccess": ""
"fetchLogsSuccess": "Logs obtidos com sucesso"
},
"logLevelSelect": {
"title": "",
"title": "Nível de log",
"values": {
"all": "",
"info": "",
"warn": "",
"error": "",
"debug": ""
"all": "Todos",
"info": "Informações",
"warn": "Avisar",
"error": "Erro",
"debug": "Depurar"
}
}
},
"queuePage": {
"title": "",
"refreshButton": "",
"flushButton": "",
"title": "Fila",
"refreshButton": "Atualizar",
"flushButton": "Fila de descarga",
"jobTable": {
"title": "",
"idHeader": "",
"urlHeader": "",
"typeHeader": "",
"activeHeader": "",
"lockedAtHeader": "",
"runCountHeader": "",
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
"lastRunTookHeader": ""
"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"
},
"metricsTable": {
"title": "",
"metricHeader": "",
"valueHeader": ""
"title": "Métricas de fila",
"metricHeader": "Métrica",
"valueHeader": "Valor"
},
"failedJobTable": {
"title": "",
"monitorIdHeader": "",
"monitorUrlHeader": "",
"failCountHeader": "",
"failedAtHeader": "",
"failReasonHeader": ""
"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": "",
"success": "",
"failed": ""
"title": "Monitores de Exportação",
"success": "Monitores exportados com sucesso!",
"failed": "Falha ao exportar monitores"
},
"monitorActions": {
"title": "",
"import": "",
"export": "",
"deleteSuccess": "",
"deleteFailed": "",
"details": ""
"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": "",
"labelVersion": "",
"title": ""
"labelDevelopedBy": "Desenvolvido pela Bluewave Labs",
"labelVersion": "Versão",
"title": "Sobre"
},
"demoMonitorsSettings": {
"buttonAddMonitors": "",
"description": "",
"title": ""
"buttonAddMonitors": "Adicionar monitores de demonstração",
"description": "Adicione monitores de amostra para fins de demonstração.",
"title": "Monitores de demonstração"
},
"emailSettings": {
"buttonSendTestEmail": "",
"description": "",
"descriptionTransport": "",
"labelAddress": "",
"labelConnectionHost": "",
"labelHost": "",
"labelIgnoreTLS": "",
"labelPassword": "",
"labelPasswordSet": "",
"labelPool": "",
"labelPort": "",
"labelRejectUnauthorized": "",
"labelRequireTLS": "",
"labelSecure": "",
"labelTLSServername": "",
"labelUser": "",
"linkTransport": "",
"placeholderUser": "",
"title": "",
"toastEmailRequiredFieldsError": ""
"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"
},
"pageSpeedSettings": {
"description": "",
"labelApiKeySet": "",
"labelApiKey": "",
"title": ""
"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"
},
"saveButtonLabel": "",
"saveButtonLabel": "Salvar",
"statsSettings": {
"clearAllStatsButton": "",
"clearAllStatsDescription": "",
"clearAllStatsDialogConfirm": "",
"clearAllStatsDialogDescription": "",
"clearAllStatsDialogTitle": "",
"description": "",
"labelTTL": "",
"labelTTLOptional": "",
"title": ""
"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"
},
"systemResetSettings": {
"buttonRemoveAllMonitors": "",
"description": "",
"dialogConfirm": "",
"dialogDescription": "",
"dialogTitle": "",
"title": ""
"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": "",
"label": "",
"title": ""
"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": "",
"title": "Configurações",
"uiSettings": {
"description": "",
"labelLanguage": "",
"labelTheme": "",
"title": ""
"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": "",
"label": "",
"selectDisabled": "",
"selectEnabled": "",
"title": ""
"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": ""
"buttonSave": "Salvar"
},
"incidentsOptionsHeaderFilterResolved": "",
"settingsSave": "",
"statusPageCreateAppearanceTitle": "",
"confirmPassword": "",
"incidentsOptionsHeaderFilterResolved": "Resolvido",
"settingsSave": "Salvar",
"statusPageCreateAppearanceTitle": "Aparência",
"confirmPassword": "Confirme sua senha",
"monitorHooks": {
"failureAddDemoMonitors": "",
"successAddDemoMonitors": ""
"failureAddDemoMonitors": "Falha ao adicionar monitores de demonstração",
"successAddDemoMonitors": "Monitores de demonstração adicionados com sucesso"
},
"settingsAppearance": "",
"settingsDisplayTimezone": "",
"settingsGeneralSettings": "",
"incidentsOptionsHeaderTotalIncidents": "",
"settingsAppearance": "Aparência",
"settingsDisplayTimezone": "Exibir fuso horário",
"settingsGeneralSettings": "Configurações gerais",
"incidentsOptionsHeaderTotalIncidents": "Total de incidentes",
"statusPage": {
"deleteSuccess": "",
"deleteFailed": "",
"createSuccess": "",
"updateSuccess": "",
"generalSettings": "",
"contents": "",
"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": [""],
"title": "",
"actionButton": ""
"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": "",
"incidentsTableResolvedAt": "",
"incidentsTableActionResolve": "",
"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": "",
"failureResolveAll": "",
"failureResolveMonitor": ""
"failureResolveOne": "Falha ao resolver o incidente.",
"failureResolveAll": "Falha ao resolver todos os incidentes.",
"failureResolveMonitor": "Falha ao resolver incidentes do monitor."
},
"checkFormError": "",
"checkFormError": "Verifique se há erros no formulário.",
"diagnosticsPage": {
"diagnosticDescription": "",
"statsDescription": "",
"diagnosticDescription": "Diagnóstico do sistema",
"statsDescription": "Estatísticas do sistema",
"gauges": {
"heapAllocationTitle": "",
"heapAllocationSubtitle": "",
"heapUsageTitle": "",
"heapUsageSubtitle": "",
"heapUtilizationTitle": "",
"heapUtilizationSubtitle": "",
"instantCpuUsageTitle": "",
"instantCpuUsageSubtitle": ""
"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": "",
"uptimeTitle": "",
"usedHeapSizeTitle": "",
"totalHeapSizeTitle": "",
"osMemoryLimitTitle": ""
"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": "",
"pageSpeedLighthouseAPI": "Use a API Lighthouse PageSpeed para monitorar seu site",
"time": {
"threeMinutes": "",
"fiveMinutes": "",
"tenMinutes": "",
"twentyMinutes": "",
"oneHour": "",
"oneDay": "",
"oneWeek": ""
"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": ""
"noOptionsFound": "Nenhuma {{unidade}} encontrada"
},
"infrastructureMonitor": {
"fallback": {
"checks": [""],
"title": "",
"actionButton": ""
"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": [""],
"title": "",
"actionButton": ""
"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": [""],
"title": "",
"actionButton": ""
"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": [""],
"title": "",
"actionButton": ""
"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": "",
"firstName": "",
"lastName": "",
"role": "",
"save": ""
"email": "E-mail",
"firstName": "Primeiro nome",
"lastName": "Sobrenome",
"role": "Funções",
"save": "Salvar"
},
"table": {
"actionHeader": "",
"roleHeader": ""
"actionHeader": "Ação",
"roleHeader": "Papel"
},
"title": "",
"title": "Editar usuário",
"toast": {
"successUserUpdate": "",
"validationErrors": ""
"successUserUpdate": "Usuário atualizado com sucesso",
"validationErrors": "Erros de validação"
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+100 -20
View File
@@ -260,7 +260,6 @@
"notifyEmails": "Также уведомлять по электронной почте на несколько адресов (скоро)",
"seperateEmails": "Вы можете разделить несколько адресов электронной почты запятой.",
"checkFrequency": "Проверить частоту",
"chooseGame": "",
"matchMethod": "Метод сопоставления",
"expectedValue": "Ожидаемое значение",
"deleteDialogTitle": "Вы действительно хотите удалить этот монитор?",
@@ -427,7 +426,7 @@
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
"settingsTestEmailUnknownError": "",
"settingsTestEmailUnknownError": "Неизвестная ошибка",
"statusMsg": {
"paused": "",
"up": "",
@@ -450,46 +449,46 @@
"toasts": {
"networkError": "",
"checkConnection": "",
"unknownError": ""
"unknownError": "Неизвестная ошибка"
}
},
"auth": {
"common": {
"navigation": {
"continue": "",
"back": ""
"back": "Назад"
},
"inputs": {
"email": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"empty": "Для продолжения введите свой адрес электронной почты",
"invalid": ""
}
},
"password": {
"label": "",
"label": "Пароль",
"rules": {
"length": {
"beginning": "",
"highlighted": ""
"beginning": "Должен быть как минимум",
"highlighted": "8 символов в длину"
},
"special": {
"beginning": "",
"highlighted": ""
"beginning": "Должен содержать как минимум",
"highlighted": "один спецсимвол"
},
"number": {
"beginning": "",
"highlighted": ""
"beginning": "Должен содержать как минимум",
"highlighted": "одно число"
},
"uppercase": {
"beginning": "",
"highlighted": ""
"beginning": "Должен содержать как минимум",
"highlighted": "одну заглавную букву"
},
"lowercase": {
"beginning": "",
"highlighted": ""
"beginning": "Должен содержать как минимум",
"highlighted": "одну прописную букву"
},
"match": {
"beginning": "",
@@ -569,7 +568,8 @@
"password": {
"incorrect": ""
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -595,7 +595,8 @@
},
"toasts": {
"success": ""
}
},
"welcome": ""
},
"forgotPassword": {
"heading": "",
@@ -847,6 +848,10 @@
"selectDisabled": "",
"selectEnabled": "",
"title": ""
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
@@ -915,7 +920,12 @@
"twentyMinutes": "",
"oneHour": "",
"oneDay": "",
"oneWeek": ""
"oneWeek": "",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
@@ -967,5 +977,75 @@
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+85 -5
View File
@@ -260,7 +260,6 @@
"notifyEmails": "Ayrıca birden fazla eposta adresine bildirim gönderebilirsiniz (yakında geliyor)",
"seperateEmails": "Birden fazla epostayı virgülle ayırabilirsiniz",
"checkFrequency": "Frekansı denetle",
"chooseGame": "Oyun seç",
"matchMethod": "Eşleşme yöntemi",
"expectedValue": "Beklenen değer",
"deleteDialogTitle": "Gerçekten bu monitörü silmek istiyor musunuz?",
@@ -569,7 +568,8 @@
"password": {
"incorrect": "Giriş yaptığınız parola sistemdekiyle uyuşmuyor"
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -595,7 +595,8 @@
},
"toasts": {
"success": "Hoşgeldiniz! Hesabınız başarıyla oluşturuldu."
}
},
"welcome": ""
},
"forgotPassword": {
"heading": "Parolanızı mı kaybettiniz?",
@@ -847,6 +848,10 @@
"selectDisabled": "Kapalı",
"selectEnabled": "Etkin",
"title": ""
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
@@ -915,7 +920,12 @@
"twentyMinutes": "",
"oneHour": "1 saat",
"oneDay": "1 gün",
"oneWeek": "1 hafta"
"oneWeek": "1 hafta",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
@@ -967,5 +977,75 @@
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+284 -200
View File
@@ -3,81 +3,81 @@
"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": "",
"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": "",
"webhookSendSuccess": "Webhook 通知發送成功",
"webhookSendError": "",
"webhookUnsupportedPlatform": "",
"distributedRightCategoryTitle": "",
"distributedStatusServerMonitors": "",
"distributedStatusServerMonitorsDescription": "",
"distributedUptimeCreateSelectURL": "",
"distributedUptimeCreateChecks": "",
"distributedUptimeCreateChecksDescription": "",
"distributedUptimeCreateIncidentNotification": "",
"distributedUptimeCreateIncidentDescription": "",
"distributedUptimeCreateAdvancedSettings": "",
"distributedUptimeDetailsNoMonitorHistory": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "",
"webhookUnsupportedPlatform": "不支援的平台: {platform}",
"distributedRightCategoryTitle": "監控",
"distributedStatusServerMonitors": "伺服器監控",
"distributedStatusServerMonitorsDescription": "監控相關伺服器狀態",
"distributedUptimeCreateSelectURL": "你可以在這裡選擇主機的 URL,同時選擇監視器類型。",
"distributedUptimeCreateChecks": "要執行的檢查項目",
"distributedUptimeCreateChecksDescription": "你可以在新增網站後,隨時新增或移除檢查項目。",
"distributedUptimeCreateIncidentNotification": "事故通知",
"distributedUptimeCreateIncidentDescription": "事故發生時,向用戶發送通知。",
"distributedUptimeCreateAdvancedSettings": "進階設定",
"distributedUptimeDetailsNoMonitorHistory": "此監控尚無檢查記錄。",
"distributedUptimeDetailsStatusHeaderUptime": "正常運行時間:",
"distributedUptimeDetailsStatusHeaderLastUpdate": "最後更新",
"notifications": {
"enableNotifications": "",
"testNotification": "",
"addOrEditNotifications": "",
"testNotification": "測試通知",
"addOrEditNotifications": "新增或編輯通知",
"slack": {
"label": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": "",
"webhookRequired": ""
"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": "",
"label": "Discord",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": "",
"webhookLabel": "Discord Webhook URL",
"webhookPlaceholder": "https://discord.com/api/webhooks/...",
"webhookRequired": ""
},
"telegram": {
"label": "",
"description": "",
"tokenLabel": "",
"tokenPlaceholder": "",
"chatIdLabel": "",
"chatIdPlaceholder": "",
"label": "Telegram",
"description": "要啟用 Telegram 通知,請使用 BotFather 用於創建和管理 Telegram 的官方機器來建立一個 Telegram 機器人。接著,取得 API 金鑰和聊天室 ID,並在此處填寫。",
"tokenLabel": "你的機器人金鑰",
"tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"chatIdLabel": "你的聊天 ID",
"chatIdPlaceholder": "-1001234567890",
"fieldsRequired": ""
},
"webhook": {
"label": "",
"description": "",
"urlLabel": "",
"urlPlaceholder": "",
"label": "Webhooks",
"description": "你可以設定自訂 webhook,在事件發生時接收通知。",
"urlLabel": "Webhook URL",
"urlPlaceholder": "https://your-server.com/webhook",
"urlRequired": ""
},
"testNotificationDevelop": "",
"testNotificationDevelop": "測試通知 2",
"integrationButton": "",
"testSuccess": "",
"testFailed": "",
@@ -85,8 +85,12 @@
"networkError": "",
"fallback": {
"title": "",
"checks": [""],
"actionButton": ""
"checks": [
"提醒團隊有關停機或性能問題",
"讓工程師知道事件發生時的情況",
"讓管理員了解系統變更情況"
],
"actionButton": "讓我們建立您的第一個通知頻道!"
},
"createButton": "",
"createTitle": "",
@@ -111,35 +115,35 @@
"failed": ""
}
},
"testLocale": "",
"add": "",
"testLocale": "測試語言環境",
"add": "新增",
"monitors": "",
"distributedUptimeStatusCreateStatusPage": "",
"distributedUptimeStatusCreateStatusPageAccess": "",
"distributedUptimeStatusCreateStatusPageReady": "",
"distributedUptimeStatusBasicInfoHeader": "",
"distributedUptimeStatusBasicInfoHeader": "基本資料",
"distributedUptimeStatusBasicInfoDescription": "",
"distributedUptimeStatusLogoHeader": "",
"distributedUptimeStatusLogoHeader": "標誌",
"distributedUptimeStatusLogoDescription": "",
"distributedUptimeStatusLogoUploadButton": "",
"distributedUptimeStatusLogoUploadButton": "上傳標誌",
"distributedUptimeStatusStandardMonitorsHeader": "",
"distributedUptimeStatusStandardMonitorsDescription": "",
"distributedUptimeStatusCreateYour": "",
"distributedUptimeStatusEditYour": "",
"distributedUptimeStatusPublishedLabel": "",
"distributedUptimeStatusCompanyNameLabel": "",
"distributedUptimeStatusCompanyNameLabel": "公司名稱",
"distributedUptimeStatusPageAddressLabel": "",
"distributedUptimeStatus30Days": "",
"distributedUptimeStatus60Days": "",
"distributedUptimeStatus90Days": "",
"distributedUptimeStatus30Days": "30日",
"distributedUptimeStatus60Days": "60日",
"distributedUptimeStatus90Days": "90日",
"distributedUptimeStatusPageNotSetUp": "",
"distributedUptimeStatusContactAdmin": "",
"distributedUptimeStatusPageNotPublic": "",
"distributedUptimeStatusPageDeleteDialog": "",
"distributedUptimeStatusPageDeleteConfirm": "",
"distributedUptimeStatusPageDeleteDescription": "",
"distributedUptimeStatusDevices": "",
"distributedUptimeStatusUpt": "",
"distributedUptimeStatusDevices": "設備",
"distributedUptimeStatusUpt": "UPT",
"distributedUptimeStatusUptBurned": "",
"distributedUptimeStatusUptLogo": "",
"incidentsTableNoIncidents": "",
@@ -169,53 +173,53 @@
"infrastructureServerUrlLabel": "",
"infrastructureDisplayNameLabel": "",
"infrastructureAuthorizationSecretLabel": "",
"gb": "",
"mb": "",
"mem": "",
"gb": "GB",
"mb": "MB",
"mem": "Mem",
"memoryUsage": "",
"cpu": "",
"cpuUsage": "",
"cpuTemperature": "",
"cpu": "CPU",
"cpuUsage": "CPU 使用量",
"cpuTemperature": "CPU 溫度",
"diskUsage": "",
"used": "",
"total": "",
"cores": "",
"cores": "核",
"frequency": "",
"status": "",
"cpuPhysical": "",
"cpuLogical": "",
"cpuFrequency": "",
"avgCpuTemperature": "",
"cpuPhysical": "CPU (實體核心)",
"cpuLogical": "CPU (邏輯核心)",
"cpuFrequency": "CPU 頻率",
"avgCpuTemperature": "平均 CPU 溫度",
"memory": "",
"disk": "",
"uptime": "",
"os": "",
"os": "OS",
"host": "",
"actions": "",
"actions": "行動",
"integrations": "",
"integrationsPrism": "",
"integrationsSlack": "",
"integrationsSlack": "Slack",
"integrationsSlackInfo": "",
"integrationsDiscord": "",
"integrationsDiscordInfo": "",
"integrationsZapier": "",
"integrationsZapierInfo": "",
"commonSave": "",
"commonSave": "儲存",
"createYour": "",
"createMonitor": "",
"pause": "",
"resume": "",
"editing": "",
"url": "",
"access": "",
"timezone": "",
"url": "UR:",
"access": "存取",
"timezone": "時區",
"features": "",
"administrator": "",
"administrator": "系統管理員?",
"loginHere": "",
"displayName": "",
"urlMonitor": "",
"portToMonitor": "",
"websiteMonitoring": "",
"websiteMonitoring": "網站監控",
"websiteMonitoringDescription": "",
"pingMonitoring": "",
"pingMonitoringDescription": "",
@@ -232,15 +236,15 @@
"maintenanceRepeat": "",
"maintenance": "",
"duration": "",
"addMonitors": "",
"addMonitors": "新增監視器",
"window": "",
"cancel": "",
"cancel": "取消",
"message": "",
"low": "",
"high": "",
"low": "低",
"high": "高",
"statusCode": "",
"date&Time": "",
"type": "",
"date&Time": "日期與時間",
"type": "類型",
"statusPageName": "",
"publicURL": "",
"repeat": "",
@@ -259,21 +263,20 @@
"notifySMS": "",
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"chooseGame": "",
"checkFrequency": "檢查頻率",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
"deleteDialogDescription": "",
"deleteDialogTitle": "您確定要刪除此監視器嗎?",
"deleteDialogDescription": "一旦刪除,該監視器無法恢復。",
"pageSpeedMonitor": "",
"shown": "",
"ago": "",
"companyName": "",
"companyName": "公司名稱",
"pageSpeedDetailsPerformanceReport": "",
"pageSpeedDetailsPerformanceReportCalculator": "",
"checkingEvery": "",
"statusPageCreateSettings": "",
"basicInformation": "",
"basicInformation": "基本資料",
"statusPageCreateBasicInfoDescription": "",
"statusPageCreateSelectTimeZoneDescription": "",
"statusPageCreateAppearanceDescription": "",
@@ -307,24 +310,24 @@
"pageSpeedWarning": "",
"pageSpeedLearnMoreLink": "",
"pageSpeedAddApiKey": "",
"update": "",
"update": "更新",
"invalidFileFormat": "",
"invalidFileSize": "",
"ClickUpload": "",
"ClickUpload": "點擊上傳",
"DragandDrop": "",
"MaxSize": "",
"SupportedFormats": "",
"FirstName": "",
"LastName": "",
"EmailDescriptionText": "",
"YourPhoto": "",
"YourPhoto": "頭像",
"PhotoDescriptionText": "",
"save": "",
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteDescriptionText": "這將刪除帳號以及所有相關的伺服器資料,此操作不可逆轉。",
"DeleteAccountWarning": "刪除您的帳號表示您將無法再次登入,且所有資料都會被移除。此操作不可逆轉。",
"DeleteWarningTitle": "",
"bulkImport": {
"title": "",
"title": "批量匯入",
"selectFileTips": "",
"selectFileDescription": "",
"selectFile": "",
@@ -336,8 +339,8 @@
"invalidFileType": "",
"uploadFailed": ""
},
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
"DeleteAccountTitle": "刪除帳戶",
"DeleteAccountButton": "刪除帳戶",
"publicLink": "",
"maskedPageSpeedKeyPlaceholder": "",
"reset": "",
@@ -352,14 +355,14 @@
},
"roles": {
"superAdmin": "",
"admin": "",
"admin": "管理員",
"teamMember": "",
"demoUser": ""
"demoUser": "示範用戶"
},
"teamPanel": {
"teamMembers": "",
"filter": {
"all": "",
"all": "全部",
"member": ""
},
"inviteTeamMember": "",
@@ -368,7 +371,7 @@
"email": "",
"selectRole": "",
"inviteLink": "",
"cancel": "",
"cancel": "取消",
"noMembers": "",
"getToken": "",
"emailToken": "",
@@ -382,7 +385,7 @@
"monitorState": {
"paused": "",
"resumed": "",
"active": ""
"active": "啟用中"
},
"menu": {
"uptime": "",
@@ -396,7 +399,7 @@
"support": "",
"discussions": "",
"docs": "",
"changelog": "",
"changelog": "版本變更記錄",
"profile": "",
"password": "",
"team": "",
@@ -408,17 +411,17 @@
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
"commonSaving": "",
"commonSaving": "儲存中...",
"navControls": "",
"incidentsPageTitle": "",
"passwordPanel": {
"passwordChangedSuccess": "",
"passwordInputIncorrect": "",
"currentPassword": "",
"currentPassword": "當前密碼",
"enterCurrentPassword": "",
"newPassword": "",
"enterNewPassword": "",
"confirmNewPassword": "",
"confirmNewPassword": "確認新密碼",
"passwordRequirements": "",
"saving": ""
},
@@ -442,22 +445,22 @@
"game": ""
},
"common": {
"appName": "",
"appName": "Checkmate",
"monitoringAgentName": "",
"buttons": {
"toggleTheme": ""
},
"toasts": {
"networkError": "",
"checkConnection": "",
"checkConnection": "請檢查您的連線",
"unknownError": ""
}
},
"auth": {
"common": {
"navigation": {
"continue": "",
"back": ""
"continue": "繼續",
"back": "返回"
},
"inputs": {
"email": {
@@ -472,23 +475,23 @@
"label": "",
"rules": {
"length": {
"beginning": "",
"beginning": "必須至少",
"highlighted": ""
},
"special": {
"beginning": "",
"beginning": "必須至少包含",
"highlighted": ""
},
"number": {
"beginning": "",
"beginning": "必須至少包含",
"highlighted": ""
},
"uppercase": {
"beginning": "",
"beginning": "必須至少包含",
"highlighted": ""
},
"lowercase": {
"beginning": "",
"beginning": "必須至少包含",
"highlighted": ""
},
"match": {
@@ -562,14 +565,15 @@
"registerLink": ""
},
"toasts": {
"success": "",
"success": "歡迎回來!您已成功登入。",
"incorrectPassword": ""
},
"errors": {
"password": {
"incorrect": ""
}
}
},
"welcome": ""
},
"registration": {
"heading": {
@@ -594,8 +598,9 @@
"login": ""
},
"toasts": {
"success": ""
}
"success": "歡迎!您的帳號已成功建立。"
},
"welcome": ""
},
"forgotPassword": {
"heading": "",
@@ -623,7 +628,7 @@
"sent": "",
"emailNotFound": "",
"redirect": "",
"success": "",
"success": "您的密碼已成功重設。",
"error": ""
}
}
@@ -634,10 +639,10 @@
"reconnected": "",
"stillUnreachable": ""
},
"alertBox": "",
"description": "",
"alertBox": "伺服器連線錯誤",
"description": "我們無法連接到伺服器。請檢查您的網路連線,若問題持續,請確認您的部署設定是否正確。",
"retryButton": {
"default": "",
"default": "重新嘗試連線",
"processing": ""
}
}
@@ -645,42 +650,42 @@
"createNotifications": {
"title": "",
"nameSettings": {
"title": "",
"description": "",
"title": "姓名",
"description": "為您的整合設定描述名稱。",
"nameLabel": "",
"namePlaceholder": ""
},
"typeSettings": {
"title": "",
"description": "",
"typeLabel": ""
"title": "類型",
"description": "請選擇您想要建立的通知頻道類型。",
"typeLabel": "類型"
},
"emailSettings": {
"title": "",
"description": "",
"title": "電郵",
"description": "目的地電子郵件位址。",
"emailLabel": "",
"emailPlaceholder": ""
},
"slackSettings": {
"title": "",
"description": "",
"title": "Slack",
"description": "在此配置您的 Slack webhook",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"pagerdutySettings": {
"title": "",
"description": "",
"title": "PagerDuty",
"description": "在此配置您的 PagerDuty 整合",
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"discordSettings": {
"title": "",
"description": "",
"title": "Discord",
"description": "在此配置您的 Discord webhook",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"webhookSettings": {
"title": "",
"title": "Webhook",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
@@ -690,8 +695,8 @@
"dialogDeleteConfirm": ""
},
"notificationConfig": {
"title": "",
"description": ""
"title": "通知",
"description": "請選擇您想要使用的通知頻道"
},
"monitorStatus": {
"checkingEvery": "",
@@ -700,7 +705,7 @@
"down": "",
"paused": ""
},
"advancedMatching": "",
"advancedMatching": "進階匹配",
"sendTestNotifications": "",
"selectAll": "",
"showAdminLoginLink": "",
@@ -718,11 +723,11 @@
"logLevelSelect": {
"title": "",
"values": {
"all": "",
"all": "全部",
"info": "",
"warn": "",
"error": "",
"debug": ""
"debug": "除錯"
}
}
},
@@ -734,8 +739,8 @@
"title": "",
"idHeader": "",
"urlHeader": "",
"typeHeader": "",
"activeHeader": "",
"typeHeader": "類型",
"activeHeader": "啟用中",
"lockedAtHeader": "",
"runCountHeader": "",
"failCountHeader": "",
@@ -774,7 +779,7 @@
"aboutSettings": {
"labelDevelopedBy": "",
"labelVersion": "",
"title": ""
"title": "關於"
},
"demoMonitorsSettings": {
"buttonAddMonitors": "",
@@ -782,7 +787,7 @@
"title": ""
},
"emailSettings": {
"buttonSendTestEmail": "",
"buttonSendTestEmail": "發送測試電子郵件",
"description": "",
"descriptionTransport": "",
"labelAddress": "",
@@ -800,25 +805,25 @@
"labelUser": "",
"linkTransport": "",
"placeholderUser": "",
"title": "",
"title": "電郵",
"toastEmailRequiredFieldsError": ""
},
"pageSpeedSettings": {
"description": "",
"labelApiKeySet": "",
"labelApiKey": "",
"title": ""
"title": "Google PageSpeed API 金鑰"
},
"saveButtonLabel": "",
"statsSettings": {
"clearAllStatsButton": "",
"clearAllStatsDescription": "",
"clearAllStatsDialogConfirm": "",
"clearAllStatsDialogDescription": "",
"clearAllStatsDialogTitle": "",
"clearAllStatsButton": "清除所有統計數據",
"clearAllStatsDescription": "清除所有統計數據,此操作不可逆轉。",
"clearAllStatsDialogConfirm": "是,請清除所有統計數據",
"clearAllStatsDialogDescription": "一旦刪除,監控歷史和統計數據將無法復原。",
"clearAllStatsDialogTitle": "您是否要清除所有統計數據?",
"description": "",
"labelTTL": "",
"labelTTLOptional": "",
"labelTTLOptional": "0 表示無限",
"title": ""
},
"systemResetSettings": {
@@ -827,19 +832,19 @@
"dialogConfirm": "",
"dialogDescription": "",
"dialogTitle": "",
"title": ""
"title": "系統重設"
},
"timezoneSettings": {
"description": "",
"label": "",
"title": ""
"title": "顯示時區"
},
"title": "",
"title": "設定",
"uiSettings": {
"description": "",
"labelLanguage": "",
"labelTheme": "",
"title": ""
"title": "外觀"
},
"urlSettings": {
"description": "",
@@ -847,15 +852,19 @@
"selectDisabled": "",
"selectEnabled": "",
"title": ""
},
"globalThresholds": {
"title": "",
"description": ""
}
},
"statusPageCreate": {
"buttonSave": ""
"buttonSave": "儲存"
},
"incidentsOptionsHeaderFilterResolved": "",
"settingsSave": "",
"statusPageCreateAppearanceTitle": "",
"confirmPassword": "",
"confirmPassword": "確認密碼",
"monitorHooks": {
"failureAddDemoMonitors": "",
"successAddDemoMonitors": ""
@@ -866,15 +875,15 @@
"incidentsOptionsHeaderTotalIncidents": "",
"statusPage": {
"deleteSuccess": "",
"deleteFailed": "",
"deleteFailed": "刪除狀態頁面失敗",
"createSuccess": "",
"updateSuccess": "",
"generalSettings": "",
"contents": "",
"contents": "內容",
"fallback": {
"checks": [""],
"title": "",
"actionButton": ""
"actionButton": "讓我們建立您的第一個狀態頁面!"
}
},
"testNotificationsDisabled": "",
@@ -909,63 +918,138 @@
},
"pageSpeedLighthouseAPI": "",
"time": {
"threeMinutes": "",
"fiveMinutes": "",
"tenMinutes": "",
"twentyMinutes": "",
"oneHour": "",
"oneDay": "",
"oneWeek": ""
"threeMinutes": "3分鐘",
"fiveMinutes": "5分鐘",
"tenMinutes": "10分鐘",
"twentyMinutes": "20分鐘",
"oneHour": "1小時",
"oneDay": "1日",
"oneWeek": "1星期",
"fourMinutes": "",
"oneMinute": "",
"twoMinutes": "",
"fifteenSeconds": "",
"thirtySeconds": ""
},
"general": {
"noOptionsFound": ""
},
"infrastructureMonitor": {
"fallback": {
"checks": [""],
"checks": ["追蹤伺服器的效能", "識別瓶頸並優化資源使用", "透過即時監控確保可靠性"],
"title": "",
"actionButton": ""
"actionButton": "讓我們建立您的第一個基礎設施監視器!"
}
},
"maintenanceWindow": {
"fallback": {
"checks": [""],
"checks": ["標記您的維護期間", "消除任何誤解", "在維護時段停止發送警報"],
"title": "",
"actionButton": ""
"actionButton": "讓我們建立您的第一個維護時段!"
}
},
"pageSpeed": {
"fallback": {
"checks": [""],
"title": "",
"actionButton": ""
"actionButton": "讓我們建立您的第一個 PageSpeed 監視器!"
}
},
"uptimeMonitor": {
"fallback": {
"checks": [""],
"title": "",
"actionButton": ""
"actionButton": "讓我們建立您的第一個正常運行時間監視器!"
}
},
"editUserPage": {
"form": {
"email": "",
"firstName": "",
"lastName": "",
"role": "",
"save": ""
"email": "電郵",
"firstName": "名",
"lastName": "姓",
"role": "角色",
"save": "儲存"
},
"table": {
"actionHeader": "",
"roleHeader": ""
"actionHeader": "行動",
"roleHeader": "角色"
},
"title": "",
"title": "編輯用戶",
"toast": {
"successUserUpdate": "",
"validationErrors": ""
}
},
"incidentsPageActionResolveMonitor": "",
"incidentsPageActionResolveAll": ""
"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": ""
},
"dataRate": "",
"dataReceived": "",
"dataSent": "",
"details": "",
"drops": "",
"errors": "",
"errorsIn": "",
"errorsOut": "",
"gameServerMonitoring": "",
"gameServerMonitoringDescription": "",
"network": "",
"networkDrops": "",
"networkErrors": "",
"networkInterface": "",
"noNetworkStatsAvailable": "",
"packetsPerSecond": "",
"packetsReceived": "",
"packetsReceivedRate": "",
"packetsSent": "",
"rate": "",
"selectInterface": ""
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { execSync } from "child_process";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
let version = "3.0-beta";
let version = "3.1.4";
return {
base: "/",
+1 -2
View File
@@ -15,9 +15,8 @@ services:
- mongodb
mongodb:
image: mongo:4.4.18
container_name: checkmate-mongodb
restart: always
command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
command: ["mongod", "--quiet", --bind_ip_all"]
ports:
- "27017:27017"
volumes:
+1 -1
View File
@@ -5,7 +5,7 @@ cd "$(dirname "$0")"
cd ../..
# Define service names and their corresponding Dockerfiles in parallel arrays
services=("mono_mongo" "mono_redis" "mono_server")
services=("mono_mongo" "mono_server")
dockerfiles=(
"./docker/dist-mono/mongoDB.Dockerfile"
"./docker/dist-mono/server.Dockerfile"
+390 -672
View File
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -28,6 +28,7 @@
"express": "^4.19.2",
"express-rate-limit": "8.0.1",
"gamedig": "^5.3.1",
"got": "14.4.7",
"handlebars": "^4.7.8",
"helmet": "^8.0.0",
"ioredis": "^5.4.2",
@@ -45,7 +46,7 @@
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
"super-simple-scheduler": "1.3.0",
"super-simple-scheduler": "1.4.0",
"swagger-ui-express": "5.0.1",
"winston": "^3.13.0"
},
+19 -16
View File
@@ -19,8 +19,10 @@ import MaintenanceWindowService from "../service/business/maintenanceWindowServi
import MonitorService from "../service/business/monitorService.js";
import papaparse from "papaparse";
import axios from "axios";
import got from "got";
import ping from "ping";
import http from "http";
import https from "https";
import Docker from "dockerode";
import net from "net";
import fs from "fs";
@@ -32,6 +34,8 @@ import mjml2html from "mjml";
import jwt from "jsonwebtoken";
import crypto from "crypto";
import { games } from "gamedig";
import jmespath from "jmespath";
import { GameDig } from "gamedig";
import { fileURLToPath } from "url";
import { ObjectId } from "mongodb";
@@ -43,8 +47,6 @@ import { ParseBoolean } from "../utils/utils.js";
// Models
import Check from "../db/models/Check.js";
import HardwareCheck from "../db/models/HardwareCheck.js";
import PageSpeedCheck from "../db/models/PageSpeedCheck.js";
import Monitor from "../db/models/Monitor.js";
import User from "../db/models/User.js";
import InviteToken from "../db/models/InviteToken.js";
@@ -52,7 +54,6 @@ import StatusPage from "../db/models/StatusPage.js";
import Team from "../db/models/Team.js";
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
import MonitorStats from "../db/models/MonitorStats.js";
import NetworkCheck from "../db/models/NetworkCheck.js";
import Notification from "../db/models/Notification.js";
import RecoveryToken from "../db/models/RecoveryToken.js";
import AppSettings from "../db/models/AppSettings.js";
@@ -61,12 +62,9 @@ import InviteModule from "../db/mongo/modules/inviteModule.js";
import CheckModule from "../db/mongo/modules/checkModule.js";
import StatusPageModule from "../db/mongo/modules/statusPageModule.js";
import UserModule from "../db/mongo/modules/userModule.js";
import HardwareCheckModule from "../db/mongo/modules/hardwareCheckModule.js";
import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js";
import MonitorModule from "../db/mongo/modules/monitorModule.js";
import NetworkCheckModule from "../db/mongo/modules/networkCheckModule.js";
import NotificationModule from "../db/mongo/modules/notificationModule.js";
import PageSpeedCheckModule from "../db/mongo/modules/pageSpeedCheckModule.js";
import RecoveryModule from "../db/mongo/modules/recoveryModule.js";
import SettingsModule from "../db/mongo/modules/settingsModule.js";
@@ -80,18 +78,15 @@ export const initializeServices = async ({ logger, envSettings, settingsService
const stringService = new StringService(translationService);
// Create DB
const checkModule = new CheckModule({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User });
const checkModule = new CheckModule({ logger, Check, Monitor, User });
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
const hardwareCheckModule = new HardwareCheckModule({ HardwareCheck, Monitor, logger });
const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow });
const monitorModule = new MonitorModule({
Monitor,
MonitorStats,
Check,
PageSpeedCheck,
HardwareCheck,
stringService,
fs,
path,
@@ -100,9 +95,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
NormalizeData,
NormalizeDataUptimeDetails,
});
const networkCheckModule = new NetworkCheckModule({ NetworkCheck });
const notificationModule = new NotificationModule({ Notification, Monitor });
const pageSpeedCheckModule = new PageSpeedCheckModule({ PageSpeedCheck });
const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService });
const settingsModule = new SettingsModule({ AppSettings });
@@ -113,19 +106,29 @@ export const initializeServices = async ({ logger, envSettings, settingsService
inviteModule,
statusPageModule,
userModule,
hardwareCheckModule,
maintenanceWindowModule,
monitorModule,
networkCheckModule,
notificationModule,
pageSpeedCheckModule,
recoveryModule,
settingsModule,
});
await db.connect();
const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService);
const networkService = new NetworkService({
axios,
got,
https,
jmespath,
GameDig,
ping,
logger,
http,
Docker,
net,
stringService,
settingsService,
});
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
const bufferService = new BufferService({ db, logger, envSettings });
const statusService = new StatusService({ db, logger, buffer: bufferService });
+1 -1
View File
@@ -94,7 +94,7 @@ class AuthController extends BaseController {
await registrationBodyValidation.validateAsync(req.body);
const { user, token } = await this.userService.registerUser(req.body, req.file);
res.success({
msg: this.stringService.authCreateUser,
msg: this.stringService.get("authCreateUser"),
data: { user, token },
});
},
@@ -26,7 +26,7 @@ class StatusPageController extends BaseController {
teamId,
});
return res.success({
msg: this.stringService.statusPageCreate,
msg: this.stringService.get("statusPageCreate"),
data: statusPage,
});
},
@@ -41,10 +41,10 @@ class StatusPageController extends BaseController {
const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file);
if (statusPage === null) {
throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound);
throw this.errorService.createNotFoundError(this.stringService.get("statusPageNotFound"));
}
return res.success({
msg: this.stringService.statusPageUpdate,
msg: this.stringService.get("statusPageUpdate"),
data: statusPage,
});
},
@@ -56,7 +56,7 @@ class StatusPageController extends BaseController {
async (req, res) => {
const statusPage = await this.db.statusPageModule.getStatusPage();
return res.success({
msg: this.stringService.statusPageByUrl,
msg: this.stringService.get("statusPageByUrl"),
data: statusPage,
});
},
@@ -71,7 +71,7 @@ class StatusPageController extends BaseController {
const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url);
return res.success({
msg: this.stringService.statusPageByUrl,
msg: this.stringService.get("statusPageByUrl"),
data: statusPage,
});
},
@@ -85,7 +85,7 @@ class StatusPageController extends BaseController {
const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId);
return res.success({
msg: this.stringService.statusPageByTeamId,
msg: this.stringService.get("statusPageByTeamId"),
data: statusPages,
});
},
@@ -95,9 +95,9 @@ class StatusPageController extends BaseController {
deleteStatusPage = this.asyncHandler(
async (req, res) => {
await this.db.deleteStatusPage(req.params.url);
await this.db.statusPageModule.deleteStatusPage(req.params.url);
return res.success({
msg: this.stringService.statusPageDelete,
msg: this.stringService.get("statusPageDelete"),
});
},
SERVICE_NAME,
+166 -89
View File
@@ -1,99 +1,176 @@
import mongoose from "mongoose";
const BaseCheckSchema = mongoose.Schema({
/**
* Reference to the associated Monitor document.
*
* @type {mongoose.Schema.Types.ObjectId}
*/
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
immutable: true,
index: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
index: true,
},
/**
* Status of the check (true for up, false for down).
*
* @type {Boolean}
*/
status: {
type: Boolean,
index: true,
},
/**
* Response time of the check in milliseconds.
*
* @type {Number}
*/
responseTime: {
type: Number,
},
/**
* HTTP status code received during the check.
*
* @type {Number}
*/
statusCode: {
type: Number,
index: true,
},
/**
* Message or description of the check result.
*
* @type {String}
*/
message: {
type: String,
},
/**
* Expiry date of the check, auto-calculated to expire after 30 days.
*
* @type {Date}
*/
expiry: {
type: Date,
default: Date.now,
expires: 60 * 60 * 24 * 30, // 30 days
},
/**
* Acknowledgment of the check.
*
* @type {Boolean}
*/
ack: {
type: Boolean,
default: false,
},
/**
* Resolution date of the check (when the check was resolved).
*
* @type {Date}
*/
ackAt: {
type: Date,
},
const cpuSchema = mongoose.Schema({
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
frequency: { type: Number, default: 0 },
temperature: { type: [Number], default: [] },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
/**
* Check Schema for MongoDB collection.
*
* Represents a check associated with a monitor, storing information
* about the status and response of a particular check event.
*/
const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true });
const memorySchema = mongoose.Schema({
total_bytes: { type: Number, default: 0 },
available_bytes: { type: Number, default: 0 },
used_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const diskSchema = mongoose.Schema({
read_speed_bytes: { type: Number, default: 0 },
write_speed_bytes: { type: Number, default: 0 },
total_bytes: { type: Number, default: 0 },
free_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const hostSchema = mongoose.Schema({
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
});
const errorSchema = mongoose.Schema({
metric: { type: [String], default: [] },
err: { type: String, default: "" },
});
const captureSchema = mongoose.Schema({
version: { type: String, default: "" },
mode: { type: String, default: "" },
});
const networkInterfaceSchema = mongoose.Schema({
name: { type: String },
bytes_sent: { type: Number, default: 0 },
bytes_recv: { type: Number, default: 0 },
packets_sent: { type: Number, default: 0 },
packets_recv: { type: Number, default: 0 },
err_in: { type: Number, default: 0 },
err_out: { type: Number, default: 0 },
drop_in: { type: Number, default: 0 },
drop_out: { type: Number, default: 0 },
fifo_in: { type: Number, default: 0 },
fifo_out: { type: Number, default: 0 },
});
const CheckSchema = new mongoose.Schema(
{
// Common fields
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
immutable: true,
index: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
index: true,
},
type: {
type: String,
enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"],
required: true,
index: true,
},
status: {
type: Boolean,
index: true,
},
responseTime: {
type: Number,
},
timings: {
type: Object,
default: {},
},
statusCode: {
type: Number,
index: true,
},
message: {
type: String,
},
expiry: {
type: Date,
default: Date.now,
expires: 60 * 60 * 24 * 30, // 30 days
},
ack: {
type: Boolean,
default: false,
},
ackAt: {
type: Date,
},
// Hardware fields
cpu: {
type: cpuSchema,
default: () => ({}),
},
memory: {
type: memorySchema,
default: () => ({}),
},
disk: {
type: [diskSchema],
default: () => [],
},
host: {
type: hostSchema,
default: () => ({}),
},
errors: {
type: [errorSchema],
default: () => [],
},
capture: {
type: captureSchema,
default: () => ({}),
},
net: {
type: [networkInterfaceSchema],
default: () => [],
},
// PageSpeed fields
accessibility: {
type: Number,
},
bestPractices: {
type: Number,
},
seo: {
type: Number,
},
performance: {
type: Number,
},
audits: {
type: Object,
},
},
{ timestamps: true }
);
CheckSchema.index({ updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: -1 });
CheckSchema.index({ teamId: 1, updatedAt: -1 });
export default mongoose.model("Check", CheckSchema);
export { BaseCheckSchema };
-100
View File
@@ -1,100 +0,0 @@
import mongoose from "mongoose";
import { BaseCheckSchema } from "./Check.js";
const cpuSchema = mongoose.Schema({
physical_core: { type: Number, default: 0 },
logical_core: { type: Number, default: 0 },
frequency: { type: Number, default: 0 },
temperature: { type: [Number], default: [] },
free_percent: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const memorySchema = mongoose.Schema({
total_bytes: { type: Number, default: 0 },
available_bytes: { type: Number, default: 0 },
used_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const diskSchema = mongoose.Schema({
read_speed_bytes: { type: Number, default: 0 },
write_speed_bytes: { type: Number, default: 0 },
total_bytes: { type: Number, default: 0 },
free_bytes: { type: Number, default: 0 },
usage_percent: { type: Number, default: 0 },
});
const hostSchema = mongoose.Schema({
os: { type: String, default: "" },
platform: { type: String, default: "" },
kernel_version: { type: String, default: "" },
});
const errorSchema = mongoose.Schema({
metric: { type: [String], default: [] },
err: { type: String, default: "" },
});
const captureSchema = mongoose.Schema({
version: { type: String, default: "" },
mode: { type: String, default: "" },
});
const networkInterfaceSchema = mongoose.Schema({
name: { type: String },
bytes_sent: { type: Number, default: 0 },
bytes_recv: { type: Number, default: 0 },
packets_sent: { type: Number, default: 0 },
packets_recv: { type: Number, default: 0 },
err_in: { type: Number, default: 0 },
err_out: { type: Number, default: 0 },
drop_in: { type: Number, default: 0 },
drop_out: { type: Number, default: 0 },
fifo_in: { type: Number, default: 0 },
fifo_out: { type: Number, default: 0 },
});
const HardwareCheckSchema = mongoose.Schema(
{
...BaseCheckSchema.obj,
cpu: {
type: cpuSchema,
default: () => ({}),
},
memory: {
type: memorySchema,
default: () => ({}),
},
disk: {
type: [diskSchema],
default: () => [],
},
host: {
type: hostSchema,
default: () => ({}),
},
errors: {
type: [errorSchema],
default: () => [],
},
capture: {
type: captureSchema,
default: () => ({}),
},
net: {
type: [networkInterfaceSchema],
default: () => [],
required: false,
},
},
{ timestamps: true }
);
HardwareCheckSchema.index({ createdAt: 1 });
HardwareCheckSchema.index({ monitorId: 1, createdAt: 1 });
HardwareCheckSchema.index({ monitorId: 1, createdAt: -1 });
export default mongoose.model("HardwareCheck", HardwareCheckSchema);
+14 -16
View File
@@ -1,6 +1,4 @@
import mongoose from "mongoose";
import HardwareCheck from "./HardwareCheck.js";
import PageSpeedCheck from "./PageSpeedCheck.js";
import Check from "./Check.js";
import MonitorStats from "./MonitorStats.js";
import StatusPage from "./StatusPage.js";
@@ -30,6 +28,18 @@ const MonitorSchema = mongoose.Schema(
type: Boolean,
default: undefined,
},
statusWindow: {
type: [Boolean],
default: [],
},
statusWindowSize: {
type: Number,
default: 5,
},
statusWindowThreshold: {
type: Number,
default: 60,
},
type: {
type: String,
required: true,
@@ -133,13 +143,7 @@ MonitorSchema.pre("findOneAndDelete", async function (next) {
throw new Error("Monitor not found");
}
if (doc?.type === "pagespeed") {
await PageSpeedCheck.deleteMany({ monitorId: doc._id });
} else if (doc?.type === "hardware") {
await HardwareCheck.deleteMany({ monitorId: doc._id });
} else {
await Check.deleteMany({ monitorId: doc._id });
}
await Check.deleteMany({ monitorId: doc._id });
// Deal with status pages
await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } });
@@ -156,13 +160,7 @@ MonitorSchema.pre("deleteMany", async function (next) {
const monitors = await this.model.find(filter).select(["_id", "type"]).lean();
for (const monitor of monitors) {
if (monitor.type === "pagespeed") {
await PageSpeedCheck.deleteMany({ monitorId: monitor._id });
} else if (monitor.type === "hardware") {
await HardwareCheck.deleteMany({ monitorId: monitor._id });
} else {
await Check.deleteMany({ monitorId: monitor._id });
}
await Check.deleteMany({ monitorId: monitor._id });
await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } });
await MonitorStats.deleteMany({ monitorId: monitor._id.toString() });
}
-4
View File
@@ -40,10 +40,6 @@ const MonitorStatsSchema = new mongoose.Schema(
type: Number,
default: 0,
},
uptBurnt: {
type: mongoose.Schema.Types.Decimal128,
required: false,
},
},
{ timestamps: true }
);
-46
View File
@@ -1,46 +0,0 @@
import mongoose from "mongoose";
import { BaseCheckSchema } from "./Check.js";
const networkInterfaceSchema = mongoose.Schema({
name: { type: String, required: true },
bytes_sent: { type: Number, default: 0 },
bytes_recv: { type: Number, default: 0 },
packets_sent: { type: Number, default: 0 },
packets_recv: { type: Number, default: 0 },
err_in: { type: Number, default: 0 },
err_out: { type: Number, default: 0 },
drop_in: { type: Number, default: 0 },
drop_out: { type: Number, default: 0 },
fifo_in: { type: Number, default: 0 },
fifo_out: { type: Number, default: 0 },
});
const captureSchema = mongoose.Schema({
version: { type: String, default: "" },
mode: { type: String, default: "" },
});
const NetworkCheckSchema = mongoose.Schema(
{
...BaseCheckSchema.obj,
data: {
type: [networkInterfaceSchema],
default: () => [],
},
capture: {
type: captureSchema,
default: () => ({}),
},
errors: {
type: mongoose.Schema.Types.Mixed,
default: null,
},
},
{ timestamps: true }
);
NetworkCheckSchema.index({ createdAt: 1 });
NetworkCheckSchema.index({ monitorId: 1, createdAt: 1 });
NetworkCheckSchema.index({ monitorId: 1, createdAt: -1 });
export default mongoose.model("NetworkCheck", NetworkCheckSchema);
-83
View File
@@ -1,83 +0,0 @@
import mongoose from "mongoose";
import { BaseCheckSchema } from "./Check.js";
const AuditSchema = mongoose.Schema({
id: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
score: { type: Number, required: true },
scoreDisplayMode: { type: String, required: true },
displayValue: { type: String, required: true },
numericValue: { type: Number, required: true },
numericUnit: { type: String, required: true },
});
const AuditsSchema = mongoose.Schema({
cls: {
type: AuditSchema,
required: true,
},
si: {
type: AuditSchema,
required: true,
},
fcp: {
type: AuditSchema,
required: true,
},
lcp: {
type: AuditSchema,
required: true,
},
tbt: {
type: AuditSchema,
required: true,
},
});
/**
* Mongoose schema for storing metrics from Google Lighthouse.
* @typedef {Object} PageSpeedCheck
* @property {mongoose.Schema.Types.ObjectId} monitorId - Reference to the Monitor model.
* @property {number} accessibility - Accessibility score.
* @property {number} bestPractices - Best practices score.
* @property {number} seo - SEO score.
* @property {number} performance - Performance score.
*/
const PageSpeedCheck = mongoose.Schema(
{
...BaseCheckSchema.obj,
accessibility: {
type: Number,
required: true,
},
bestPractices: {
type: Number,
required: true,
},
seo: {
type: Number,
required: true,
},
performance: {
type: Number,
required: true,
},
audits: {
type: AuditsSchema,
required: true,
},
},
{ timestamps: true }
);
/**
* Mongoose model for storing metrics from Google Lighthouse.
* @typedef {mongoose.Model<PageSpeedCheck>} LighthouseMetricsModel
*/
PageSpeedCheck.index({ createdAt: 1 });
PageSpeedCheck.index({ monitorId: 1, createdAt: 1 });
PageSpeedCheck.index({ monitorId: 1, createdAt: -1 });
export default mongoose.model("PageSpeedCheck", PageSpeedCheck);
+3 -1
View File
@@ -1,6 +1,6 @@
import mongoose from "mongoose";
import AppSettings from "../models/AppSettings.js";
import { runMigrations } from "./migration/index.js";
class MongoDB {
static SERVICE_NAME = "MongoDB";
@@ -65,6 +65,8 @@ class MongoDB {
service: this.SERVICE_NAME,
method: "connect",
});
await runMigrations();
} catch (error) {
this.logger.error({
message: error.message,
@@ -0,0 +1,18 @@
import Monitor from "../../models/Monitor.js";
async function migrateStatusWindowThreshold() {
try {
const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } });
for (const monitor of monitors) {
monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100;
await monitor.save();
console.log(`Migrated monitor ${monitor._id}: statusWindowThreshold set to ${monitor.statusWindowThreshold}`);
}
console.log("StatusWindowThreshold migration complete.");
return true;
} catch (err) {
console.error("Migration error:", err);
return false;
}
}
export { migrateStatusWindowThreshold };
+7
View File
@@ -0,0 +1,7 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
const runMigrations = async () => {
await migrateStatusWindowThreshold();
};
export { runMigrations };
+2 -26
View File
@@ -31,7 +31,7 @@ class CheckModule {
}
};
getChecksByMonitor = async ({ monitorId, type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => {
getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => {
try {
status = status === "true" ? true : status === "false" ? false : undefined;
page = parseInt(page);
@@ -79,19 +79,7 @@ class CheckModule {
skip = page * rowsPerPage;
}
const checkModels = {
http: this.Check,
ping: this.Check,
docker: this.Check,
port: this.Check,
pagespeed: this.PageSpeedCheck,
hardware: this.HardwareCheck,
game: this.Check,
};
const Model = checkModels[type];
const checks = await Model.aggregate([
const checks = await this.Check.aggregate([
{ $match: matchStage },
{ $sort: { createdAt: sortOrder } },
{
@@ -166,18 +154,6 @@ class CheckModule {
const aggregatePipeline = [
{ $match: matchStage },
{
$unionWith: {
coll: "hardwarechecks",
pipeline: [{ $match: matchStage }],
},
},
{
$unionWith: {
coll: "pagespeedchecks",
pipeline: [{ $match: matchStage }],
},
},
{ $sort: { createdAt: sortOrder } },
{
@@ -1,22 +0,0 @@
const SERVICE_NAME = "hardwareCheckModule";
class HardwareCheckModule {
constructor({ HardwareCheck, Monitor, logger }) {
this.HardwareCheck = HardwareCheck;
this.Monitor = Monitor;
this.logger = logger;
}
createHardwareChecks = async (hardwareChecks) => {
try {
await this.HardwareCheck.insertMany(hardwareChecks, { ordered: false });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createHardwareChecks";
throw error;
}
};
}
export default HardwareCheckModule;
+10 -36
View File
@@ -11,25 +11,10 @@ import {
const SERVICE_NAME = "monitorModule";
class MonitorModule {
constructor({
Monitor,
MonitorStats,
Check,
PageSpeedCheck,
HardwareCheck,
stringService,
fs,
path,
fileURLToPath,
ObjectId,
NormalizeData,
NormalizeDataUptimeDetails,
}) {
constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
this.Monitor = Monitor;
this.MonitorStats = MonitorStats;
this.Check = Check;
this.PageSpeedCheck = PageSpeedCheck;
this.HardwareCheck = HardwareCheck;
this.stringService = stringService;
this.fs = fs;
this.path = path;
@@ -38,15 +23,6 @@ class MonitorModule {
this.NormalizeData = NormalizeData;
this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails;
this.CHECK_MODEL_LOOKUP = {
http: Check,
ping: Check,
docker: Check,
port: Check,
pagespeed: PageSpeedCheck,
hardware: HardwareCheck,
};
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -146,19 +122,18 @@ class MonitorModule {
};
//Helper
getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => {
getMonitorChecks = async (monitorId, dateRange, sortOrder) => {
const indexSpec = {
monitorId: 1,
createdAt: sortOrder, // This will be 1 or -1
updatedAt: sortOrder, // This will be 1 or -1
};
const [checksAll, checksForDateRange] = await Promise.all([
model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
model
.find({
monitorId,
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
})
this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
this.Check.find({
monitorId,
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
})
.hint(indexSpec)
.lean(),
]);
@@ -295,9 +270,8 @@ class MonitorModule {
const sort = sortOrder === "asc" ? 1 : -1;
// Get Checks for monitor in date range requested
const model = this.CHECK_MODEL_LOOKUP[monitor.type];
const dates = this.getDateRange(dateRange);
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, model, dates, sort);
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort);
// Build monitor stats
const monitorStats = {
@@ -339,7 +313,7 @@ class MonitorModule {
};
const dateString = formatLookup[dateRange];
const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const stats = hardwareStats[0];
@@ -5,12 +5,12 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
{
$match: {
monitorId: new ObjectId(monitorId),
createdAt: { $gte: dates.start, $lte: dates.end },
updatedAt: { $gte: dates.start, $lte: dates.end },
},
},
{
$sort: {
createdAt: 1,
updatedAt: 1,
},
},
{
@@ -173,6 +173,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
{
$match: {
monitorId: monitor._id,
type: "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
@@ -218,14 +219,14 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
{
$project: {
diskCount: {
$size: "$disk",
$size: { $ifNull: ["$disk", []] },
},
netCount: { $size: "$net" },
netCount: { $size: { $ifNull: ["$net", []] } },
},
},
{
$lookup: {
from: "hardwarechecks",
from: "checks",
let: {
diskCount: "$diskCount",
netCount: "$netCount",
@@ -381,7 +382,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
},
net: {
$map: {
input: { $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }] },
input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$net", 0] }, []] } }] },
as: "netIndex",
in: {
name: {
@@ -409,7 +410,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
input: {
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
},
as: "iface",
in: "$$iface.bytes_sent",
},
@@ -418,7 +421,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
tLast: {
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
},
},
in: {
$cond: [
@@ -444,7 +449,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
input: {
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
},
as: "iface",
in: "$$iface.bytes_recv",
},
@@ -453,7 +460,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
tLast: {
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
},
},
in: {
$cond: [
@@ -479,7 +488,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
input: {
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
},
as: "iface",
in: "$$iface.packets_sent",
},
@@ -488,7 +499,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
tLast: {
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
},
},
in: {
$cond: [
@@ -522,9 +535,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$map: {
input: {
$arrayElemAt: [
"$net",
{ $ifNull: ["$net", []] },
{
$subtract: [{ $size: "$net" }, 1],
$subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
},
],
},
@@ -538,9 +551,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: {
$arrayElemAt: [
"$updatedAts",
{ $ifNull: ["$updatedAts", []] },
{
$subtract: [{ $size: "$updatedAts" }, 1],
$subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
},
],
},
@@ -566,7 +579,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$arrayElemAt: [
{
$map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] },
input: {
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
},
as: "iface",
in: "$$iface.err_in",
},
@@ -575,7 +590,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
],
},
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] },
tLast: {
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
},
},
in: {
$cond: [
@@ -609,9 +626,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$map: {
input: {
$arrayElemAt: [
"$net",
{ $ifNull: ["$net", []] },
{
$subtract: [{ $size: "$net" }, 1],
$subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
},
],
},
@@ -625,9 +642,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: {
$arrayElemAt: [
"$updatedAts",
{ $ifNull: ["$updatedAts", []] },
{
$subtract: [{ $size: "$updatedAts" }, 1],
$subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
},
],
},
@@ -664,9 +681,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$map: {
input: {
$arrayElemAt: [
"$net",
{ $ifNull: ["$net", []] },
{
$subtract: [{ $size: "$net" }, 1],
$subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
},
],
},
@@ -680,9 +697,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: {
$arrayElemAt: [
"$updatedAts",
{ $ifNull: ["$updatedAts", []] },
{
$subtract: [{ $size: "$updatedAts" }, 1],
$subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
},
],
},
@@ -719,9 +736,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$map: {
input: {
$arrayElemAt: [
"$net",
{ $ifNull: ["$net", []] },
{
$subtract: [{ $size: "$net" }, 1],
$subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
},
],
},
@@ -735,9 +752,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: {
$arrayElemAt: [
"$updatedAts",
{ $ifNull: ["$updatedAts", []] },
{
$subtract: [{ $size: "$updatedAts" }, 1],
$subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
},
],
},
@@ -970,12 +987,7 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, row
// Add checks
if (limit) {
let checksCollection = "checks";
if (type === "pagespeed") {
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
}
const checksCollection = "checks";
monitorsPipeline.push({
$lookup: {
from: checksCollection,
@@ -1040,12 +1052,8 @@ const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsP
// Add checks
if (limit) {
let checksCollection = "checks";
if (type === "pagespeed") {
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
}
const checksCollection = "checks";
pipeline.push({
$lookup: {
from: checksCollection,
@@ -1159,46 +1167,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
},
]
: []),
...(limit
? [
{
$lookup: {
from: "pagespeedchecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "pagespeedchecks",
},
},
]
: []),
...(limit
? [
{
$lookup: {
from: "hardwarechecks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "hardwarechecks",
},
},
]
: []),
{
$addFields: {
@@ -1209,14 +1177,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] },
then: "$standardchecks",
},
{
case: { $eq: ["$type", "pagespeed"] },
then: "$pagespeedchecks",
},
{
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
],
default: [],
},
@@ -1226,8 +1186,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
{
$project: {
standardchecks: 0,
pagespeedchecks: 0,
hardwarechecks: 0,
},
},
],
@@ -1,30 +0,0 @@
const SERVICE_NAME = "networkCheckModule";
class NetworkCheckModule {
constructor({ NetworkCheck }) {
this.NetworkCheck = NetworkCheck;
}
createNetworkCheck = async (networkCheckData) => {
try {
const networkCheck = await new this.NetworkCheck(networkCheckData);
await networkCheck.save();
return networkCheck;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createNetworkCheck";
throw error;
}
};
getNetworkChecksByMonitorId = async (monitorId, limit = 100) => {
try {
const networkChecks = await this.NetworkCheck.find({ monitorId }).sort({ createdAt: -1 }).limit(limit);
return networkChecks;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNetworkChecksByMonitorId";
throw error;
}
};
}
export default NetworkCheckModule;
@@ -1,31 +0,0 @@
// import PageSpeedCheck from "../../models/PageSpeedCheck.js";
const SERVICE_NAME = "pageSpeedCheckModule";
class PageSpeedCheckModule {
constructor({ PageSpeedCheck }) {
this.PageSpeedCheck = PageSpeedCheck;
}
createPageSpeedChecks = async (pageSpeedChecks) => {
try {
await this.PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false });
return true;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createPageSpeedCheck";
throw error;
}
};
deletePageSpeedChecksByMonitorId = async (monitorId) => {
try {
const result = await this.PageSpeedCheck.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deletePageSpeedChecksByMonitorId";
throw error;
}
};
}
export default PageSpeedCheckModule;
+1 -1
View File
@@ -18,7 +18,7 @@ const startApp = async () => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8"));
const frontendPath = path.join(__dirname, "public");
const frontendPath = path.join(__dirname, "..", "public");
// Create services
const settingsService = new SettingsService(AppSettings);
+1 -2
View File
@@ -33,10 +33,9 @@ class CheckService {
throw this.errorService.createAuthorizationError();
}
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
const result = await this.db.checkModule.getChecksByMonitor({
monitorId,
type,
sortOrder,
dateRange,
filter,
@@ -54,6 +54,7 @@ class SuperSimpleQueue {
id: monitorId.toString(),
template: "monitor-job",
repeat: monitor.interval,
active: monitor.isActive,
data: monitor.toObject(),
});
};
@@ -87,7 +88,7 @@ class SuperSimpleQueue {
};
updateJob = async (monitor) => {
this.scheduler.updateJob(monitor._id.toString(), monitor.interval);
this.scheduler.updateJob(monitor._id.toString(), { repeat: monitor.interval, data: monitor.toObject() });
};
shutdown = async () => {
@@ -140,6 +141,7 @@ class SuperSimpleQueue {
monitorId: job.id,
monitorUrl: job?.data?.url || null,
monitorType: job?.data?.type || null,
monitorInterval: job?.data?.interval || null,
active: job.active,
lockedAt: job.lockedAt,
runCount: job.runCount || 0,
@@ -2,6 +2,16 @@ const SERVICE_NAME = "JobQueueHelper";
class SuperSimpleQueueHelper {
static SERVICE_NAME = SERVICE_NAME;
/**
* @param {{
* db: import("../database").Database,
* logger: import("../logger").Logger,
* networkService: import("../networkService").NetworkService,
* statusService: import("../statusService").StatusService,
* notificationService: import("../notificationService").NotificationService
* }}
*/
constructor({ db, logger, networkService, statusService, notificationService }) {
this.db = db;
this.logger = logger;
@@ -31,7 +41,8 @@ class SuperSimpleQueueHelper {
});
return;
}
const networkResponse = await this.networkService.getStatus(monitor);
const networkResponse = await this.networkService.requestStatus(monitor);
if (!networkResponse) {
throw new Error("No network response");
}
@@ -1,32 +1,13 @@
const SERVICE_NAME = "BufferService";
const TYPE_MAP = {
http: "checks",
ping: "checks",
port: "checks",
docker: "checks",
pagespeed: "pagespeedChecks",
hardware: "hardwareChecks",
game: "checks",
};
class BufferService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, logger, envSettings }) {
this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 5000 : 1000 * 60 * 1; // 1 minute
this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 1000 : 1000 * 60 * 1; // 1 minute
this.db = db;
this.logger = logger;
this.SERVICE_NAME = SERVICE_NAME;
this.buffers = {
checks: [],
pagespeedChecks: [],
hardwareChecks: [],
};
this.OPERATION_MAP = {
checks: this.db.checkModule.createChecks,
pagespeedChecks: this.db.pageSpeedCheckModule.createPageSpeedChecks,
hardwareChecks: this.db.hardwareCheckModule.createHardwareChecks,
};
this.buffer = [];
this.scheduleNextFlush();
this.logger.info({
message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`,
@@ -39,9 +20,9 @@ class BufferService {
return BufferService.SERVICE_NAME;
}
addToBuffer({ check, type }) {
addToBuffer({ check }) {
try {
this.buffers[TYPE_MAP[type]].push(check);
this.buffer.push(check);
} catch (error) {
this.logger.error({
message: error.message,
@@ -55,7 +36,7 @@ class BufferService {
scheduleNextFlush() {
this.bufferTimer = setTimeout(async () => {
try {
await this.flushBuffers();
await this.flushBuffer();
} catch (error) {
this.logger.error({
message: `Error in flush cycle: ${error.message}`,
@@ -69,35 +50,24 @@ class BufferService {
}
}, this.BUFFER_TIMEOUT);
}
async flushBuffers() {
let items = 0;
for (const [bufferName, buffer] of Object.entries(this.buffers)) {
items += buffer.length;
const operation = this.OPERATION_MAP[bufferName];
if (!operation) {
this.logger.error({
message: `No operation found for ${bufferName}`,
service: this.SERVICE_NAME,
method: "flushBuffers",
});
continue;
}
try {
await operation(buffer);
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "flushBuffers",
stack: error.stack,
});
}
this.buffers[bufferName] = [];
async flushBuffer() {
let items = this.buffer.length;
try {
await this.db.checkModule.createChecks(this.buffer);
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "flushBuffer",
stack: error.stack,
});
}
this.buffer = [];
this.logger.debug({
message: `Flushed ${items} items`,
service: this.SERVICE_NAME,
method: "flushBuffers",
method: "flushBuffer",
});
items = 0;
}
+261 -324
View File
@@ -1,23 +1,11 @@
import jmespath from "jmespath";
import https from "https";
import { GameDig } from "gamedig";
import { match } from "assert";
const SERVICE_NAME = "NetworkService";
const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push";
/**
* Constructs a new NetworkService instance.
*
* @param {Object} axios - The axios instance for HTTP requests.
* @param {Object} ping - The ping utility for network checks.
* @param {Object} logger - The logger instance for logging.
* @param {Object} http - The HTTP utility for network operations.
* @param {Object} net - The net utility for network operations.
*/
class NetworkService {
static SERVICE_NAME = SERVICE_NAME;
constructor(axios, ping, logger, http, Docker, net, stringService, settingsService) {
constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) {
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
@@ -29,6 +17,10 @@ class NetworkService {
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
this.axios = axios;
this.got = got;
this.https = https;
this.jmespath = jmespath;
this.GameDig = GameDig;
this.ping = ping;
this.logger = logger;
this.http = http;
@@ -38,153 +30,144 @@ class NetworkService {
this.settingsService = settingsService;
}
get serviceName() {
return NetworkService.SERVICE_NAME;
}
/**
* Times the execution of an asynchronous operation.
*
* @param {Function} operation - The asynchronous operation to be timed.
* @returns {Promise<Object>} An object containing the response, response time, and optionally an error.
* @property {Object|null} response - The response from the operation, or null if an error occurred.
* @property {number} responseTime - The time taken for the operation to complete, in milliseconds.
* @property {Error} [error] - The error object if an error occurred during the operation.
*/
// Helper functions
async timeRequest(operation) {
const startTime = Date.now();
const start = process.hrtime.bigint();
try {
const response = await operation();
const endTime = Date.now();
const responseTime = endTime - startTime;
return { response, responseTime };
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response, responseTime: elapsedMs };
} catch (error) {
const endTime = Date.now();
const responseTime = endTime - startTime;
return { response: null, responseTime, error };
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response: null, responseTime: elapsedMs, error };
}
}
// Main entry point
async requestStatus(monitor) {
const type = monitor?.type || "unknown";
switch (type) {
case this.TYPE_PING:
return await this.requestPing(monitor);
case this.TYPE_HTTP:
return await this.requestHttp(monitor);
case this.TYPE_PAGESPEED:
return await this.requestPageSpeed(monitor);
case this.TYPE_HARDWARE:
return await this.requestHardware(monitor);
case this.TYPE_DOCKER:
return await this.requestDocker(monitor);
case this.TYPE_PORT:
return await this.requestPort(monitor);
case this.TYPE_GAME:
return await this.requestGame(monitor);
default:
return await this.handleUnsupportedType(type);
}
}
/**
* Performs a ping check to a specified host to verify its availability.
* @async
* @param {Object} monitor - The monitor configuration object
* @param {string} monitor.url - The host URL to ping
* @param {string} monitor._id - The unique identifier of the monitor
* @returns {Promise<Object>} A promise that resolves to a ping response object
* @returns {string} pingResponse.monitorId - The ID of the monitor
* @returns {string} pingResponse.type - The type of monitor (always "ping")
* @returns {number} pingResponse.responseTime - The time taken for the ping
* @returns {Object} pingResponse.payload - The raw ping response data
* @returns {boolean} pingResponse.status - Whether the host is alive (true) or not (false)
* @returns {number} pingResponse.code - Status code (200 for success, PING_ERROR for failure)
* @returns {string} pingResponse.message - Success or failure message
* @throws {Error} If there's an error during the ping operation
*/
async requestPing(monitor) {
try {
const url = monitor.url;
const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url));
if (!monitor?.url) {
throw new Error("Monitor URL is required");
}
const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url));
if (!response) {
throw new Error("Ping failed - no result returned");
}
const pingResponse = {
monitorId: monitor._id,
type: "ping",
status: response.alive,
code: 200,
responseTime,
message: "Success",
payload: response,
};
if (error) {
pingResponse.status = false;
pingResponse.code = this.PING_ERROR;
pingResponse.message = "No response";
pingResponse.code = 200;
pingResponse.message = "Ping failed";
return pingResponse;
}
pingResponse.code = 200;
pingResponse.status = response.alive;
pingResponse.message = "Success";
return pingResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestPing";
throw error;
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestPing";
throw err;
}
}
/**
* Performs an HTTP GET request to a specified URL with optional validation of response data.
* @async
* @param {Object} monitor - The monitor configuration object
* @param {string} monitor.url - The URL to make the HTTP request to
* @param {string} [monitor.secret] - Optional Bearer token for authentication
* @param {string} monitor._id - The unique identifier of the monitor
* @param {string} monitor.name - The name of the monitor
* @param {string} monitor.teamId - The team ID associated with the monitor
* @param {string} monitor.type - The type of monitor
* @param {boolean} [monitor.ignoreTlsErrors] - Whether to ignore TLS certificate errors
* @param {string} [monitor.jsonPath] - Optional JMESPath expression to extract data from JSON response
* @param {string} [monitor.matchMethod] - Method to match response data ('include', 'regex', or exact match)
* @param {string} [monitor.expectedValue] - Expected value to match against response data
* @returns {Promise<Object>} A promise that resolves to an HTTP response object
* @returns {string} httpResponse.monitorId - The ID of the monitor
* @returns {string} httpResponse.teamId - The team ID
* @returns {string} httpResponse.type - The type of monitor
* @returns {number} httpResponse.responseTime - The time taken for the request
* @returns {Object} httpResponse.payload - The response data
* @returns {boolean} httpResponse.status - Whether the request was successful and matched expected value (if specified)
* @returns {number} httpResponse.code - HTTP status code or NETWORK_ERROR
* @returns {string} httpResponse.message - Success or failure message
* @throws {Error} If there's an error during the HTTP request or data validation
*/
async requestHttp(monitor) {
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
const httpResponse = {
monitorId: _id,
teamId: teamId,
type,
};
try {
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
const config = {};
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
if (ignoreTlsErrors === true) {
config.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
if (!url) {
throw new Error("Monitor URL is required");
}
const { response, responseTime, error } = await this.timeRequest(() => this.axios.get(url, config));
const httpResponse = {
monitorId: _id,
teamId,
type,
responseTime,
payload: response?.data,
const config = {
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
};
if (error) {
const code = error.response?.status || this.NETWORK_ERROR;
httpResponse.code = code;
httpResponse.status = false;
httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError;
if (ignoreTlsErrors) {
config.agent = {
https: new this.https.Agent({
rejectUnauthorized: false,
}),
};
}
const response = await this.got(url, config);
let payload;
const contentType = response.headers["content-type"];
if (contentType && contentType.includes("application/json")) {
try {
payload = JSON.parse(response.body);
} catch {
payload = response.body;
}
} else {
payload = response.body;
}
httpResponse.code = response.statusCode;
httpResponse.status = response.ok;
httpResponse.message = response.statusMessage;
httpResponse.responseTime = response.timings.phases.total || 0;
httpResponse.payload = payload;
httpResponse.timings = response.timings || {};
if (!expectedValue && !jsonPath) {
return httpResponse;
}
httpResponse.code = response.status;
if (expectedValue) {
let ok = false;
if (matchMethod === "equal") ok = payload === expectedValue;
if (matchMethod === "include") ok = payload.includes(expectedValue);
if (matchMethod === "regex") ok = new RegExp(expectedValue).test(payload);
if (!expectedValue) {
// not configure expected value, return
httpResponse.status = true;
httpResponse.message = this.http.STATUS_CODES[response.status];
return httpResponse;
if (ok === true) {
return httpResponse;
} else {
httpResponse.code = 500;
httpResponse.status = false;
return httpResponse;
}
}
// validate if response data match expected value
let result = response?.data;
this.logger.info({
service: this.SERVICE_NAME,
method: "requestHttp",
message: `Job: [${name}](${_id}) match result with expected value`,
details: { expectedValue, result, jsonPath, matchMethod },
});
if (jsonPath) {
const contentType = response.headers["content-type"];
@@ -196,160 +179,165 @@ class NetworkService {
}
try {
result = jmespath.search(result, jsonPath);
} catch (error) {
this.jmespath.search(payload, jsonPath);
} catch {
httpResponse.status = false;
httpResponse.message = this.stringService.httpJsonPathError;
return httpResponse;
}
}
if (result === null || result === undefined) {
return httpResponse;
} catch (err) {
if (err.name === "HTTPError" || err.name === "RequestError") {
httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR;
httpResponse.status = false;
httpResponse.message = this.stringService.httpEmptyResult;
httpResponse.message = err?.response?.statusCode || err.message;
httpResponse.responseTime = err?.timings?.phases?.total || 0;
httpResponse.payload = null;
httpResponse.timings = err?.timings || {};
return httpResponse;
}
let match;
result = typeof result === "object" ? JSON.stringify(result) : result.toString();
if (matchMethod === "include") match = result.includes(expectedValue);
else if (matchMethod === "regex") match = new RegExp(expectedValue).test(result);
else match = result === expectedValue;
httpResponse.status = match;
httpResponse.message = match ? this.stringService.httpMatchSuccess : this.stringService.httpMatchFail;
return httpResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestHttp";
throw error;
err.service = this.SERVICE_NAME;
err.method = "requestHttp";
throw err;
}
}
/**
* Checks the performance of a webpage using Google's PageSpeed Insights API.
* @async
* @param {Object} monitor - The monitor configuration object
* @param {string} monitor.url - The URL of the webpage to analyze
* @returns {Promise<Object|undefined>} A promise that resolves to a pagespeed response object or undefined if API key is missing
* @returns {string} response.monitorId - The ID of the monitor
* @returns {string} response.type - The type of monitor
* @returns {number} response.responseTime - The time taken for the analysis
* @returns {boolean} response.status - Whether the analysis was successful
* @returns {number} response.code - HTTP status code from the PageSpeed API
* @returns {string} response.message - Success or failure message
* @returns {Object} response.payload - The PageSpeed analysis results
* @throws {Error} If there's an error during the PageSpeed analysis
*/
async requestPagespeed(monitor) {
async requestPageSpeed(monitor) {
try {
const url = monitor.url;
const updatedMonitor = { ...monitor };
let pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
if (!url) {
throw new Error("Monitor URL is required");
}
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
const dbSettings = await this.settingsService.getDBSettings();
if (dbSettings?.pagespeedApiKey) {
pagespeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
} else {
this.logger.warn({
message: "Pagespeed API key not found, job not executed",
message: "PageSpeed API key not found, job not executed",
service: this.SERVICE_NAME,
method: "requestPagespeed",
details: { url },
});
return;
}
updatedMonitor.url = pagespeedUrl;
return await this.requestHttp(updatedMonitor);
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestPagespeed";
throw error;
return await this.requestHttp({
...monitor,
url: pageSpeedUrl,
});
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestPageSpeed";
throw err;
}
}
async requestHardware(monitor) {
try {
return await this.requestHttp(monitor);
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestHardware";
throw error;
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestHardware";
throw err;
}
}
/**
* Checks the status of a Docker container by its ID.
* @async
* @param {Object} monitor - The monitor configuration object
* @param {string} monitor.url - The Docker container ID to check
* @param {string} monitor._id - The unique identifier of the monitor
* @param {string} monitor.type - The type of monitor
* @returns {Promise<Object>} A promise that resolves to a docker response object
* @returns {string} dockerResponse.monitorId - The ID of the monitor
* @returns {string} dockerResponse.type - The type of monitor
* @returns {number} dockerResponse.responseTime - The time taken for the container inspection
* @returns {boolean} dockerResponse.status - Whether the container is running (true) or not (false)
* @returns {number} dockerResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure)
* @returns {string} dockerResponse.message - Success or failure message
* @throws {Error} If the container is not found or if there's an error inspecting the container
*/
async requestDocker(monitor) {
try {
if (!monitor.url) {
throw new Error("Monitor URL is required");
}
const docker = new this.Docker({
socketPath: "/var/run/docker.sock",
handleError: true, // Enable error handling
});
const containers = await docker.listContainers({ all: true });
const containerExists = containers.some((c) => c.Id.startsWith(monitor.url));
if (!containerExists) {
throw new Error(this.stringService.dockerNotFound);
}
const container = docker.getContainer(monitor.url);
const { response, responseTime, error } = await this.timeRequest(() => container.inspect());
const dockerResponse = {
monitorId: monitor._id,
type: monitor.type,
responseTime,
};
const containers = await docker.listContainers({ all: true });
// Normalize input: strip leading slashes and convert to lowercase for comparison
const normalizedInput = monitor.url.replace(/^\/+/, "").toLowerCase();
// Priority-based matching to avoid ambiguity:
// 1. Exact full ID match (64-char)
let exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
// 2. Exact container name match (case-insensitive)
let exactNameMatch = containers.find((c) =>
c.Names.some((name) => {
const cleanName = name.replace(/^\/+/, "").toLowerCase();
return cleanName === normalizedInput;
})
);
// 3. Partial ID match (fallback for backwards compatibility)
let partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
// Select container based on priority
let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
// Return negative response if no container
if (!targetContainer) {
this.logger.warn({
message: `No container found for "${monitor.url}".`,
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
dockerResponse.code = 404;
dockerResponse.status = false;
dockerResponse.message = this.stringService.dockerNotFound;
return dockerResponse;
}
// Return negative response if ambiguous matches exist
const matchTypes = [];
if (exactIdMatch) matchTypes.push("exact ID");
if (exactNameMatch) matchTypes.push("exact name");
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
if (matchTypes.length > 1) {
this.logger.warn({
message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`,
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
dockerResponse.status = 404;
dockerResponse.status = false;
dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
return dockerResponse;
}
const container = docker.getContainer(targetContainer.Id);
const { response, responseTime, error } = await this.timeRequest(() => container.inspect());
dockerResponse.responseTime = responseTime;
dockerResponse.status = response?.State?.Status === "running" ? true : false;
dockerResponse.code = 200;
dockerResponse.message = "Docker container status fetched successfully";
if (error) {
dockerResponse.status = false;
dockerResponse.code = error.statusCode || this.NETWORK_ERROR;
dockerResponse.message = error.reason || "Failed to fetch Docker container information";
return dockerResponse;
}
dockerResponse.status = response?.State?.Status === "running" ? true : false;
dockerResponse.code = 200;
dockerResponse.message = "Docker container status fetched successfully";
return dockerResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestDocker";
throw error;
} catch (err) {
err.service = this.SERVICE_NAME;
err.method = "requestDocker";
throw err;
}
}
/**
* Attempts to establish a TCP connection to a specified host and port.
* @async
* @param {Object} monitor - The monitor configuration object
* @param {string} monitor.url - The host URL to connect to
* @param {number} monitor.port - The port number to connect to
* @param {string} monitor._id - The unique identifier of the monitor
* @param {string} monitor.type - The type of monitor
* @returns {Promise<Object>} A promise that resolves to a port response object
* @returns {string} portResponse.monitorId - The ID of the monitor
* @returns {string} portResponse.type - The type of monitor
* @returns {number} portResponse.responseTime - The time taken for the connection attempt
* @returns {boolean} portResponse.status - Whether the connection was successful
* @returns {number} portResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure)
* @returns {string} portResponse.message - Success or failure message
* @throws {Error} If the connection times out or encounters an error
*/
async requestPort(monitor) {
try {
const { url, port } = monitor;
@@ -381,21 +369,21 @@ class NetworkService {
});
const portResponse = {
code: 200,
status: response.success,
message: this.stringService.portSuccess,
monitorId: monitor._id,
type: monitor.type,
responseTime,
};
if (error) {
portResponse.status = false;
portResponse.code = this.NETWORK_ERROR;
portResponse.status = false;
portResponse.message = this.stringService.portFail;
return portResponse;
}
portResponse.status = response.success;
portResponse.code = 200;
portResponse.message = this.stringService.portSuccess;
return portResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
@@ -404,19 +392,55 @@ class NetworkService {
}
}
/**
* Handles unsupported job types by throwing an error with details.
*
* @param {string} type - The unsupported job type that was provided
* @throws {Error} An error with service name, method name and unsupported type message
*/
handleUnsupportedType(type) {
async requestGame(monitor) {
try {
const { url, port, gameId } = monitor;
const gameResponse = {
code: 200,
status: true,
message: "Success",
monitorId: monitor._id,
type: "game",
};
const state = await this.GameDig.query({
type: gameId,
host: url,
port: port,
}).catch((error) => {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "requestGame",
details: { url, port, gameId },
});
});
if (!state) {
gameResponse.code = this.NETWORK_ERROR;
gameResponse.status = false;
gameResponse.message = "No response";
return gameResponse;
}
gameResponse.responseTime = state.ping;
gameResponse.payload = state;
return gameResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestPing";
throw error;
}
}
async handleUnsupportedType(type) {
const err = new Error(`Unsupported type: ${type}`);
err.service = this.SERVICE_NAME;
err.method = "getStatus";
throw err;
}
// Other network requests unrelated to monitoring:
async requestWebhook(type, url, body) {
try {
const response = await this.axios.post(url, body, {
@@ -471,93 +495,6 @@ class NetworkService {
throw error;
}
}
/**
* Requests the status of a game monitor.
*
* @param {Object} monitor - The monitor object to request the status for.
* @returns {Promise<Object>} The response from the game status request.
* @throws {Error} Throws an error if the request fails or if the monitor is not configured correctly.
* @property {string} monitorId - The ID of the monitor.
* @property {string} type - The type of the monitor (should be "game").
* @property {number} responseTime - The time taken for the request.
* @property {Object|null} payload - The game state response or null if the request failed.
* @property {boolean} status - Indicates if the request was successful (true) or not (false).
* @property {number} code - The status code of the request (200 for success, NETWORK_ERROR for failure).
* @property {string} message - A message indicating the result of the request.
*/
async requestGame(monitor) {
try {
const { url, port, gameId } = monitor;
const gameResponse = {
code: 200,
status: true,
message: "Success",
monitorId: monitor._id,
type: "game",
};
const state = await GameDig.query({
type: gameId,
host: url,
port: port,
}).catch((error) => {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "requestGame",
details: { url, port, gameId },
});
});
if (!state) {
gameResponse.status = false;
gameResponse.code = this.NETWORK_ERROR;
gameResponse.message = "No response";
return gameResponse;
}
gameResponse.responseTime = state.ping;
gameResponse.payload = state;
return gameResponse;
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestPing";
throw error;
}
}
/**
* Gets the status of a job based on its type and returns the appropriate response.
*
* @param {Object} job - The job object containing the data for the status request.
* @param {Object} job.data - The data object within the job.
* @param {string} job.data.type - The type of the job (e.g., "ping", "http", "pagespeed", "hardware").
* @returns {Promise<Object>} The response object from the appropriate request method.
* @throws {Error} Throws an error if the job type is unsupported.
*/
async getStatus(monitor) {
const type = monitor.type ?? "unknown";
switch (type) {
case this.TYPE_PING:
return await this.requestPing(monitor);
case this.TYPE_HTTP:
return await this.requestHttp(monitor);
case this.TYPE_PAGESPEED:
return await this.requestPagespeed(monitor);
case this.TYPE_HARDWARE:
return await this.requestHardware(monitor);
case this.TYPE_DOCKER:
return await this.requestDocker(monitor);
case this.TYPE_PORT:
return await this.requestPort(monitor);
case this.TYPE_GAME:
return await this.requestGame(monitor);
default:
return this.handleUnsupportedType(type);
}
}
}
export default NetworkService;
@@ -1,17 +1,14 @@
import MonitorStats from "../../db/models/MonitorStats.js";
import { safelyParseFloat } from "../../utils/dataUtils.js";
const SERVICE_NAME = "StatusService";
class StatusService {
static SERVICE_NAME = SERVICE_NAME;
/**
* Creates an instance of StatusService.
*
* @param {Object} db - The database instance.
* @param {Object} logger - The logger instance.
*/
constructor({ db, logger, buffer }) {
* @param {{
* buffer: import("./bufferService.js").BufferService
* }}
*/ constructor({ db, logger, buffer }) {
this.db = db;
this.logger = logger;
this.buffer = buffer;
@@ -24,7 +21,7 @@ class StatusService {
async updateRunningStats({ monitor, networkResponse }) {
try {
const monitorId = monitor._id;
const { responseTime, status, upt_burnt } = networkResponse;
const { responseTime, status } = networkResponse;
// Get stats
let stats = await MonitorStats.findOne({ monitorId });
if (!stats) {
@@ -36,8 +33,6 @@ class StatusService {
totalDownChecks: 0,
uptimePercentage: 0,
lastCheck: null,
timeSInceLastCheck: 0,
uptBurnt: 0,
});
}
@@ -82,12 +77,6 @@ class StatusService {
// latest check
stats.lastCheckTimestamp = new Date().getTime();
// UPT burned
if (typeof upt_burnt !== "undefined" && upt_burnt !== null) {
const currentUptBurnt = safelyParseFloat(stats.uptBurnt);
const newUptBurnt = safelyParseFloat(upt_burnt);
stats.uptBurnt = currentUptBurnt + newUptBurnt;
}
await stats.save();
return true;
} catch (error) {
@@ -119,7 +108,8 @@ class StatusService {
* @returns {boolean} returnObject.prevStatus - The previous status of the monitor
*/
updateStatus = async (networkResponse) => {
this.insertCheck(networkResponse);
const check = this.buildCheck(networkResponse);
await this.insertCheck(check);
try {
const { monitorId, status, code } = networkResponse;
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
@@ -127,31 +117,67 @@ class StatusService {
// Update running stats
this.updateRunningStats({ monitor, networkResponse });
// No change in monitor status, return early
if (monitor.status === status)
// If the status window size has changed, empty
while (monitor.statusWindow.length > monitor.statusWindowSize) {
monitor.statusWindow.shift();
}
// Update status sliding window
monitor.statusWindow.push(status);
if (monitor.statusWindow.length > monitor.statusWindowSize) {
monitor.statusWindow.shift();
}
if (monitor.status === undefined || monitor.status === null) {
monitor.status = status;
}
let newStatus = monitor.status;
let statusChanged = false;
const prevStatus = monitor.status;
// Return early if not enough data points
if (monitor.statusWindow.length < monitor.statusWindowSize) {
await monitor.save();
return {
monitor,
statusChanged: false,
prevStatus: monitor.status,
prevStatus,
code,
timestamp: new Date().getTime(),
timestamp: Date.now(),
};
}
// Monitor status changed, save prev status and update monitor
this.logger.info({
service: this.SERVICE_NAME,
message: `${monitor.name} went from ${this.getStatusString(monitor.status)} to ${this.getStatusString(status)}`,
prevStatus: monitor.status,
newStatus: status,
});
// Check if threshold has been met
const failures = monitor.statusWindow.filter((s) => s === false).length;
const failureRate = (failures / monitor.statusWindow.length) * 100;
const prevStatus = monitor.status;
monitor.status = status;
// If threshold has been met and the monitor is not already down, mark down:
if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) {
newStatus = false;
statusChanged = true;
}
// If the failure rate is below the threshold and the monitor is down, recover:
else if (failureRate < monitor.statusWindowThreshold && monitor.status === false) {
newStatus = true;
statusChanged = true;
}
if (statusChanged) {
this.logger.info({
service: this.SERVICE_NAME,
message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`,
prevStatus,
newStatus,
});
}
monitor.status = newStatus;
await monitor.save();
return {
monitor,
statusChanged: true,
statusChanged,
prevStatus,
code,
timestamp: new Date().getTime(),
@@ -192,14 +218,17 @@ class StatusService {
conn_took,
connect_took,
tls_took,
timings,
} = networkResponse;
const check = {
monitorId,
teamId,
type,
status,
statusCode: code,
responseTime,
timings: timings || {},
message,
first_byte_took,
body_read_took,
@@ -262,25 +291,24 @@ class StatusService {
* @param {Object} networkResponse.payload - The payload of the response.
* @returns {Promise<void>} A promise that resolves when the check is inserted.
*/
insertCheck = async (networkResponse) => {
insertCheck = async (check) => {
try {
const check = this.buildCheck(networkResponse);
if (typeof check === "undefined") {
this.logger.warn({
message: "Failed to build check",
service: this.SERVICE_NAME,
method: "insertCheck",
details: networkResponse,
});
return;
return false;
}
this.buffer.addToBuffer({ check, type: networkResponse.type });
this.buffer.addToBuffer({ check });
return true;
} catch (error) {
this.logger.error({
message: error.message,
service: error.service || this.SERVICE_NAME,
method: error.method || "insertCheck",
details: error.details || `Error inserting check for monitor: ${networkResponse?.monitorId}`,
details: error.details || `Error inserting check for monitor: ${check?.monitorId}`,
stack: error.stack,
});
}
+18 -417
View File
@@ -25,423 +25,24 @@ class StringService {
return this._language;
}
// Auth Messages
get dontHaveAccount() {
return this.translationService.getTranslation("dontHaveAccount");
}
get email() {
return this.translationService.getTranslation("email");
}
get forgotPassword() {
return this.translationService.getTranslation("forgotPassword");
}
get password() {
return this.translationService.getTranslation("password");
}
get signUp() {
return this.translationService.getTranslation("signUp");
}
get submit() {
return this.translationService.getTranslation("submit");
}
get title() {
return this.translationService.getTranslation("title");
}
get continue() {
return this.translationService.getTranslation("continue");
}
get enterEmail() {
return this.translationService.getTranslation("enterEmail");
}
get authLoginTitle() {
return this.translationService.getTranslation("authLoginTitle");
}
get authLoginEnterPassword() {
return this.translationService.getTranslation("authLoginEnterPassword");
}
get commonPassword() {
return this.translationService.getTranslation("commonPassword");
}
get commonBack() {
return this.translationService.getTranslation("commonBack");
}
get authForgotPasswordTitle() {
return this.translationService.getTranslation("authForgotPasswordTitle");
}
get authForgotPasswordResetPassword() {
return this.translationService.getTranslation("authForgotPasswordResetPassword");
}
get createPassword() {
return this.translationService.getTranslation("createPassword");
}
get createAPassword() {
return this.translationService.getTranslation("createAPassword");
}
get authRegisterAlreadyHaveAccount() {
return this.translationService.getTranslation("authRegisterAlreadyHaveAccount");
}
get commonAppName() {
return this.translationService.getTranslation("commonAppName");
}
get authLoginEnterEmail() {
return this.translationService.getTranslation("authLoginEnterEmail");
}
get authRegisterTitle() {
return this.translationService.getTranslation("authRegisterTitle");
}
get monitorGetAll() {
return this.translationService.getTranslation("monitorGetAll");
}
get monitorGetById() {
return this.translationService.getTranslation("monitorGetById");
}
get monitorGetByIdSuccess() {
return this.translationService.getTranslation("monitorGetByIdSuccess");
}
get monitorCreate() {
return this.translationService.getTranslation("monitorCreate");
}
get bulkMonitorsCreate() {
return this.translationService.getTranslation("bulkMonitorsCreate");
}
get monitorEdit() {
return this.translationService.getTranslation("monitorEdit");
}
get monitorDelete() {
return this.translationService.getTranslation("monitorDelete");
}
get monitorPause() {
return this.translationService.getTranslation("monitorPause");
}
get monitorResume() {
return this.translationService.getTranslation("monitorResume");
}
get monitorDemoAdded() {
return this.translationService.getTranslation("monitorDemoAdded");
}
get monitorStatsById() {
return this.translationService.getTranslation("monitorStatsById");
}
get monitorCertificate() {
return this.translationService.getTranslation("monitorCertificate");
}
// Maintenance Window Messages
get maintenanceWindowCreate() {
return this.translationService.getTranslation("maintenanceWindowCreate");
}
get maintenanceWindowGetById() {
return this.translationService.getTranslation("maintenanceWindowGetById");
}
get maintenanceWindowGetByTeam() {
return this.translationService.getTranslation("maintenanceWindowGetByTeam");
}
get maintenanceWindowDelete() {
return this.translationService.getTranslation("maintenanceWindowDelete");
}
get maintenanceWindowEdit() {
return this.translationService.getTranslation("maintenanceWindowEdit");
}
// Webhook Messages
get webhookUnsupportedPlatform() {
return this.translationService.getTranslation("webhookUnsupportedPlatform");
}
get webhookSendError() {
return this.translationService.getTranslation("webhookSendError");
}
get webhookSendSuccess() {
return this.translationService.getTranslation("webhookSendSuccess");
}
get telegramRequiresBotTokenAndChatId() {
return this.translationService.getTranslation("telegramRequiresBotTokenAndChatId");
}
get webhookUrlRequired() {
return this.translationService.getTranslation("webhookUrlRequired");
}
get platformRequired() {
return this.translationService.getTranslation("platformRequired");
}
get testNotificationFailed() {
return this.translationService.getTranslation("testNotificationFailed");
}
get monitorUpAlert() {
return this.translationService.getTranslation("monitorUpAlert");
}
get monitorDownAlert() {
return this.translationService.getTranslation("monitorDownAlert");
}
getWebhookUnsupportedPlatform(platform) {
return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform);
}
getWebhookSendError(platform) {
return this.translationService.getTranslation("webhookSendError").replace("{platform}", platform);
}
getMonitorStatus(name, status, url) {
const translationKey = status === true ? "monitorStatusUp" : "monitorStatusDown";
return this.translationService.getTranslation(translationKey).replace("{name}", name).replace("{url}", url);
}
// Error Messages
get unknownError() {
return this.translationService.getTranslation("unknownError");
}
get friendlyError() {
return this.translationService.getTranslation("friendlyError");
}
get authIncorrectPassword() {
return this.translationService.getTranslation("authIncorrectPassword");
}
get unauthorized() {
return this.translationService.getTranslation("unauthorized");
}
get authAdminExists() {
return this.translationService.getTranslation("authAdminExists");
}
get authInviteNotFound() {
return this.translationService.getTranslation("authInviteNotFound");
}
get unknownService() {
return this.translationService.getTranslation("unknownService");
}
get noAuthToken() {
return this.translationService.getTranslation("noAuthToken");
}
get invalidAuthToken() {
return this.translationService.getTranslation("invalidAuthToken");
}
get expiredAuthToken() {
return this.translationService.getTranslation("expiredAuthToken");
}
// Queue Messages
get queueGetMetrics() {
return this.translationService.getTranslation("queueGetMetrics");
}
get queueGetJobs() {
return this.translationService.getTranslation("queueGetJobs");
}
get queueAddJob() {
return this.translationService.getTranslation("queueAddJob");
}
get queueObliterate() {
return this.translationService.getTranslation("queueObliterate");
}
// Job Queue Messages
get jobQueueDeleteJobSuccess() {
return this.translationService.getTranslation("jobQueueDeleteJobSuccess");
}
get jobQueuePauseJob() {
return this.translationService.getTranslation("jobQueuePauseJob");
}
get jobQueueResumeJob() {
return this.translationService.getTranslation("jobQueueResumeJob");
}
// Status Page Messages
get statusPageByUrl() {
return this.translationService.getTranslation("statusPageByUrl");
}
get statusPageCreate() {
return this.translationService.getTranslation("statusPageCreate");
}
get statusPageDelete() {
return this.translationService.getTranslation("statusPageDelete");
}
get statusPageUpdate() {
return this.translationService.getTranslation("statusPageUpdate");
}
get statusPageNotFound() {
return this.translationService.getTranslation("statusPageNotFound");
}
get statusPageByTeamId() {
return this.translationService.getTranslation("statusPageByTeamId");
}
get statusPageUrlNotUnique() {
return this.translationService.getTranslation("statusPageUrlNotUnique");
}
// Docker Messages
get dockerFail() {
return this.translationService.getTranslation("dockerFail");
}
get dockerNotFound() {
return this.translationService.getTranslation("dockerNotFound");
}
get dockerSuccess() {
return this.translationService.getTranslation("dockerSuccess");
}
// Port Messages
get portFail() {
return this.translationService.getTranslation("portFail");
}
get portSuccess() {
return this.translationService.getTranslation("portSuccess");
}
// Alert Messages
get alertCreate() {
return this.translationService.getTranslation("alertCreate");
}
get alertGetByUser() {
return this.translationService.getTranslation("alertGetByUser");
}
get alertGetByMonitor() {
return this.translationService.getTranslation("alertGetByMonitor");
}
get alertGetById() {
return this.translationService.getTranslation("alertGetById");
}
get alertEdit() {
return this.translationService.getTranslation("alertEdit");
}
get alertDelete() {
return this.translationService.getTranslation("alertDelete");
}
getDeletedCount(count) {
return this.translationService.getTranslation("deletedCount").replace("{count}", count);
}
get pingSuccess() {
return this.translationService.getTranslation("pingSuccess");
}
get getAppSettings() {
return this.translationService.getTranslation("getAppSettings");
}
get httpNetworkError() {
return this.translationService.getTranslation("httpNetworkError");
}
get httpNotJson() {
return this.translationService.getTranslation("httpNotJson");
}
get httpJsonPathError() {
return this.translationService.getTranslation("httpJsonPathError");
}
get httpEmptyResult() {
return this.translationService.getTranslation("httpEmptyResult");
}
get httpMatchSuccess() {
return this.translationService.getTranslation("httpMatchSuccess");
}
get httpMatchFail() {
return this.translationService.getTranslation("httpMatchFail");
}
get updateAppSettings() {
return this.translationService.getTranslation("updateAppSettings");
}
get insufficientPermissions() {
return this.translationService.getTranslation("insufficientPermissions");
}
getDbFindMonitorById(monitorId) {
return this.translationService.getTranslation("dbFindMonitorById").replace("${monitorId}", monitorId);
}
get dbUserExists() {
return this.translationService.getTranslation("dbUserExists");
}
get testEmailSubject() {
return this.translationService.getTranslation("testEmailSubject");
}
get verifyOwnerNotFound() {
return this.translationService.getTranslation("verifyOwnerNotFound");
}
get verifyOwnerUnauthorized() {
return this.translationService.getTranslation("verifyOwnerUnauthorized");
}
get dbUserNotFound() {
return this.translationService.getTranslation("dbUserNotFound");
/**
* Get translation by key with optional parameters
* @param {string} key - Translation key
* @param {Object} params - Optional parameters to replace in the translation
* @returns {string} Translated string with replaced parameters
*/
get(key, params = {}) {
let translation = this.translationService.getTranslation(key);
// Replace parameters in the translation
Object.keys(params).forEach(paramKey => {
const paramValue = params[paramKey];
// Support both {param} and ${param} syntax
translation = translation.replace(new RegExp(`\\{${paramKey}\\}`, 'g'), paramValue);
translation = translation.replace(new RegExp(`\\$\\{${paramKey}\\}`, 'g'), paramValue);
});
return translation;
}
}
@@ -7,7 +7,7 @@ class TranslationService {
this.logger = logger;
this.translations = {};
this._language = "en";
this.localesDir = path.join(process.cwd(), "locales");
this.localesDir = path.join(process.cwd(), "src/locales");
}
get serviceName() {
+29 -11
View File
@@ -155,6 +155,8 @@ const createMonitorBodyValidation = joi.object({
name: joi.string().required(),
description: joi.string().required(),
type: joi.string().required(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
url: joi.string().required(),
ignoreTlsErrors: joi.boolean().default(false),
port: joi.number(),
@@ -183,6 +185,8 @@ const createMonitorsBodyValidation = joi.array().items(
const editMonitorBodyValidation = joi.object({
name: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
description: joi.string(),
interval: joi.number(),
notifications: joi.array().items(joi.string()),
@@ -576,17 +580,31 @@ const createNotificationBodyValidation = joi.object({
}),
address: joi.when("type", {
is: "email",
then: joi.string().email().required().messages({
"string.empty": "E-mail address cannot be empty",
"any.required": "E-mail address is required",
"string.email": "Please enter a valid e-mail address",
}),
otherwise: joi.string().uri().required().messages({
"string.empty": "Webhook URL cannot be empty",
"any.required": "Webhook URL is required",
"string.uri": "Please enter a valid Webhook URL",
}),
switch: [
{
is: "email",
then: joi.string().email().required().messages({
"string.empty": "E-mail address cannot be empty",
"any.required": "E-mail address is required",
"string.email": "Please enter a valid e-mail address",
}),
},
{
is: "pager_duty",
then: joi.string().required().messages({
"string.empty": "PagerDuty integration key cannot be empty",
"any.required": "PagerDuty integration key is required",
}),
},
{
is: joi.string().valid("webhook", "slack", "discord"),
then: joi.string().uri().required().messages({
"string.empty": "Webhook URL cannot be empty",
"any.required": "Webhook URL is required",
"string.uri": "Please enter a valid Webhook URL",
}),
},
],
}),
});