From ed5d5986ff487c64839d0939855333006bc79556 Mon Sep 17 00:00:00 2001 From: mohadeseh safari Date: Tue, 27 May 2025 20:28:53 -0400 Subject: [PATCH 1/4] Fix #2343: Enable Save button when timezone changes - Added timezone field to frontend validation schema - Added timezone field to backend validation schema - Fixed state synchronization in Settings component --- client/src/Pages/Settings/index.jsx | 2 ++ client/src/Validation/validation.js | 1 + server/validation/joi.js | 1 + 3 files changed, 4 insertions(+) diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 0f586b391..b86deaf8c 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -85,6 +85,8 @@ const Settings = () => { if (name === "timezone") { dispatch(setTimezone({ timezone: value })); + // Make sure to update settingsData with the new timezone value + setSettingsData(newSettingsData); } if (name === "mode") { diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 068340ba8..211e62995 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -277,6 +277,7 @@ const settingsValidation = joi.object({ }), pagespeedApiKey: joi.string().allow("").optional(), language: joi.string().required(), + timezone: joi.string().allow("").optional(), systemEmailHost: joi.string().allow(""), systemEmailPort: joi.number().allow(null, ""), systemEmailAddress: joi.string().allow(""), diff --git a/server/validation/joi.js b/server/validation/joi.js index 1f4b1c35c..e0e563e84 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -427,6 +427,7 @@ const updateAppSettingsBodyValidation = joi.object({ checkTTL: joi.number().allow(""), pagespeedApiKey: joi.string().allow(""), language: joi.string().allow(""), + timezone: joi.string().allow(""), // showURL: joi.bool().required(), systemEmailHost: joi.string().allow(""), systemEmailPort: joi.number().allow(""), From 126fda6bc06b568100b35a516988c6e8c2b1c8f3 Mon Sep 17 00:00:00 2001 From: mohadeseh safari Date: Thu, 29 May 2025 19:34:21 -0400 Subject: [PATCH 2/4] remove call --- client/src/Pages/Settings/index.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index b86deaf8c..0f586b391 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -85,8 +85,6 @@ const Settings = () => { if (name === "timezone") { dispatch(setTimezone({ timezone: value })); - // Make sure to update settingsData with the new timezone value - setSettingsData(newSettingsData); } if (name === "mode") { From 5018a2dfde59fa2760789a845581666a59d7e3ce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 29 May 2025 17:09:47 -0700 Subject: [PATCH 3/4] add config for webhook --- server/service/notificationService.js | 201 ++++++++++++++------------ 1 file changed, 108 insertions(+), 93 deletions(-) diff --git a/server/service/notificationService.js b/server/service/notificationService.js index e4f343c80..ed74ff539 100755 --- a/server/service/notificationService.js +++ b/server/service/notificationService.js @@ -1,11 +1,12 @@ const SERVICE_NAME = "NotificationService"; const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot"; -const PLATFORM_TYPES = ["telegram", "slack", "discord"]; +const PLATFORM_TYPES = ["telegram", "slack", "discord", "webhook"]; const MESSAGE_FORMATTERS = { telegram: (messageText, chatId) => ({ chat_id: chatId, text: messageText }), slack: (messageText) => ({ text: messageText }), discord: (messageText) => ({ content: messageText }), + webhook: (messageText) => ({ text: messageText }), }; class NotificationService { @@ -42,49 +43,57 @@ class NotificationService { formatNotificationMessage(monitor, status, platform, chatId, code, timestamp) { // Format timestamp using the local system timezone const formatTime = (timestamp) => { - const date = new Date(timestamp); - - // Get timezone abbreviation and format the date - const timeZoneAbbr = date.toLocaleTimeString('en-US', { timeZoneName: 'short' }) - .split(' ').pop(); - - // Format the date with readable format - return date.toLocaleString('en-US', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false - }).replace(/(\d+)\/(\d+)\/(\d+),\s/, '$3-$1-$2 ') + ' ' + timeZoneAbbr; + const date = new Date(timestamp); + + // Get timezone abbreviation and format the date + const timeZoneAbbr = date + .toLocaleTimeString("en-US", { timeZoneName: "short" }) + .split(" ") + .pop(); + + // Format the date with readable format + return ( + date + .toLocaleString("en-US", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) + .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") + + " " + + timeZoneAbbr + ); }; - + // Get formatted time - const formattedTime = timestamp - ? formatTime(timestamp) - : formatTime(new Date().getTime()); - + const formattedTime = timestamp + ? formatTime(timestamp) + : formatTime(new Date().getTime()); + // Create different messages based on status with extra spacing let messageText; if (status === true) { messageText = this.stringService.monitorUpAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || 'Unknown'); + .replace("{monitorName}", monitor.name) + .replace("{time}", formattedTime) + .replace("{code}", code || "Unknown"); } else { messageText = this.stringService.monitorDownAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || 'Unknown'); + .replace("{monitorName}", monitor.name) + .replace("{time}", formattedTime) + .replace("{code}", code || "Unknown"); } - + if (!PLATFORM_TYPES.includes(platform)) { - return undefined; + return undefined; } - + return MESSAGE_FORMATTERS[platform](messageText, chatId); - } + } /** * Sends a webhook notification to a specified platform. @@ -105,56 +114,56 @@ class NotificationService { const { monitor, status, code } = networkResponse; const { platform } = notification; const { webhookUrl, botToken, chatId } = notification.config; - + // Early return if platform is not supported if (!PLATFORM_TYPES.includes(platform)) { - this.logger.warn({ - message: this.stringService.getWebhookUnsupportedPlatform(platform), - service: this.SERVICE_NAME, - method: "sendWebhookNotification", - details: { platform } - }); - return false; + this.logger.warn({ + message: this.stringService.getWebhookUnsupportedPlatform(platform), + service: this.SERVICE_NAME, + method: "sendWebhookNotification", + details: { platform }, + }); + return false; } - + // Early return for telegram if required fields are missing if (platform === "telegram" && (!botToken || !chatId)) { - this.logger.warn({ - message: "Missing required fields for Telegram notification", - service: this.SERVICE_NAME, - method: "sendWebhookNotification", - details: { platform } - }); - return false; + this.logger.warn({ + message: "Missing required fields for Telegram notification", + service: this.SERVICE_NAME, + method: "sendWebhookNotification", + details: { platform }, + }); + return false; } - + let url = webhookUrl; if (platform === "telegram") { - url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; + url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`; } - + const message = this.formatNotificationMessage( - monitor, - status, - platform, - chatId, - code, // Pass the code field directly - networkResponse.timestamp + monitor, + status, + platform, + chatId, + code, // Pass the code field directly + networkResponse.timestamp ); - + try { - const response = await this.networkService.requestWebhook(platform, url, message); - return response.status; + const response = await this.networkService.requestWebhook(platform, url, message); + return response.status; } catch (error) { - this.logger.error({ - message: this.stringService.getWebhookSendError(platform), - service: this.SERVICE_NAME, - method: "sendWebhookNotification", - stack: error.stack, - }); - return false; + this.logger.error({ + message: this.stringService.getWebhookSendError(platform), + service: this.SERVICE_NAME, + method: "sendWebhookNotification", + stack: error.stack, + }); + return false; } - } + } /** * Sends an email notification for hardware infrastructure alerts @@ -197,33 +206,33 @@ class NotificationService { async handleStatusNotifications(networkResponse) { try { - // If status hasn't changed, we're done - if (networkResponse.statusChanged === false) return false; - // if prevStatus is undefined, monitor is resuming, we're done - if (networkResponse.prevStatus === undefined) return false; - - const notifications = await this.db.getNotificationsByMonitorId( - networkResponse.monitorId - ); - - for (const notification of notifications) { - if (notification.type === "email") { - await this.sendEmail(networkResponse, notification.address); - } else if (notification.type === "webhook") { - await this.sendWebhookNotification(networkResponse, notification); + // If status hasn't changed, we're done + if (networkResponse.statusChanged === false) return false; + // if prevStatus is undefined, monitor is resuming, we're done + if (networkResponse.prevStatus === undefined) return false; + + const notifications = await this.db.getNotificationsByMonitorId( + networkResponse.monitorId + ); + + for (const notification of notifications) { + if (notification.type === "email") { + await this.sendEmail(networkResponse, notification.address); + } else if (notification.type === "webhook") { + await this.sendWebhookNotification(networkResponse, notification); + } + // Handle other types of notifications here } - // Handle other types of notifications here - } - return true; + return true; } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "handleNotifications", - stack: error.stack, - }); + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "handleNotifications", + stack: error.stack, + }); } - } + } /** * Handles status change notifications for a monitor * @@ -257,7 +266,13 @@ class NotificationService { const alerts = { cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, - disk: disk?.some(d => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, + disk: + disk?.some( + (d) => + diskThreshold !== -1 && + typeof d?.usage_percent === "number" && + d?.usage_percent > diskThreshold + ) ?? false, }; const notifications = await this.db.getNotificationsByMonitorId( From 70055c712b0cc2caacfcfaaf0acfb0e2138dae23 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 30 May 2025 15:03:32 -0700 Subject: [PATCH 4/4] refactor i18n --- client/src/Components/I18nLoader/index.jsx | 16 ++++++++++++++++ client/src/Components/Inputs/Select/index.jsx | 3 ++- client/src/Components/LanguageSelector.jsx | 7 ++++--- client/src/Features/Settings/settingsSlice.js | 1 - client/src/Features/UI/uiSlice.js | 4 ++-- client/src/Pages/Settings/SettingsURL.jsx | 2 +- client/src/Pages/Settings/index.jsx | 1 - .../Monitors/Components/StatusBoxes/index.jsx | 3 +++ .../Components/StatusBoxes/statusBox.jsx | 12 ++++++------ client/src/Utils/NetworkService.js | 1 - client/src/Utils/i18n.js | 12 ++---------- client/src/main.jsx | 3 ++- 12 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 client/src/Components/I18nLoader/index.jsx diff --git a/client/src/Components/I18nLoader/index.jsx b/client/src/Components/I18nLoader/index.jsx new file mode 100644 index 000000000..7c3cbfb85 --- /dev/null +++ b/client/src/Components/I18nLoader/index.jsx @@ -0,0 +1,16 @@ +import i18n from "../../Utils/i18n"; +import { useSelector } from "react-redux"; +import { useEffect } from "react"; +const I18nLoader = () => { + const language = useSelector((state) => state.ui.language); + + useEffect(() => { + if (language && i18n.language !== language) { + i18n.changeLanguage(language); + } + }, [language]); + + return null; +}; + +export default I18nLoader; diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/Inputs/Select/index.jsx index 927247d77..a31fedf66 100644 --- a/client/src/Components/Inputs/Select/index.jsx +++ b/client/src/Components/Inputs/Select/index.jsx @@ -164,7 +164,8 @@ Select.propTypes = { value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, items: PropTypes.arrayOf( PropTypes.shape({ - _id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + _id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]) + .isRequired, name: PropTypes.string.isRequired, }) ).isRequired, diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx index 66faf78ac..284d0db05 100644 --- a/client/src/Components/LanguageSelector.jsx +++ b/client/src/Components/LanguageSelector.jsx @@ -3,15 +3,17 @@ import { Box, MenuItem, Select, Stack } from "@mui/material"; import { useTheme } from "@emotion/react"; import "flag-icons/css/flag-icons.min.css"; import { useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; +import { setLanguage } from "../Features/UI/uiSlice"; const LanguageSelector = () => { const { i18n } = useTranslation(); const theme = useTheme(); const { language } = useSelector((state) => state.ui); - + const dispatch = useDispatch(); const handleChange = (event) => { const newLang = event.target.value; - i18n.changeLanguage(newLang); + dispatch(setLanguage(newLang)); }; const languages = Object.keys(i18n.options.resources || {}); @@ -32,7 +34,6 @@ const LanguageSelector = () => { } if (parsedLang.includes("-")) { parsedLang = parsedLang.split("-")[1].toLowerCase(); - console.log("parsedLang", parsedLang); } const flag = parsedLang ? `fi fi-${parsedLang}` : null; diff --git a/client/src/Features/Settings/settingsSlice.js b/client/src/Features/Settings/settingsSlice.js index 02724af02..d0725627a 100644 --- a/client/src/Features/Settings/settingsSlice.js +++ b/client/src/Features/Settings/settingsSlice.js @@ -5,7 +5,6 @@ const initialState = { isLoading: false, apiBaseUrl: "", logLevel: "debug", - language: "gb", pagespeedApiKey: "", }; diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index bd6e92804..3addfbb7a 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -24,7 +24,7 @@ const initialState = { greeting: { index: 0, lastUpdate: null }, timezone: "America/Toronto", distributedUptimeEnabled: false, - language: "gb", + language: "en", starPromptOpen: true, }; @@ -57,7 +57,7 @@ const uiSlice = createSlice({ setTimezone(state, action) { state.timezone = action.payload.timezone; }, - setLanguage(state, action) { + setLanguage: (state, action) => { state.language = action.payload; }, setStarPromptOpen: (state, action) => { diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/Settings/SettingsURL.jsx index 64804c903..4b4cf7683 100644 --- a/client/src/Pages/Settings/SettingsURL.jsx +++ b/client/src/Pages/Settings/SettingsURL.jsx @@ -22,7 +22,7 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL }) => {