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);
}