diff --git a/.github/scripts/download-translations.js b/.github/scripts/download-translations.js index 185992fd5..b814a2ba2 100644 --- a/.github/scripts/download-translations.js +++ b/.github/scripts/download-translations.js @@ -7,7 +7,7 @@ import { URLSearchParams } from "url"; const API_TOKEN = process.env.POEDITOR_API; const PROJECT_ID = process.env.POEDITOR_PROJECT_ID; const LANGUAGES = ( - process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn" + process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th" ).split(","); const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json"; diff --git a/.github/workflows/poeditor-sync.yml b/.github/workflows/poeditor-sync.yml index 87396d3d0..c9bb92b72 100644 --- a/.github/workflows/poeditor-sync.yml +++ b/.github/workflows/poeditor-sync.yml @@ -7,7 +7,7 @@ on: languages: description: "Languages to synchronize (comma separated, e.g.: tr,en,es)" required: false - default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn" + default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th" format: description: "Export format (key_value_json or json)" required: false diff --git a/README.md b/README.md index 048cf0706..b93794d4f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart). -Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. +Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. ### Using a Custom CA diff --git a/client/package-lock.json b/client/package-lock.json index 32446c5d0..ee1ab1bc1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -45,7 +45,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.6", "prettier": "^3.3.3", - "vite": "6.3.5" + "vite": "6.3.6" } }, "node_modules/@ampproject/remapping": { @@ -2422,13 +2422,13 @@ } }, "node_modules/axios": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", - "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -3756,6 +3756,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -6375,9 +6389,9 @@ } }, "node_modules/vite": { - "version": "6.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", - "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", + "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", "dependencies": { "esbuild": "^0.25.0", diff --git a/client/package.json b/client/package.json index 92da6fd13..3108f7121 100644 --- a/client/package.json +++ b/client/package.json @@ -58,6 +58,6 @@ "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.6", "prettier": "^3.3.3", - "vite": "6.3.5" + "vite": "6.3.6" } } diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx index fa8a6ebe9..f0a701517 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useCallback } from "react"; const useInfrastructureMonitorForm = () => { const [infrastructureMonitor, setInfrastructureMonitor] = useState({ url: "", @@ -6,6 +6,8 @@ const useInfrastructureMonitorForm = () => { notifications: [], notify_email: false, interval: 0.25, + statusWindowSize: 5, + statusWindowThreshold: 60, cpu: false, usage_cpu: "", memory: false, @@ -29,9 +31,10 @@ const useInfrastructureMonitorForm = () => { [event.target.name]: event.target.checked, }); }; - const initializeInfrastructureMonitorForCreate = (globalSettings) => { + const initializeInfrastructureMonitorForCreate = useCallback((globalSettings) => { const gt = globalSettings?.data?.settings?.globalThresholds || {}; - setInfrastructureMonitor({ + setInfrastructureMonitor((prev) => ({ + ...prev, url: "", name: "", notifications: [], @@ -45,17 +48,20 @@ const useInfrastructureMonitorForm = () => { temperature: gt.temperature !== undefined, usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", secret: "", - }); - }; + })); + }, []); - const initializeInfrastructureMonitorForUpdate = (monitor) => { + const initializeInfrastructureMonitorForUpdate = useCallback((monitor) => { const MS_PER_MINUTE = 60000; const { thresholds = {} } = monitor; - setInfrastructureMonitor({ + setInfrastructureMonitor((prev) => ({ + ...prev, url: monitor.url.replace(/^https?:\/\//, ""), name: monitor.name || "", notifications: monitor.notifications || [], interval: monitor.interval / MS_PER_MINUTE, + statusWindowSize: monitor.statusWindowSize, + statusWindowThreshold: monitor.statusWindowThreshold, cpu: thresholds.usage_cpu !== undefined, usage_cpu: thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "", @@ -75,8 +81,8 @@ const useInfrastructureMonitorForm = () => { ? (thresholds.usage_temperature * 100).toString() : "", secret: monitor.secret || "", - }); - }; + })); + }, []); return { infrastructureMonitor, setInfrastructureMonitor, diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx index da564ae7c..9b3ee0d04 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -12,6 +12,8 @@ const useInfrastructureSubmit = () => { ? infrastructureMonitor.url : infrastructureMonitor.name, interval: infrastructureMonitor.interval * MS_PER_MINUTE, + statusWindowSize: infrastructureMonitor.statusWindowSize, + statusWindowThreshold: infrastructureMonitor.statusWindowThreshold, cpu: infrastructureMonitor.cpu, ...(infrastructureMonitor.cpu ? { usage_cpu: infrastructureMonitor.usage_cpu } diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index 28007c538..a9d16c926 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -86,7 +86,14 @@ const CreateInfrastructureMonitor = () => { setHttps(monitor.url.startsWith("https")); initializeInfrastructureMonitorForUpdate(monitor); } - }, [isCreate, monitor, globalSettings, globalSettingsLoading]); + }, [ + isCreate, + monitor, + globalSettings, + globalSettingsLoading, + initializeInfrastructureMonitorForCreate, + initializeInfrastructureMonitorForUpdate, + ]); // Handlers const onSubmit = async (event) => { @@ -281,6 +288,39 @@ const CreateInfrastructureMonitor = () => { setNotifications={infrastructureMonitor.notifications} /> + + + + {t("createMonitorPage.incidentConfigTitle")} + + + {t("createMonitorPage.incidentConfigDescription")} + + + + + + + template หรือ sample ของเราได้", + "selectFile": "เลือกไฟล์", + "parsingFailed": "การแยกวิเคราะห์ล้มเหลว", + "uploadSuccess": "สร้างมอนิเตอร์เรียบร้อยแล้ว!", + "validationFailed": "การตรวจสอบล้มเหลว", + "noFileSelected": "ยังไม่ได้เลือกไฟล์", + "fallbackPage": "นำเข้าไฟล์เพื่ออัปโหลดรายการเซิร์ฟเวอร์เป็นกลุ่ม", + "invalidFileType": "ประเภทไฟล์ไม่ถูกต้อง", + "uploadFailed": "อัปโหลดล้มเหลว" + }, + "DeleteAccountTitle": "ลบบัญชี", + "DeleteAccountButton": "ลบบัญชี", + "publicLink": "ลิงก์สาธารณะ", + "maskedPageSpeedKeyPlaceholder": "*************************************", + "reset": "รีเช็ต", + "ignoreTLSError": "ละเว้นข้อผิดพลาด TLS/SSL", + "tlsErrorIgnored": "ละเว้นข้อผิดพลาด TLS/SSL แล้ว", + "ignoreTLSErrorDescription": "ละเว้นข้อผิดพลาด TLS/SSL และดำเนินการตรวจสอบความพร้อมใช้งานของเว็บไซต์ต่อ", + "createNew": "สร้างใหม่", + "greeting": { + "prepend": "สวัสดี", + "append": "ช่วงบ่ายคือเวลาเล่นของคุณ—มาทำให้สุดยอดกันเถอะ!", + "overview": "นี่คือภาพรวมระบบมอนิเตอร์ประเภท {{type}} ของคุณ" + }, + "roles": { + "superAdmin": "Super admin", + "admin": "แอดมิน", + "teamMember": "สมาชิกในทีม", + "demoUser": "ผู้ใช้ Demo" + }, + "teamPanel": { + "teamMembers": "สมาชิกในทีม", + "filter": { + "all": "ทั้งหมด", + "member": "สมาชิก" + }, + "inviteTeamMember": "เชิญสมาชิกเข้าทีม", + "inviteNewTeamMember": "เชิญสมาชิกทีมใหม่", + "inviteDescription": "เมื่อคุณเพิ่มสมาชิกทีมใหม่ เขาจะสามารถเข้าถึงมอนิเตอร์ทั้งหมดได้", + "email": "อีเมล", + "selectRole": "เลือกบทบาท", + "inviteLink": "ลิงก์เชิญ", + "cancel": "ยกเลิก", + "noMembers": "ยังไม่มีสมาชิกทีมในบทบาทนี้", + "getToken": "รับโทเค็น", + "emailToken": "Token ทางอีเมล", + "table": { + "name": "ชื่อ", + "email": "อีเมล", + "role": "บทบาท", + "created": "สร้างเมื่อ" + }, + "addTeamMember": { + "addMemberMenu": "เพิ่มสมาชิกทีม", + "title": "ลงทะเบียนสมาชิกทีมใหม่", + "description": "สร้างผู้ใช้ใหม่และแชร์ข้อมูลการเข้าสู่ระบบให้กับพวกเขา วิธีนี้จะทำให้สมาชิกสามารถเข้าถึงตัวตรวจสอบทั้งหมดได้ทันที", + "addButton": "เพิ่มสมาชิก" + }, + "register": "ลงทะเบียนสมาชิกทีม", + "registerToast": { + "success": "สร้างผู้ใช้เรียบร้อยแล้ว กรุณาแชร์ข้อมูลการเข้าสู่ระบบให้สมาชิกอย่างปลอดภัย", + "dbUserExists": "ผู้ใช้นี้มีอยู่แล้ว", + "unknownError": "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ" + }, + "registerTeamMember": { + "title": "ลงทะเบียนสมาชิกทีม", + "auth": { + "common": { + "inputs": { + "firstName": { + "errors": { + "empty": "กรุณากรอกชื่อ", + "pattern": "ชื่อต้องมีเฉพาะตัวอักษร เว้นวรรค เครื่องหมายอัญประกาศ หรือขีดกลาง" + } + }, + "lastName": { + "errors": { + "empty": "กรุณากรอกนามสกุล", + "pattern": "นามสกุลต้องมีเฉพาะตัวอักษร เว้นวรรค เครื่องหมายอัญประกาศ หรือขีดกลาง" + } + }, + "email": { + "errors": { + "empty": "กรุณากรอกอีเมลเพื่อดำเนินการต่อ", + "invalid": "กรุณาตรวจสอบความถูกต้องของอีเมลอีกครั้ง" + } + }, + "role": { + "errors": { + "empty": "ต้องระบุบทบาท" + } + } + } + } + } + }, + "role": "บทบาท" + }, + "monitorState": { + "paused": "หยุดชั่วคราว", + "resumed": "ดำเนินการต่อ", + "active": "ใช้งานอยู่" + }, + "menu": { + "uptime": "ความพร้อมใช้งาน", + "pagespeed": "ความเร็วหน้าเว็บ", + "infrastructure": "โครงสร้างพื้นฐาน", + "incidents": "เหตุการณ์", + "statusPages": "หน้าสถานะ", + "maintenance": "การบำรุงรักษา", + "integrations": "การรวมระบบ", + "settings": "การตั้งค่า", + "support": "การสนับสนุน", + "discussions": "การสนทนา", + "docs": "เอกสาร", + "changelog": "บันทึกการเปลี่ยนแปลง", + "profile": "โปรไฟล์", + "password": "รหัสผ่าน", + "team": "ทีม", + "logOut": "ออกจากระบบ", + "notifications": "การแจ้งเตือน", + "logs": "Logs" + }, + "settingsEmailUser": "อีเมลผู้ใช้ - ชื่อผู้ใช้สำหรับการยืนยันตัวตน จะใช้แทนอีเมลหากระบุไว้", + "state": "สถานะ", + "statusBreadCrumbsStatusPages": "หน้าสถานะ", + "statusBreadCrumbsDetails": "รายละเอียด", + "commonSaving": "กำลังบันทึก...", + "navControls": "การควบคุม", + "incidentsPageTitle": "เหตุการณ์", + "passwordPanel": { + "passwordChangedSuccess": "รหัสผ่านของคุณถูกเปลี่ยนเรียบร้อยแล้ว", + "passwordInputIncorrect": "คุณป้อนรหัสผ่านที่ไม่ถูกต้อง", + "currentPassword": "รหัสผ่านปัจจุบัน", + "enterCurrentPassword": "กรอกรหัสผ่านปัจจุบันของคุณ", + "newPassword": "รหัสผ่านใหม่", + "enterNewPassword": "กรอกรหัสผ่านใหม่ของคุณ", + "confirmNewPassword": "ยืนยันรหัสผ่านใหม่อีกครั้ง", + "passwordRequirements": "รหัสผ่านใหม่ต้องมีอย่างน้อย 8 ตัวอักษร และต้องมีตัวอักษรใหญ่ ตัวอักษรเล็ก ตัวเลข และอักขระพิเศษอย่างละอย่าง 1", + "saving": "กำลังบันทึก..." + }, + "emailSent": "ส่งอีเมลเรียบร้อยแล้ว", + "failedToSendEmail": "ส่งอีเมลล้มเหลว", + "settingsTestEmailSuccess": "ส่งอีเมลทดสอบเรียบร้อยแล้ว", + "settingsTestEmailFailed": "ส่งอีเมลทดสอบล้มเหลว", + "settingsTestEmailFailedWithReason": "ส่งอีเมลทดสอบล้มเหลว: {{reason}}", + "settingsTestEmailUnknownError": "ข้อผิดพลาดไม่ทราบสาเหตุ", + "statusMsg": { + "paused": "การตรวจสอบถูกพักชั่วคราว", + "up": "เว็บไซต์ของคุณใช้งานได้", + "down": "เว็บไซต์ของคุณล่ม", + "pending": "รอดำเนินการ..." + }, + "uptimeGeneralInstructions": { + "http": "กรอก URL หรือ IP ที่ต้องการตรวจสอบ (เช่น https://example.com/\n หรือ 192.168.1.100) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", + "ping": "กรอกที่อยู่ IP หรือชื่อโฮสต์ที่ต้องการ ping (เช่น 192.168.1.100 หรือ example.com) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", + "docker": "กรอกชื่อหรือ ID ของคอนเทนเนอร์ Docker คุณสามารถใช้ชื่อคอนเทนเนอร์ (เช่น my-app) หรือ ID ของคอนเทนเนอร์ (ID เต็ม 64 ตัวอักษร หรือ ID สั้น)", + "port": "กรอก URL หรือ IP ของเซิร์ฟเวอร์ หมายเลขพอร์ต และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", + "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม" + }, + "common": { + "appName": "Checkmate", + "monitoringAgentName": "Capture", + "buttons": { + "toggleTheme": "สลับโหมดสว่าง & มืด" + }, + "toasts": { + "networkError": "ข้อผิดพลาดเครือข่าย", + "checkConnection": "กรุณาตรวจสอบการเชื่อมต่อของคุณ", + "unknownError": "ข้อผิดพลาดไม่ทราบสาเหตุ" + } + }, + "auth": { + "common": { + "navigation": { + "continue": "ดำเนินการต่อ", + "back": "ย้อนกลับ" + }, + "inputs": { + "email": { + "label": "อีเมล", + "placeholder": "jordan.ellis@domain.com", + "errors": { + "empty": "เพื่อดำเนินการต่อ กรุณากรอกที่อยู่อีเมลของคุณ", + "invalid": "กรุณาตรวจสอบความถูกต้องของอีเมลที่กรอก" + } + }, + "password": { + "label": "รหัสผ่าน", + "rules": { + "length": { + "beginning": "ต้องมีความยาวอย่างน้อย", + "highlighted": "8 ตัวอักษร" + }, + "special": { + "beginning": "ต้องมีอย่างน้อย", + "highlighted": "อักขระพิเศษ 1 ตัว" + }, + "number": { + "beginning": "ต้องมีอย่างน้อย", + "highlighted": "ตัวเลข 1 ตัว" + }, + "uppercase": { + "beginning": "ต้องมีอย่างน้อย", + "highlighted": "ตัวอักษรใหญ่ 1 ตัว" + }, + "lowercase": { + "beginning": "ต้องมีอย่างน้อย", + "highlighted": "ตัวอักษรเล็ก 1 ตัว" + }, + "match": { + "beginning": "รหัสผ่าน", + "highlighted": "ต้องตรงกัน" + } + }, + "errors": { + "empty": "กรุณากรอกรหัสผ่านของคุณ", + "length": "รหัสผ่านต้องมีความยาวอย่างน้อย 8 ตัวอักษร", + "uppercase": "รหัสผ่านต้องมีตัวอักษรใหญ่อย่างน้อย 1 ตัว", + "lowercase": "รหัสผ่านต้องมีตัวอักษรเล็กอย่างน้อย 1 ตัว", + "number": "รหัสผ่านต้องมีตัวเลขอย่างน้อย 1 ตัว", + "special": "รหัสผ่านต้องมีอักขระพิเศษอย่างน้อย 1 ตัว", + "incorrect": "รหัสผ่านที่คุณกรอกไม่ตรงกับข้อมูลของเรา" + } + }, + "passwordConfirm": { + "label": "ยืนยันรหัสผ่าน", + "placeholder": "กรอกรหัสผ่านอีกครั้งเพื่อยืนยัน", + "errors": { + "empty": "กรุณากรอกรหัสผ่านอีกครั้งเพื่อยืนยัน (ช่วยลดข้อผิดพลาดจากการพิมพ์ผิด)", + "different": "รหัสผ่านที่กรอกไม่ตรงกัน อาจมีตัวใดตัวหนึ่งพิมพ์ผิด" + } + }, + "firstName": { + "label": "ชื่อ", + "placeholder": "Brownyroll", + "errors": { + "empty": "กรุณากรอกชื่อของคุณ", + "length": "ชื่อต้องมีความยาวไม่เกิน 50 ตัวอักษร", + "pattern": "ชื่อต้องประกอบด้วยตัวอักษร ช่องว่าง เครื่องหมาย ' หรือ - เท่านั้น" + } + }, + "lastName": { + "label": "นามสกุล", + "placeholder": ".", + "errors": { + "empty": "กรุณากรอกนามสกุลของคุณ", + "length": "นามสกุลต้องมีความยาวไม่เกิน 50 ตัวอักษร", + "pattern": "นามสกุลต้องประกอบด้วยตัวอักษร ช่องว่าง เครื่องหมาย ' หรือ - เท่านั้น" + } + } + }, + "errors": { + "validation": "เกิดข้อผิดพลาดในการตรวจสอบข้อมูล" + }, + "fields": { + "password": { + "errors": { + "incorrect": "รหัสผ่านที่คุณป้อนไม่ตรงกับข้อมูลของเรา" + } + }, + "role": { + "errors": { + "min": "ต้องมีอย่างน้อยหนึ่งบทบาท" + } + } + } + }, + "login": { + "heading": "เข้าสู่ระบบเพื่อดำเนินการต่อ", + "subheadings": { + "stepOne": "กรอกอีเมลของคุณ", + "stepTwo": "กรอกรหัสผ่านของคุณ" + }, + "links": { + "forgotPassword": "ลืมรหัสผ่าน?", + "register": "ยังไม่มีบัญชีใช่ไหม?", + "forgotPasswordLink": "รีเซ็ตรหัสผ่าน", + "registerLink": "สมัครสมาชิกที่นี่" + }, + "toasts": { + "success": "ยินดีต้อนรับกลับ! คุณเข้าสู่ระบบเรียบร้อยแล้ว", + "incorrectPassword": "รหัสผ่านไม่ถูกต้อง" + }, + "errors": { + "password": { + "incorrect": "รหัสผ่านที่คุณป้อนไม่ตรงกับข้อมูลของเรา" + } + }, + "welcome": "ยินดีต้อนรับกลับสู่ Checkmate!" + }, + "registration": { + "heading": { + "superAdmin": "สร้าง Super Admin", + "user": "สมัครสมาชิก" + }, + "subheadings": { + "stepOne": "กรอกรายละเอียดส่วนตัวของคุณ", + "stepTwo": "กรอกอีเมลของคุณ", + "stepThree": "สร้างรหัสผ่านของคุณ" + }, + "description": { + "superAdmin": "สร้างบัญชี Super Admin เพื่อเริ่มต้นใช้งาน", + "user": "สมัครสมาชิกผู้ใช้และขอสิทธิ์เข้าถึงมอนิเตอร์จาก Super Admin" + }, + "gettingStartedButton": { + "superAdmin": "สร้างบัญชี Super Admin", + "user": "สมัครสมาชิกผู้ใช้ทั่วไป" + }, + "termsAndPolicies": "การสร้างบัญชีถือว่าคุณยอมรับ ข้อกำหนดการให้บริการ และ นโยบายความเป็นส่วนตัว ของเรา", + "links": { + "login": "มีบัญชีแล้ว? เข้าสู่ระบบ" + }, + "toasts": { + "success": "ยินดีต้อนรับ! สร้างบัญชีเรียบร้อยแล้ว" + }, + "welcome": "ยินดีต้อนรับสู่ Checkmate!" + }, + "forgotPassword": { + "heading": "ลืมรหัสผ่าน?", + "subheadings": { + "stepOne": "ไม่ต้องกังวล เราจะส่งคำแนะนำเพื่อรีเซ็ตรหัสผ่านให้คุณ", + "stepTwo": "เราได้ส่งลิงก์รีเซ็ตรหัสผ่านไปยัง ", + "stepThree": "รหัสผ่านใหม่ต้องแตกต่างจากรหัสผ่านที่ใช้ก่อนหน้านี้", + "stepFour": "รีเซ็ตรหัสผ่านเรียบร้อยแล้ว คลิกด้านล่างเพื่อเข้าสู่ระบบทันที" + }, + "buttons": { + "openEmail": "เปิดแอปอีเมล", + "resetPassword": "รีเซ็ตรหัสผ่าน" + }, + "imageAlts": { + "passwordKey": "", + "email": "", + "lock": "", + "passwordConfirm": "" + }, + "links": { + "login": "กลับไปที่ เข้าสู่ระบบ", + "resend": "ไม่ได้รับอีเมล? คลิกเพื่อส่งอีกครั้ง" + }, + "toasts": { + "sent": "คำแนะนำถูกส่งไปยัง .", + "emailNotFound": "ไม่พบอีเมลนี้", + "redirect": "กำลังเปลี่ยนหน้าใน ...", + "success": "รีเซ็ตรหัสผ่านสำเร็จแล้ว", + "error": "ไม่สามารถรีเซ็ตรหัสผ่านได้ กรุณาลองอีกครั้งภายหลังหรือติดต่อฝ่ายสนับสนุน" + } + } + }, + "errorPages": { + "serverUnreachable": { + "toasts": { + "reconnected": "เชื่อมต่อกับเซิร์ฟเวอร์สำเร็จแล้ว", + "stillUnreachable": "เซิร์ฟเวอร์ยังไม่สามารถเข้าถึงได้ กรุณาลองอีกครั้งภายหลัง" + }, + "alertBox": "เกิดข้อผิดพลาดการเชื่อมต่อเซิร์ฟเวอร์", + "description": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ได้ กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณหรือยืนยันการตั้งค่า deployment หากปัญหายังคงอยู่", + "retryButton": { + "default": "ลองเชื่อมต่ออีกครั้ง", + "processing": "กำลังเชื่อมต่อ…" + } + } + }, + "createNotifications": { + "title": "สร้างช่องทางแจ้งเตือน", + "nameSettings": { + "title": "ชื่อ", + "description": "ชื่อที่บ่งบอกถึงการตั้งค่าการรวมระบบของคุณ", + "nameLabel": "ชื่อ", + "namePlaceholder": "เช่น การแจ้งเตือนผ่าน Slack" + }, + "typeSettings": { + "title": "ประเภท", + "description": "เลือกประเภทของช่องทางแจ้งเตือนที่คุณต้องการสร้าง", + "typeLabel": "ประเภท" + }, + "emailSettings": { + "title": "อีเมล", + "description": "ที่อยู่อีเมลปลายทาง", + "emailLabel": "อีเมล", + "emailPlaceholder": "e.g. john@example.com" + }, + "slackSettings": { + "title": "Slack", + "description": "ตั้งค่า Slack webhook ของคุณที่นี่", + "webhookLabel": "URL ของ Slack webhook", + "webhookPlaceholder": "https://hooks.slack.com/services/..." + }, + "pagerdutySettings": { + "title": "PagerDuty", + "description": "ตั้งค่าการรวมระบบ PagerDuty ของคุณที่นี่", + "integrationKeyLabel": "คีย์การรวมระบบ", + "integrationKeyPlaceholder": "1234567890" + }, + "discordSettings": { + "title": "Discord", + "description": "ตั้งค่า Discord webhook ของคุณที่นี่", + "webhookLabel": "URL ของ Discord Webhook", + "webhookPlaceholder": "https://your-server.com/webhook" + }, + "webhookSettings": { + "title": "Webhook", + "description": "ตั้งค่า webhook ของคุณที่นี่", + "webhookLabel": "Webhook URL", + "webhookPlaceholder": "https://your-server.com/webhook" + }, + "testNotification": "ทดสอบการแจ้งเตือน", + "dialogDeleteTitle": "คุณแน่ใจหรือไม่ว่าต้องการลบการแจ้งเตือนนี้?", + "dialogDeleteConfirm": "ลบ" + }, + "notificationConfig": { + "title": "การแจ้งเตือน", + "description": "เลือกช่องทางแจ้งเตือนที่คุณต้องการใช้" + }, + "monitorStatus": { + "checkingEvery": "ตรวจสอบทุก {{interval}}", + "withCaptureAgent": "โดยใช้ Capture agent {{version}}", + "up": "ออนไลน์", + "down": "ออฟไลน์", + "paused": "หยุดชั่วคราว" + }, + "advancedMatching": "การจับคู่ขั้นสูง", + "sendTestNotifications": "ส่งการแจ้งเตือนทดสอบ", + "selectAll": "Select all", + "showAdminLoginLink": "แสดงลิงก์ \"ผู้ดูแลระบบ? เข้าสู่ระบบที่นี่\" บนหน้าสถานะ", + "logsPage": { + "title": "Logs", + "description": "ระบบ Logs 1000 บรรทัดล่าสุด", + "tabs": { + "queue": "คิวงาน", + "logs": "Logs เซิร์ฟเวอร์", + "diagnostics": "วินิจฉัย" + }, + "toast": { + "fetchLogsSuccess": "ดึง Logs สำเร็จแล้ว" + }, + "logLevelSelect": { + "title": "Logs level", + "values": { + "all": "All", + "info": "Info", + "warn": "Warn", + "error": "Error", + "debug": "Debug" + } + } + }, + "queuePage": { + "title": "Queue", + "refreshButton": "Refresh", + "flushButton": "Flush queue", + "jobTable": { + "title": "งานที่อยู่ในคิวปัจจุบัน", + "idHeader": "รหัสมอนิเตอร์", + "urlHeader": "URL", + "typeHeader": "Type", + "activeHeader": "ใช้งานอยู่", + "lockedAtHeader": "ล็อกเมื่อ", + "runCountHeader": "จำนวนรัน", + "failCountHeader": "จำนวนล้มเหลว", + "lastRunHeader": "รันล่าสุดเมื่อ", + "lastFinishedAtHeader": "เสร็จสิ้นล่าสุดเมื่อ", + "lastRunTookHeader": "รันครั้งล่าสุดใช้เวลา", + "intervalHeader": "ช่วงเวลา" + }, + "metricsTable": { + "title": "คิวเมตริก", + "metricHeader": "เมตริก", + "valueHeader": "Value" + }, + "failedJobTable": { + "title": "งานที่ล้มเหลว", + "monitorIdHeader": "รหัสมอนิเตอร์", + "monitorUrlHeader": "URL มอนิเตอร์", + "failCountHeader": "จำนวนล้มเหลว", + "failedAtHeader": "ล้มเหลวล่าสุดเมื่อ", + "failReasonHeader": "สาเหตุที่ล้มเหลว" + } + }, + "export": { + "title": "ส่งออกมอนิเตอร์", + "success": "ส่งออกมอนิเตอร์สำเร็จแล้ว", + "failed": "ไม่สามารถส่งออกมอนิเตอร์ได้" + }, + "monitorActions": { + "title": "ส่งออก/นำเข้า", + "import": "นำเข้ามอนิเตอร์", + "export": "ส่งออกมอนิเตอร์", + "deleteSuccess": "ลบมอนิเตอร์สำเร็จ", + "deleteFailed": "ลบมอนิเตอร์ล้มเหลว", + "details": "รายละเอียด" + }, + "settingsPage": { + "aboutSettings": { + "labelDevelopedBy": "พัฒนาโดย Bluewave Labs", + "labelVersion": "เวอร์ชัน", + "title": "เกี่ยวกับ" + }, + "demoMonitorsSettings": { + "buttonAddMonitors": "เพิ่มมอนิเตอร์ตัวอย่าง", + "description": "เพิ่มมอนิเตอร์ตัวอย่างเพื่อการสาธิต", + "title": "มอนิเตอร์สาธิต" + }, + "emailSettings": { + "buttonSendTestEmail": "ส่งอีเมลทดสอบ", + "description": "ตั้งค่าอีเมลสำหรับระบบของคุณ ใช้สำหรับส่งการแจ้งเตือนและเตือนภัย", + "descriptionTransport": "สร้าง SMTP transport สำหรับ NodeMailer", + "labelAddress": "ที่อยู่อีเมล - ใช้สำหรับการยืนยันตัวตน", + "labelConnectionHost": "โฮสต์เชื่อมต่ออีเมล - ชื่อโฮสต์ที่ใช้ในคำทักทาย HELO/EHLO", + "labelHost": "โฮสต์อีเมล - ชื่อโฮสต์หรือ IP สำหรับเชื่อมต่อ", + "labelIgnoreTLS": "ปิดใช้งาน STARTTLS: ไม่ใช้ TLS แม้เซิร์ฟเวอร์รองรับ", + "labelPassword": "รหัสผ่านอีเมล - สำหรับยืนยันตัวตน", + "labelPasswordSet": "ตั้งรหัสผ่านเรียบร้อยแล้ว คลิก รีเซ็ต เพื่อเปลี่ยน", + "labelPool": "เปิดใช้ connection pooling: ใช้การเชื่อมต่อเดิมซ้ำเพื่อเพิ่มประสิทธิภาพ", + "labelPort": "พอร์ตอีเมล - พอร์ตสำหรับเชื่อมต่อ", + "labelRejectUnauthorized": "ปฏิเสธใบรับรองไม่ถูกต้อง: ปฏิเสธการเชื่อมต่อที่ใช้ใบรับรอง self-signed หรือไม่เชื่อถือ", + "labelRequireTLS": "บังคับ STARTTLS: ต้องการอัปเกรด TLS ล้มเหลวหากไม่รองรับ", + "labelSecure": "ใช้ SSL (แนะนำ): เข้ารหัสการเชื่อมต่อด้วย SSL/TLS", + "labelTLSServername": "ชื่อเซิร์ฟเวอร์ TLS - ชื่อโฮสต์เสริมสำหรับตรวจสอบ TLS เมื่อโฮสต์เป็น IP", + "labelUser": "ผู้ใช้อีเมล - ชื่อผู้ใช้สำหรับยืนยันตัวตน ใช้แทนที่อีเมลถ้ามีการระบุ", + "linkTransport": "ดูข้อกำหนดที่นี่", + "placeholderUser": "ปล่อยว่างหากไม่จำเป็น", + "title": "อีเมล", + "toastEmailRequiredFieldsError": "ต้องระบุอีเมล, โฮสต์, พอร์ต และรหัสผ่าน" + }, + "pageSpeedSettings": { + "description": "ใส่ Google PageSpeed API Key ของคุณเพื่อเปิดใช้งานการตรวจสอบ PageSpeed ของ Google คลิก รีเซ็ต เพื่ออัปเดต Key", + "labelApiKeySet": "ตั้งค่า API key แล้ว คลิก รีเซ็ต เพื่อเปลี่ยน", + "labelApiKey": "PageSpeed API key", + "title": "Google PageSpeed API key" + }, + "saveButtonLabel": "บันทึก", + "statsSettings": { + "clearAllStatsButton": "ล้างสถิติทั้งหมด", + "clearAllStatsDescription": "ล้างสถิติทั้งหมด การกระทำนี้ไม่สามารถย้อนกลับได้", + "clearAllStatsDialogConfirm": "ใช่ ล้างสถิติทั้งหมด", + "clearAllStatsDialogDescription": "เมื่อถูกลบแล้ว ประวัติการตรวจสอบและสถิติไม่สามารถกู้คืนได้", + "clearAllStatsDialogTitle": "คุณต้องการล้างสถิติทั้งหมดหรือไม่?", + "description": "กำหนดระยะเวลาที่ต้องการเก็บข้อมูลประวัติ คุณยังสามารถล้างข้อมูลทั้งหมดที่มีอยู่ได้", + "labelTTL": "จำนวนวันที่ต้องการเก็บประวัติการตรวจสอบ", + "labelTTLOptional": "0 หมายถึงไม่จำกัด", + "title": "ประวัติการตรวจสอบ" + }, + "systemResetSettings": { + "buttonRemoveAllMonitors": "ลบมอนิเตอร์ทั้งหมด", + "description": "ลบมอนิเตอร์ทั้งหมดออกจากระบบของคุณ", + "dialogConfirm": "ใช่ ลบมอนิเตอร์ทั้งหมด", + "dialogDescription": "เมื่อถูกลบแล้ว มอนิเตอร์ไม่สามารถกู้คืนได้", + "dialogTitle": "คุณต้องการลบมอนิเตอร์ทั้งหมดหรือไม่?", + "title": "รีเซ็ตระบบ" + }, + "timezoneSettings": { + "description": "เลือกโซนเวลาที่ใช้แสดงวันที่และเวลาในแอปทั้งหมด", + "label": "โซนเวลาที่แสดง", + "title": "โซนเวลาที่แสดง" + }, + "title": "การตั้งค่า", + "uiSettings": { + "description": "สลับโหมดสว่าง/มืด หรือเปลี่ยนภาษาอินเทอร์เฟซผู้ใช้", + "labelLanguage": "ภาษา", + "labelTheme": "โหมดธีม", + "title": "รูปลักษณ์" + }, + "urlSettings": { + "description": "แสดง IP/URL ของมอนิเตอร์บนหน้าสถานะสาธารณะ หากปิดใช้งาน จะแสดงเฉพาะชื่อมอนิเตอร์เพื่อป้องกันข้อมูลสำคัญ", + "label": "แสดง IP/URL บนหน้าสถานะ", + "selectDisabled": "ปิดใช้งาน", + "selectEnabled": "เปิดใช้งาน", + "title": "IP/URL ของมอนิเตอร์บนหน้าสถานะ" + }, + "globalThresholds": { + "title": "เกณฑ์การตรวจสอบแบบรวม", + "description": "กำหนดค่าเกณฑ์ของ CPU, หน่วยความจำ, ดิสก์ และอุณหภูมิ หากระบุค่าไว้ ระบบจะเปิดใช้งานการตรวจสอบโดยอัตโนมัติ" + } + }, + "statusPageCreate": { + "buttonSave": "บันทึก" + }, + "incidentsOptionsHeaderFilterResolved": "แก้ไขแล้ว", + "settingsSave": "บันทึก", + "statusPageCreateAppearanceTitle": "รูปลักษณ์", + "confirmPassword": "ยืนยันรหัสผ่าน", + "monitorHooks": { + "failureAddDemoMonitors": "ไม่สามารถเพิ่มมอนิเตอร์ตัวอย่างได้", + "successAddDemoMonitors": "เพิ่มมอนิเตอร์ตัวอย่างสำเร็จ" + }, + "settingsAppearance": "รูปลักษณ์", + "settingsDisplayTimezone": "โซนเวลาที่แสดง", + "settingsGeneralSettings": "การตั้งค่าทั่วไป", + "incidentsOptionsHeaderTotalIncidents": "จำนวนเหตุการณ์ทั้งหมด", + "statusPage": { + "deleteSuccess": "ลบหน้าสถานะสำเร็จ", + "deleteFailed": "ลบหน้าสถานะล้มเหลว", + "createSuccess": "สร้างหน้าสถานะสำเร็จ", + "updateSuccess": "อัปเดตหน้าสถานะสำเร็จ", + "generalSettings": "การตั้งค่าทั่วไป", + "contents": "เนื้อหา", + "fallback": { + "checks": [ + "ตรวจสอบและแสดงสุขภาพของบริการแบบเรียลไทม์", + "ติดตามหลายบริการและแชร์สถานะ", + "แจ้งผู้ใช้ให้ทราบเกี่ยวกับการหยุดทำงานและประสิทธิภาพ" + ], + "title": "หน้าสถานะใช้สำหรับ:", + "actionButton": "มาสร้างหน้าสถานะแรกกันเถอะ!" + } + }, + "testNotificationsDisabled": "ยังไม่มีการตั้งค่าการแจ้งเตือนสำหรับมอนิเตอร์นี้ คุณต้องเพิ่มโดยคลิกปุ่ม 'ตั้งค่า'", + "incidentsTableResolvedAt": "แก้ไขเมื่อ", + "incidentsTableActionResolve": "แก้ไข", + "checkHooks": { + "failureResolveOne": "แก้ไขเหตุการณ์ล้มเหลว", + "failureResolveAll": "แก้ไขเหตุการณ์ทั้งหมดล้มเหลว", + "failureResolveMonitor": "ไม่สามารถแก้ไขเหตุการณ์ของตัวตรวจสอบได้" + }, + "checkFormError": "กรุณาตรวจสอบแบบฟอร์มสำหรับข้อผิดพลาด", + "diagnosticsPage": { + "diagnosticDescription": "วินิจฉัยระบบ", + "statsDescription": "สถิติระบบ", + "gauges": { + "heapAllocationTitle": "การจัดสรร Heap", + "heapAllocationSubtitle": "% ของหน่วยความจำที่ใช้ได้", + "heapUsageTitle": "การใช้งาน Heap", + "heapUsageSubtitle": "% ของหน่วยความจำที่ใช้ได้", + "heapUtilizationTitle": "การใช้งาน Heap", + "heapUtilizationSubtitle": "% ของที่จัดสรรแล้ว", + "instantCpuUsageTitle": "การใช้งาน CPU ทันที", + "instantCpuUsageSubtitle": "% ของเวลา 1 วินาทีที่ CPU ใช้" + }, + "stats": { + "eventLoopDelayTitle": "ความล่าช้าวงจร Event Loop", + "uptimeTitle": "เวลาทำงาน", + "usedHeapSizeTitle": "ขนาดที่ใช้ Heap", + "totalHeapSizeTitle": "รวมขนาด Heap", + "osMemoryLimitTitle": "ขีดจำกัดหน่วยความจำ OS" + } + }, + "pageSpeedLighthouseAPI": "ใช้ Lighthouse PageSpeed API เพื่อตรวจสอบเว็บไซต์ของคุณ", + "time": { + "threeMinutes": "3 นาที", + "fiveMinutes": "5 นาที", + "tenMinutes": "10 นาที", + "twentyMinutes": "20 นาที", + "oneHour": "1 ชั่วโมง", + "oneDay": "1 วัน", + "oneWeek": "1 สัปดาห์", + "fourMinutes": "4 นาที", + "oneMinute": "1 นาที", + "twoMinutes": "2 นาที", + "fifteenSeconds": "15 วินาที", + "thirtySeconds": "30 วินาที" + }, + "general": { + "noOptionsFound": "ไม่พบ {{unit}}" + }, + "infrastructureMonitor": { + "fallback": { + "checks": [ + "ติดตามประสิทธิภาพของเซิร์ฟเวอร์ของคุณ", + "ระบุจุดคอขวดและเพิ่มประสิทธิภาพการใช้งาน", + "มั่นใจในความเสถียรด้วยการตรวจสอบแบบเรียลไทม์" + ], + "title": "ตัวตรวจสอบโครงสร้างพื้นฐานใช้สำหรับ:", + "actionButton": "มาสร้างตัวตรวจสอบโครงสร้างพื้นฐานตัวแรกกันเถอะ!" + } + }, + "maintenanceWindow": { + "fallback": { + "checks": [ + "กำหนดช่วงเวลาบำรุงรักษา", + "ลดความเข้าใจผิด", + "หยุดส่งการแจ้งเตือนในช่วงเวลาบำรุงรักษา" + ], + "title": "ช่วงเวลาบำรุงรักษาใช้สำหรับ:", + "actionButton": "มาสร้างช่วงเวลาบำรุงรักษาแรกกันเถอะ!" + } + }, + "pageSpeed": { + "fallback": { + "checks": [ + "รายงานเกี่ยวกับประสบการณ์ผู้ใช้ของหน้าเว็บ", + "ช่วยวิเคราะห์ความเร็วของหน้าเว็บ", + "ให้คำแนะนำในการปรับปรุงหน้าเว็บ" + ], + "title": "ตัวตรวจสอบ PageSpeed ใช้สำหรับ:", + "actionButton": "มาสร้างตัวตรวจสอบ PageSpeed ตัวแรกกันเถอะ!" + } + }, + "uptimeMonitor": { + "fallback": { + "checks": [ + "ตรวจสอบว่าเว็บไซต์หรือเซิร์ฟเวอร์ออนไลน์และตอบสนองหรือไม่", + "แจ้งทีมเมื่อเกิดปัญหาการหยุดทำงานหรือประสิทธิภาพ", + "ตรวจสอบ HTTP endpoints, ping, คอนเทนเนอร์ และพอร์ต", + "ติดตามสถิติ uptime และความเสถียรย้อนหลัง" + ], + "title": "ตัวตรวจสอบ Uptime ใช้สำหรับ:", + "actionButton": "มาสร้างตัวตรวจสอบ Uptime ตัวแรกกันเถอะ!" + } + }, + "editUserPage": { + "form": { + "email": "อีเมล", + "firstName": "ชื่อจริง", + "lastName": "นามสกุล", + "role": "บทบาท", + "save": "บันทึก" + }, + "table": { + "actionHeader": "การดำเนินการ", + "roleHeader": "บทบาท" + }, + "title": "แก้ไขผู้ใช้", + "toast": { + "successUserUpdate": "อัปเดตผู้ใช้เรียบร้อยแล้ว", + "validationErrors": "ข้อผิดพลาดในการตรวจสอบข้อมูล" + } + }, + "incidentsPageActionResolveMonitor": "แก้ไขเหตุการณ์ของตัวตรวจสอบ", + "incidentsPageActionResolveAll": "แก้ไขเหตุการณ์ทั้งหมด", + "matchMethodOptions": { + "equal": "Equal", + "equalPlaceholder": "success", + "include": "Include", + "includePlaceholder": "ok", + "regex": "Regex", + "regexPlaceholder": "^(success|ok)$", + "text": "วิธีการจับคู่" + }, + "monitorType": { + "docker": { + "label": "ชื่อ/ID คอนเทนเนอร์", + "namePlaceholder": "คอนเทนเนอร์ของฉัน", + "placeholder": "my-app or abcd1234" + }, + "http": { + "label": "URL ที่ต้องการตรวจสอบ", + "namePlaceholder": "Google", + "placeholder": "google.com" + }, + "ping": { + "label": "ที่อยู่ IP ที่ต้องการตรวจสอบ", + "namePlaceholder": "Google", + "placeholder": "1.1.1.1" + }, + "port": { + "label": "ที่อยู่ IP ที่ต้องการตรวจสอบ", + "namePlaceholder": "Localhost:5173", + "placeholder": "localhost" + }, + "game": { + "label": "URL ที่ต้องการมอนิเตอร์", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" + } + }, + "uptimeAdvancedMatching": { + "jsonPath": "JSON Path" + }, + "bytesPerSecond": "ไบต์ต่อวินาที", + "bytesReceived": "ไบต์ที่รับ", + "bytesSent": "ไบต์ที่ส่ง", + "chooseGame": "เลือกเกม", + "createMonitorPage": { + "incidentConfigDescription": "การใช้หน้าต่างเลื่อนจะช่วยกำหนดเวลาที่ตัวตรวจสอบล้มเหลว สถานะของตัวตรวจสอบจะเปลี่ยนก็ต่อเมื่อเปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนตรงตามค่าที่กำหนด", + "incidentConfigStatusWindowLabel": "ต้องมีการตรวจสอบกี่ครั้งในหน้าต่างเลื่อน?", + "incidentConfigStatusWindowThresholdLabel": "ต้องการให้กี่เปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนล้มเหลว/สำเร็จ ก่อนที่จะเปลี่ยนสถานะของตัวตรวจสอบ?", + "incidentConfigTitle": "เหตุการณ์" + }, + "dataRate": "อัตราการส่งข้อมูล", + "dataReceived": "ข้อมูลที่รับ", + "dataSent": "ข้อมูลที่ส่ง", + "details": "รายละเอียด", + "drops": "แพ็กเก็ตที่หล่น", + "errors": "ข้อผิดพลาด", + "errorsIn": "ข้อผิดพลาดขาเข้า", + "errorsOut": "ข้อผิดพลาดขาออก", + "gameServerMonitoring": "มอนิเตอร์เซิร์ฟเวอร์เกม", + "gameServerMonitoringDescription": "ตรวจสอบว่าเซิร์ฟเวอร์เกมของคุณกำลังทำงานอยู่หรือไม่", + "network": "เน็ตเวอร์", + "networkDrops": "การดรอปของเน็ตเวอร์", + "networkErrors": "เน็ตเวอร์ ล้มเหลว", + "networkInterface": "เน็ตเวอร์ อินเทอร์เฟซ", + "noNetworkStatsAvailable": "ไม่มีสถิติของเน็ตเวอร์", + "packetsPerSecond": "แพ็กเก็ตต่อวินาที", + "packetsReceived": "แพ็กเก็ตที่รับ", + "packetsReceivedRate": "อัตราการรับแพ็กเก็ต", + "packetsSent": "แพ็กเก็ตที่ส่ง", + "rate": "อัตรา", + "selectInterface": "เลือกอินเทอร์เฟซ" +} diff --git a/client/vite.config.js b/client/vite.config.js index 158dc5e4e..07b5de748 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -5,7 +5,7 @@ import { execSync } from "child_process"; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ""); - let version = "3.1.8"; + let version = "3.2.0"; return { base: "/", diff --git a/server/src/locales/en.json b/server/src/locales/en.json index 74296d0a5..1345f84a8 100755 --- a/server/src/locales/en.json +++ b/server/src/locales/en.json @@ -149,10 +149,12 @@ "statusPageByTeamId": "Got status pages by team id successfully", "httpNetworkError": "Network error", "httpNotJson": "Response data is not json", + "httpExpectedValueFail": "Expected value did not match", "httpJsonPathError": "Failed to parse json data", "httpEmptyResult": "Result is empty", "httpMatchSuccess": "Response data match successfully", "httpMatchFail": "Failed to match response data", + "httpJsonPathFail": "JSONPath did not match expected value", "webhookSendSuccess": "Webhook notification sent successfully", "telegramRequiresBotTokenAndChatId": "Telegram notifications require both botToken and chatId", "webhookUrlRequired": "Webhook URL is required", @@ -162,5 +164,18 @@ "monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n", "sendTestEmail": "Test email sent successfully", "errorForValidEmailAddress": "A valid recipient email address is required.", - "testEmailSubject": "Test E-mail from Checkmate" + "testEmailSubject": "Test E-mail from Checkmate", + "discordNotification": { + "monitor": "Monitor", + "status": "Status", + "statusCode": "Status Code", + "time": "Time", + "up": "UP", + "down": "DOWN", + "checkmate": "Checkmate", + "url": "URL", + "unknown": "Unknown", + "uptimeAlert": "Uptime Alert: One of your monitors is back online", + "downtimeAlert": "Downtime Alert: One of your monitors went offline" + } } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index ac8da9169..b348e2534 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -28,11 +28,12 @@ class SuperSimpleQueueHelper { return async (monitor) => { try { const monitorId = monitor._id; + const teamId = monitor.teamId; if (!monitorId) { throw new Error("No monitor id"); } - const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId); + const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); if (maintenanceWindowActive) { this.logger.info({ message: `Monitor ${monitorId} is in maintenance window`, @@ -77,8 +78,11 @@ class SuperSimpleQueueHelper { }; }; - async isInMaintenanceWindow(monitorId) { - const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId(monitorId); + async isInMaintenanceWindow(monitorId, teamId) { + const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ + monitorId: monitorId.toString(), + teamId: teamId.toString(), + }); // Check for active maintenance window: const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { if (window.active) { diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js index 50cf16b85..47ef6878f 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.js @@ -1,5 +1,3 @@ -import { match } from "assert"; - const SERVICE_NAME = "NetworkService"; class NetworkService { @@ -107,7 +105,7 @@ class NetworkService { } async requestHttp(monitor) { - const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; + const { url, secret, _id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; const httpResponse = { monitorId: _id, teamId: teamId, @@ -156,33 +154,61 @@ class NetworkService { return httpResponse; } - if (expectedValue) { + if (expectedValue && !jsonPath) { let ok = false; if (matchMethod === "equal") ok = payload === expectedValue; - if (matchMethod === "include") ok = payload.includes(expectedValue); - if (matchMethod === "regex") ok = new RegExp(expectedValue).test(payload); + if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue); + if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload); if (ok === true) { return httpResponse; } else { httpResponse.code = 500; httpResponse.status = false; + httpResponse.message = this.stringService.httpExpectedValueFail; return httpResponse; } } if (jsonPath) { const contentType = response.headers["content-type"]; - const isJson = contentType?.includes("application/json"); if (!isJson) { httpResponse.status = false; httpResponse.message = this.stringService.httpNotJson; return httpResponse; } - try { - this.jmespath.search(payload, jsonPath); + const extracted = this.jmespath.search(payload, jsonPath); + if (expectedValue) { + let ok = false; + if (matchMethod === "equal") ok = extracted === expectedValue; + if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue); + if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted); + + if (ok) { + httpResponse.extracted = extracted; + return httpResponse; + } else { + httpResponse.status = false; + httpResponse.code = 500; + httpResponse.message = this.stringService.httpJsonPathFail; + httpResponse.extracted = extracted; + return httpResponse; + } + } else { + const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null; + if (!isFalsey) { + httpResponse.extracted = extracted; + return httpResponse; + } else { + httpResponse.status = false; + httpResponse.code = 500; + httpResponse.message = this.stringService.httpJsonPathFail; + httpResponse.extracted = extracted; + return httpResponse; + } + } } catch { httpResponse.status = false; httpResponse.message = this.stringService.httpJsonPathError; diff --git a/server/src/service/infrastructure/notificationService.js b/server/src/service/infrastructure/notificationService.js index 8c34519f3..6c5b8b25e 100644 --- a/server/src/service/infrastructure/notificationService.js +++ b/server/src/service/infrastructure/notificationService.js @@ -16,7 +16,7 @@ class NotificationService { return NotificationService.SERVICE_NAME; } - sendNotification = async ({ notification, subject, content, html }) => { + sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => { const { type, address } = notification; if (type === "email") { @@ -28,9 +28,11 @@ class NotificationService { // Create a body for webhooks let body = { text: content }; if (type === "discord") { - body = { content }; + body = !discordContent ? { content } : discordContent; + } + if (type === "webhook") { + body = !webhookBody ? { content } : webhookBody; } - if (type === "slack" || type === "discord" || type === "webhook") { const response = await this.networkService.requestWebhook(type, address, body); return response.status; @@ -55,7 +57,6 @@ class NotificationService { const notificationIDs = networkResponse.monitor?.notifications ?? []; if (notificationIDs.length === 0) return false; - if (networkResponse.monitor.type === "hardware") { const thresholds = networkResponse?.monitor?.thresholds; @@ -63,30 +64,29 @@ class NotificationService { const metrics = networkResponse?.payload?.data ?? null; if (metrics === null) return false; // No metrics, we're done - const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse); + const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); if (alerts.length === 0) return false; const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); - const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts); - - const success = await this.notifyAll({ notificationIDs, subject, html, content }); + const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); + const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); + const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); return success; } // Status monitors const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse); - const content = await this.notificationUtils.buildWebhookMessage(networkResponse); - const success = this.notifyAll({ notificationIDs, subject, html, content }); + const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse); + const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent }); return success; } - async notifyAll({ notificationIDs, subject, html, content }) { + async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) { const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs); - // Map each notification to a test promise const promises = notifications.map(async (notification) => { try { - await this.sendNotification({ notification, subject, content, html }); + await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody }); return true; } catch (err) { return false; diff --git a/server/src/service/infrastructure/notificationUtils.js b/server/src/service/infrastructure/notificationUtils.js index 22c7f4df7..d6b92cc0e 100644 --- a/server/src/service/infrastructure/notificationUtils.js +++ b/server/src/service/infrastructure/notificationUtils.js @@ -70,7 +70,25 @@ class NotificationUtils { .replace("{time}", formattedTime) .replace("{code}", code || "Unknown"); } - return messageText; + const dn = this.stringService.discordNotification; + let discordMessageText = { + embeds: [ + { + title: status ? dn.uptimeAlert : dn.downtimeAlert, + color: status ? 5763719 : 15548997, + + fields: [ + { name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true }, + { name: dn.status, value: status ? dn.up : dn.down, inline: true }, + { name: dn.statusCode, value: String(code ?? dn.unknown), inline: true }, + { name: dn.time, value: formattedTime, inline: true }, + { name: dn.url, value: monitor?.url ?? dn.unknown, inline: false }, + ], + footer: { text: dn.checkmate }, + }, + ], + }; + return [messageText, discordMessageText]; }; buildHardwareAlerts = async (networkResponse) => { @@ -88,6 +106,43 @@ class NotificationUtils { }; const alertsToSend = []; + const discordEmbeds = []; + const monitorInfoFields = [ + { name: "Monitor", value: monitor.name, inline: true }, + { name: "URL", value: monitor.url, inline: false }, + ]; + const formatDiscordAlert = { + cpu: () => ({ + title: "CPU alert", + description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + color: 15548997, + fields: monitorInfoFields, + footer: { text: "Checkmate" }, + }), + + memory: () => ({ + title: "Memory alert", + description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + color: 15548997, + fields: monitorInfoFields, + footer: { text: "Checkmate" }, + }), + + disk: () => ({ + title: "Disk alert", + description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + color: 15548997, + footer: { text: "Checkmate" }, + fields: [ + ...monitorInfoFields, + ...(Array.isArray(disk) ? disk : []).map((d, idx) => ({ + name: `Disk ${idx}`, + value: `${(d?.usage_percent * 100).toFixed(0)}%`, + inline: true, + })), + ], + }), + }; const alertTypes = ["cpu", "memory", "disk"]; for (const type of alertTypes) { // Iterate over each alert type to see if any need to be decremented @@ -108,11 +163,13 @@ class NotificationUtils { .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, }; alertsToSend.push(formatAlert[type]()); + discordEmbeds.push(formatDiscordAlert[type]()); } } } await monitor.save(); - return alertsToSend; + const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; + return [alertsToSend, discordPayload]; }; buildHardwareEmail = async (networkResponse, alerts) => { @@ -124,8 +181,15 @@ class NotificationUtils { return { subject, html }; }; - buildHardwareNotificationMessage = (alerts) => { - return alerts.map((alert) => alert).join("\n"); + buildHardwareNotificationMessage = (alerts, monitor) => { + const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; + const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts] : []; + return alertText.map((alert) => alert).join("\n"); + }; + buildHardwareWebhookBody = (alerts, monitor) => { + const content = alerts.map((alert) => alert).join("\n"); + const body = { text: content, name: monitor.name, url: monitor.url }; + return body; }; } diff --git a/server/src/service/system/stringService.js b/server/src/service/system/stringService.js index 5df3410f9..231550be7 100755 --- a/server/src/service/system/stringService.js +++ b/server/src/service/system/stringService.js @@ -216,6 +216,10 @@ class StringService { return this.translationService.getTranslation("monitorDownAlert"); } + get discordNotification() { + return this.translationService.getTranslation("discordNotification"); + } + getWebhookUnsupportedPlatform(platform) { return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform); }