Merge pull request #155 from operacle/develop

Merge pull request  from operacle/develop
This commit is contained in:
Tola Leng
2025-08-29 00:45:32 +07:00
committed by GitHub
106 changed files with 15277 additions and 27268 deletions
+2
View File
@@ -22,6 +22,8 @@ dist-ssr
*.njsproj
*.sln
*.sw?
pocketbase.exe
application/src/lib/pocketbase.ts
server/pb_data/
Dockerfile
application/src/lib/pocketbase-prod.ts
+20 -11
View File
@@ -130,7 +130,7 @@ docker run -d \
## Sponsors
🙏 We're incredibly thankful to our sponsors. Your contributions enable us to maintain infrastructure (hosting, domains), run tests, and continue developing valuable features.
### 🥈 Silver Uptime Ally
### 🥈 Silver Uptime
<a href="https://github.com/michaelortnerit">
<img src="https://avatars.githubusercontent.com/u/135371107?v=4" width="75" height="75" style="border-radius: 50%; display: block;" />
@@ -143,16 +143,25 @@ docker run -d \
</a>
### 🤝 Ecosystem & Community Partner
<a href="https://github.com/gitbookio" style="display: inline-block; margin-right: 10px; vertical-align: middle;">
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
width="75" height="75"
style="border-radius: 50%;" />
</a>
<a href="https://m.do.co/c/0c27ef82475f" style="display: inline-block; vertical-align: middle;">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg"
height="45"
alt="DigitalOcean Logo">
</a>
<div style="display: flex; align-items: center; gap: 10px;">
<a href="https://github.com/gitbookio">
<img src="https://avatars.githubusercontent.com/u/7111340?s=200&v=4"
width="75" height="75"
style="border-radius: 50%;"
alt="GitBook Logo" />
</a>
<a href="https://m.do.co/c/0c27ef82475f">
<img src="https://cdn.checkcle.io/images/sponsor/digitalocean_checkcle.png"
height="50"
alt="DigitalOcean Logo" />
</a>
</div>
---
+285 -15
View File
@@ -27,7 +27,7 @@
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slider": "^1.2.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
@@ -43,6 +43,8 @@
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"jspdf": "^3.0.1",
"jspdf-autotable": "^5.0.2",
"lucide-react": "^0.462.0",
"next-themes": "^0.3.0",
"pocketbase": "^0.19.0",
@@ -91,13 +93,10 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz",
"integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
@@ -1811,12 +1810,12 @@
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
"integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==",
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz",
"integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0"
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
@@ -1833,6 +1832,62 @@
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.1.tgz",
@@ -2832,6 +2887,13 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/raf": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
"integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
"license": "MIT",
"optional": true
},
"node_modules/@types/react": {
"version": "18.3.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
@@ -2853,6 +2915,13 @@
"@types/react": "*"
}
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
@@ -3206,6 +3275,18 @@
"node": ">=10"
}
},
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"atob": "bin/atob.js"
},
"engines": {
"node": ">= 4.5.0"
}
},
"node_modules/autoprefixer": {
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
@@ -3250,6 +3331,16 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -3318,6 +3409,18 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/btoa": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
"integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
"license": "(MIT OR Apache-2.0)",
"bin": {
"btoa": "bin/btoa.js"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3358,6 +3461,26 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvg": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
"integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@types/raf": "^3.4.0",
"core-js": "^3.8.3",
"raf": "^3.4.1",
"regenerator-runtime": "^0.13.7",
"rgbcolor": "^1.0.1",
"stackblur-canvas": "^2.0.0",
"svg-pathdata": "^6.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3843,6 +3966,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/core-js": {
"version": "3.45.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.0.tgz",
"integrity": "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3856,6 +3991,16 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4064,6 +4209,16 @@
"csstype": "^3.0.2"
}
},
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optional": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -4424,6 +4579,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4647,6 +4808,20 @@
"node": ">= 0.4"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"optional": true,
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4848,6 +5023,33 @@
"dev": true,
"license": "MIT"
},
"node_modules/jspdf": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
"integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.7",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"fflate": "^0.8.1"
},
"optionalDependencies": {
"canvg": "^3.0.11",
"core-js": "^3.6.0",
"dompurify": "^3.2.4",
"html2canvas": "^1.0.0-rc.5"
}
},
"node_modules/jspdf-autotable": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.2.tgz",
"integrity": "sha512-YNKeB7qmx3pxOLcNeoqAv3qTS7KuvVwkFe5AduCawpop3NOkBUtqDToxNc225MlNecxT4kP2Zy3z/y/yvGdXUQ==",
"license": "MIT",
"peerDependencies": {
"jspdf": "^2 || ^3"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5208,6 +5410,13 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT",
"optional": true
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5450,6 +5659,16 @@
],
"license": "MIT"
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"license": "MIT",
"optional": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -5708,10 +5927,11 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT",
"optional": true
},
"node_modules/resolve": {
"version": "1.22.8",
@@ -5750,6 +5970,16 @@
"node": ">=0.10.0"
}
},
"node_modules/rgbcolor": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
"integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
"license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
"optional": true,
"engines": {
"node": ">= 0.8.15"
}
},
"node_modules/rollup": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
@@ -5883,6 +6113,16 @@
"node": ">=0.10.0"
}
},
"node_modules/stackblur-canvas": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
"integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.14"
}
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -6039,6 +6279,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg-pathdata": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
"integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tailwind-merge": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
@@ -6095,6 +6345,16 @@
"tailwindcss": ">=3.0.0 || insiders"
}
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"optional": true,
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -6314,6 +6574,16 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"optional": true,
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/vaul": {
"version": "0.9.9",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz",
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@@ -7,6 +7,7 @@ import { StatusCards } from "./StatusCards";
import { ServiceFilters } from "./ServiceFilters";
import { ServicesTable } from "./ServicesTable";
import { AddServiceDialog } from "@/components/services/AddServiceDialog";
import { useLanguage } from "@/contexts/LanguageContext";
interface DashboardContentProps {
services: Service[];
@@ -15,6 +16,7 @@ interface DashboardContentProps {
}
export const DashboardContent = ({ services, isLoading, error }: DashboardContentProps) => {
const { t } = useLanguage();
const [filter, setFilter] = useState<string>("all");
const [searchTerm, setSearchTerm] = useState<string>("");
const [isAddDialogOpen, setIsAddDialogOpen] = useState<boolean>(false);
@@ -31,7 +33,7 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
return (
<div className="flex flex-col items-center justify-center h-full gap-4 text-foreground">
<p>Error loading service data.</p>
<Button onClick={() => window.location.reload()}>Retry</Button>
<Button onClick={() => window.location.reload()}>{t('retry')}</Button>
</div>
);
}
@@ -40,12 +42,12 @@ export const DashboardContent = ({ services, isLoading, error }: DashboardConten
<main className="flex-1 flex flex-col overflow-auto bg-background p-6 pb-0">
<div className="flex flex-col flex-1">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-foreground">Overview</h2>
<h2 className="text-2xl font-bold text-foreground">{t('overview')}</h2>
<Button
className="text-primary-foreground"
onClick={() => setIsAddDialogOpen(true)}
>
<Plus className="w-4 h-4 mr-2" /> New Service
<Plus className="w-4 h-4 mr-2" /> {t('newService')}
</Button>
</div>
@@ -116,6 +116,9 @@ export const Header = ({
<DropdownMenuItem onClick={() => setLanguage("de")} className={language === "de" ? "bg-accent" : ""}>
{t("Deutsch")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ko")} className={language === "ko" ? "bg-accent" : ""}>
{t("korean")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage("ja")} className={language === "ja" ? "bg-accent" : ""}>
{t("japanese")}
</DropdownMenuItem>
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
import { Plus } from "lucide-react";
import { useLanguage } from "@/contexts/LanguageContext";
interface ServiceFiltersProps {
filter: string;
@@ -19,10 +20,11 @@ export const ServiceFilters = ({
setSearchTerm,
servicesCount
}: ServiceFiltersProps) => {
const { t } = useLanguage();
return (
<div className="mb-6 flex justify-between items-center">
<div className="flex items-center">
<h3 className="text-xl font-semibold mr-2 text-foreground">Currently Monitoring</h3>
<h3 className="text-xl font-semibold mr-2 text-foreground">{t('currentlyMonitoring')}</h3>
<span className="bg-secondary text-secondary-foreground px-2 py-0.5 rounded text-sm">
{servicesCount}
</span>
@@ -30,10 +32,10 @@ export const ServiceFilters = ({
<div className="flex space-x-4">
<Select value={filter} onValueChange={setFilter}>
<SelectTrigger className="w-40 bg-card border-border">
<SelectValue placeholder="All Types" />
<SelectValue placeholder={t('allTypes')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="all">{t('allTypes')}</SelectItem>
<SelectItem value="HTTP">HTTP</SelectItem>
<SelectItem value="PING">PING</SelectItem>
<SelectItem value="TCP">TCP</SelectItem>
@@ -43,7 +45,7 @@ export const ServiceFilters = ({
<div className="relative">
<Input
className="w-72 bg-card border-border"
placeholder="Search"
placeholder={t('search')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
@@ -3,12 +3,15 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ArrowUp, ArrowDown, Pause, AlertTriangle } from "lucide-react";
import { Service } from "@/services/serviceService";
import { useTheme } from "@/contexts/ThemeContext";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface StatusCardsProps {
services: Service[];
}
export const StatusCards = ({ services }: StatusCardsProps) => {
const { t } = useLanguage();
// Count services by status
const upServices = services.filter(s => s.status === "up").length;
const downServices = services.filter(s => s.status === "down").length;
@@ -42,7 +45,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">UP SERVICES</CardTitle>
<CardTitle className="text-sm font-medium text-white">{t("upServices")}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{upServices}</span>
@@ -74,7 +77,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">DOWN SERVICES</CardTitle>
<CardTitle className="text-sm font-medium text-white">{t("downServices")}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{downServices}</span>
@@ -106,7 +109,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">PAUSED SERVICES</CardTitle>
<CardTitle className="text-sm font-medium text-white">{t("pausedServices")}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{pausedServices}</span>
@@ -138,7 +141,7 @@ export const StatusCards = ({ services }: StatusCardsProps) => {
></div>
</div>
<CardHeader className="pb-2 relative z-10">
<CardTitle className="text-sm font-medium text-white">WARNING SERVICES</CardTitle>
<CardTitle className="text-sm font-medium text-white">{t("warningServices")}</CardTitle>
</CardHeader>
<CardContent className="flex items-center justify-between relative z-10">
<span className="text-5xl font-bold text-white">{warningServices}</span>
@@ -25,19 +25,20 @@ export const IncidentDetailContent = ({
onClose,
assignedUser
}: IncidentDetailContentProps) => {
// Fetch assigned user details if one wasn't provided and there's an assigned_to field
// Fetch assigned user details if none was provided; prefer server field
const assigneeId = incident?.assigned_users || incident?.assigned_to;
const { data: fetchedUser } = useQuery({
queryKey: ['user', incident?.assigned_to],
queryKey: ['user', assigneeId],
queryFn: async () => {
if (!incident?.assigned_to) return null;
if (!assigneeId) return null;
try {
return await userService.getUser(incident.assigned_to);
return await userService.getUser(assigneeId);
} catch (error) {
console.error("Failed to fetch assigned user:", error);
return null;
}
},
enabled: !!incident?.assigned_to && !assignedUser,
enabled: !!assigneeId && !assignedUser,
staleTime: 300000 // Cache for 5 minutes
});
@@ -17,7 +17,7 @@ export const IncidentDetailDialog = ({
onOpenChange,
incident
}: IncidentDetailDialogProps) => {
// Fetch user data for assigned_to field
// Fetch user data for assigned field (prefer assigned_users, fallback to assigned_to)
const { data: users = [] } = useQuery({
queryKey: ['users'],
queryFn: async () => {
@@ -25,12 +25,12 @@ export const IncidentDetailDialog = ({
return Array.isArray(usersList) ? usersList : [];
},
staleTime: 300000, // Cache for 5 minutes
enabled: !!incident?.assigned_to && open // Only run query if there's an assigned_to value and dialog is open
enabled: !!(incident?.assigned_users || incident?.assigned_to) && open // Only run query if there's an assigned value and dialog is open
});
// Find the assigned user
const assignedUser = incident?.assigned_to
? users.find(user => user.id === incident.assigned_to)
// Find the assigned user (prefer assigned_users, fallback to assigned_to)
const assignedUser = (incident?.assigned_users || incident?.assigned_to)
? users.find(user => user.id === (incident?.assigned_users || incident?.assigned_to))
: null;
if (!incident) return null;
@@ -23,19 +23,19 @@ export {
// Legacy component - keeping this for backward compatibility with other imports
export const IncidentDetailSections = ({ incident }: { incident: IncidentItem | null }) => {
// Fetch assigned user details if there's an assigned_to field
// Fetch assigned user details if there's an assigned field (prefer assigned_users, fallback to assigned_to)
const { data: assignedUser } = useQuery({
queryKey: ['user', incident?.assigned_to],
queryKey: ['user', incident?.assigned_users || incident?.assigned_to],
queryFn: async () => {
if (!incident?.assigned_to) return null;
if (!(incident?.assigned_users || incident?.assigned_to)) return null;
try {
return await userService.getUser(incident.assigned_to);
return await userService.getUser(incident?.assigned_users || incident?.assigned_to);
} catch (error) {
console.error("Failed to fetch assigned user:", error);
return null;
}
},
enabled: !!incident?.assigned_to,
enabled: !!(incident?.assigned_users || incident?.assigned_to),
staleTime: 300000 // Cache for 5 minutes
});
@@ -31,8 +31,8 @@ export const AssignmentSection: React.FC<AssignmentSectionProps> = ({ incident,
</Avatar>
<span>{assignedUser.full_name || assignedUser.username}</span>
</div>
) : incident.assigned_to ? (
<span>{incident.assigned_to}</span>
) : (incident.assigned_users || incident.assigned_to) ? (
<span>{incident.assigned_users || incident.assigned_to}</span>
) : (
<span className="text-muted-foreground italic">{t('unassigned')}</span>
)}
@@ -50,8 +50,8 @@ export const BasicInfoSection: React.FC<BasicInfoSectionProps> = ({ incident, as
</Avatar>
<span>{assignedUser.full_name || assignedUser.username}</span>
</div>
) : incident.assigned_to ? (
<span>{incident.assigned_to}</span>
) : (incident.assigned_users || incident.assigned_to) ? (
<span>{incident.assigned_users || incident.assigned_to}</span>
) : (
<span className="text-muted-foreground italic">{t('unassigned')}</span>
)}
@@ -68,7 +68,7 @@ export const IncidentBasicFields: React.FC = () => {
const selectedUser = users.find(user => user.id === form.getValues('assigned_to'));
// Function to get user initials from name
const getUserInitials = (user: any): string => {
const getUserInitials = (user: { full_name?: string; username: string }): string => {
if (user.full_name) {
const nameParts = user.full_name.split(' ');
if (nameParts.length > 1) {
@@ -25,7 +25,7 @@ export const useIncidentEditForm = (
impact: (incident.impact?.toLowerCase() || 'minor') as any,
priority: (incident.priority?.toLowerCase() || 'medium') as any,
service_id: incident.service_id || '',
assigned_to: incident.assigned_to || '',
assigned_to: incident.assigned_users || incident.assigned_to || '',
root_cause: incident.root_cause || '',
resolution_steps: incident.resolution_steps || '',
lessons_learned: incident.lessons_learned || '',
@@ -45,11 +45,14 @@ export const useIncidentEditForm = (
impact: data.impact,
priority: data.priority,
service_id: data.service_id,
assigned_to: data.assigned_to, // This is the user ID from the form
...(data.assigned_to
? { assigned_to: data.assigned_to, assigned_users: data.assigned_to }
: {}),
root_cause: data.root_cause,
resolution_steps: data.resolution_steps,
lessons_learned: data.lessons_learned,
});
toast({
title: t('incidentUpdated'),
@@ -57,6 +57,7 @@ export const useIncidentForm = (onSuccess: () => void, onClose: () => void) => {
priority: data.priority,
service_id: data.service_id,
assigned_to: data.assigned_to,
assigned_users: data.assigned_to, // map to server field
root_cause: data.root_cause,
resolution_steps: data.resolution_steps,
lessons_learned: data.lessons_learned,
@@ -86,7 +86,7 @@ export const IncidentTableRow = memo(({
</Badge>
</TableCell>
<TableCell>
<AssignedUserCell userId={localItem.assigned_to} />
<AssignedUserCell userId={localItem.assigned_users || localItem.assigned_to} />
</TableCell>
<TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
<div className="flex justify-end items-center space-x-2">
@@ -9,6 +9,7 @@ import {
import { ServiceForm } from "./ServiceForm";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useQueryClient } from "@tanstack/react-query";
import { useLanguage } from "@/contexts/LanguageContext";
interface AddServiceDialogProps {
open: boolean;
@@ -16,6 +17,7 @@ interface AddServiceDialogProps {
}
export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps) {
const { t } = useLanguage();
const queryClient = useQueryClient();
const handleSuccess = async () => {
// Immediately invalidate and refetch services data
@@ -32,9 +34,9 @@ export function AddServiceDialog({ open, onOpenChange }: AddServiceDialogProps)
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle className="text-xl">Create New Service</DialogTitle>
<DialogTitle className="text-xl">{t("createNewService")}</DialogTitle>
<DialogDescription>
Fill in the details to create a new service to monitor.
{t("createNewServiceDesc")}
</DialogDescription>
</DialogHeader>
<ScrollArea className="flex-1 pr-4 overflow-auto" style={{ height: "calc(80vh - 180px)" }}>
@@ -3,22 +3,24 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/comp
import { Input } from "@/components/ui/input";
import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface ServiceBasicFieldsProps {
form: UseFormReturn<ServiceFormData>;
}
export function ServiceBasicFields({ form }: ServiceBasicFieldsProps) {
const { t } = useLanguage();
return (
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Service Name</FormLabel>
<FormLabel>{t('serviceName')}</FormLabel>
<FormControl>
<Input
placeholder="Enter a descriptive name for your service"
placeholder={t('serviceNameDesc')}
{...field}
/>
</FormControl>
@@ -6,12 +6,14 @@ import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types";
import { ServiceUrlField } from "./ServiceUrlField";
import { useState } from "react";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface ServiceConfigFieldsProps {
form: UseFormReturn<ServiceFormData>;
}
export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
const { t } = useLanguage();
const [isCustomInterval, setIsCustomInterval] = useState(false);
const intervalValue = form.watch("interval");
@@ -35,7 +37,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
name="interval"
render={({ field }) => (
<FormItem>
<FormLabel>Check Interval</FormLabel>
<FormLabel>{t("checkInterval")}</FormLabel>
{!isCustomInterval ? (
<FormControl>
<Select onValueChange={handleIntervalChange} value={isCustomInterval ? "custom" : field.value}>
@@ -43,13 +45,13 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="30">30 seconds</SelectItem>
<SelectItem value="60">1 minute</SelectItem>
<SelectItem value="300">5 minutes</SelectItem>
<SelectItem value="900">15 minutes</SelectItem>
<SelectItem value="1800">30 minutes</SelectItem>
<SelectItem value="3600">1 hour</SelectItem>
<SelectItem value="custom">Custom</SelectItem>
<SelectItem value="30">30 {t("seconds")}</SelectItem>
<SelectItem value="60">1 {t("minute")}</SelectItem>
<SelectItem value="300">5 {t("minutes")}</SelectItem>
<SelectItem value="900">15 {t("minutes")}</SelectItem>
<SelectItem value="1800">30 {t("minutes")}</SelectItem>
<SelectItem value="3600">1 {t("hour")}</SelectItem>
<SelectItem value="custom">{t("custom")}</SelectItem>
</SelectContent>
</Select>
</FormControl>
@@ -58,7 +60,7 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
<FormControl>
<Input
type="number"
placeholder="Enter interval in seconds"
placeholder={t("checkIntervalPlaceholder")}
value={field.value}
onChange={field.onChange}
min="10"
@@ -72,14 +74,14 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
}}
className="text-sm text-blue-600 hover:text-blue-800"
>
Back to presets
{t("backToPresets")}
</button>
</div>
)}
<FormDescription className="text-xs">
{isCustomInterval
? "Enter custom interval in seconds (minimum 10 seconds)"
: "How often to check the service status"
? t("checkIntervalDescCustom")
: t("checkIntervalDesc")
}
</FormDescription>
<FormMessage />
@@ -92,22 +94,22 @@ export function ServiceConfigFields({ form }: ServiceConfigFieldsProps) {
name="retries"
render={({ field }) => (
<FormItem>
<FormLabel>Retry Attempts</FormLabel>
<FormLabel>{t("retryAttempts")}</FormLabel>
<FormControl>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<Select onValueChange={field.onChange} value={field.value ?? "3"}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 attempt</SelectItem>
<SelectItem value="2">2 attempts</SelectItem>
<SelectItem value="3">3 attempts</SelectItem>
<SelectItem value="5">5 attempts</SelectItem>
<SelectItem value="1">1 {t("attempt")}</SelectItem>
<SelectItem value="2">2 {t("attempts")}</SelectItem>
<SelectItem value="3">3 {t("attempts")}</SelectItem>
<SelectItem value="5">5 {t("attempts")}</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormDescription className="text-xs">
Number of retry attempts before marking as down
{t("retryAttemptsDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState, useEffect } from "react";
import { useToast } from "@/hooks/use-toast";
import { serviceSchema, ServiceFormData } from "./types";
import { useServiceSchema, ServiceFormData } from "./types";
import { ServiceBasicFields } from "./ServiceBasicFields";
import { ServiceTypeField } from "./ServiceTypeField";
import { ServiceConfigFields } from "./ServiceConfigFields";
@@ -14,6 +14,7 @@ import { Service } from "@/types/service.types";
import { ServiceRegionalFields } from "./ServiceRegionalFields";
import { getServiceFormDefaults, mapServiceToFormData, mapFormDataToServiceData } from "./serviceFormUtils";
import { useQueryClient } from "@tanstack/react-query";
import {useLanguage} from "@/contexts/LanguageContext.tsx";
interface ServiceFormProps {
onSuccess: () => void;
@@ -30,10 +31,13 @@ export function ServiceForm({
isEdit = false,
onSubmitStart
}: ServiceFormProps) {
const { t } = useLanguage();
const { toast } = useToast();
const [isSubmitting, setIsSubmitting] = useState(false);
const queryClient = useQueryClient();
const serviceSchema = useServiceSchema();
// Initialize form with default values
const form = useForm<ServiceFormData>({
resolver: zodResolver(serviceSchema),
@@ -124,23 +128,23 @@ export function ServiceForm({
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 pb-6">
<div className="space-y-6">
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Basic Information</h3>
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('basicInformation')}</h3>
<ServiceBasicFields form={form} />
<ServiceTypeField form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Configuration</h3>
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('configuration')}</h3>
<ServiceConfigFields form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Regional Monitoring</h3>
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('regionalMonitoring')}</h3>
<ServiceRegionalFields form={form} />
</div>
<div className="space-y-4">
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">Notifications</h3>
<h3 className="text-sm font-medium text-muted-foreground border-b pb-2">{t('notifications')}</h3>
<ServiceNotificationFields form={form} />
</div>
</div>
@@ -148,7 +152,7 @@ export function ServiceForm({
<ServiceFormActions
isSubmitting={isSubmitting}
onCancel={onCancel}
submitLabel={isEdit ? "Update Service" : "Create Service"}
submitLabel={isEdit ? t("updateService") : t("createService")}
/>
</form>
</Form>
@@ -9,12 +9,14 @@ import { useQuery } from "@tanstack/react-query";
import { alertConfigService, AlertConfiguration } from "@/services/alertConfigService";
import { serviceNotificationTemplateService, ServiceNotificationTemplate } from "@/services/serviceNotificationTemplateService";
import { useState, useEffect } from "react";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
interface ServiceNotificationFieldsProps {
form: UseFormReturn<ServiceFormData>;
}
export function ServiceNotificationFields({ form }: ServiceNotificationFieldsProps) {
const { t } = useLanguage();
const [alertConfigs, setAlertConfigs] = useState<AlertConfiguration[]>([]);
// Get the current form values for debugging
@@ -88,10 +90,10 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-base">
Enable Notifications
{t("enableNotifications")}
</FormLabel>
<FormDescription>
Enable or disable notifications for this service
{t("enableNotificationsDesc")}
</FormDescription>
</div>
<FormControl>
@@ -115,11 +117,11 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
name="notificationChannels"
render={({ field }) => (
<FormItem>
<FormLabel>Notification Channels</FormLabel>
<FormLabel>{t("notificationChannels")}</FormLabel>
<FormDescription>
{notificationStatus === "enabled"
? "Select notification channels for this service"
: "Enable notifications first to select channels"}
? t("notificationChannelsEnabledDesc")
: t("notificationChannelsDesc")}
</FormDescription>
{/* Display selected channels as badges */}
@@ -144,7 +146,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
value="" // Always reset to empty after selection
>
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder="Add a notification channel" />
<SelectValue placeholder={t("notificationChannelsPlaceholder")} />
</SelectTrigger>
<SelectContent>
{alertConfigs
@@ -169,7 +171,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
return (
<FormItem>
<FormLabel>Alert Template</FormLabel>
<FormLabel>{t("alertTemplate")}</FormLabel>
<FormControl>
<Select
onValueChange={(value) => {
@@ -179,7 +181,7 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
disabled={notificationStatus !== "enabled" || isLoadingTemplates}
>
<SelectTrigger className={notificationStatus !== "enabled" ? 'opacity-50' : ''}>
<SelectValue placeholder={isLoadingTemplates ? "Loading templates..." : "Select an alert template"} />
<SelectValue placeholder={isLoadingTemplates ? t("alertTemplateLoading") : t("alertTemplatePlaceholder")} />
</SelectTrigger>
<SelectContent>
{serviceTemplates?.map((template) => (
@@ -192,8 +194,8 @@ export function ServiceNotificationFields({ form }: ServiceNotificationFieldsPro
</FormControl>
<FormDescription>
{notificationStatus === "enabled"
? "Choose a template for alert messages"
: "Enable notifications first to select template"}
? t("alertTemplateEnabledDesc")
: t("alertTemplateDesc")}
</FormDescription>
</FormItem>
);
@@ -9,12 +9,14 @@ import { regionalService } from "@/services/regionalService";
import { MapPin, Loader2, X, Plus } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
interface ServiceRegionalFieldsProps {
form: UseFormReturn<ServiceFormData>;
}
export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
const { t } = useLanguage();
const regionalMonitoringEnabled = form.watch("regionalMonitoringEnabled");
const currentRegionalAgents = form.watch("regionalAgents") || [];
@@ -79,10 +81,10 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
<div className="space-y-0.5">
<FormLabel className="text-base font-medium flex items-center gap-2">
<MapPin className="h-4 w-4" />
Regional Monitoring
{t("regionalMonitoring")}
</FormLabel>
<div className="text-sm text-muted-foreground">
Assign this service to regional monitoring agents for distributed monitoring
{t("regionalMonitoringDesc")}
</div>
</div>
<FormControl>
@@ -107,7 +109,7 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
name="regionalAgents"
render={({ field }) => (
<FormItem>
<FormLabel>Regional Agents</FormLabel>
<FormLabel>{t("regionalAgents")}</FormLabel>
{/* Display selected agents */}
{currentRegionalAgents.length > 0 && (
@@ -140,12 +142,12 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
<SelectTrigger>
<SelectValue placeholder={
isLoading
? "Loading agents..."
? t("regionalAgentsLoading")
: availableAgents.length > 0
? "Select additional regional agents..."
? t("regionalAgentsAvailablePlaceholder")
: currentRegionalAgents.length > 0
? "All available agents selected"
: "No regional agents available"
? t("regionalAgentsAllSelected")
: t("regionalAgentsNoAvailable")
} />
</SelectTrigger>
</FormControl>
@@ -154,14 +156,14 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
<SelectItem value="loading" disabled>
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Loading agents...
t("regionalAgentsLoading")
</div>
</SelectItem>
) : availableAgents.length === 0 ? (
<SelectItem value="no-agents" disabled>
{currentRegionalAgents.length > 0
? "All available agents selected"
: "No online regional agents available"
? t("regionalAgentsAllSelected")
: t("regionalAgentsNoOnlineAvailable")
}
</SelectItem>
) : (
@@ -190,13 +192,13 @@ export function ServiceRegionalFields({ form }: ServiceRegionalFieldsProps) {
{regionalMonitoringEnabled && onlineAgents.length === 0 && !isLoading && (
<p className="text-sm text-amber-600">
No online regional agents found. Services will use default monitoring.
{t("regionalAgentsNotFoundMessage")}
</p>
)}
{currentRegionalAgents.length === 0 && regionalMonitoringEnabled && (
<p className="text-sm text-orange-600">
No regional agents selected. Service will use default monitoring.
{t("regionalAgentsNotSelectedMessage")}
</p>
)}
@@ -4,12 +4,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Globe, Wifi, Server, Globe2 } from "lucide-react";
import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
interface ServiceTypeFieldProps {
form: UseFormReturn<ServiceFormData>;
}
export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
const { t } = useLanguage();
const getServiceIcon = (type: string) => {
switch (type) {
case "http":
@@ -31,7 +33,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>Service Type</FormLabel>
<FormLabel>{t("serviceType")}</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
@@ -56,7 +58,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>HTTP/S</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
Monitor websites and REST APIs with HTTP/HTTPS Protocol
{t("serviceTypeHTTPDesc")}
</p>
</div>
</SelectItem>
@@ -67,7 +69,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>PING</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
Monitor host availability with PING Protocol
{t("serviceTypePINGDesc")}
</p>
</div>
</SelectItem>
@@ -78,7 +80,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>TCP</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
Monitor TCP port connectivity with TCP Protocol
{t("serviceTypeTCPDesc")}
</p>
</div>
</SelectItem>
@@ -89,7 +91,7 @@ export function ServiceTypeField({ form }: ServiceTypeFieldProps) {
<span>DNS</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
Monitor DNS resolution
{t("serviceTypeDNSDesc")}
</p>
</div>
</SelectItem>
@@ -3,12 +3,14 @@ import { FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescripti
import { Input } from "@/components/ui/input";
import { UseFormReturn } from "react-hook-form";
import { ServiceFormData } from "./types";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
interface ServiceUrlFieldProps {
form: UseFormReturn<ServiceFormData>;
}
export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
const { t } = useLanguage();
const serviceType = form.watch("type");
const getPlaceholder = () => {
@@ -22,35 +24,35 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
case "dns":
return "example.com";
default:
return "Enter URL or hostname";
return t("targetDefaultPlaceholder");
}
};
const getDescription = () => {
switch (serviceType) {
case "http":
return "Enter the full URL including protocol (http:// or https://)";
return t("targetHTTPDesc");
case "ping":
return "Enter hostname or IP address to ping";
return t("targetPINGDesc");
case "tcp":
return "Enter hostname or IP address for TCP connection test";
return t("targetTCPDesc");
case "dns":
return "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)";
return t("targetDNSDesc");
default:
return "Enter the target URL or hostname for monitoring";
return t("targetDefaultDesc");
}
};
const getFieldLabel = () => {
switch (serviceType) {
case "dns":
return "Domain Name";
return t("targetDNS");
case "ping":
return "Hostname/IP";
case "tcp":
return "Hostname/IP";
default:
return "Target URL/Host";
return t("targetDefault");
}
};
@@ -99,7 +101,7 @@ export function ServiceUrlField({ form }: ServiceUrlFieldProps) {
/>
</FormControl>
<FormDescription className="text-xs">
Enter the port number for TCP connection test
{t("targetTCPPortDesc")}
</FormDescription>
<FormMessage />
</FormItem>
@@ -1,22 +1,42 @@
import { z } from "zod";
import type { ZodSchema } from "zod";
import { useLanguage } from "@/contexts/LanguageContext";
export const serviceSchema = z.object({
name: z.string().min(1, "Service name is required"),
type: z.enum(["http", "ping", "tcp", "dns"]),
url: z.string().min(1, "URL/Domain/Host is required").refine((value) => {
// Basic validation - more specific validation can be added per type
return value.trim().length > 0;
}, "Please enter a valid URL, hostname, or domain"),
port: z.string().optional(),
interval: z.string(),
retries: z.string(),
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
notificationChannels: z.array(z.string()).optional(),
alertTemplate: z.string().optional(),
// Regional monitoring fields - now supports multiple agents
regionalMonitoringEnabled: z.boolean().optional(),
regionalAgents: z.array(z.string()).optional(),
});
export type ServiceFormData = z.infer<typeof serviceSchema>;
export type ServiceFormData = {
name: string;
type: "http" | "ping" | "tcp" | "dns";
url: string;
port?: string;
interval: string;
retries: string;
notificationStatus?: "enabled" | "disabled";
notificationChannels?: string[];
alertTemplate?: string;
regionalMonitoringEnabled?: boolean;
regionalAgents?: string[];
};
// Hook to use the schema with translations
export const useServiceSchema = () => {
const { t } = useLanguage();
return z.object({
name: z.string().min(1, t("serviceNameRequired")),
type: z.enum(["http", "ping", "tcp", "dns"]),
url: z.string()
.min(1, t("urlDomainHostRequired"))
.refine(
(value) => value.trim().length > 0,
t("enterValidUrlHostnameDomain")
),
port: z.string().optional(),
interval: z.string(),
retries: z.string(),
notificationStatus: z.enum(["enabled", "disabled"]).optional(),
notificationChannels: z.array(z.string()).optional(),
alertTemplate: z.string().optional(),
// Regional monitoring fields - now supports multiple agents
regionalMonitoringEnabled: z.boolean().optional(),
regionalAgents: z.array(z.string()).optional(),
});
};
@@ -1,50 +1,293 @@
import React, { useEffect, useState } from 'react';
import { format } from 'date-fns';
import {
Card, CardContent, CardDescription, CardHeader, CardTitle,
} from "@/components/ui/card";
import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import {
Github, FileText, Twitter, MessageCircle, Code2, ServerIcon,
} from "lucide-react";
import { Github, FileText, Twitter, MessageCircle, Code2, ServerIcon, FolderOpen, Database, CheckCircle } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { useLanguage } from "@/contexts/LanguageContext";
import { useTheme } from "@/contexts/ThemeContext";
import { useSystemSettings } from "@/hooks/useSystemSettings";
import { pb } from "@/lib/pocketbase";
import { toast } from "@/components/ui/use-toast";
export const AboutSystem: React.FC = () => {
const { t } = useLanguage();
const { theme } = useTheme();
const { systemName } = useSystemSettings();
const [isImporting, setIsImporting] = useState(false);
const [mergeFields, setMergeFields] = useState(true);
const [importResult, setImportResult] = useState<{
success: boolean;
created: number;
updated: number;
skipped: number;
} | null>(null);
const [version, setVersion] = useState<string>('...');
const [releaseDate, setReleaseDate] = useState<string>('...');
// Helper function to check if a collection is a system collection
const isSystemCollection = (name: string): boolean => {
return name.startsWith('_') || ['users'].includes(name);
};
useEffect(() => {
const fetchLatestRelease = async () => {
try {
const res = await fetch('https://api.github.com/repos/operacle/checkcle/releases/latest');
const data = await res.json();
setVersion(data.tag_name || 'v1.x.x');
setReleaseDate(data.published_at ? format(new Date(data.published_at), 'MMMM d, yyyy') : t('unknown'));
} catch (err) {
setVersion('v1.x.x');
setReleaseDate(t('unknown'));
// Helper function to validate collection schema
const validateCollectionSchema = (collection: any): string | null => {
if (!collection.name) return "Collection name is required";
if (typeof collection.name !== 'string') return "Collection name must be a string";
if (!/^[a-z][a-z0-9_]*$/.test(collection.name)) {
return "Collection name must start with a letter and contain only lowercase letters, numbers, and underscores";
}
if (collection.name.length < 3 || collection.name.length > 32) {
return "Collection name must be between 3 and 32 characters";
}
// Validate schema array if present
if (collection.schema && !Array.isArray(collection.schema)) {
return "Collection schema must be an array";
}
// Validate each field in the schema
if (collection.schema) {
for (const field of collection.schema) {
if (!field.name || typeof field.name !== 'string') {
return `Invalid field name in collection '${collection.name}'`;
}
if (!field.type || typeof field.type !== 'string') {
return `Invalid field type for '${field.name}' in collection '${collection.name}'`;
}
}
};
fetchLatestRelease();
}, [t]);
}
return null;
};
const handleSchemaImport = async (schemaData: string) => {
if (!schemaData.trim()) {
toast({
title: "Error",
description: "No schema data provided",
variant: "destructive",
});
return;
}
try {
setIsImporting(true);
setImportResult(null);
// Parse the JSON to validate it
const collections = JSON.parse(schemaData);
if (!Array.isArray(collections)) {
throw new Error("Schema must be an array of collections");
}
// Check if user is authenticated as admin
if (!pb.authStore.isValid) {
throw new Error("Authentication required. Please log in as an admin.");
}
// Create/update collections one by one using PocketBase client
let successCount = 0;
let errorCount = 0;
let updatedCount = 0;
let skippedCount = 0;
const errors = [];
for (const collection of collections) {
try {
// Skip system collections
if (isSystemCollection(collection.name)) {
skippedCount++;
continue;
}
// Validate collection schema
const validationError = validateCollectionSchema(collection);
if (validationError) {
throw new Error(validationError);
}
// Check if collection already exists
const existingCollections = await pb.collections.getFullList();
const existingCollection = existingCollections.find(c => c.name === collection.name);
if (existingCollection) {
if (mergeFields) {
try {
// Get existing schema
const existingSchema = existingCollection.schema || [];
const newSchema = collection.schema || [];
// Merge schemas - add new fields, keep existing ones
const mergedSchema = [...existingSchema];
let fieldsAdded = 0;
for (const newField of newSchema) {
const existingFieldIndex = mergedSchema.findIndex(f => f.name === newField.name);
if (existingFieldIndex >= 0) {
} else {
// Add new field
mergedSchema.push(newField);
fieldsAdded++;
}
}
// Update the collection with merged schema
const updateData = {
...collection,
schema: mergedSchema
};
const updatedCollection = await pb.collections.update(existingCollection.id, updateData);
updatedCount++;
} catch (mergeError) {
throw new Error(`Failed to merge fields: ${mergeError.message || mergeError}`);
}
} else {
skippedCount++;
continue;
}
} else {
// Create new collection
try {
const newCollection = await pb.collections.create(collection);
successCount++;
} catch (createError) {
// Extract more detailed error information
let errorMessage = createError.message || createError;
if (createError.data) {
// Try to extract field-specific errors
if (createError.data.data) {
const fieldErrors = Object.entries(createError.data.data).map(([field, msgs]) =>
`${field}: ${Array.isArray(msgs) ? msgs.join(', ') : msgs}`
).join('; ');
if (fieldErrors) {
errorMessage += ` - Field errors: ${fieldErrors}`;
}
}
}
throw new Error(`Failed to create collection: ${errorMessage}`);
}
}
} catch (error) {
errorCount++;
const errorMsg = `Failed to process collection '${collection.name}': ${error.message || error}`;
errors.push(errorMsg);
}
}
// Set import result for display
setImportResult({
success: successCount + updatedCount > 0,
created: successCount,
updated: updatedCount,
skipped: skippedCount
});
// Show results
const totalProcessed = successCount + updatedCount;
if (totalProcessed > 0 || skippedCount > 0) {
let description = '';
if (totalProcessed > 0) {
description = `Successfully processed ${totalProcessed} collection(s)`;
if (successCount > 0) description += ` (${successCount} created)`;
if (updatedCount > 0) description += ` (${updatedCount} updated)`;
}
if (skippedCount > 0) {
if (description) description += `, `;
description += `${skippedCount} skipped (system collections or existing)`;
}
if (errorCount > 0) description += `. ${errorCount} failed.`;
toast({
title: "Import Complete",
description,
variant: totalProcessed > errorCount ? "default" : "destructive",
});
} else if (errorCount > 0) {
toast({
title: "Import Failed",
description: "No collections were processed successfully.",
variant: "destructive",
});
} else {
toast({
title: "No Changes",
description: "All collections already exist and no changes were made.",
variant: "default",
});
}
if (errors.length > 0) {
// Show detailed errors for debugging
if (errors.length <= 3) {
toast({
title: "Detailed Errors",
description: errors.join('; '),
variant: "destructive",
});
}
}
} catch (error) {
toast({
title: "Import Failed",
description: error instanceof Error ? error.message : "Failed to import schema",
variant: "destructive",
});
setImportResult({
success: false,
created: 0,
updated: 0,
skipped: 0
});
} finally {
setIsImporting(false);
}
};
const handleLoadLocalSchema = async () => {
try {
setIsImporting(true);
setImportResult(null);
const response = await fetch('/upload/data/pb_schema_latest.json');
if (!response.ok) {
throw new Error(`Failed to load local schema: ${response.status} ${response.statusText}`);
}
const schemaText = await response.text();
// Validate that it's valid JSON
JSON.parse(schemaText);
// Directly import the schema
await handleSchemaImport(schemaText);
} catch (error) {
toast({
title: "Import Failed",
description: error instanceof Error ? error.message : "Failed to load and import local schema file",
variant: "destructive",
});
setImportResult({
success: false,
created: 0,
updated: 0,
skipped: 0
});
setIsImporting(false);
}
};
return (
<div className="space-y-6 animate-fade-in">
<div>
<h1 className="text-3xl font-bold tracking-tight">{t('aboutSystem')}</h1>
<p className="text-muted-foreground text-base leading-relaxed mt-2">
</p>
</div>
<Separator />
<div className="grid gap-8 md:grid-cols-2">
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
<CardHeader className="bg-muted/50 pb-4">
@@ -58,7 +301,7 @@ export const AboutSystem: React.FC = () => {
<div className="flex flex-col space-y-3 pt-2">
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{t('systemVersion')}</span>
<span className="text-foreground font-medium">{version}</span>
<span className="text-foreground font-medium">{t('version')} 1.5.1</span>
</div>
<Separator className="my-1" />
<div className="flex justify-between items-center">
@@ -68,22 +311,20 @@ export const AboutSystem: React.FC = () => {
<Separator className="my-1" />
<div className="flex justify-between items-center">
<span className="text-muted-foreground">{t('releasedOn')}</span>
<span className="text-foreground font-medium">{releaseDate}</span>
<span className="text-foreground font-medium">Auguest 21, 2025</span>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
<CardHeader className="bg-muted/50 pb-4">
<CardTitle className="flex items-center gap-2">
<Code2 className={`h-5 w-5 ${theme === 'dark' ? 'text-emerald-400' : 'text-emerald-600'}`} />
<span>{t('links')}</span>
</CardTitle>
<CardDescription className="font-medium text-base">
{systemName || 'CheckCle'} {t('resources').toLowerCase()}
</CardDescription>
<CardDescription className="font-medium text-base">{systemName || 'CheckCle'} {t('resources').toLowerCase()}</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-6">
<div className="grid grid-cols-1 gap-3">
@@ -107,8 +348,90 @@ export const AboutSystem: React.FC = () => {
</CardContent>
</Card>
</div>
<Separator />
<Card className="overflow-hidden border border-border transition-all duration-300 hover:shadow-md">
<CardHeader className="bg-muted/50 pb-4">
<CardTitle className="flex items-center gap-2">
<Database className={`h-5 w-5 ${theme === 'dark' ? 'text-blue-400' : 'text-blue-600'}`} />
<span>Update Schema</span>
</CardTitle>
<CardDescription className="font-medium text-base">Automatic import collections schema</CardDescription>
</CardHeader>
<CardContent className="space-y-4 pt-6">
<div className="space-y-3">
<div className="flex items-center space-x-2">
<Checkbox
id="merge-fields"
checked={mergeFields}
onCheckedChange={(checked) => setMergeFields(checked === true)}
/>
<Label htmlFor="merge-fields" className="text-sm font-medium">
Merge fields with existing collections (safe - preserves data)
</Label>
</div>
</div>
<div className="flex gap-2 flex-wrap">
<Button
onClick={handleLoadLocalSchema}
disabled={isImporting}
className={`flex items-center gap-2 transition-all duration-200 ${
isImporting ? 'animate-pulse' : ''
}`}
>
<Database className={`h-4 w-4 ${isImporting ? 'animate-spin' : ''}`} />
{isImporting ? 'Importing...' : 'Click to update Schema'}
</Button>
</div>
{importResult && (
<div className={`mt-4 p-4 rounded-lg border ${
importResult.success
? 'bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800'
: 'bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800'
} animate-in fade-in-0 slide-in-from-bottom-2 duration-300`}>
<div className="flex items-center gap-2">
<CheckCircle className={`h-5 w-5 ${
importResult.success
? 'text-green-600 dark:text-green-400'
: 'text-red-600 dark:text-red-400'
}`} />
<span className={`font-medium ${
importResult.success
? 'text-green-800 dark:text-green-200'
: 'text-red-800 dark:text-red-200'
}`}>
{importResult.success ? 'Import Successful' : 'Import Failed'}
</span>
</div>
<div className={`mt-2 text-sm ${
importResult.success
? 'text-green-700 dark:text-green-300'
: 'text-red-700 dark:text-red-300'
}`}>
{importResult.created > 0 && `${importResult.created} collections created`}
{importResult.updated > 0 && (importResult.created > 0 ? ', ' : '') + `${importResult.updated} collections updated`}
{importResult.skipped > 0 && ((importResult.created > 0 || importResult.updated > 0) ? ', ' : '') + `${importResult.skipped} collections skipped`}
</div>
</div>
)}
<div className="text-sm text-muted-foreground">
<p className="mb-2">
<strong>Instructions:</strong>
</p>
<ul className="list-disc list-inside space-y-1 ml-2">
<li><strong>Merge fields:</strong> Safely add new fields to existing collections, preserves all data</li>
<li>System collections (starting with _) and users collection will be skipped automatically</li>
<li>Only authenticated admins can perform schema imports</li>
</ul>
</div>
</CardContent>
</Card>
</div>
);
};
export default AboutSystem;
export default AboutSystem;
@@ -170,7 +170,7 @@ const DataRetentionSettings = () => {
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
<AlertTriangle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-blue-700 dark:text-blue-300">
<span className="font-medium">Permission Notice:</span> As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins.
<span className="font-medium">{t("permissionNotice")}</span> As an admin user, you do not have access to data retention settings. These settings can only be accessed and modified by Super Admins.
</AlertDescription>
</Alert>
</CardContent>
@@ -143,7 +143,7 @@ const GeneralSettingsPanel: React.FC<GeneralSettingsPanelProps> = () => {
<Alert className="border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-blue-700 dark:text-blue-300">
<span className="font-medium">Permission Notice:</span> As an admin user, you do not have access to view or modify system and mail settings. These settings can only be accessed and modified by Super Admins. Contact your Super Admin if you need to make changes to system configuration or mail settings.
<span className="font-medium">{t("permissionNotice")}</span> {t("permissionNoticeAddUser")}
</AlertDescription>
</Alert>
</CardContent>
@@ -153,11 +153,11 @@ const GeneralSettingsPanel: React.FC<GeneralSettingsPanelProps> = () => {
}
if (isLoading) {
return <div className="p-4">Loading settings...</div>;
return <div className="p-4">{t("loadingSettings")}</div>;
}
if (error) {
return <div className="p-4 text-red-500">Error loading settings</div>;
return <div className="p-4 text-red-500">{t("loadingSettingsError")}</div>;
}
return (
@@ -36,7 +36,7 @@ interface NotificationChannelDialogProps {
const baseSchema = z.object({
notify_name: z.string().min(1, "Name is required"),
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "webhook"]),
notification_type: z.enum(["telegram", "discord", "slack", "signal", "google_chat", "email", "ntfy", "pushover", "webhook"]),
enabled: z.boolean().default(true),
service_id: z.string().default("global"),
template_id: z.string().optional(),
@@ -78,12 +78,23 @@ const emailSchema = baseSchema.extend({
smtp_password: z.string().min(1, "SMTP password is required"),
});
const ntfySchema = baseSchema.extend({
notification_type: z.literal("ntfy"),
ntfy_endpoint: z.string().url("Must be a valid NTFY endpoint URL"),
});
const webhookSchema = baseSchema.extend({
notification_type: z.literal("webhook"),
webhook_url: z.string().url("Must be a valid URL"),
webhook_payload_template: z.string().optional(),
});
const pushoverSchema = baseSchema.extend({
notification_type: z.literal("pushover"),
api_token: z.string().min(1, "API token is required"),
user_key: z.string().min(1, "User key is required"),
});
const formSchema = z.discriminatedUnion("notification_type", [
telegramSchema,
discordSchema,
@@ -91,7 +102,9 @@ const formSchema = z.discriminatedUnion("notification_type", [
signalSchema,
googleChatSchema,
emailSchema,
webhookSchema
ntfySchema,
pushoverSchema,
webhookSchema,
]);
type FormValues = z.infer<typeof formSchema>;
@@ -133,6 +146,18 @@ const notificationTypeOptions = [
description: "Send notifications via email",
icon: "/upload/notification/email.png"
},
{
value: "ntfy",
label: "NTFY",
description: "Send notifications via NTFY push service",
icon: "/upload/notification/ntfy.png"
},
{
value: "pushover",
label: "Pushover",
description: "Send push notifications via Pushover",
icon: "/upload/notification/pushover.png"
},
{
value: "webhook",
label: "Webhook",
@@ -540,6 +565,62 @@ export const NotificationChannelDialog = ({
</>
)}
{notificationType === "ntfy" && (
<FormField
control={form.control}
name="ntfy_endpoint"
render={({ field }) => (
<FormItem>
<FormLabel>NTFY Endpoint</FormLabel>
<FormControl>
<Input placeholder="https://ntfy.sh/your-topic" {...field} />
</FormControl>
<FormDescription>
The NTFY endpoint URL including your topic (e.g., https://ntfy.sh/checkcle)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
{notificationType === "pushover" && (
<>
<FormField
control={form.control}
name="api_token"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input placeholder="Your Pushover API token" {...field} type="password" />
</FormControl>
<FormDescription>
Your Pushover application API token
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="user_key"
render={({ field }) => (
<FormItem>
<FormLabel>User Key</FormLabel>
<FormControl>
<Input placeholder="Your Pushover user key" {...field} />
</FormControl>
<FormDescription>
Your Pushover user key (or group key)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{notificationType === "webhook" && (
<>
<FormField
@@ -10,7 +10,10 @@ import { UserCog, Loader2, AlertCircle, Info, Users, ShieldAlert } from "lucide-
import { Button } from "@/components/ui/button";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { authService } from "@/services/authService";
import { useLanguage } from "@/contexts/LanguageContext.tsx";
const UserManagement = () => {
const { t } = useLanguage();
const {
users,
loading,
@@ -44,21 +47,21 @@ const UserManagement = () => {
<AccordionTrigger className="py-4 px-5 bg-card hover:bg-card/90 hover:no-underline rounded-lg text-lg font-medium flex items-center w-full">
<div className="flex items-center">
<UserCog className="h-5 w-5 mr-2 text-green-500" />
<span>User Management</span>
<span>{t("userManagement")}</span>
</div>
</AccordionTrigger>
<AccordionContent className="p-4 pt-6 bg-background rounded-b-lg">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">User Management</h2>
<h2 className="text-2xl font-bold">{t("userManagement")}</h2>
{isSuperAdmin && <button onClick={() => setIsAddUserDialogOpen(true)} className="bg-green-500 hover:bg-green-600 text-white py-2 px-4 rounded-md flex items-center">
<span className="mr-1">+</span> Add User
<span className="mr-1">+</span> {t("addUser")}
</button>}
</div>
{!isSuperAdmin && <Alert className="mb-6 border-blue-200 bg-blue-50 dark:bg-blue-950 dark:border-blue-800">
<ShieldAlert className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<AlertDescription className="text-blue-700 dark:text-blue-300">
<span className="font-medium">Permission Notice:</span> As an admin user, you have access to view and modify existing user details. However, only Super Admins have permission to create new user accounts. Contact your Super Admin if you need to add a new user.
<span className="font-medium">{t("permissionNotice")}</span> As an admin user, you have access to view and modify existing user details. However, only Super Admins have permission to create new user accounts. Contact your Super Admin if you need to add a new user.
</AlertDescription>
</Alert>}
+24 -12
View File
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, ReactNode } from "react";
import React, { createContext, useContext, useState, ReactNode, useCallback } from "react";
import { translations, Language, TranslationModule, TranslationKey } from "@/translations";
type LanguageContextType = {
@@ -21,27 +21,39 @@ export const useLanguage = () => {
export const LanguageProvider = ({ children }: { children: ReactNode }) => {
const [language, setLanguage] = useState<Language>("en");
const fallbackLanguage: Language = "en";
const t = <M extends TranslationModule>(key: string, module?: M): string => {
const t = useCallback(<M extends TranslationModule>(key: string, module?: M): string => {
const langPack = translations[language];
const fallbackPack = translations[fallbackLanguage];
if (module) {
const translatedValue = translations[language][module][key as TranslationKey<M>];
return typeof translatedValue === "string" ? translatedValue : key;
const valCur = langPack?.[module]?.[key as TranslationKey<M>];
if (typeof valCur === "string") return valCur;
const valEn = fallbackPack?.[module]?.[key as TranslationKey<M>];
if (typeof valEn === "string") return valEn;
return key;
}
for (const mod in translations[language]) {
const moduleKey = mod as TranslationModule;
const translatedValue = translations[language][moduleKey][key as any];
if (translatedValue && typeof translatedValue === "string") {
return translatedValue;
}
for (const mod in langPack) {
const m = mod as TranslationModule;
const val = langPack[m]?.[key as any];
if (typeof val === "string") return val;
}
for (const mod in fallbackPack) {
const m = mod as TranslationModule;
const val = fallbackPack[m]?.[key as any];
if (typeof val === "string") return val;
}
return key;
};
}, [language]);
return (
<LanguageContext.Provider value={{ language, setLanguage, t }}>
{children}
</LanguageContext.Provider>
);
};
};
+11 -3
View File
@@ -7,7 +7,7 @@ export interface AlertConfiguration {
collectionId?: string;
collectionName?: string;
service_id: string;
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "webhook";
notification_type: "telegram" | "discord" | "slack" | "signal" | "google_chat" | "email" | "ntfy" | "pushover" | "webhook";
telegram_chat_id?: string;
discord_webhook_url?: string;
signal_number?: string;
@@ -28,7 +28,9 @@ export interface AlertConfiguration {
smtp_password?: string;
webhook_id?: string;
channel_id?: string;
// Webhook fields for alert_configurations
ntfy_endpoint?: string;
api_token?: string;
user_key?: string;
webhook_url?: string;
webhook_payload_template?: string;
}
@@ -82,9 +84,15 @@ export const alertConfigService = {
cleanConfig.smtp_server = config.smtp_server || "";
cleanConfig.smtp_port = config.smtp_port || "";
cleanConfig.smtp_password = config.smtp_password || "";
} else if (config.notification_type === "ntfy") {
cleanConfig.ntfy_endpoint = config.ntfy_endpoint || "";
} else if (config.notification_type === "webhook") {
} else if (config.notification_type === "pushover") {
cleanConfig.api_token = config.api_token || "";
cleanConfig.user_key = config.user_key || "";
} else if (config.notification_type === "webhook") {
cleanConfig.webhook_url = config.webhook_url || "";
cleanConfig.webhook_payload_template = config.webhook_payload_template || "";
@@ -56,7 +56,9 @@ export const createIncident = async (data: CreateIncidentInput): Promise<void> =
affected_systems: data.affected_systems,
priority: data.priority.toLowerCase(),
service_id: data.service_id || '',
assigned_to: data.assigned_to || '', // Direct user ID assignment
//assigned_to: data.assigned_to || '', // Direct user ID assignment
//2025-08-18: // map UI field -> server schema
assigned_users: typeof data.assigned_to === 'string' ? data.assigned_to : '',
root_cause: data.root_cause || '',
resolution_steps: data.resolution_steps || '',
lessons_learned: data.lessons_learned || '',
@@ -82,28 +84,41 @@ export const createIncident = async (data: CreateIncidentInput): Promise<void> =
export const updateIncident = async (id: string, data: Partial<IncidentItem>): Promise<void> => {
try {
console.log(`Updating incident ${id} with:`, data);
// Make sure impact and priority are lowercase
const payload = {
const payload: Record<string, any> = {
...data,
impact: data.impact?.toLowerCase(),
priority: data.priority?.toLowerCase(),
status: data.status ? formatStatus(data.status) : undefined,
impact_status: data.status ? data.status.toLowerCase() : undefined,
...(data.status?.toLowerCase() === 'resolved' && !data.resolution_time
? { resolution_time: new Date().toISOString() }
: {})
...(data.impact ? { impact: data.impact.toLowerCase() } : {}),
...(data.priority ? { priority: data.priority.toLowerCase() } : {}),
...(data.status
? {
status: formatStatus(data.status),
impact_status: data.status.toLowerCase(),
}
: {}),
// set only if assigned_to was provided
...(typeof data.assigned_to === 'string'
? {
assigned_users: data.assigned_to, // server field
assigned_to: data.assigned_to, // legacy field for backward compatibility
}
: {}),
// add resolution_time only if status changes to resolved
...((data.status?.toLowerCase() === 'resolved' && !data.resolution_time)
? { resolution_time: new Date().toISOString() }
: {}),
};
console.log("Final payload for update:", payload);
await pb.collection('incidents').update(id, payload);
// Invalidate cache after update
invalidateCache();
console.log(`Incident ${id} updated successfully`);
} catch (error) {
console.error('Error updating incident:', error);
throw error;
}
};
@@ -1,6 +1,7 @@
import jsPDF from 'jspdf';
import { IncidentItem } from '../types';
import { userService } from '@/services/userService';
import {
addBasicInfoSection,
addDescriptionSection,
@@ -22,6 +23,18 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
throw new Error('Invalid incident data');
}
// Fetch assigned user data if available
let assignedUser: { full_name?: string; username?: string } | null = null;
const assigneeId = incident?.assigned_users || incident?.assigned_to;
if (assigneeId) {
try {
assignedUser = await userService.getUser(assigneeId);
} catch (error) {
console.warn('Failed to fetch assigned user for PDF:', error);
// Continue without user data
}
}
try {
// Create new PDF document with portrait orientation
const doc = new jsPDF({
@@ -85,7 +98,7 @@ export const generatePdf = async (incident: IncidentItem): Promise<string> => {
}
// Add assignment section
yPos = addAssignmentSection(doc, incident, yPos);
yPos = addAssignmentSection(doc, incident, yPos, assignedUser);
// Check if we need to add a new page
if (yPos > 250) {
@@ -146,7 +146,7 @@ export const addResolutionSection = (doc: jsPDF, incident: IncidentItem, yPos: n
/**
* Add the assignment information section to the PDF
*/
export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number): number => {
export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: number, assignedUser?: { full_name?: string; username?: string } | null): number => {
doc.setFontSize(14);
doc.setTextColor(30, 64, 175); // Blue-800
doc.text('Assignment Information', 15, yPos);
@@ -158,8 +158,16 @@ export const addAssignmentSection = (doc: jsPDF, incident: IncidentItem, yPos: n
yPos += 10;
doc.setFontSize(10);
doc.setTextColor(0, 0, 0);
const assignedTo = incident.assigned_to || 'Not assigned';
doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
if (assignedUser) {
// Show user name if available
const userName = assignedUser.full_name || assignedUser.username || 'Unknown User';
doc.text(`Assigned to: ${userName}`, 15, yPos);
} else {
// Fallback to ID if no user data available
const assignedTo = incident.assigned_users || incident.assigned_to || 'Not assigned';
doc.text(`Assigned to: ${assignedTo}`, 15, yPos);
}
return yPos + 10;
};
+6 -3
View File
@@ -5,7 +5,8 @@ export type IncidentItem = {
service_id?: string;
timestamp?: string;
description: string;
assigned_to?: string;
assigned_to?: string; // legacy UI field
assigned_users?: string; // server field
resolution_time?: string;
impact: string;
affected_systems: string;
@@ -32,7 +33,8 @@ export type CreateIncidentInput = {
affected_systems: string;
priority: string;
service_id?: string;
assigned_to?: string;
assigned_to?: string; // legacy UI field
assigned_users?: string; // server field
root_cause?: string;
resolution_steps?: string;
lessons_learned?: string;
@@ -50,7 +52,8 @@ export type UpdateIncidentInput = {
affected_systems?: string;
priority?: string;
service_id?: string;
assigned_to?: string;
assigned_to?: string; // legacy UI field
assigned_users?: string; // server field
root_cause?: string;
resolution_steps?: string;
lessons_learned?: string;
+2 -1
View File
@@ -2,7 +2,7 @@
import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = {
aboutCheckcle: "Über Checkcle",
aboutCheckCle: "Über Checkcle",
systemDescription: "Checkcle ist ein Open-Source-Überwachungs-Stack, der Echtzeit-Einblicke in Server- und Dienstzustände, Vorfallmanagement und operative Transparenz bietet. Veröffentlicht unter der MIT-Lizenz.",
systemVersion: "Systemversion",
license: "Lizenz",
@@ -15,6 +15,7 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "Schnelle Aktionen",
quickActionsDescription: "Greifen Sie schnell auf gängige Überwachungsvorgänge und -funktionen zu. Wählen Sie unten eine Aktion aus, um loszulegen.",
quickTips: "Schnelle Tipps",
releasedOn: "Veröffentlicht",
};
+4 -2
View File
@@ -6,9 +6,11 @@ export const commonTranslations: CommonTranslations = {
logout: "Abmelden",
language: "Sprache",
english: "Englisch",
khmer: "Khmer",
khmer: "ភាសាខ្មែរ",
german: "Deutsch",
simplifiedChinese: "Simplified Chinese",
korean: "한국어",
japanese: "日本語",
simplifiedChinese: "简体中文",
goodMorning: "Guten Morgen",
goodAfternoon: "Guten Nachmittag",
goodEvening: "Guten Abend",
@@ -51,4 +51,13 @@ export const incidentTranslations: IncidentTranslations = {
configuration: 'Konfiguration',
failedToUpdateStatus: 'Fehler beim Aktualisieren des Status',
inProgress: 'In Bearbeitung',
enterRootCause: 'Grundursache eingeben',
enterIncidentTitle: 'Vorfalltitel eingeben',
enterIncidentDescription: 'Vorfallbeschreibung eingeben',
enterServiceId: 'Service-IDs durch Komma getrennt eingeben',
selectAssignedUser: 'Zugewiesenen Benutzer auswählen',
noAssignedUser: 'Der Vorfall ist derzeit keinem Benutzer zugewiesen',
affectedSystems: 'Betroffene Systeme',
enterAffectedSystems: 'Betroffene Systeme eingeben',
separateSystemsWithComma: 'Betroffene Systeme durch Komma getrennt eingeben',
};
@@ -64,4 +64,17 @@ export const maintenanceTranslations: MaintenanceTranslations = {
noScheduledMaintenance: 'Keine geplanten Wartungen',
noMaintenanceWindows: 'Für diesen Zeitraum sind keine Wartungsfenster vorhanden. Erstellen Sie eines über die Schaltfläche „Wartung erstellen“.',
maintenanceCreatedSuccess: 'Wartungsfenster erfolgreich erstellt',
editMaintenanceWindow: 'Wartung bearbeiten',
editMaintenanceDesc: 'Wartungsdetails bearbeiten',
priorityDescription: 'Wartungspriorität auswählen',
statusDescription: 'Wartungsstatus auswählen',
impactLevel: 'Auswirkungsgrad',
impactLevelDescription: 'Auswirkungsgrad auswählen',
selectAssignedUsers: 'Zugewiesene Benutzer auswählen',
assignedPersonnelDescription: 'Derzeit zugewiesene Benutzer',
mutedNotifications: 'Benachrichtigungen deaktiviert',
notificationsAreMuted: 'Benachrichtigungen sind für diese Wartung derzeit deaktiviert',
selectNotificationChannel: 'Benachrichtigungskanal hinzufügen',
enableNotificationsFirst: 'Aktivieren Sie zuerst Benachrichtigungen, um einen Kanal auszuwählen',
updateMaintenance: 'Wartung aktualisieren',
};
+89 -4
View File
@@ -1,13 +1,98 @@
import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = {
// Dienst-Tabelle
serviceName: "Dienstname",
serviceType: "Diensttyp",
// Service Table
serviceStatus: "Dienststatus",
responseTime: "Antwortzeit",
uptime: "Betriebszeit",
lastChecked: "Zuletzt überprüft",
noServices: "Keine Dienste entsprechen Ihren Filterkriterien.",
currentlyMonitoring: "Derzeit überwacht",
retry: "Wiederholen",
overview: "Überblick",
newService: "Neuer Dienst",
rowsPerPage: "Zeilen pro Seite",
search: "Suchen",
allTypes: "Alle Arten",
createNewService: "Neuen Service hinzufügen",
createNewServiceDesc: "Geben Sie detaillierte Informationen ein, um einen neuen zu erstellen, den Sie überwachen möchten.",
// ServiceBasicFields.tsx
serviceName: "Dienstname",
serviceNameDesc: "Geben Sie einen aussagekräftigen Namen für Ihren Service ein",
// ServiceConfigFields.tsx
checkInterval: "Prüfintervall",
seconds: "Sekunden",
minute: "Minute",
minutes: "Minuten",
hour: "Stunde",
hours: "Stunden",
custom: "Benutzerdefiniert",
checkIntervalPlaceholder: "Intervall in Sekunden eingeben",
backToPresets: "Zurück zu Voreinstellungen",
checkIntervalDesc: "Wie oft der Dienststatus überprüft werden soll",
checkIntervalDescCustom: "Benutzerdefiniertes Intervall in Sekunden eingeben (mindestens 10 Sekunden)",
retryAttempts: "Wiederholungsversuche",
attempt: "Versuch",
attempts: "Versuche",
retryAttemptsDesc: "Anzahl der Wiederholungsversuche, bevor der Dienst als ausgefallen markiert wird",
// ServiceForm.tsx
updateService: "Dienst aktualisieren",
createService: "Dienst erstellen",
// ServiceNotificationFields.tsx
enableNotifications: "Benachrichtigungen aktivieren",
enableNotificationsDesc: "Benachrichtigungen für diesen Dienst aktivieren oder deaktivieren",
notificationChannels: "Benachrichtigungskanäle",
notificationChannelsEnabledDesc: "Benachrichtigungskanäle für diesen Dienst auswählen",
notificationChannelsDesc: "Aktivieren Sie zuerst Benachrichtigungen, um Kanäle auszuwählen",
notificationChannelsPlaceholder: "Benachrichtigungskanal hinzufügen",
alertTemplate: "Alarmvorlage",
alertTemplateLoading: "Lade Vorlagen...",
alertTemplatePlaceholder: "Alarmvorlage auswählen",
alertTemplateEnabledDesc: "Wählen Sie eine Vorlage für Alarmmeldungen",
alertTemplateDesc: "Aktivieren Sie zuerst Benachrichtigungen, um eine Vorlage auszuwählen",
// ServiceTypeField.tsx
serviceType: "Diensttyp",
serviceTypeHTTPDesc: "Webseiten und REST-APIs mit HTTP/HTTPS-Protokoll überwachen",
serviceTypePINGDesc: "Host-Verfügbarkeit mit PING-Protokoll überwachen",
serviceTypeTCPDesc: "TCP-Port-Konnektivität mit TCP-Protokoll überwachen",
serviceTypeDNSDesc: "DNS-Auflösung überwachen",
// ServiceRegionalFields.tsx
regionalMonitoring: "Regionale Überwachung",
regionalMonitoringDesc: "Diesen Dienst regionalen Überwachungsagenten für verteilte Überwachung zuweisen",
regionalAgents: "Regionale Agenten",
regionalAgentsLoading: "Lade Agenten...",
regionalAgentsAvailablePlaceholder: "Zusätzliche regionale Agenten auswählen...",
regionalAgentsAllSelected: "Alle verfügbaren Agenten ausgewählt",
regionalAgentsNoAvailable: "Keine regionalen Agenten verfügbar",
regionalAgentsNoOnlineAvailable: "Keine regionalen Agenten online verfügbar",
regionalAgentsNotFoundMessage: "Keine regionalen Online-Agenten gefunden. Dienste verwenden die Standardüberwachung.",
regionalAgentsNotSelectedMessage: "Keine regionalen Agenten ausgewählt. Der Dienst verwendet die Standardüberwachung.",
// ServiceUrlField.tsx
targetDefault: "Ziel-URL/Host",
targetDNS: "Domainname",
targetHTTPDesc: "Geben Sie die vollständige URL inklusive Protokoll ein (http:// oder https://)",
targetPINGDesc: "Geben Sie den Hostnamen oder die IP-Adresse zum Pingen ein",
targetTCPDesc: "Geben Sie Hostname oder IP-Adresse für TCP-Verbindungstest ein",
targetTCPPortDesc: "Geben Sie die Portnummer für den TCP-Verbindungstest ein",
targetDNSDesc: "Domainnamen für DNS-Überwachung eingeben (A, AAAA, MX, etc.)",
targetDefaultDesc: "Geben Sie die Ziel-URL oder den Hostnamen für die Überwachung ein",
targetDefaultPlaceholder: "URL oder Hostname eingeben",
// types.ts
serviceNameRequired: "Dienstname ist erforderlich",
urlDomainHostRequired: "URL/Domain/Host ist erforderlich",
enterValidUrlHostnameDomain: "Bitte eine gültige URL, einen Hostnamen oder eine Domain eingeben",
// Dashboard
upServices: "DIENSTE ONLINE",
downServices: "DIENSTE OFFLINE",
pausedServices: "DIENSTE PAUSIERT",
warningServices: "DIENSTE MIT WARNUNG",
};
+9 -2
View File
@@ -23,7 +23,7 @@ export const settingsTranslations: SettingsTranslations = {
enableTLS: "TLS aktivieren",
localName: "Lokaler Name",
// Test Email
// Test Email
testEmail: "Test Email",
sendTestEmail: "Send test email",
emailTemplate: "Email template",
@@ -47,5 +47,12 @@ export const settingsTranslations: SettingsTranslations = {
testConnection: "Verbindung testen",
testingConnection: "Verbindung wird getestet...",
connectionSuccess: "Verbindung erfolgreich",
connectionFailed: "Verbindung fehlgeschlagen"
connectionFailed: "Verbindung fehlgeschlagen",
// Ergänzte fehlende Einträge
addUser: "Benutzer hinzufügen",
permissionNotice: "Berechtigungshinweis:",
permissionNoticeAddUser: "Als Admin-Benutzer haben Sie keinen Zugriff auf die Anzeige oder Änderung von System- und E-Mail-Einstellungen. Diese Einstellungen können nur von Super-Admins aufgerufen und geändert werden. Wenden Sie sich an Ihren Super-Admin, wenn Sie Änderungen an der Systemkonfiguration oder den E-Mail-Einstellungen vornehmen müssen.",
loadingSettings: "Einstellungen werden geladen...",
loadingSettingsError: "Fehler beim Laden der Einstellungen",
};
+2 -1
View File
@@ -112,6 +112,7 @@ export const sslTranslations: SSLTranslations = {
created: "Erstellt",
lastUpdated: "Zuletzt aktualisiert",
lastNotification: "Letzte Benachrichtigung",
collectionId: "Sammlungs-ID"
collectionId: "Sammlungs-ID",
noCertificatesFound: "Keine Zertifikate gefunden",
}
+2 -1
View File
@@ -2,7 +2,7 @@
import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = {
aboutCheckcle: "About Checkcle",
aboutCheckCle: "About Checkcle",
systemDescription: "Checkcle is an open-source monitoring stack offering real-time insights into server and service health, incident management, and operational transparency. Released as MIT License.",
systemVersion: "System Version",
license: "License",
@@ -15,4 +15,5 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "Quick Actions",
quickActionsDescription: "Access common monitoring operations and features quickly. Select an action below to get started.",
quickTips: "Quick Tips",
releasedOn: "Released On",
};
+5 -3
View File
@@ -5,10 +5,12 @@ export const commonTranslations: CommonTranslations = {
welcome: "Welcome",
logout: "Logout",
language: "Language",
english: "English",
khmer: "Khmer",
english: "Englisch",
khmer: "ភាសាខ្មែរ",
german: "Deutsch",
simplifiedChinese: "Simplified Chinese",
korean: "한국어",
japanese: "日本語",
simplifiedChinese: "简体中文",
goodMorning: "Good morning",
goodAfternoon: "Good afternoon",
goodEvening: "Good evening",
@@ -52,4 +52,13 @@ export const incidentTranslations: IncidentTranslations = {
configuration: 'Configuration',
failedToUpdateStatus: 'Failed to update status',
inProgress: 'In Progress',
enterRootCause: 'Enter Root Cause',
enterIncidentTitle: 'Enter Incident Title',
enterIncidentDescription: 'Enter Incident Description',
enterServiceId: 'Enter Service-IDs seperated by comma',
selectAssignedUser: 'Select Assigned User',
noAssignedUser: 'Incident is currently not assigned to any user',
affectedSystems: 'Affected Systems',
enterAffectedSystems: 'Enter Affected Systems',
separateSystemsWithComma: 'Enter affected systems seperated by comma',
};
@@ -65,4 +65,17 @@ export const maintenanceTranslations: MaintenanceTranslations = {
noScheduledMaintenance: 'No Scheduled Maintenance',
noMaintenanceWindows: 'There are no maintenance windows for this period. Create one by clicking the "Create Maintenance" button.',
maintenanceCreatedSuccess: 'Maintenance window created successfully',
editMaintenanceWindow: 'Edit Maintenance',
editMaintenanceDesc: 'Edit maintenance details',
priorityDescription: 'Select maintenance priority',
statusDescription: 'Select maintenance status',
impactLevel: 'Impact Level',
impactLevelDescription: 'Select impact level',
selectAssignedUsers: 'Select Assigned Users',
assignedPersonnelDescription: 'Currently assigned users',
mutedNotifications: 'Notifications disabled',
notificationsAreMuted: 'Notifications are currently muted for this maintenance',
selectNotificationChannel: 'Add a notification channel',
enableNotificationsFirst: 'Enable notifications first to select channel',
updateMaintenance: 'Update Maintenance',
};
+89 -3
View File
@@ -2,11 +2,97 @@
import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = {
serviceName: "Service Name",
serviceType: "Service Type",
serviceStatus: "Service Status",
responseTime: "Response Time",
uptime: "Uptime",
lastChecked: "Last Checked",
noServices: "No services match your filter criteria.",
};
currentlyMonitoring: "Currently Monitoring",
retry: "Retry",
overview: "Overview",
newService: "NewService",
rowsPerPage: "Rows Per Page",
search: "Search",
allTypes: "All Types",
createNewService: "Create New Service",
createNewServiceDesc: "Fill in the details to create a new service to monitor.",
// ServiceBasicFields.tsx
serviceName: "Service Name",
serviceNameDesc: "Enter a descriptive name for your service",
// ServiceConfigFields.tsx
checkInterval: "Check Interval",
seconds: "seconds",
minute: "minute",
minutes: "minutes",
hour: "hour",
hours: "hours",
custom: "Custom",
checkIntervalPlaceholder: "Enter interval in seconds",
backToPresets: "Back to presets",
checkIntervalDesc: "How often to check the service status",
checkIntervalDescCustom: "Enter custom interval in seconds (minimum 10 seconds)",
retryAttempts: "Retry Attempts",
attempt: "attempt",
attempts: "attempts",
retryAttemptsDesc: "Number of retry attempts before marking as down",
// ServiceForm.tsx
updateService: "Update Service",
createService: "Create Service",
// ServiceNotificationFields.tsx
enableNotifications: "Enable Notifications",
enableNotificationsDesc: "Enable or disable notifications for this service",
notificationChannels: "Notification Channels",
notificationChannelsEnabledDesc: "Select notification channels for this service",
notificationChannelsDesc: "Enable notifications first to select channels",
notificationChannelsPlaceholder: "Add a notification channel",
alertTemplate: "Alert Template",
alertTemplateLoading: "Loading templates...",
alertTemplatePlaceholder: "Select an alert template",
alertTemplateEnabledDesc: "Choose a template for alert messages",
alertTemplateDesc: "Enable notifications first to select template",
// ServiceTypeField.tsx
serviceType: "Service Type",
serviceTypeHTTPDesc: "Monitor websites and REST APIs with HTTP/HTTPS Protocol",
serviceTypePINGDesc: "Monitor host availability with PING Protocol",
serviceTypeTCPDesc: "Monitor TCP port connectivity with TCP Protocol",
serviceTypeDNSDesc: "Monitor DNS resolution",
// ServiceRegionalFields.tsx
regionalMonitoring: "Regional Monitoring",
regionalMonitoringDesc: "Assign this service to regional monitoring agents for distributed monitoring",
regionalAgents: "Regional Agents",
regionalAgentsLoading: "Loading agents...",
regionalAgentsAvailablePlaceholder: "Select additional regional agents...",
regionalAgentsAllSelected: "All available agents selected",
regionalAgentsNoAvailable: "No regional agents available",
regionalAgentsNoOnlineAvailable: "No online regional agents available",
regionalAgentsNotFoundMessage: "No online regional agents found. Services will use default monitoring.",
regionalAgentsNotSelectedMessage: "No regional agents selected. Service will use default monitoring.",
// ServiceUrlField.tsx
targetDefault: "Target URL/Host",
targetDNS: "Domain Name",
targetHTTPDesc: "Enter the full URL including protocol (http:// or https://)",
targetPINGDesc: "Enter hostname or IP address to ping",
targetTCPDesc: "Enter hostname or IP address for TCP connection test",
targetTCPPortDesc: "Enter the port number for TCP connection test",
targetDNSDesc: "Enter domain name for DNS record monitoring (A, AAAA, MX, etc.)",
targetDefaultDesc: "Enter the target URL or hostname for monitoring",
targetDefaultPlaceholder: "Enter URL or hostname",
// types.ts
serviceNameRequired: "Service name is required",
urlDomainHostRequired: "URL/Domain/Host is required",
enterValidUrlHostnameDomain: "Please enter a valid URL, hostname, or domain",
// Dashboard
upServices: "UP SERVICES",
downServices: "DOWN SERVICES",
pausedServices: "PAUSED SERVICES",
warningServices: "WARNING SERVICES",
};
+13 -6
View File
@@ -2,18 +2,18 @@
import { SettingsTranslations } from '../types/settings';
export const settingsTranslations: SettingsTranslations = {
// Tabs
// General Settings - Tabs
systemSettings: "System Settings",
mailSettings: "Mail Settings",
// System Settings
// General Settings - System Settings
appName: "Application Name",
appURL: "Application URL",
senderName: "Sender Name",
senderEmail: "Sender Email Address",
hideControls: "Hide Controls",
// Mail Settings
// General Settings - Mail Settings
smtpSettings: "SMTP Configuration",
smtpEnabled: "Enable SMTP",
smtpHost: "SMTP Host",
@@ -24,7 +24,7 @@ export const settingsTranslations: SettingsTranslations = {
enableTLS: "Enable TLS",
localName: "Local Name",
// Test Email
// General Settings - Test Email
testEmail: "Test Email",
sendTestEmail: "Send test email",
emailTemplate: "Email template",
@@ -39,7 +39,7 @@ export const settingsTranslations: SettingsTranslations = {
enterEmailAddress: "Enter email address",
sending: "Sending...",
// Actions and status
// General Settings - Actions and status
save: "Save Changes",
saving: "Saving...",
settingsUpdated: "Settings updated successfully",
@@ -48,5 +48,12 @@ export const settingsTranslations: SettingsTranslations = {
testConnection: "Test Connection",
testingConnection: "Testing Connection...",
connectionSuccess: "Connection successful",
connectionFailed: "Connection failed"
connectionFailed: "Connection failed",
// User Management
addUser: "Add User",
permissionNotice: "Permission Notice:",
permissionNoticeAddUser: "As an admin user, you do not have access to view or modify system and mail settings. These settings can only be accessed and modified by Super Admins. Contact your Super Admin if you need to make changes to system configuration or mail settings.",
loadingSettings: "Loading settings...",
loadingSettingsError: "Error loading settings",
};
+2 -1
View File
@@ -112,5 +112,6 @@ export const sslTranslations: SSLTranslations = {
created: "Created",
lastUpdated: "Last Updated",
lastNotification: "Last Notification",
collectionId: "Collection ID"
collectionId: "Collection ID",
noCertificatesFound: "No Certificates Found",
};
+3 -1
View File
@@ -1,15 +1,17 @@
import enTranslations from './en';
import kmTranslations from './km';
import deTranslations from './de';
import koTranslations from './ko';
import jaTranslations from './ja';
import zhcnTranslations from './zhcn';
export type Language = "en" | "km" | "de" | "ja" | "zhcn";
export type Language = "en" | "km" | "de" | "ko" | "ja" | "zhcn";
export const translations = {
en: enTranslations,
km: kmTranslations,
de: deTranslations,
ko: koTranslations,
ja: jaTranslations,
zhcn: zhcnTranslations,
};
+2 -1
View File
@@ -1,7 +1,7 @@
import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = {
aboutCheckcle: "Checkcleについて",
aboutCheckCle: "Checkcleについて",
systemDescription: "Checkcleは、サーバーとサービスの健全性に関するリアルタイム監視、インシデント管理、運用の透明性を提供するオープンソースの監視スタックです。MIT ライセンスの下で公開されています。",
systemVersion: "システムバージョン",
license: "ライセンス",
@@ -14,4 +14,5 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "クイックアクション",
quickActionsDescription: "一般的な監視操作と機能に素早くアクセスできます。開始するには、以下のアクションを選択してください。",
quickTips: "クイックヒント",
releasedOn: "公開日",
};
+4 -3
View File
@@ -4,11 +4,12 @@ export const commonTranslations: CommonTranslations = {
welcome: "ようこそ",
logout: "ログアウト",
language: "言語",
english: "English",
khmer: "Khmer",
english: "Englisch",
khmer: "ភាសាខ្មែរ",
german: "Deutsch",
simplifiedChinese: "简体中文",
korean: "한국어",
japanese: "日本語",
simplifiedChinese: "简体中文",
goodMorning: "おはようございます",
goodAfternoon: "こんにちは",
goodEvening: "こんばんは",
@@ -51,4 +51,5 @@ export const incidentTranslations: IncidentTranslations = {
configuration: '設定',
failedToUpdateStatus: 'ステータスの更新に失敗しました',
inProgress: '進行中',
enterRootCause: '原因を入力してください',
};
@@ -2,10 +2,20 @@ import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = {
serviceName: "サービス名",
serviceNameDesc: "サービスのわかりやすい名前を入力してください",
serviceType: "サービスタイプ",
serviceStatus: "サービスステータス",
responseTime: "応答時間",
uptime: "稼働時間",
lastChecked: "最終チェック",
noServices: "フィルタ条件に一致するサービスがありません。",
currentlyMonitoring: "現在監視中",
retry: "再試行",
overview: "概要",
newService: "新しいサービス",
rowsPerPage: "1ページあたりの行数",
search: "検索",
allTypes: "すべてのタイプ",
createNewService: "新しいサービスを作成",
createNewServiceDesc: "監視する新しいサービスを作成するには、詳細を入力してください。",
};
+2 -1
View File
@@ -111,5 +111,6 @@ export const sslTranslations: SSLTranslations = {
created: "作成日",
lastUpdated: "最終更新",
lastNotification: "最終通知",
collectionId: "コレクションID"
collectionId: "コレクションID",
noCertificatesFound: "証明書が見つかりません",
};
+2 -1
View File
@@ -2,7 +2,7 @@
import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = {
aboutCheckcle: "អំពី Checkcle",
aboutCheckCle: "អំពី Checkcle",
systemDescription: "Checkcle គឺជាស្តាកការត្រួតពិនិត្យប្រភពបើកទូលាយដែលផ្តល់នូវការយល់ដឹងជាក់ស្តែងអំពីសុខភាពម៉ាស៊ីនមេ និងសេវាកម្ម, ការគ្រប់គ្រងឧបទ្ទវហេតុ, និងតម្លាភាពនៃប្រតិបត្តិការ។ ចេញផ្សាយជាអាជ្ញាបណ្ណ MIT។",
systemVersion: "កំណែប្រព័ន្ធ",
license: "អាជ្ញាបណ្ណ",
@@ -15,4 +15,5 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "សកម្មភាពរហ័ស",
quickActionsDescription: "ចូលប្រើប្រតិបត្តិការត្រួតពិនិត្យ និងមុខងារទូទៅយ៉ាងរហ័ស។ ជ្រើសរើសសកម្មភាពខាងក្រោមដើម្បីចាប់ផ្តើម។",
quickTips: "គន្លឹះរហ័ស",
releasedOn: "បានចេញផ្សាយនៅថ្ងៃទី",
};
+4 -2
View File
@@ -5,9 +5,11 @@ export const commonTranslations: CommonTranslations = {
welcome: "សូមស្វាគមន៍",
logout: "ចាកចេញ",
language: "ភាសា",
english: "អង់គ្លេស",
khmer: "ខ្មែរ",
english: "Englisch",
khmer: "ភាសាខ្មែរ",
german: "Deutsch",
korean: "한국어",
japanese: "日本語",
simplifiedChinese: "简体中文",
goodMorning: "អរុណសួស្តី",
goodAfternoon: "ទិវាសួស្តី",
@@ -52,4 +52,5 @@ export const incidentTranslations: IncidentTranslations = {
configuration: "ការកំណត់រចនាសម្ព័ន្ធ",
failedToUpdateStatus: "បរាជ័យក្នុងការធ្វើបច្ចុប្បន្នភាពស្ថានភាព",
inProgress: "កំពុងដំណើរការ",
enterRootCause: 'បញ្ចូលហេតុផលដើម',
};
@@ -3,10 +3,20 @@ import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = {
serviceName: "ឈ្មោះសេវាកម្ម",
serviceNameDesc: "បញ្ចូលឈ្មោះពិពណ៌នាសម្រាប់សេវាកម្មរបស់អ្នក",
serviceType: "ប្រភេទសេវាកម្ម",
serviceStatus: "ស្ថានភាពសេវាកម្ម",
responseTime: "ពេលវេលាឆ្លើយតប",
uptime: "ពេលវេលាដំណើរការ",
lastChecked: "ពិនិត្យចុងក្រោយ",
noServices: "មិនមានសេវាកម្មដែលត្រូវនឹងលក្ខណៈវិនិច្ឆ័យរបស់អ្នក។",
currentlyMonitoring: "កំពុងតាមដានពេលនេះ",
retry: "សាកល្បងម្ដងទៀត",
overview: "ទិដ្ឋភាពទូទៅ",
newService: "សេវាកម្មថ្មី",
rowsPerPage: "ចំនួនជួរដេកក្នុងមួយទំព័រ",
search: "ស្វែងរក",
allTypes: "គ្រប់ប្រភេទ",
createNewService: "បង្កើតសេវាកម្មថ្មី",
createNewServiceDesc: "បញ្ចូលព័ត៌មានលម្អិតដើម្បីបង្កើតសេវាកម្មថ្មីដែលត្រូវតាមដាន។",
};
+2 -1
View File
@@ -112,5 +112,6 @@ export const sslTranslations: SSLTranslations = {
created: "បានបង្កើត",
lastUpdated: "បានធ្វើបច្ចុប្បន្នភាពចុងក្រោយ",
lastNotification: "ការជូនដំណឹងចុងក្រោយ",
collectionId: "លេខសម្គាល់ប្រមូលផ្តុំ"
collectionId: "លេខសម្គាល់ប្រមូលផ្តុំ",
noCertificatesFound: "រកមិនឃើញវិញ្ញាបនប័ត្រ",
};
+18
View File
@@ -0,0 +1,18 @@
import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = {
aboutCheckCle: "About Checkcle",
systemDescription: "Checkcle는 서버 및 서비스 상태, 사고 관리, 운영 투명성에 대한 실시간 인사이트를 제공하는 오픈소스 모니터링 스택입니다. MIT 라이선스로 공개됨.",
systemVersion: "시스템 버전",
license: "라이선스",
mitLicense: "MIT 라이선스",
links: "링크",
viewOnGithub: "GitHub에서 보기",
viewDocumentation: "문서 보기",
followOnX: "X에서 팔로우",
joinDiscord: "Discord 참여",
quickActions: "빠른 작업",
quickActionsDescription: "자주 사용하는 모니터링 작업과 기능에 빠르게 접근하세요. 아래에서 작업을 선택하여 시작할 수 있습니다.",
quickTips: "빠른 팁",
releasedOn: "출시일",
};
+31
View File
@@ -0,0 +1,31 @@
import { CommonTranslations } from '../types/common';
export const commonTranslations: CommonTranslations = {
welcome: "환영합니다",
logout: "로그아웃",
language: "언어",
english: "Englisch",
khmer: "ភាសាខ្មែរ",
german: "Deutsch",
korean: "한국어",
japanese: "日本語",
simplifiedChinese: "简体中文",
goodMorning: "좋은 아침입니다",
goodAfternoon: "좋은 오후입니다",
goodEvening: "좋은 저녁입니다",
profile: "프로필",
settings: "설정",
documentation: "문서",
notifications: "알림",
close: "닫기",
cancel: "취소",
view: "보기",
edit: "편집",
delete: "삭제",
status: "상태",
time: "시간",
title: "제목",
description: "설명",
success: "성공",
error: "오류",
};
@@ -0,0 +1,64 @@
import { IncidentTranslations } from '../types/incident';
export const incidentTranslations: IncidentTranslations = {
incidentManagement: '인시던트 관리',
incidentsManagementDesc: '서비스 인시던트 및 해결 과정을 추적하고 관리합니다',
unresolvedIncidents: '미해결',
resolvedIncidents: '해결됨',
activeIncidents: '활성 인시던트',
criticalIssues: '중대한 문제',
avgResolutionTime: '평균 해결 시간',
noIncidents: '활성 인시던트 없음',
createIncident: '인시던트 생성',
investigating: '조사 중',
identified: '확인됨',
monitoring: '모니터링 중',
resolved: '해결됨',
scheduleIncidentManagement: '일정 및 인시던트 관리',
incidentName: '인시던트 이름',
incidentStatus: '인시던트 상태',
highPriority: '높은 우선순위',
configurationSettings: '구성 설정',
incidentCreatedSuccess: '인시던트가 성공적으로 생성되었습니다',
basicInfo: '기본 정보',
serviceId: '서비스 ID',
assignedTo: '담당자',
unassigned: '미할당',
timeline: '타임라인',
incidentTime: '인시던트 발생 시간',
resolutionTime: '해결 시간',
systems: '시스템',
noSystems: '영향을 받은 시스템 없음',
impactAnalysis: '영향 분석',
rootCause: '근본 원인',
resolutionSteps: '해결 단계',
lessonsLearned: '교훈',
resolutionDetails: '해결 상세',
assignment: '할당',
download: '다운로드',
downloadPdf: 'PDF 다운로드',
print: '인쇄',
confidentialNote: '이 문서는 기밀이며 내부 용도로만 사용됩니다.',
generatedOn: '생성일',
enterResolutionSteps: '인시던트 해결 단계 입력',
enterLessonsLearned: '인시던트 교훈 입력',
editIncident: '인시던트 편집',
editIncidentDesc: '인시던트 세부 정보 업데이트',
updating: '업데이트 중...',
update: '업데이트',
create: '생성',
creating: '생성 중...',
configuration: '구성',
failedToUpdateStatus: '상태 업데이트 실패',
inProgress: '진행 중',
enterRootCause: '근본 원인 입력',
enterIncidentTitle: '인시던트 제목 입력',
enterIncidentDescription: '인시던트 설명 입력',
enterServiceId: '서비스 ID를 콤마로 구분하여 입력',
selectAssignedUser: '담당자 선택',
noAssignedUser: '현재 인시던트에 할당된 사용자가 없습니다',
affectedSystems: '영향을 받은 시스템',
enterAffectedSystems: '영향을 받은 시스템 입력',
separateSystemsWithComma: '영향 시스템을 콤마로 구분하여 입력',
};
+25
View File
@@ -0,0 +1,25 @@
import { Translations } from '../types';
import { commonTranslations } from './common';
import { menuTranslations } from './menu';
import { loginTranslations } from './login';
import { aboutTranslations } from './about';
import { servicesTranslations } from './services';
import { maintenanceTranslations } from './maintenance';
import { incidentTranslations } from './incident';
import { sslTranslations } from './ssl';
import { settingsTranslations } from './settings';
const enTranslations: Translations = {
common: commonTranslations,
menu: menuTranslations,
login: loginTranslations,
about: aboutTranslations,
services: servicesTranslations,
maintenance: maintenanceTranslations,
incident: incidentTranslations,
ssl: sslTranslations,
settings: settingsTranslations
};
export default enTranslations;
+23
View File
@@ -0,0 +1,23 @@
import { LoginTranslations } from '../types/login';
export const loginTranslations: LoginTranslations = {
signInToYourAccount: "계정 로그인",
dontHaveAccount: "계정이 없으신가요?",
createOne: "계정 생성",
signInWithGoogle: "Google로 로그인",
orContinueWith: "또는",
email: "이메일",
password: "비밀번호",
forgot: "잊으셨나요?",
signIn: "로그인",
signingIn: "로그인 중...",
loginSuccessful: "로그인 성공",
loginSuccessMessage: "성공적으로 로그인되었습니다.",
loginFailed: "로그인 실패",
authenticationFailed: "인증 실패",
bySigningIn: "로그인하면 다음에 동의하게 됩니다:",
termsAndConditions: "이용약관",
and: "및",
privacyPolicy: "개인정보 처리방침",
};
@@ -0,0 +1,81 @@
import { MaintenanceTranslations } from '../types/maintenance';
export const maintenanceTranslations: MaintenanceTranslations = {
scheduledMaintenance: '예정된 유지보수',
scheduledMaintenanceDesc: '시스템 및 서비스에 계획된 유지보수 일정을 보고 관리합니다',
upcomingMaintenance: '예정',
ongoingMaintenance: '진행 중',
completedMaintenance: '완료됨',
createMaintenanceWindow: '유지보수 생성',
totalScheduledHours: '총 예정 시간',
maintenanceName: '유지보수 이름',
maintenanceStatus: '상태',
scheduledStart: '예정 시작',
scheduledEnd: '예정 종료',
affectedServices: '영향받는 서비스',
impact: '영향',
minor: '경미',
major: '중대',
critical: '심각',
none: '없음',
actions: '작업',
scheduled: '예정됨',
inprogress: '진행 중',
completed: '완료됨',
cancelled: '취소됨',
markAsInProgress: '진행 중으로 표시',
markAsCompleted: '완료로 표시',
markAsCancelled: '취소로 표시',
confirmDelete: '삭제 확인',
deleteMaintenanceConfirmation: '이 유지보수 일정을 삭제하시겠습니까?',
thisActionCannotBeUndone: '이 작업은 되돌릴 수 없습니다.',
maintenanceDeleted: '유지보수 삭제됨',
maintenanceDeletedDesc: '유지보수 일정이 성공적으로 삭제되었습니다.',
errorDeletingMaintenance: '유지보수 삭제 중 오류가 발생했습니다.',
statusUpdated: '상태 업데이트됨',
maintenanceStatusUpdated: '유지보수 상태가 성공적으로 업데이트되었습니다.',
errorUpdatingMaintenanceStatus: '유지보수 상태 업데이트 중 오류가 발생했습니다.',
createMaintenance: '유지보수 생성',
createMaintenanceDesc: '서비스를 위한 새 유지보수 일정을 예약합니다',
enterTitle: '유지보수 제목 입력',
enterDescription: '유지보수 상세 설명 입력',
startTime: '시작 시간',
endTime: '종료 시간',
selectDate: '날짜 선택',
enterAffectedServices: '영향받는 서비스 입력',
separateServicesWithComma: '여러 서비스를 콤마로 구분하여 입력',
priority: '우선순위',
selectPriority: '우선순위 선택',
selectStatus: '상태 선택',
selectImpact: '영향 선택',
notifySubscribers: '구독자에게 알림',
notifySubscribersDesc: '유지보수 시작 시 모든 구독자에게 알림 전송',
maintenanceCreated: '유지보수 생성됨',
maintenanceCreatedDesc: '유지보수 일정이 성공적으로 예약되었습니다.',
errorCreatingMaintenance: '유지보수 생성 중 오류 발생',
errorFetchingMaintenanceData: '유지보수 데이터 가져오기 중 오류 발생',
low: '낮음',
medium: '중간',
high: '높음',
created: '생성됨',
lastUpdated: '마지막 업데이트',
subscribersWillBeNotified: '유지보수가 시작되면 구독자에게 알림이 전송됩니다',
noNotifications: '알림이 전송되지 않습니다',
noScheduledMaintenance: '예정된 유지보수 없음',
noMaintenanceWindows: '이 기간에 유지보수 일정이 없습니다. "유지보수 생성" 버튼을 클릭하여 새 일정을 생성하세요.',
maintenanceCreatedSuccess: '유지보수 일정이 성공적으로 생성되었습니다',
editMaintenanceWindow: '유지보수 편집',
editMaintenanceDesc: '유지보수 세부정보 편집',
priorityDescription: '유지보수 우선순위 선택',
statusDescription: '유지보수 상태 선택',
impactLevel: '영향 수준',
impactLevelDescription: '영향 수준 선택',
selectAssignedUsers: '담당 사용자 선택',
assignedPersonnelDescription: '현재 할당된 사용자',
mutedNotifications: '알림 비활성화됨',
notificationsAreMuted: '현재 유지보수 알림이 비활성화되어 있습니다',
selectNotificationChannel: '알림 채널 추가',
enableNotificationsFirst: '채널을 선택하려면 먼저 알림을 활성화하세요',
updateMaintenance: '유지보수 업데이트',
};
+21
View File
@@ -0,0 +1,21 @@
import { MenuTranslations } from '../types/menu';
export const menuTranslations: MenuTranslations = {
uptimeMonitoring: "가동 시간 모니터링",
instanceMonitoring: "인스턴스 모니터링",
sslDomain: "SSL & 도메인",
scheduleIncident: "일정 및 인시던트",
operationalPage: "운영 페이지",
reports: "리포트",
regionalMonitoring: "지역 모니터링",
settingPanel: "설정 패널",
generalSettings: "일반 설정",
userManagement: "사용자 관리",
notificationSettings: "알림 설정",
alertsTemplates: "알림 템플릿",
rolesManagement: "권한 관리",
dataRetention: "데이터 보존",
backupSettings: "백업 설정",
aboutSystem: "시스템 정보",
};
@@ -0,0 +1,89 @@
import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = {
serviceStatus: "서비스 상태",
responseTime: "응답 시간",
uptime: "가동 시간",
lastChecked: "마지막 확인",
noServices: "필터 조건에 맞는 서비스가 없습니다.",
currentlyMonitoring: "현재 모니터링 중",
retry: "재시도",
overview: "개요",
newService: "새 서비스",
rowsPerPage: "페이지당 행",
search: "검색",
allTypes: "모든 유형",
createNewService: "새 서비스 생성",
createNewServiceDesc: "모니터링할 새 서비스의 세부 정보를 입력하세요.",
serviceName: "서비스 이름",
serviceNameDesc: "서비스에 대한 설명적인 이름을 입력하세요",
checkInterval: "체크 간격",
seconds: "초",
minute: "분",
minutes: "분",
hour: "시간",
hours: "시간",
custom: "사용자 정의",
checkIntervalPlaceholder: "초 단위로 간격 입력",
backToPresets: "기본값으로 돌아가기",
checkIntervalDesc: "서비스 상태를 확인하는 간격",
checkIntervalDescCustom: "사용자 정의 간격 입력 (최소 10초)",
retryAttempts: "재시도 횟수",
attempt: "회",
attempts: "회",
retryAttemptsDesc: "서비스 다운으로 표시되기 전 재시도 횟수",
updateService: "서비스 업데이트",
createService: "서비스 생성",
enableNotifications: "알림 활성화",
enableNotificationsDesc: "서비스 알림을 활성화하거나 비활성화합니다",
notificationChannels: "알림 채널",
notificationChannelsEnabledDesc: "서비스 알림 채널 선택",
notificationChannelsDesc: "채널을 선택하려면 먼저 알림을 활성화하세요",
notificationChannelsPlaceholder: "알림 채널 추가",
alertTemplate: "알림 템플릿",
alertTemplateLoading: "템플릿 로딩 중...",
alertTemplatePlaceholder: "알림 템플릿 선택",
alertTemplateEnabledDesc: "알림 메시지용 템플릿 선택",
alertTemplateDesc: "템플릿 선택 전 알림을 활성화하세요",
serviceType: "서비스 유형",
serviceTypeHTTPDesc: "HTTP/HTTPS 프로토콜로 웹사이트 및 REST API 모니터링",
serviceTypePINGDesc: "PING 프로토콜로 호스트 가용성 모니터링",
serviceTypeTCPDesc: "TCP 프로토콜로 TCP 포트 연결 상태 모니터링",
serviceTypeDNSDesc: "DNS 해석 모니터링",
regionalMonitoring: "지역 모니터링",
regionalMonitoringDesc: "이 서비스를 지역 모니터링 에이전트에 할당하여 분산 모니터링",
regionalAgents: "지역 에이전트",
regionalAgentsLoading: "에이전트 로딩 중...",
regionalAgentsAvailablePlaceholder: "추가 지역 에이전트 선택...",
regionalAgentsAllSelected: "모든 사용 가능한 에이전트 선택됨",
regionalAgentsNoAvailable: "사용 가능한 지역 에이전트 없음",
regionalAgentsNoOnlineAvailable: "온라인 지역 에이전트 없음",
regionalAgentsNotFoundMessage: "온라인 지역 에이전트 없음. 기본 모니터링 사용",
regionalAgentsNotSelectedMessage: "지역 에이전트 선택 안됨. 기본 모니터링 사용",
targetDefault: "대상 URL/호스트",
targetDNS: "도메인 이름",
targetHTTPDesc: "프로토콜 포함 전체 URL 입력 (http:// 또는 https://)",
targetPINGDesc: "호스트 이름 또는 IP 주소 입력",
targetTCPDesc: "TCP 연결 테스트용 호스트 이름 또는 IP 입력",
targetTCPPortDesc: "TCP 연결 테스트용 포트 번호 입력",
targetDNSDesc: "DNS 레코드 모니터링용 도메인 이름 입력 (A, AAAA, MX 등)",
targetDefaultDesc: "모니터링 대상 URL 또는 호스트 이름 입력",
targetDefaultPlaceholder: "URL 또는 호스트 이름 입력",
serviceNameRequired: "서비스 이름은 필수입니다",
urlDomainHostRequired: "URL/도메인/호스트는 필수입니다",
enterValidUrlHostnameDomain: "유효한 URL, 호스트 이름 또는 도메인을 입력하세요",
upServices: "가동 서비스",
downServices: "다운 서비스",
pausedServices: "일시정지 서비스",
warningServices: "경고 서비스",
};
@@ -0,0 +1,59 @@
import { SettingsTranslations } from '../types/settings';
export const settingsTranslations: SettingsTranslations = {
// General Settings - Tabs
systemSettings: "시스템 설정",
mailSettings: "메일 설정",
// General Settings - System Settings
appName: "앱 이름",
appURL: "앱 URL",
senderName: "발신자 이름",
senderEmail: "발신자 이메일 주소",
hideControls: "컨트롤 숨기기",
// General Settings - Mail Settings
smtpSettings: "SMTP 구성",
smtpEnabled: "SMTP 활성화",
smtpHost: "SMTP 호스트",
smtpPort: "SMTP 포트",
smtpUsername: "SMTP 사용자 이름",
smtpPassword: "SMTP 비밀번호",
smtpAuthMethod: "인증 방법",
enableTLS: "TLS 활성화",
localName: "로컬 이름",
// General Settings - Test Email
testEmail: "테스트 이메일",
sendTestEmail: "테스트 이메일 전송",
emailTemplate: "이메일 템플릿",
verification: "검증",
passwordReset: "비밀번호 재설정",
confirmEmailChange: "이메일 변경 확인",
otp: "OTP",
loginAlert: "로그인 알림",
authCollection: "인증 컬렉션",
selectCollection: "컬렉션 선택",
toEmailAddress: "받는 이메일",
enterEmailAddress: "이메일 주소 입력",
sending: "전송 중...",
// General Settings - Actions and status
save: "변경 사항 저장",
saving: "저장 중...",
settingsUpdated: "설정이 성공적으로 업데이트되었습니다",
errorSavingSettings: "설정 저장 중 오류 발생",
errorFetchingSettings: "설정 불러오기 중 오류 발생",
testConnection: "연결 테스트",
testingConnection: "연결 테스트 중...",
connectionSuccess: "연결 성공",
connectionFailed: "연결 실패",
// User Management
addUser: "사용자 추가",
permissionNotice: "권한 안내:",
permissionNoticeAddUser: "관리자 계정은 시스템 및 메일 설정을 확인하거나 변경할 수 없습니다. 이 설정은 슈퍼 관리자만 접근 및 수정할 수 있습니다. 시스템 구성 또는 메일 설정 변경이 필요하면 슈퍼 관리자에게 문의하세요.",
loadingSettings: "설정 로딩 중...",
loadingSettingsError: "설정 로딩 중 오류 발생",
};
+117
View File
@@ -0,0 +1,117 @@
import { SSLTranslations } from '../types/ssl';
export const sslTranslations: SSLTranslations = {
// Page and section titles
sslDomainManagement: "SSL & 도메인 관리",
monitorSSLCertificates: "도메인의 SSL 인증서를 모니터링하고 관리",
addSSLCertificate: "SSL 인증서 추가",
editSSLCertificate: "SSL 인증서 편집",
deleteSSLCertificate: "SSL 인증서 삭제",
sslCertificateDetails: "SSL 인증서 상세",
detailedInfo: "상세 정보",
viewDetailedInformation: "상세 정보 보기",
// Status related
valid: "유효",
expiringSoon: "곧 만료",
expired: "만료됨",
pending: "대기 중",
// Statistics and cards
validCertificates: "유효 인증서",
expiringSoonCertificates: "곧 만료 인증서",
expiredCertificates: "만료 인증서",
// Form fields
domain: "도메인",
domainName: "도메인 이름",
domainCannotChange: "생성 후 도메인은 변경할 수 없습니다",
warningThreshold: "경고 임계값",
warningThresholdDays: "경고 임계값(일)",
expiryThreshold: "만료 임계값",
expiryThresholdDays: "만료 임계값(일)",
notificationChannel: "알림 채널",
chooseChannel: "알림 채널 선택",
whereToSend: "알림 전송 위치",
daysBeforeExpiration: "만료 전 경고 받을 일수",
daysBeforeCritical: "만료 임박 시 알림 받을 일수",
getNotifiedExpiration: "인증서 만료 시 알림 받기",
getNotifiedCritical: "인증서 임박 시 알림 받기",
// Table headers and fields
issuer: "발급자",
expirationDate: "만료일",
daysLeft: "남은 일수",
status: "상태",
lastNotified: "마지막 알림",
actions: "작업",
validFrom: "유효 시작",
validUntil: "유효 종료",
validityDays: "유효 일수",
validityPeriod: "유효 기간",
organization: "조직",
commonName: "공통 이름",
serialNumber: "일련번호",
algorithm: "알고리즘",
subjectAltNames: "대체 이름",
subjectAlternativeNames: "대체 이름",
resolvedIP: "해결된 IP",
issuedTo: "발급 대상",
days: "일",
// Buttons and actions
addDomain: "도메인 추가",
refreshAll: "모두 새로고침",
cancel: "취소",
addCertificate: "인증서 추가",
check: "확인",
view: "보기",
edit: "편집",
delete: "삭제",
close: "닫기",
saveChanges: "변경 사항 저장",
updating: "업데이트 중",
// Sections in detail view
basicInformation: "기본 정보",
validity: "유효성",
issuerInfo: "발급자 정보",
technicalDetails: "기술 세부정보",
technicalInformation: "기술 정보",
monitoringConfig: "모니터링 구성",
monitoringConfiguration: "모니터링 구성",
recordInfo: "레코드 정보",
certificateDetails: "인증서 상세",
// Notifications and messages
sslCertificateAdded: "SSL 인증서가 성공적으로 추가되었습니다",
sslCertificateUpdated: "SSL 인증서가 성공적으로 업데이트되었습니다",
sslCertificateDeleted: "SSL 인증서가 성공적으로 삭제되었습니다",
sslCertificateRefreshed: "{domain}의 SSL 인증서가 성공적으로 갱신되었습니다",
allCertificatesRefreshed: "총 {count}개의 인증서가 성공적으로 갱신되었습니다",
someCertificatesFailed: "{success}개의 인증서 갱신 성공, {failed}개 실패",
failedToAddCertificate: "SSL 인증서 추가 실패",
failedToLoadCertificates: "SSL 인증서 불러오기 실패",
failedToUpdateCertificate: "SSL 인증서 업데이트 실패",
failedToDeleteCertificate: "SSL 인증서 삭제 실패",
failedToCheckCertificate: "SSL 인증서 확인 실패",
noCertificatesToRefresh: "갱신할 인증서 없음",
startingRefreshAll: "{count}개의 인증서 갱신 시작",
checkingSSLCertificate: "SSL 인증서 확인 중...",
deleteConfirmation: "인증서를 삭제하시겠습니까?",
deleteWarning: "이 작업은 되돌릴 수 없으며 인증서를 영구적으로 삭제합니다.",
// Misc
unknown: "알 수 없음",
never: "없음",
none: "없음",
loadingChannels: "채널 로딩 중...",
noChannelsFound: "알림 채널 없음",
noSSLCertificates: "SSL 인증서 없음",
created: "생성됨",
lastUpdated: "마지막 업데이트",
lastNotification: "마지막 알림",
collectionId: "컬렉션 ID",
noCertificatesFound: "인증서 없음",
};
@@ -13,4 +13,5 @@ export interface AboutTranslations {
quickActions: string;
quickActionsDescription: string;
quickTips: string;
releasedOn: string;
}
@@ -6,6 +6,7 @@ export interface CommonTranslations {
english: string;
khmer: string;
german: string;
korean: string;
japanese: string;
simplifiedChinese: string;
goodMorning: string;
@@ -50,4 +50,13 @@ export interface IncidentTranslations {
configuration: string;
failedToUpdateStatus: string;
inProgress: string;
enterRootCause: string;
enterIncidentTitle: string;
enterIncidentDescription: string;
enterServiceId: string;
selectAssignedUser: string;
noAssignedUser: string;
affectedSystems: string;
enterAffectedSystems: string;
separateSystemsWithComma: string;
}
@@ -63,4 +63,17 @@ export interface MaintenanceTranslations {
noScheduledMaintenance: string;
noMaintenanceWindows: string;
maintenanceCreatedSuccess: string;
editMaintenanceWindow: string;
editMaintenanceDesc: string;
priorityDescription: string;
statusDescription: string;
impactLevel: string;
impactLevelDescription: string;
selectAssignedUsers: string;
assignedPersonnelDescription: string;
mutedNotifications: string;
notificationsAreMuted: string;
selectNotificationChannel: string;
enableNotificationsFirst: string;
updateMaintenance: string;
}
+89 -2
View File
@@ -1,10 +1,97 @@
export interface ServicesTranslations {
serviceName: string;
serviceType: string;
serviceStatus: string;
responseTime: string;
uptime: string;
lastChecked: string;
noServices: string;
currentlyMonitoring: string;
retry: string;
overview: string;
newService: string;
rowsPerPage: string;
search: string;
allTypes: string;
createNewService: string;
createNewServiceDesc: string;
// ServiceBasicFields.tsx
serviceName: string;
serviceNameDesc: string;
// ServiceConfigFields.tsx
checkInterval: string;
seconds: string;
minute: string;
minutes: string;
hour: string;
hours: string;
custom: string;
checkIntervalPlaceholder: string;
backToPresets: string;
checkIntervalDesc: string;
checkIntervalDescCustom: string;
retryAttempts: string;
attempt: string;
attempts: string;
retryAttemptsDesc: string;
// ServiceForm.tsx
updateService: string;
createService: string;
// ServiceNotificationFields.tsx
enableNotifications: string;
enableNotificationsDesc: string;
notificationChannels: string;
notificationChannelsEnabledDesc: string;
notificationChannelsDesc: string;
notificationChannelsPlaceholder: string;
alertTemplate: string;
alertTemplateLoading: string;
alertTemplatePlaceholder: string;
alertTemplateEnabledDesc: string;
alertTemplateDesc: string;
// ServiceTypeField.tsx
serviceType: string;
serviceTypeHTTPDesc: string;
serviceTypePINGDesc: string;
serviceTypeTCPDesc: string;
serviceTypeDNSDesc: string;
// ServiceRegionalFields.tsx
regionalMonitoring: string;
regionalMonitoringDesc: string;
regionalAgents: string;
regionalAgentsLoading: string;
regionalAgentsAvailablePlaceholder: string;
regionalAgentsAllSelected: string;
regionalAgentsNoAvailable: string;
regionalAgentsNoOnlineAvailable: string;
regionalAgentsNotFoundMessage: string;
regionalAgentsNotSelectedMessage: string;
// ServiceUrlField.tsx
targetDefault: string;
targetDNS: string;
targetHTTPDesc: string;
targetPINGDesc: string;
targetTCPDesc: string;
targetTCPPortDesc: string;
targetDNSDesc: string;
targetDefaultDesc: string;
targetDefaultPlaceholder: string;
// types.ts
serviceNameRequired: string;
urlDomainHostRequired: string;
enterValidUrlHostnameDomain: string;
// Dashboard
upServices: string;
downServices: string;
pausedServices: string;
warningServices: string;
}
+12 -5
View File
@@ -1,17 +1,17 @@
export interface SettingsTranslations {
// Tabs
// General Settings - Tabs
systemSettings: string;
mailSettings: string;
// System Settings
// General Settings - System Settings
appName: string;
appURL: string;
senderName: string;
senderEmail: string;
hideControls: string;
// Mail Settings
// General Settings - Mail Settings
smtpSettings?: string;
smtpEnabled: string;
smtpHost: string;
@@ -22,7 +22,7 @@ export interface SettingsTranslations {
enableTLS: string;
localName: string;
// Test Email
// General Settings - Test Email
testEmail: string;
sendTestEmail: string;
emailTemplate: string;
@@ -37,7 +37,7 @@ export interface SettingsTranslations {
enterEmailAddress: string;
sending: string;
// Actions and status
// General Settings - Actions and status
save: string;
saving: string;
settingsUpdated: string;
@@ -47,4 +47,11 @@ export interface SettingsTranslations {
testingConnection: string;
connectionSuccess: string;
connectionFailed: string;
// User Management
addUser: string;
permissionNotice: string;
permissionNoticeAddUser: string;
loadingSettings: string;
loadingSettingsError: string;
}
@@ -111,4 +111,5 @@ export interface SSLTranslations {
lastUpdated: string;
lastNotification: string;
collectionId: string;
noCertificatesFound: string;
}
+2 -1
View File
@@ -2,7 +2,7 @@
import { AboutTranslations } from '../types/about';
export const aboutTranslations: AboutTranslations = {
aboutCheckcle: "关于 Checkcle",
aboutCheckCle: "关于 Checkcle",
systemDescription: "Checkcle 是一个开源监控平台,可提供有关服务器和服务健康状况的实时洞察、事件管理以及透明化运营。以 MIT 许可证发布。",
systemVersion: "系统版本",
license: "许可证",
@@ -15,4 +15,5 @@ export const aboutTranslations: AboutTranslations = {
quickActions: "快速操作",
quickActionsDescription: "快速访问常用的监控操作和功能。选择下面的操作开始。",
quickTips: "快速提示",
releasedOn: "发布于",
};
+4 -2
View File
@@ -5,9 +5,11 @@ export const commonTranslations: CommonTranslations = {
welcome: "欢迎",
logout: "注销",
language: "语言",
english: "English",
khmer: "Khmer",
english: "Englisch",
khmer: "ភាសាខ្មែរ",
german: "Deutsch",
korean: "한국어",
japanese: "日本語",
simplifiedChinese: "简体中文",
goodMorning: "早上好",
goodAfternoon: "下午好",
@@ -52,4 +52,5 @@ export const incidentTranslations: IncidentTranslations = {
configuration: '配置',
failedToUpdateStatus: '更新状态失败',
inProgress: '进行中',
enterRootCause: '输入故障根源',
};
@@ -4,7 +4,7 @@ import { MaintenanceTranslations } from '../types/maintenance';
export const maintenanceTranslations: MaintenanceTranslations = {
scheduledMaintenance: '计划维护',
scheduledMaintenanceDesc: '查看和管理您系统和服务的计划维护窗口',
upcomingMaintenance: '即将',
upcomingMaintenance: '计划中',
ongoingMaintenance: '进行中',
completedMaintenance: '已完成',
createMaintenanceWindow: '创建维护',
+1 -1
View File
@@ -2,7 +2,7 @@
import { MenuTranslations } from '../types/menu';
export const menuTranslations: MenuTranslations = {
uptimeMonitoring: "Uptime 监控",
uptimeMonitoring: "在线监控",
instanceMonitoring: "实例监控",
sslDomain: "SSL & 域名",
scheduleIncident: "计划与事件",
+90 -4
View File
@@ -2,11 +2,97 @@
import { ServicesTranslations } from '../types/services';
export const servicesTranslations: ServicesTranslations = {
serviceName: "服务名称",
serviceType: "服务类型",
serviceStatus: "服务状态",
responseTime: "响应时间",
uptime: "Uptime",
uptime: "在线时间",
lastChecked: "最后检查时间",
noServices: "没有符合您筛选条件的服务。",
};
currentlyMonitoring: "当前监控",
retry: "重试",
overview: "概览",
newService: "新增服务",
rowsPerPage: "每页行数",
search: "搜索",
allTypes: "所有类型",
createNewService: "添加新服务",
createNewServiceDesc: "填写详细信息以创建要监控的新服务。",
// ServiceBasicFields.tsx
serviceName: "服务名称",
serviceNameDesc: "为你的服务输入一个描述性名称",
// ServiceConfigFields.tsx
checkInterval: "检查间隔",
seconds: "秒",
minute: "分钟",
minutes: "分钟",
hour: "小时",
hours: "小时",
custom: "自定义",
checkIntervalPlaceholder: "输入间隔时间(秒)",
backToPresets: "回到预设",
checkIntervalDesc: "多久检查一次服务状态",
checkIntervalDescCustom: "输入自定义间隔时间(以秒为单位,最小10秒)",
retryAttempts: "重试次数",
attempt: "次",
attempts: "次",
retryAttemptsDesc: "在标记为下线之前重试的次数",
// ServiceForm.tsx
updateService: "更新服务",
createService: "创建服务",
// ServiceNotificationFields.tsx
enableNotifications: "启用通知",
enableNotificationsDesc: "为本服务启用或禁用通知",
notificationChannels: "通知渠道",
notificationChannelsEnabledDesc: "为本服务选择通知渠道",
notificationChannelsDesc: "先启用通知以选择渠道",
notificationChannelsPlaceholder: "添加一个通知渠道",
alertTemplate: "警报模板",
alertTemplateLoading: "正在加载模板...",
alertTemplatePlaceholder: "选择一个警报模板",
alertTemplateEnabledDesc: "选择一个警报消息的模板",
alertTemplateDesc: "先启用通知才能选择模板",
// ServiceTypeField.tsx
serviceType: "服务类型",
serviceTypeHTTPDesc: "使用 HTTP/HTTPS 协议监控网站和 RESTAPI",
serviceTypePINGDesc: "使用 PING 协议监控主机可用性",
serviceTypeTCPDesc: "使用 TCP 协议监控 TCP 端口连接性",
serviceTypeDNSDesc: "监控 DNS 解析",
// ServiceRegionalFields.tsx
regionalMonitoring: "区域监控",
regionalMonitoringDesc: "将此服务分配给区域监控代理以进行分布式监控",
regionalAgents: "区域代理",
regionalAgentsLoading: "加载代理中...",
regionalAgentsAvailablePlaceholder: "选择额外的区域代理...",
regionalAgentsAllSelected: "已选择所有可用代理",
regionalAgentsNoAvailable: "无可用区域代理",
regionalAgentsNoOnlineAvailable: "无可用在线区域代理",
regionalAgentsNotFoundMessage: "未找到在线区域代理。服务将使用默认监控。",
regionalAgentsNotSelectedMessage: "未选择区域代理。服务将使用默认监控。",
// ServiceUrlField.tsx
targetDefault: "目标 URL/Host",
targetDNS: "域名",
targetHTTPDesc: "输入完整的 URL,包括协议(http:// 或 https://",
targetPINGDesc: "输入要 ping 的主机名或 IP 地址",
targetTCPDesc: "输入主机名或 IP 地址以进行 TCP 连接测试",
targetTCPPortDesc: "输入用于 TCP 连接测试的端口号",
targetDNSDesc: "输入要监控的 DNS 记录域名(A、AAAA、MX等)",
targetDefaultDesc: "输入要监控的目标 URL 或主机名",
targetDefaultPlaceholder: "输入 URL 或主机名",
// types.ts
serviceNameRequired: "服务名称是必填项",
urlDomainHostRequired: "URL/域名/主机名是必填项",
enterValidUrlHostnameDomain: "请输入有效的URL、主机名或域名",
// Dashboard
upServices: "在线服务",
downServices: "下线服务",
pausedServices: "暂停服务",
warningServices: "告警服务",
};
+14 -7
View File
@@ -2,18 +2,18 @@
import { SettingsTranslations } from '../types/settings';
export const settingsTranslations: SettingsTranslations = {
// Tabs
// General Settings - Tabs
systemSettings: "系统设置",
mailSettings: "邮件设置",
// System Settings
// General Settings - System Settings
appName: "应用名称",
appURL: "应用 URL",
senderName: "发送者名称",
senderEmail: "发送者邮箱地址",
hideControls: "隐藏控件",
// Mail Settings
// General Settings - Mail Settings
smtpSettings: "SMTP 配置",
smtpEnabled: "启用 SMTP",
smtpHost: "SMTP 主机",
@@ -24,7 +24,7 @@ export const settingsTranslations: SettingsTranslations = {
enableTLS: "启用 TLS",
localName: "本地名称",
// Test Email
// General Settings - Test Email
testEmail: "测试邮箱",
sendTestEmail: "发送测试邮箱",
emailTemplate: "邮箱模板",
@@ -39,7 +39,7 @@ export const settingsTranslations: SettingsTranslations = {
enterEmailAddress: "输入收件人邮箱地址",
sending: "发送中...",
// Actions and status
// General Settings - Actions and status
save: "保存变更",
saving: "保存中...",
settingsUpdated: "设置已成功更新",
@@ -48,5 +48,12 @@ export const settingsTranslations: SettingsTranslations = {
testConnection: "测试连接",
testingConnection: "测试连接中...",
connectionSuccess: "连接成功",
connectionFailed: "连接失败"
};
connectionFailed: "连接失败",
// User Management
addUser: "添加用户",
permissionNotice: "权限问题:",
permissionNoticeAddUser: "作为管理员用户,您无权查看或修改系统及邮件设置。这些设置仅超级管理员可访问和修改。如需更改系统配置或邮件设置,请联系您的超级管理员。",
loadingSettings: "加载设置中...",
loadingSettingsError: "加载设置时出错",
};
+2 -1
View File
@@ -112,5 +112,6 @@ export const sslTranslations: SSLTranslations = {
created: "创建时间",
lastUpdated: "最后更新时间",
lastNotification: "最后通知时间",
collectionId: "集合 ID"
collectionId: "集合 ID",
noCertificatesFound: "未找到证书",
};
+23 -7
View File
@@ -55,7 +55,7 @@ The roadmap is divided into the following stages:
## 🗂 Milestone Overview
### 📦 v1.0.0-1.3.0 Initial Release _(Target: Q2 2025)_
### 🚀 v1.0.0-1.3.0 Initial Release _(Target: Q2 2025)_
**Status:** ✅ Completed
**Goal:** MVP feature completion, API stability, and core use-case readiness.
@@ -64,7 +64,7 @@ The roadmap is divided into the following stages:
---
### 🚀 v1.4 - 1.5 Feature Enhancements _(Target: Q3 2025)_
**Status:** 🔧 In Progress
**Status:** ✅ Completed
#### Tentative Features:
- [ ] ✅ Server and Service Table row clickable to detail page.
- [ ] ✅ Implement pagination for the SSL dashboard table
@@ -73,14 +73,31 @@ The roadmap is divided into the following stages:
- [ ] ✅ Improve Uptime Service and Server connection update based on status and notification.
- [ ] ✅ Improve SSL perform the initial check automatically after creation
- [ ] ✅ Rate limiting and abuse protection
- [ ] ✅ Add ntfy push notifications
### 🚀 v1.6.0 Feature Enhancements _(Target: Q3 2025)_
**Status:** 🔧 In Progress
#### Tentative Features:
- [ ] ✅ Allow user to update the schema directly from the dashboard
- [ ] ✅ Add Korean translations and update types. By @taking [#143](https://github.com/operacle/checkcle/pull/143)
- [ ] ✅ Add Simplified Chinese translation. By @sqkkyzx [#128](https://github.com/operacle/checkcle/pull/128)
- [ ] ✅ improve i18n and add new translations. @sqkkyzx [#132](https://github.com/operacle/checkcle/pull/132)
- [ ] ✅ fix(incident): unify assigned user handling with fallback and UI update. By @ghotso [#142](https://github.com/operacle/checkcle/pull/142)
- [ ] ✅ sync German translations across incident, maintenance, services & settings. By @ghotso [#141](https://github.com/operacle/checkcle/pull/141)
- [ ] ✅ fix(services-form): max retry attempts. By @ghotso [#147](https://github.com/operacle/checkcle/pull/147)
- [ ] ✅ Add Pushover notifications
- [ ] 🔧 Enhance the Data retention feature
- [ ] 🔧 Improve docker agent for server monitoring
- [ ] 🔧 Improve the Server Agent on Unraid
- [ ] 🔧 Server Windows Agent
- [ ] 🔧 Improve the Operational status page
- [ ] 🔧 More Uptime Service Type (HTTP keyword, HTTP json)
- [ ] 🔧 Server support with cpu temperature
- [ ] 🔧 Server upport with multiple disks
- [ ] 🔧 Server upport with multiple disks/volume
- [ ] 🔧 Server support with Multiple Network Interfaces
- [ ] 🔧 Improve the Operational status page
- [ ] 🔧 Server network bandwidth monitoring
- [ ] 🎯 Improve the Schedule and Incident for automation
- [ ] 🎯 Uptime Monitoring option for choose: HTTP/HTTPS
- [ ] 🎯 Uptime Monitoring option for choose: HTTP/HTTPS. Add Option to Bypass SSL Verification in Uptime
- [ ] 🎯 Add 2FA support
---
@@ -91,8 +108,7 @@ These are community-suggested or experimental features under review:
- [ ] Grouping uptime services
- [ ] OIDC Connect | OAuth2 integration
- [ ] Mobile PWA support
- [ ] Cloud-native Helm/Kubernetes deployment
- [ ] PWA support
- [ ] Server outbound and inbound traffic usage
Youre welcome to propose features via [GitHub Discussions](https://github.com/operacle/checkcle/discussions) or open an issue with the `feature-request` template.
+6
View File
@@ -0,0 +1,6 @@
{
"name": "checkcle",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
-23030
View File
File diff suppressed because it is too large Load Diff
@@ -1,137 +1,144 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_2018671343");
return app.delete(collection);
try {
const collection = app.findCollectionByNameOrId("pbc_2018671343");
return app.delete(collection);
} catch (e) {
console.warn("Skip delete (server_processes):", e?.message);
}
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text407168695",
"max": 0,
"min": 0,
"name": "server_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1431356653",
"max": 0,
"min": 0,
"name": "pid",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "number1391558374",
"max": null,
"min": null,
"name": "memory_percent",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number2651913466",
"max": null,
"min": null,
"name": "cpu_percent",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2063623452",
"max": 0,
"min": 0,
"name": "status",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_2018671343",
"indexes": [],
"listRule": "",
"name": "server_processes",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
try {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text407168695",
"max": 0,
"min": 0,
"name": "server_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1431356653",
"max": 0,
"min": 0,
"name": "pid",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text1579384326",
"max": 0,
"min": 0,
"name": "name",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "number1391558374",
"max": null,
"min": null,
"name": "memory_percent",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number2651913466",
"max": null,
"min": null,
"name": "cpu_percent",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text2063623452",
"max": 0,
"min": 0,
"name": "status",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_2018671343",
"indexes": [],
"listRule": "",
"name": "server_processes",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})
return app.save(collection);
} catch (e) {
console.warn("Skip rollback (server_processes):", e?.message);
}
});
@@ -1,170 +1,178 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
const collection = app.findCollectionByNameOrId("pbc_3414192583");
return app.delete(collection);
try {
const collection = app.findCollectionByNameOrId("pbc_3414192583");
return app.delete(collection);
} catch (e) {
console.warn("Skip delete (server_notifications):", e?.message);
}
}, (app) => {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text407168695",
"max": 0,
"min": 0,
"name": "server_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3065852031",
"max": 0,
"min": 0,
"name": "message",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text887233555",
"max": 0,
"min": 0,
"name": "notification_type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "date3805952114",
"max": "",
"min": "",
"name": "read_at",
"presentable": false,
"required": false,
"system": false,
"type": "date"
},
{
"hidden": false,
"id": "number1377725610",
"max": null,
"min": null,
"name": "cpu_threshold",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number3087505384",
"max": null,
"min": null,
"name": "ram_threshold",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number2833320134",
"max": null,
"min": null,
"name": "disk_threshold",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number1826572927",
"max": null,
"min": null,
"name": "network_threshold",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number2184331740",
"max": null,
"min": null,
"name": "notification_config_id",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_3414192583",
"indexes": [],
"listRule": "",
"name": "server_notifications",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
try {
const collection = new Collection({
"createRule": "",
"deleteRule": "",
"fields": [
{
"autogeneratePattern": "[a-z0-9]{15}",
"hidden": false,
"id": "text3208210256",
"max": 15,
"min": 15,
"name": "id",
"pattern": "^[a-z0-9]+$",
"presentable": false,
"primaryKey": true,
"required": true,
"system": true,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text407168695",
"max": 0,
"min": 0,
"name": "server_id",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text3065852031",
"max": 0,
"min": 0,
"name": "message",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"autogeneratePattern": "",
"hidden": false,
"id": "text887233555",
"max": 0,
"min": 0,
"name": "notification_type",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
},
{
"hidden": false,
"id": "date3805952114",
"max": "",
"min": "",
"name": "read_at",
"presentable": false,
"required": false,
"system": false,
"type": "date"
},
{
"hidden": false,
"id": "number1377725610",
"max": null,
"min": null,
"name": "cpu_threshold",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number3087505384",
"max": null,
"min": null,
"name": "ram_threshold",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number2833320134",
"max": null,
"min": null,
"name": "disk_threshold",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number1826572927",
"max": null,
"min": null,
"name": "network_threshold",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "number2184331740",
"max": null,
"min": null,
"name": "notification_config_id",
"onlyInt": false,
"presentable": false,
"required": false,
"system": false,
"type": "number"
},
{
"hidden": false,
"id": "autodate2990389176",
"name": "created",
"onCreate": true,
"onUpdate": false,
"presentable": false,
"system": false,
"type": "autodate"
},
{
"hidden": false,
"id": "autodate3332085495",
"name": "updated",
"onCreate": true,
"onUpdate": true,
"presentable": false,
"system": false,
"type": "autodate"
}
],
"id": "pbc_3414192583",
"indexes": [],
"listRule": "",
"name": "server_notifications",
"system": false,
"type": "base",
"updateRule": "",
"viewRule": ""
});
return app.save(collection);
})
return app.save(collection);
} catch (e) {
console.warn("Skip rollback (server_notifications):", e?.message);
}
});

Some files were not shown because too many files have changed in this diff Show More