diff --git a/.gitignore b/.gitignore index 2299e795a..f6e292bc9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .VSCodeCounter *.sh mongo -node_modules/ \ No newline at end of file +node_modules/ +docs/architecture \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 84fc4c3a6..038814543 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2472,11 +2472,13 @@ } }, "node_modules/axios": { - "version": "1.13.4", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, diff --git a/client/src/App.tsx b/client/src/App.tsx index 0a6b364f8..a8a25669e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, type CSSProperties } from "react"; +import { type CSSProperties } from "react"; import { useSelector } from "react-redux"; import "react-toastify/dist/ReactToastify.css"; import { ToastContainer } from "react-toastify"; @@ -6,7 +6,6 @@ import { ThemeProvider } from "@emotion/react"; import lightTheme from "./Utils/Theme/lightTheme"; import darkTheme from "./Utils/Theme/darkTheme"; import { CssBaseline, GlobalStyles } from "@mui/material"; -import { logger } from "./Utils/Logger"; // Import the logger import { Routes } from "./Routes"; import AppLayout from "@/Components/v2/layout/AppLayout"; import type { RootState } from "@/Types/state"; @@ -14,13 +13,6 @@ import type { RootState } from "@/Types/state"; function App() { const mode = useSelector((state: RootState) => state.ui.mode); - // Cleanup - useEffect(() => { - return () => { - logger.cleanup(); - }; - }, []); - const theme = mode === "light" ? lightTheme : darkTheme; return ( diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx index a83ebcf5d..1be76988b 100644 --- a/client/src/Components/v2/design-elements/StatusBox.tsx +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -2,7 +2,6 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import { BaseBox } from "@/Components/v2/design-elements"; -import Background from "@/assets/Images/background-grid.svg?react"; import { useTranslation } from "react-i18next"; import type { SxProps } from "@mui/material"; @@ -18,6 +17,7 @@ export const BGBox = ({ children, sx }: StatusBoxProps) => { return ( { > - - + top={0} + left={0} + right={0} + bottom={0} + sx={{ + pointerEvents: "none", + backgroundImage: ` + linear-gradient(${theme.palette.divider} 1px, transparent 1px), + linear-gradient(90deg, ${theme.palette.divider} 1px, transparent 1px) + `, + backgroundSize: "24px 24px", + maskImage: + "linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)", + WebkitMaskImage: + "linear-gradient(135deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 30%, rgba(0,0,0,0.4) 100%)", + }} + /> {children} ); diff --git a/client/src/Components/v1/I18nLoader/index.jsx b/client/src/Components/v2/i18nLoader/index.tsx similarity index 63% rename from client/src/Components/v1/I18nLoader/index.jsx rename to client/src/Components/v2/i18nLoader/index.tsx index 676e63d7b..f0555afed 100644 --- a/client/src/Components/v1/I18nLoader/index.jsx +++ b/client/src/Components/v2/i18nLoader/index.tsx @@ -1,8 +1,9 @@ -import i18n from "../../../Utils/i18n.js"; +import i18n from "@/Utils/i18n.js"; import { useSelector } from "react-redux"; import { useEffect } from "react"; +import type { RootState } from "@/store"; const I18nLoader = () => { - const language = useSelector((state) => state.ui.language ?? "en"); + const language = useSelector((state: RootState) => state.ui.language ?? "en"); useEffect(() => { if (language && i18n.language !== language) { diff --git a/client/src/Components/v2/inputs/ImageUpload.tsx b/client/src/Components/v2/inputs/ImageUpload.tsx index 6c9fc1783..0d0b77b40 100644 --- a/client/src/Components/v2/inputs/ImageUpload.tsx +++ b/client/src/Components/v2/inputs/ImageUpload.tsx @@ -38,11 +38,11 @@ export const ImageUpload = ({ const isValidType = accept.some((type) => file.type.includes(type)); const isValidSize = file.size <= maxSize; if (!isValidType) { - setLocalError(t("common.errors.invalidFileFormat")); + setLocalError(t("components.imageUpload.errors.invalidFileFormat")); return; } if (!isValidSize) { - setLocalError(t("common.errors.invalidFileSize")); + setLocalError(t("components.imageUpload.errors.invalidFileSize")); return; } setLocalError(null); @@ -137,15 +137,15 @@ export const ImageUpload = ({ color="primary" fontWeight={500} > - {t("common.imageUpload.clickToUpload")} + {t("components.imageUpload.clickToUpload")} {" "} - {t("common.imageUpload.orDragAndDrop")} + {t("components.imageUpload.orDragAndDrop")} - {accept.join(", ").toUpperCase()} • {t("common.imageUpload.maxSize")}{" "} + {accept.join(", ").toUpperCase()} • {t("components.imageUpload.maxSize")}{" "} {Math.round(maxSize / 1024 / 1024)}MB diff --git a/client/src/Components/v2/inputs/Slider.tsx b/client/src/Components/v2/inputs/Slider.tsx index 97a16ac2c..83037ab54 100644 --- a/client/src/Components/v2/inputs/Slider.tsx +++ b/client/src/Components/v2/inputs/Slider.tsx @@ -41,7 +41,10 @@ export const SliderInput = forwardRef( "& .MuiSlider-thumb": { backgroundColor: "#fff", "&:hover, &.Mui-focusVisible": { - boxShadow: `0 0 0 8px ${theme.palette.primary.main}20`, + boxShadow: "none", + }, + "&:active": { + boxShadow: "none", }, }, "& .MuiSlider-valueLabel": { diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx index 1c25c339e..d3aa62a69 100644 --- a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -3,7 +3,7 @@ import Typography from "@mui/material/Typography"; import { BaseChart } from "@/Components/v2/design-elements"; import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "@/Utils/timeUtilsLegacy"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; import { useTranslation } from "react-i18next"; import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts"; import { getResponseTimeColor } from "@/Utils/MonitorUtils"; diff --git a/client/src/Features/UI/uiSlice.ts b/client/src/Features/UI/uiSlice.ts index 0675784b9..84e8248db 100644 --- a/client/src/Features/UI/uiSlice.ts +++ b/client/src/Features/UI/uiSlice.ts @@ -12,11 +12,6 @@ interface SidebarState { collapsed: boolean; } -interface GreetingState { - index: number; - lastUpdate: string | null; -} - interface UIState { monitors: TableState; team: TableState; @@ -26,7 +21,6 @@ interface UIState { sidebar: SidebarState; mode: ThemeMode; showURL: boolean; - greeting: GreetingState; timezone: string; distributedUptimeEnabled: boolean; language: string; @@ -60,7 +54,6 @@ const initialState: UIState = { }, mode: initialMode, showURL: false, - greeting: { index: 0, lastUpdate: null }, timezone: "America/Toronto", distributedUptimeEnabled: false, language: "en", @@ -94,13 +87,7 @@ const uiSlice = createSlice({ setShowURL: (state, action: PayloadAction) => { state.showURL = action.payload; }, - setGreeting: ( - state, - action: PayloadAction<{ index: number; lastUpdate: string | null }> - ) => { - state.greeting.index = action.payload.index; - state.greeting.lastUpdate = action.payload.lastUpdate; - }, + setTimezone: (state, action: PayloadAction<{ timezone: string }>) => { state.timezone = action.payload.timezone; }, @@ -124,7 +111,6 @@ export const { setCollapsed, setMode, setShowURL, - setGreeting, setTimezone, setDistributedUptimeEnabled, setLanguage, diff --git a/client/src/Pages/Auth/SetNewPassword/index.tsx b/client/src/Pages/Auth/SetNewPassword/index.tsx index 09bd565f1..2e99be254 100644 --- a/client/src/Pages/Auth/SetNewPassword/index.tsx +++ b/client/src/Pages/Auth/SetNewPassword/index.tsx @@ -97,27 +97,27 @@ const SetNewPasswordPage = () => { /> @@ -127,7 +127,7 @@ const SetNewPasswordPage = () => { fullWidth loading={loading} > - {t("auth.forgotPassword.buttons.resetPassword")} + {t("common.buttons.resetPassword")} { render={({ field, fieldState }) => ( field.onChange(Number(e.target.value) || 0)} + value={field.value === 0 ? "" : field.value} + onChange={(e) => { + const val = e.target.value; + field.onChange(val === "" ? 0 : Number(val)); + }} type="number" - fieldLabel={t("portToMonitor")} - placeholder="5173" + fieldLabel={t("pages.createMonitor.form.general.option.port.label")} + placeholder={t( + "pages.createMonitor.form.general.option.port.placeholder" + )} fullWidth error={!!fieldState.error} helperText={fieldState.error?.message ?? ""} @@ -351,10 +356,12 @@ const CreateMonitorPage = () => { )} /> } /> + {/* Alert Thresholds - only for hardware type */} + {generalSettingsConfig.showSecret && ( + + ( + `${value}%`} + /> + )} + /> + ( + `${value}%`} + /> + )} + /> + ( + `${value}%`} + /> + )} + /> + ( + `${value}°C`} + /> + )} + /> + + } + /> + )} + { "pages.createMonitor.form.advanced.option.matchMethod.label" )} > - {t("matchMethodOptions.equal")} - - {t("matchMethodOptions.include")} + + {t( + "pages.createMonitor.form.advanced.option.matchMethod.equal" + )} + + + {t( + "pages.createMonitor.form.advanced.option.matchMethod.include" + )} + + + {t( + "pages.createMonitor.form.advanced.option.matchMethod.regex" + )} - {t("matchMethodOptions.regex")} )} /> diff --git a/client/src/Pages/Incidents/Components/DialogResolution.tsx b/client/src/Pages/Incidents/Components/DialogResolution.tsx index 648af52a9..a102b1de3 100644 --- a/client/src/Pages/Incidents/Components/DialogResolution.tsx +++ b/client/src/Pages/Incidents/Components/DialogResolution.tsx @@ -46,10 +46,9 @@ export const DialogResolution = ({ return ( setComment(e.target.value)} fullWidth diff --git a/client/src/Pages/Incidents/Components/IncidentTable.tsx b/client/src/Pages/Incidents/Components/IncidentTable.tsx index 121f33123..a9b4bc104 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable.tsx +++ b/client/src/Pages/Incidents/Components/IncidentTable.tsx @@ -9,13 +9,13 @@ import type { Monitor } from "@/Types/Monitor"; import type { ActionMenuItem } from "@/Components/v2/actions-menu"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { TypeToPathMap } from "@/Utils/monitorUtilsLegacy.js"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; import Typography from "@mui/material/Typography"; import { useTheme } from "@mui/material/styles"; import Box from "@mui/material/Box"; +import { getMonitorPath } from "@/Utils/MonitorUtils"; interface IncidentsTableProps { title?: string; @@ -62,7 +62,7 @@ export const IncidentsTable = ({ label: t("pages.incidents.table.actions.goToMonitor"), action: () => { if (monitor) { - const path = TypeToPathMap[monitor.type as keyof typeof TypeToPathMap]; + const path = getMonitorPath(monitor.type); if (path && monitor.id) { navigate(`/${path}/${monitor.id}`); } diff --git a/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx b/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx index 42378bb67..876969d3d 100644 --- a/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx +++ b/client/src/Pages/Maintenance/MaintenanceWindowTable.tsx @@ -196,13 +196,13 @@ export const MaintenanceWindowTable = ({ /> { setDeleteDialogOpen(false); setSelectedWindow(null); }} onConfirm={handleDelete} - confirmText={t("delete")} loading={deleteLoading} /> diff --git a/client/src/Pages/NotFound/index.tsx b/client/src/Pages/NotFound/index.tsx index 709f808be..868a1573a 100644 --- a/client/src/Pages/NotFound/index.tsx +++ b/client/src/Pages/NotFound/index.tsx @@ -9,8 +9,8 @@ import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; interface NotFoundProps { - title: string; - desc: string; + title?: string; + desc?: string; } const NotFoundPage = ({ title, desc }: NotFoundProps) => { diff --git a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx index d738671ac..0e8f444c9 100644 --- a/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx +++ b/client/src/Pages/StatusPage/Status/Components/HeaderStatusPageControls.tsx @@ -64,7 +64,7 @@ export const HeaderStatusPageControls = ({ }, }} > - {t("publicLink")} + {t("components.headerStatusPageControls.publicLink")} diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 1b509507e..b1826b867 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -96,7 +96,6 @@ const StatusPageView = () => { }} /> )} - {t("statusPageStatusServiceStatus")} { - const mode = useSelector((state) => state.ui.mode); + const mode = useSelector((state: RootState) => state.ui.mode); const v2theme = mode === "light" ? lightTheme : darkTheme; return ( @@ -390,12 +391,11 @@ const Routes = () => { /> - + } diff --git a/client/src/Utils/Logger.js b/client/src/Utils/Logger.js deleted file mode 100644 index 849e10854..000000000 --- a/client/src/Utils/Logger.js +++ /dev/null @@ -1,74 +0,0 @@ -const LOG_LEVEL = import.meta.env.VITE_APP_LOG_LEVEL || "debug"; -const NO_OP = () => {}; - -class Logger { - constructor() { - let logLevel = LOG_LEVEL; - this.updateLogLevel(logLevel); - // Defer store subscription to avoid circular dependency during HMR - setTimeout(() => { - try { - import("../store").then(({ default: store }) => { - if (store) { - this.unsubscribe = store.subscribe(() => { - logLevel = "debug"; - this.updateLogLevel(logLevel); - }); - } - }); - } catch (e) { - // Store not ready yet, ignore - } - }, 0); - } - - updateLogLevel(logLevel) { - if (logLevel === "none") { - this.info = NO_OP; - this.error = NO_OP; - this.warn = NO_OP; - this.log = NO_OP; - return; - } - - if (logLevel === "error") { - this.error = console.error.bind(console); - this.info = NO_OP; - this.warn = NO_OP; - this.log = NO_OP; - return; - } - - if (logLevel === "warn") { - this.error = console.error.bind(console); - this.warn = console.warn.bind(console); - this.info = NO_OP; - this.log = NO_OP; - return; - } - - if (logLevel === "info") { - this.error = console.error.bind(console); - this.warn = console.warn.bind(console); - this.info = console.info.bind(console); - this.log = NO_OP; - return; - } - - if (logLevel === "debug") { - this.error = console.error.bind(console); - this.warn = console.warn.bind(console); - this.info = console.info.bind(console); - this.log = console.log.bind(console); - return; - } - } - - cleanup() { - if (this.unsubscribe) { - this.unsubscribe(); - } - } -} - -export const logger = new Logger(); diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts index 703579fc6..02c291efc 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/MonitorUtils.ts @@ -1,4 +1,4 @@ -import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor"; +import type { MonitorStatus, MonitorType } from "@/Types/Monitor"; import type { PaletteKey } from "@/Utils/Theme/v2Theme"; import type { ValueType } from "@/Components/v2/design-elements/StatusLabel"; @@ -7,11 +7,11 @@ export const getMonitorPath = (type: MonitorType): string => { http: "uptime", port: "uptime", ping: "uptime", - hardware: "hardware", - pagespeed: "pagespeed", - docker: "docker", - game: "game-servers", + game: "uptime", unknown: "uptime", + docker: "uptime", + hardware: "infrastructure", + pagespeed: "pagespeed", }; return pathMap[type]; }; @@ -81,40 +81,3 @@ export const formatUrl = (url: string, maxLength: number = 55) => { ? `${strippedUrl.slice(0, maxLength)}…` : strippedUrl; }; - -export interface IStatusPageHeaderConfig { - paletteKey: PaletteKey; - message: string; -} -export const getStatusPageHeaderConfig = ( - monitors: Monitor[], - t: any -): IStatusPageHeaderConfig => { - if (!monitors || monitors.length === 0) { - return { paletteKey: "error", message: "No monitors available" }; - } - - const allUp = monitors.every((monitor) => monitor.status === "up"); - const anyDown = monitors.some((monitor) => monitor.status === "down"); - const allDown = monitors.every((monitor) => monitor.status === "down"); - - if (allUp) - return { - paletteKey: "success", - message: t("statusPage.details.statusHeader.allUp"), - }; - if (allDown) - return { - paletteKey: "error", - message: t("statusPage.details.statusHeader.allDown"), - }; - if (anyDown) - return { - paletteKey: "warning", - message: t("statusPage.details.statusHeader.anyDown"), - }; - return { - paletteKey: "warning", - message: t("statusPage.details.statusHeader.anyDown"), - }; -}; diff --git a/client/src/Utils/ReadMe.md b/client/src/Utils/ReadMe.md deleted file mode 100644 index 6488752d1..000000000 --- a/client/src/Utils/ReadMe.md +++ /dev/null @@ -1 +0,0 @@ -#Utils folder diff --git a/client/src/Utils/fileUtils.js b/client/src/Utils/fileUtils.js deleted file mode 100644 index 1c35738dd..000000000 --- a/client/src/Utils/fileUtils.js +++ /dev/null @@ -1,11 +0,0 @@ -export const formatBytes = (bytes) => { - if (bytes === 0) return "0 Bytes"; - const megabytes = bytes / (1024 * 1024); - return megabytes.toFixed(2) + " MB"; -}; - -export const checkImage = (url) => { - const img = new Image(); - img.src = url; - return img.naturalWidth !== 0; -}; diff --git a/client/src/Utils/greeting.jsx b/client/src/Utils/greeting.jsx deleted file mode 100644 index c4afbf88c..000000000 --- a/client/src/Utils/greeting.jsx +++ /dev/null @@ -1,197 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Typography } from "@mui/material"; -import { useDispatch, useSelector } from "react-redux"; -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { setGreeting } from "../Features/UI/uiSlice"; - -const early = [ - { - prepend: "Rise and shine", - append: "If you’re up this early, you might as well be a legend!", - emoji: "☕", - }, - { - prepend: "Good morning", - append: "The world’s still asleep, but you’re already awesome!", - emoji: "🦉", - }, - { - prepend: "Good morning", - append: "Are you a wizard? Only magical people are up at this hour!", - emoji: "🌄", - }, - { - prepend: "Up before the roosters", - append: "Ready to tackle the day before it even starts?", - emoji: "🐓", - }, - { - prepend: "Early bird special", - append: "Let’s get things done while everyone else is snoozing!", - emoji: "🌟", - }, -]; - -const morning = [ - { - prepend: "Good morning", - append: "Is it coffee o’clock yet, or should we start with high fives?", - emoji: "☕", - }, - { - prepend: "Morning", - append: "The sun is up, and so are you—time to be amazing!", - emoji: "🌞", - }, - { - prepend: "Good morning", - append: "Time to make today the best thing since sliced bread!", - emoji: "🥐", - }, - { - prepend: "Morning", - append: "Let’s kick off the day with more energy than a double espresso!", - emoji: "🚀", - }, - { - prepend: "Rise and shine", - append: "You’re about to make today so great, even Monday will be jealous!", - emoji: "🌟", - }, -]; - -const afternoon = [ - { - prepend: "Good afternoon", - append: "How about a break to celebrate how awesome you’re doing?", - emoji: "🥪", - }, - { - prepend: "Afternoon", - append: "If you’re still going strong, you’re officially a rockstar!", - emoji: "🌞", - }, - { - prepend: "Hey there", - append: "The afternoon is your playground—let’s make it epic!", - emoji: "🍕", - }, - { - prepend: "Good afternoon", - append: "Time to crush the rest of the day like a pro!", - emoji: "🏆", - }, - { - prepend: "Afternoon", - append: "Time to turn those afternoon slumps into afternoon triumphs!", - emoji: "🎉", - }, -]; - -const evening = [ - { - prepend: "Good evening", - append: "Time to wind down and think about how you crushed today!", - emoji: "🌇", - }, - { - prepend: "Evening", - append: "You’ve earned a break—let’s make the most of these evening vibes!", - emoji: "🍹", - }, - { - prepend: "Hey there", - append: "Time to relax and bask in the glow of your day’s awesomeness!", - emoji: "🌙", - }, - { - prepend: "Good evening", - append: "Ready to trade productivity for chill mode?", - emoji: "🛋️ ", - }, - { - prepend: "Evening", - append: "Let’s call it a day and toast to your success!", - emoji: "🕶️", - }, -]; - -/** - * Greeting component that displays a personalized greeting message - * based on the time of day and the user's first name. - * - * @component - * @example - * return ; - * - * @param {Object} props - * @param {string} props.type - The type of monitor to be displayed in the message - * @returns {JSX.Element} The rendered Greeting component - */ - -const Greeting = ({ type = "" }) => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const { firstName } = useSelector((state) => state.auth.user); - const index = useSelector((state) => state.ui.greeting?.index ?? 0); - const lastUpdate = useSelector((state) => state.ui.greeting?.lastUpdate ?? null); - - const now = new Date(); - const hour = now.getHours(); - - useEffect(() => { - const hourDiff = lastUpdate ? hour - lastUpdate : null; - - if (!lastUpdate || hourDiff >= 1) { - let random = Math.floor(Math.random() * 5); - dispatch(setGreeting({ index: random, lastUpdate: hour })); - } - }, [dispatch, hour, lastUpdate]); - - let greetingArray = - hour < 6 ? early : hour < 12 ? morning : hour < 18 ? afternoon : evening; - const { prepend, append, emoji } = greetingArray[index]; - - return ( - - - - {t("greeting.prepend", { defaultValue: prepend })},{" "} - - - {firstName} {emoji} - - - - {t("greeting.append", { defaultValue: append })} —{" "} - {t("greeting.overview", { type: t(`menu.${type}`) })} - - - ); -}; - -Greeting.propTypes = { - type: PropTypes.string, -}; - -export default Greeting; diff --git a/client/src/Utils/i18n.js b/client/src/Utils/i18n.ts similarity index 55% rename from client/src/Utils/i18n.js rename to client/src/Utils/i18n.ts index 030091d48..db448a065 100644 --- a/client/src/Utils/i18n.js +++ b/client/src/Utils/i18n.ts @@ -1,16 +1,26 @@ import i18n from "i18next"; import { initReactI18next } from "react-i18next"; +import type { Resource } from "i18next"; const primaryLanguage = "en"; -// Load all translation files eagerly -const translations = import.meta.glob("../locales/*.json", { eager: true }); +interface TranslationModule { + default?: Record; + [key: string]: unknown; +} -const resources = {}; +// Load all translation files eagerly +const translations = import.meta.glob("../locales/*.json", { + eager: true, +}); + +const resources: Resource = {}; Object.keys(translations).forEach((path) => { - const langCode = path.match(/\/([^/]+)\.json$/)[1]; + const match = path.match(/\/([^/]+)\.json$/); + if (!match) return; + const langCode = match[1]; resources[langCode] = { - translation: translations[path].default || translations[path], + translation: translations[path].default ?? translations[path], }; }); diff --git a/client/src/Utils/monitorUtilsLegacy.js b/client/src/Utils/monitorUtilsLegacy.js deleted file mode 100644 index e3645b0d0..000000000 --- a/client/src/Utils/monitorUtilsLegacy.js +++ /dev/null @@ -1,47 +0,0 @@ -import { capitalizeFirstLetter } from "./stringUtils"; - -/** - * Helper function to get duration since last check or the last date checked - * @param {Array} checks Array of check objects. - * @param {boolean} duration Whether the function should return the duration since last checked or the date itself - * @returns {number} Timestamp of the most recent check. - */ -export const getLastChecked = (checks, duration = true) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - - // Data is sorted newest -> oldest, so newest check is the most recent - if (!duration) { - return new Date(checks[0].createdAt); - } - return new Date() - new Date(checks[0].createdAt); -}; - -export const parseDomainName = (url) => { - url = url.replace(/^https?:\/\//, ""); - // Remove leading/trailing dots - url = url.replace(/^\.+|\.+$/g, ""); - // Split by dots - const parts = url.split("."); - // Remove common prefixes and empty parts and exclude the last element of the array (the last element should be the TLD) - const cleanParts = parts.filter((part) => part !== "www" && part !== "").slice(0, -1); - // If there's more than one part, append the two words and capitalize the first letters (e.g. ["api", "test"] -> "Api Test") - const domainPart = - cleanParts.length > 1 - ? cleanParts.map((part) => capitalizeFirstLetter(part)).join(" ") - : capitalizeFirstLetter(cleanParts[0]); - - if (domainPart) return domainPart; - - return url; -}; - -export const TypeToPathMap = { - http: "uptime", - port: "uptime", - docker: "uptime", - ping: "uptime", - hardware: "infrastructure", - pagespeed: "pagespeed", -}; diff --git a/client/src/Utils/roleUtils.js b/client/src/Utils/roleUtils.js deleted file mode 100644 index f359246b2..000000000 --- a/client/src/Utils/roleUtils.js +++ /dev/null @@ -1,13 +0,0 @@ -export const ROLES = { - SUPERADMIN: "superadmin", - ADMIN: "admin", - USER: "user", - DEMO: "demo", -}; - -export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; - -export const EDITABLE_ROLES = [ - { role: ROLES.ADMIN, _id: ROLES.ADMIN }, - { role: ROLES.USER, _id: ROLES.USER }, -]; diff --git a/client/src/Utils/stringUtils.js b/client/src/Utils/stringUtils.js deleted file mode 100644 index bb62d0169..000000000 --- a/client/src/Utils/stringUtils.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Helper function to get first letter capitalized string - * @param {string} str String whose first letter is to be capitalized - * @returns A string with first letter capitalized - */ -export const capitalizeFirstLetter = (str) => { - if (str === null || str === undefined) { - return ""; - } - if (typeof str !== "string") { - throw new TypeError("Input must be a string"); - } - if (str.length === 0) { - return ""; - } - return str.charAt(0).toUpperCase() + str.slice(1); -}; - -/** - * Helper function to get first letter as a lower case string - * @param {string} str String whose first letter is to be lower cased - * @returns A string with first letter lower cased - */ - -export const toLowerCaseFirstLetter = (str) => { - if (str === null || str === undefined) { - return ""; - } - if (typeof str !== "string") { - throw new TypeError("Input must be a string"); - } - if (str.length === 0) { - return ""; - } - return str.charAt(0).toLowerCase() + str.slice(1); -}; - -/** - * Checks if a string is null, undefined, or empty (including strings with only whitespace). - * @param {string} str - The string to check. - * @returns {boolean} - Returns true if the string is null, undefined, or empty. - */ -export const isEmpty = (str) => { - // Check if string is null, undefined, or empty (including whitespace only) - return str === null || typeof str === "undefined" || str.trim().length === 0; -}; diff --git a/client/src/Utils/timeUtilsLegacy.js b/client/src/Utils/timeUtilsLegacy.js deleted file mode 100644 index 365572f6b..000000000 --- a/client/src/Utils/timeUtilsLegacy.js +++ /dev/null @@ -1,167 +0,0 @@ -import dayjs from "dayjs"; -import duration from "dayjs/plugin/duration"; -import utc from "dayjs/plugin/utc"; -import timezone from "dayjs/plugin/timezone"; -import customParseFormat from "dayjs/plugin/customParseFormat"; - -dayjs.extend(utc); -dayjs.extend(timezone); -dayjs.extend(customParseFormat); -dayjs.extend(duration); - -export const MS_PER_SECOND = 1000; -export const MS_PER_MINUTE = 60 * MS_PER_SECOND; -export const MS_PER_HOUR = 60 * MS_PER_MINUTE; -export const MS_PER_DAY = 24 * MS_PER_HOUR; -export const MS_PER_WEEK = MS_PER_DAY * 7; - -export const formatDuration = (ms) => { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - let dateStr = ""; - - days && (dateStr += `${days}d `); - hours && (dateStr += `${hours % 24}h `); - minutes && (dateStr += `${minutes % 60}m `); - seconds && (dateStr += `${seconds % 60}s `); - - dateStr === "" && (dateStr = "0s"); - - return dateStr; -}; - -export const formatDurationRounded = (ms) => { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - let time = ""; - if (days > 0) { - time += `${days} day${days !== 1 ? "s" : ""}`; - return time; - } - if (hours > 0) { - time += `${hours} hour${hours !== 1 ? "s" : ""}`; - return time; - } - if (minutes > 0) { - time += `${minutes} minute${minutes !== 1 ? "s" : ""}`; - return time; - } - if (seconds > 0) { - time += `${seconds} second${seconds !== 1 ? "s" : ""}`; - return time; - } - - return time; -}; - -export const formatDurationSplit = (ms) => { - const seconds = Math.floor(ms / 1000); - const minutes = Math.floor(seconds / 60); - const hours = Math.floor(minutes / 60); - const days = Math.floor(hours / 24); - - return days > 0 - ? { time: days, format: days === 1 ? "day" : "days" } - : hours > 0 - ? { time: hours, format: hours === 1 ? "hour" : "hours" } - : minutes > 0 - ? { time: minutes, format: minutes === 1 ? "minute" : "minutes" } - : seconds > 0 - ? { time: seconds, format: seconds === 1 ? "second" : "seconds" } - : { time: 0, format: "seconds" }; -}; - -export const getHumanReadableDuration = (ms) => { - const durationObj = dayjs.duration(ms); - - const parts = { - days: Math.floor(durationObj.asDays()), - hours: durationObj.hours(), - minutes: durationObj.minutes(), - seconds: durationObj.seconds(), - milliseconds: durationObj.milliseconds(), - }; - - const result = []; - - if (parts.days > 0) { - result.push(`${parts.days}d`); - } - if (parts.hours > 0) { - result.push(`${parts.hours}h`); - } - if (result.length < 2 && parts.minutes > 0) { - result.push(`${parts.minutes}m`); - } - if (result.length < 2 && parts.seconds > 0) { - result.push(`${parts.seconds}s`); - } - if (result.length < 2 && parts.milliseconds > 0 && parts.seconds < 1) { - result.push(`${parts.milliseconds.toFixed(2)}ms`); - } - - if (result.length === 0) { - // fallback for durations < 1s - return "0s"; - } - - return result.join(" "); -}; - -export const formatDate = (date, customOptions) => { - const options = { - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - hour12: true, - ...customOptions, - }; - - // Return the date using the specified options - return date - .toLocaleString("en-US", options) - .replace(/\b(AM|PM)\b/g, (match) => match.toLowerCase()); -}; - -export const formatDateWithTz = (timestamp, format, timezone) => { - const formattedDate = dayjs(timestamp).tz(timezone).format(format); - return formattedDate; -}; - -export const tickDateFormatLookup = (range) => { - switch (range) { - case "recent": - return "h:mm A"; - case "day": - return "h A"; - case "week": - return "MMM D"; - case "month": - return "MMM D"; - default: - return "MMM D, h A"; - } -}; - -export const tooltipDateFormatLookup = (range) => { - switch (range) { - case "recent": - return "MMM D, h:mm A"; - case "day": - return "MMM D, h:mm A"; - case "week": - return "ddd, MMM D"; - case "month": - return "ddd, MMM D"; - default: - return "MMM D, h:mm A"; - } -}; diff --git a/client/src/Utils/utils.js b/client/src/Utils/utils.js deleted file mode 100644 index 81ec44b06..000000000 --- a/client/src/Utils/utils.js +++ /dev/null @@ -1,15 +0,0 @@ -export const safelyParseFloat = (value) => { - const parsedValue = parseFloat(value); - if (isNaN(parsedValue)) { - return 0; - } - return parsedValue; -}; - -export const formatMonitorUrl = (url, maxLength = 55) => { - if (!url) return ""; - const strippedUrl = url.replace(/^https?:\/\//, ""); - return strippedUrl.length > maxLength - ? `${strippedUrl.slice(0, maxLength)}…` - : strippedUrl; -}; diff --git a/client/src/Validation/error.js b/client/src/Validation/error.js deleted file mode 100644 index 5738edf40..000000000 --- a/client/src/Validation/error.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Update errors if passed id matches the error.details[0].path, otherwise remove - * the error for the id - * @param {*} prev Previous errors * - * @param {*} id ID of the field whose error is to be either updated or removed - * @param {*} error the error object - * @returns the Update Errors with the specific field with id being either removed or updated - */ - -const buildErrors = (prev, id, error) => { - const updatedErrors = { ...prev }; - if (error && id == error.details[0].path) { - updatedErrors[id] = error.details[0].message ?? "Validation error"; - } else { - delete updatedErrors[id]; - } - return updatedErrors; -}; - -/** - * Processes Joi validation errors and returns a filtered object of error messages for fields that have been touched. - * - * @param {Object} validation - The Joi validation result object. - * @param {Object} validation.error - The error property of the validation result containing details of validation failures. - * @param {Object[]} validation.error.details - An array of error details from the Joi validation. Each item contains information about the path and the message. - * @param {Object} touchedErrors - An object representing which fields have been interacted with. Keys are field IDs (field names), and values are booleans indicating whether the field has been touched. - * @returns {Object} - An object where keys are the field IDs (if they exist in `touchedErrors` and are in the error details) and values are their corresponding error messages. - */ -const getTouchedFieldErrors = (validation, touchedErrors) => { - let newErrors = {}; - - if (validation?.error) { - newErrors = validation.error.details.reduce((errors, detail) => { - const fieldId = detail.path[0]; - if (touchedErrors[fieldId] && !(fieldId in errors)) { - errors[fieldId] = detail.message; - } - return errors; - }, {}); - } - - return newErrors; -}; -/** - * - * @param {*} form The form object of the submitted form data - * @param {*} validation The Joi validation rules - * @param {*} setErrors The function used to set the local errors - * @returns true if there is no error or false if there is error after validating the form - * the error will be reset to {} if returns false; otherwise the errors object will be set with - * the new value - */ -const hasValidationErrors = (form, validation, setErrors) => { - const { error } = validation.validate(form, { - abortEarly: false, - }); - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - if ( - ![ - "clientHost", - "refreshTokenSecret", - "dbConnectionString", - "refreshTokenTTL", - "jwtTTL", - "notify-email-list", - "_id", - "__v", - "createdAt", - "updatedAt", - ].includes(err.path[0]) - ) { - newErrors[err.path[0]] = err.message ?? "Validation error"; - } - // Handle conditionally usage number required cases - if (!form.cpu || form.usage_cpu) { - newErrors["usage_cpu"] = null; - } - if (!form.memory || form.usage_memory) { - newErrors["usage_memory"] = null; - } - if (!form.disk || form.usage_disk) { - newErrors["usage_disk"] = null; - } - if (!form.temperature || form.usage_temperature) { - newErrors["usage_temperature"] = null; - } - }); - if (Object.values(newErrors).some((v) => v)) { - setErrors(newErrors); - return true; - } else { - setErrors({}); - return false; - } - } - setErrors({}); - return false; -}; -export { buildErrors, hasValidationErrors, getTouchedFieldErrors }; diff --git a/client/src/Validation/login.ts b/client/src/Validation/login.ts index 03eb8d4e2..f14463734 100644 --- a/client/src/Validation/login.ts +++ b/client/src/Validation/login.ts @@ -2,11 +2,10 @@ import { z } from "zod"; export const loginSchema = z.object({ email: z - .string() - .min(1, "auth.common.inputs.email.errors.empty") - .email("auth.common.inputs.email.errors.invalid") + .email("Please enter a valid email address") + .min(1, "Please enter your email address") .transform((val) => val.toLowerCase().trim()), - password: z.string().min(1, "auth.common.inputs.password.errors.empty"), + password: z.string().min(1, "Please enter your password"), }); export type LoginFormData = z.infer; diff --git a/client/src/Validation/recovery.ts b/client/src/Validation/recovery.ts index 356f28738..4a244ed66 100644 --- a/client/src/Validation/recovery.ts +++ b/client/src/Validation/recovery.ts @@ -2,9 +2,8 @@ import { z } from "zod"; export const recoverySchema = z.object({ email: z - .string() - .min(1, "auth.common.inputs.email.errors.empty") - .email("auth.common.inputs.email.errors.invalid") + .email("Please enter a valid email address") + .min(1, "Please enter your email address") .transform((val) => val.toLowerCase().trim()), }); diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 27d585074..fa92f2e99 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -1,6 +1,18 @@ import joi from "joi"; import dayjs from "dayjs"; -import { ROLES } from "../Utils/roleUtils"; +export const ROLES = { + SUPERADMIN: "superadmin", + ADMIN: "admin", + USER: "user", + DEMO: "demo", +}; + +export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; + +export const EDITABLE_ROLES = [ + { role: ROLES.ADMIN, _id: ROLES.ADMIN }, + { role: ROLES.USER, _id: ROLES.USER }, +]; const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number."; diff --git a/client/src/assets/Images/Google.png b/client/src/assets/Images/Google.png deleted file mode 100644 index 490f4a201..000000000 Binary files a/client/src/assets/Images/Google.png and /dev/null differ diff --git a/client/src/assets/Images/avatar_placeholder.png b/client/src/assets/Images/avatar_placeholder.png deleted file mode 100644 index 38e896811..000000000 Binary files a/client/src/assets/Images/avatar_placeholder.png and /dev/null differ diff --git a/client/src/assets/Images/background-grid.svg b/client/src/assets/Images/background-grid.svg deleted file mode 100644 index 988f1da77..000000000 --- a/client/src/assets/Images/background-grid.svg +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/create-placeholder-dark.svg b/client/src/assets/Images/create-placeholder-dark.svg deleted file mode 100644 index 2dd68750b..000000000 --- a/client/src/assets/Images/create-placeholder-dark.svg +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/create-placeholder.svg b/client/src/assets/Images/create-placeholder.svg deleted file mode 100644 index bdd9ef945..000000000 --- a/client/src/assets/Images/create-placeholder.svg +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/data_placeholder.svg b/client/src/assets/Images/data_placeholder.svg deleted file mode 100644 index bf840b95f..000000000 --- a/client/src/assets/Images/data_placeholder.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/data_placeholder_dark.svg b/client/src/assets/Images/data_placeholder_dark.svg deleted file mode 100644 index 7e2a811d5..000000000 --- a/client/src/assets/Images/data_placeholder_dark.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/jupiter_logo_banner_dark.svg b/client/src/assets/Images/jupiter_logo_banner_dark.svg deleted file mode 100644 index 3d3e23b14..000000000 --- a/client/src/assets/Images/jupiter_logo_banner_dark.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/jupiter_logo_banner_light.svg b/client/src/assets/Images/jupiter_logo_banner_light.svg deleted file mode 100644 index 4744e0bcb..000000000 --- a/client/src/assets/Images/jupiter_logo_banner_light.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/logo_placeholder.svg b/client/src/assets/Images/logo_placeholder.svg deleted file mode 100644 index 82f307b2d..000000000 --- a/client/src/assets/Images/logo_placeholder.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/client/src/assets/Images/solana_logo_banner_dark.svg b/client/src/assets/Images/solana_logo_banner_dark.svg deleted file mode 100644 index 08ec87cb1..000000000 --- a/client/src/assets/Images/solana_logo_banner_dark.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/Images/solana_logo_banner_light.svg b/client/src/assets/Images/solana_logo_banner_light.svg deleted file mode 100644 index 774018a6a..000000000 --- a/client/src/assets/Images/solana_logo_banner_light.svg +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/client/src/assets/icons/discord-icon.svg b/client/src/assets/icons/discord-icon.svg deleted file mode 100644 index c03e8e127..000000000 --- a/client/src/assets/icons/discord-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/client/src/assets/icons/slack-icon.svg b/client/src/assets/icons/slack-icon.svg deleted file mode 100644 index ff256fd4d..000000000 --- a/client/src/assets/icons/slack-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/client/src/assets/icons/upt_logo.png b/client/src/assets/icons/upt_logo.png deleted file mode 100644 index d443d0897..000000000 Binary files a/client/src/assets/icons/upt_logo.png and /dev/null differ diff --git a/client/src/assets/icons/zapier-icon.svg b/client/src/assets/icons/zapier-icon.svg deleted file mode 100644 index 6ee0b5eca..000000000 --- a/client/src/assets/icons/zapier-icon.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 7eb833308..8e18d8e2a 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1,189 +1,4 @@ { - "aboutus": "About Us", - "access": "Access", - "actions": "Actions", - "add": "Add", - "addMonitors": "Add monitors", - "advancedMatching": "Advanced matching", - "area": "Area", - "auth": { - "common": { - "errors": { - "validation": "Error validating data." - }, - "fields": { - "role": { - "errors": { - "min": "At least one role is required" - } - } - }, - "inputs": { - "email": { - "errors": { - "empty": "To continue, please enter your email address", - "invalid": "Please recheck validity of entered email address" - }, - "label": "Email", - "placeholder": "jordan.ellis@domain.com" - }, - "firstName": { - "errors": { - "empty": "Please enter your name", - "length": "Name must be less than 50 characters", - "pattern": "Name must contain only letters, spaces, apostrophes, or hyphens" - }, - "label": "Name", - "placeholder": "Jordan" - }, - "lastName": { - "errors": { - "empty": "Please enter your surname", - "length": "Surname must be less than 50 characters", - "pattern": "Surname must contain only letters, spaces, apostrophes, or hyphens" - }, - "label": "Surname", - "placeholder": "Ellis" - }, - "password": { - "errors": { - "empty": "Please enter your password", - "length": "Password must be at least 8 characters long", - "lowercase": "Password must contain at least 1 lowercase letter", - "number": "Password must contain at least 1 number", - "special": "Password must contain at least 1 special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)", - "uppercase": "Password must contain at least 1 uppercase letter" - }, - "label": "Password", - "rules": { - "length": { - "beginning": "Must be at least", - "highlighted": "8 characters long" - }, - "lowercase": { - "beginning": "Must contain at least", - "highlighted": "one lower character" - }, - "match": { - "beginning": "Passwords", - "highlighted": "must match" - }, - "number": { - "beginning": "Must contain at least", - "highlighted": "one number" - }, - "special": { - "beginning": "Must contain at least", - "highlighted": "one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" - }, - "uppercase": { - "beginning": "Must contain at least", - "highlighted": "one upper character" - } - } - }, - "passwordConfirm": { - "errors": { - "different": "Entered passwords don't match, so one of them is probably mistyped", - "empty": "Please enter your password again for confirmation (helps with typos)" - }, - "label": "Confirm password", - "placeholder": "Re-enter password to confirm" - } - }, - "navigation": { - "continue": "Continue" - }, - "passwordRules": { - "length": "Must be at least 8 characters long", - "special": "Must contain at least one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)", - "number": "Must contain at least one number", - "uppercase": "Must contain at least one upper character", - "lowercase": "Must contain at least one lower character", - "match": "Passwords must match" - } - }, - "forgotPassword": { - "buttons": { - "openEmail": "Open email app", - "resetPassword": "Reset password" - }, - "heading": "Forgot password?", - "links": { - "login": "Go back to Log In", - "resend": "Didn't receive the email? Click to resend" - }, - "subheadings": { - "stepFour": "Your password has been successfully reset. Click below to log in magically.", - "stepOne": "No worries, we'll send you reset instructions.", - "stepThree": "Your new password must be different from previously used passwords.", - "stepTwo": "We sent a password reset link to " - }, - "toasts": { - "emailNotFound": "Email not found.", - "error": "Unable to reset password. Please try again later or contact support.", - "redirect": "Redirecting in ...", - "sent": "Instructions sent to .", - "success": "Your password was reset successfully." - } - }, - "login": { - "errors": { - "password": { - "incorrect": "The password you provided does not match our records" - } - }, - "heading": "Log in to continue", - "links": { - "forgotPassword": "Forgot password?", - "forgotPasswordLink": "Reset password", - "register": "Do not have an account?", - "registerLink": "Register here" - }, - "toasts": { - "incorrectPassword": "Incorrect password", - "success": "Welcome back! You're successfully logged in." - }, - "welcome": "Welcome back to Checkmate!" - }, - "registration": { - "description": { - "superAdmin": "Create your super admin account to get started", - "user": "Sign up as a user and ask super admin for access to your monitors" - }, - "heading": { - "user": "Sign Up" - }, - "toasts": { - "success": "Welcome! Your account was created successfully." - }, - "welcome": "Welcome to Checkmate!" - } - }, - "avgCpuTemperature": "Average CPU Temperature", - "bar": "Bar", - "basicInformation": "Basic Information", - "bulkImport": { - "fallbackPage": "Import a file to upload a list of servers in bulk", - "invalidFileType": "Invalid file type", - "noFileSelected": "No file selected", - "selectFile": "Select File", - "selectFileDescription": "You can download our or sample", - "selectFileTips": "Select CSV file to upload", - "title": "Bulk Import", - "uploadFailed": "Upload failed", - "uploadSuccess": "Monitors created successfully!" - }, - "bytesSent": "Bytes Sent", - "addMember": "Add member", - "cancel": "Cancel", - "checkHooks": { - "failureResolveOne": "Failed to resolve incident." - }, - "chooseGame": "Choose game", - "city": "CITY", - "ClickUpload": "Click to upload", - "close": "Close", "common": { "auth": { "roles": { @@ -222,7 +37,8 @@ "testNotifications": "Test notifications", "toggleTheme": "Toggles light & dark", "flushQueue": "Flush queue", - "notFound": "Go to the main dashboard" + "notFound": "Go to the main dashboard", + "resetPassword": "Reset password" }, "charts": { "labels": { @@ -290,28 +106,23 @@ "interval": "Interval", "active": "Active" } - }, - "toasts": { - "checkConnection": "Please check your connection", - "networkError": "Network error", - "unknownError": "Unknown error" - }, + } + }, + "components": { "imageUpload": { "clickToUpload": "Click to upload", "dragAndDrop": "or drag and drop", "supportedFormats": "Supported formats", "maxSize": "Max size", - "orDragAndDrop": "or drag and drop" + "orDragAndDrop": "or drag and drop", + "errors": { + "invalidFileSize": "File size is too large!", + "invalidFileFormat": "Unsupported file format!" + } + }, + "headerStatusPageControls": { + "publicLink": "Public link" }, - "errors": { - "invalidFileType": "Invalid file type", - "fileTooLarge": "File too large" - } - }, - "commonSave": "Save", - "commonSaving": "Saving...", - "companyName": "Company name", - "components": { "headerTimeRange": { "labels": { "day": "Day", @@ -376,229 +187,6 @@ "description": "See the latest releases and help grow the community on GitHub" } }, - "configure": "Configure", - "confirmPassword": "Confirm password", - "cores": "Cores", - "cpu": "CPU", - "cpuFrequency": "CPU Frequency", - "cpuLogical": "CPU (Logical)", - "cpuPhysical": "CPU (Physical)", - "cpuTemperature": "CPU Temperature", - "cpuUsage": "CPU usage", - "createA": "Create a", - "createMaintenance": "Create maintenance", - "createMaintenanceWindow": "Create maintenance window", - "createMonitor": "Create monitor", - "createNew": "Create new", - "delete": "Delete", - "generateToken": "Generate token", - "DeleteAccountButton": "Remove account", - "DeleteAccountTitle": "Remove account", - "DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.", - "DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.", - "deleteStatusPage": "Do you want to delete this status page?", - "deleteStatusPageConfirm": "Yes, delete status page", - "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", - "DeleteWarningTitle": "Really remove this account?", - "details": "Details", - "device": "Device", - "diagnosticsPage": { - "diagnosticDescription": "System diagnostics", - "gauges": { - "heapAllocationSubtitle": "% of available memory", - "heapAllocationTitle": "Heap allocation", - "heapUsageSubtitle": "% of available memory", - "heapUsageTitle": "Heap usage", - "heapUtilizationSubtitle": "% of allocated", - "heapUtilizationTitle": "Heap utilization", - "instantCpuUsageSubtitle": "% of 1s used by CPU", - "instantCpuUsageTitle": "Instant CPU usage" - }, - "stats": { - "eventLoopDelayTitle": "Event loop delay", - "osMemoryLimitTitle": "OS Memory Limit", - "totalHeapSizeTitle": "Total heap size", - "uptimeTitle": "Uptime", - "usedHeapSizeTitle": "Used heap size" - } - }, - "disk": "Disk", - "diskUsage": "Disk Usage", - "displayName": "Display name", - "download": "Download", - "DragandDrop": "drag and drop", - "duration": "Duration", - "edit": "Edit", - "editing": "Editing...", - "editMaintenance": "Edit maintenance", - "editUserPage": { - "form": { - "email": "Email", - "firstName": "First name", - "lastName": "Last name", - "role": "Roles", - "save": "Save" - }, - "table": { - "actionHeader": "Action", - "roleHeader": "Role" - }, - "title": "Edit user", - "toast": { - "successUserUpdate": "User updated successfully", - "validationErrors": "Validation errors" - } - }, - "EmailDescriptionText": "This is your current email address — it cannot be changed.", - "errorPages": { - "serverUnreachable": { - "alertBox": "Server Connection Error", - "description": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.", - "retryButton": { - "default": "Retry connection", - "processing": "Connecting..." - }, - "toasts": { - "reconnected": "Successfully reconnected to the server.", - "stillUnreachable": "Server is still unreachable. Please try again later." - } - } - }, - "errors": "Errors", - "expectedValue": "Expected value", - "failedToSendEmail": "Failed to send email", - "FirstName": "First name", - "frequency": "Frequency", - "friendlyNameInput": "Friendly name", - "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", - "gb": "GB", - "general": { - "noOptionsFound": "No {{unit}} found" - }, - "greeting": { - "append": "The afternoon is your playground—let's make it epic!", - "overview": "Here's an overview of your {{type}} monitors.", - "prepend": "Hey there" - }, - "high": "high", - "host": "Host", - "http": "HTTP", - "https": "HTTPS", - "incidentsPage": { - "resolveIncidentDialogCommentLabel": "Comment (optional)", - "resolveIncidentDialogCommentPlaceholder": "Add a comment about the resolution...", - "resolveIncidentDialogConfirm": "Resolve", - "resolveIncidentDialogTitle": "Resolve Incident" - }, - "integrations": "Integrations", - "integrationsDiscord": "Discord", - "integrationsDiscordInfo": "Connect with Discord and view incidents directly in a channel", - "integrationsPrism": "Connect Prism to your favorite service.", - "integrationsSlack": "Slack", - "integrationsSlackInfo": "Connect with Slack and see incidents in a channel", - "integrationsZapier": "Zapier", - "integrationsZapierInfo": "Send all incidents to Zapier, and then see them everywhere", - "invalidFileFormat": "Unsupported file format!", - "invalidFileSize": "File size is too large!", - "inviteNoTokenFound": "No invite token found", - "LastName": "Last name", - "logsPage": { - "logLevelSelect": { - "title": "Log level", - "values": { - "all": "All", - "debug": "Debug", - "error": "Error", - "info": "Info", - "warn": "Warn" - } - }, - "noLogs": "No logs found", - "table": { - "level": "Level", - "logs": "logs", - "message": "Message", - "method": "Method", - "service": "Service", - "timestamp": "Timestamp" - }, - "tabs": { - "diagnostics": "Diagnostics", - "logs": "Server logs", - "queue": "Job queue" - }, - "title": "Logs" - }, - "low": "low", - "maintenance": "maintenance", - "maintenanceRepeat": "Maintenance Repeat", - "maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?", - "maintenanceWindowDescription": "During maintenance windows, all monitoring is suspended for selected monitors. No network checks will be performed, preventing any status updates or notifications from being triggered. Your monitors will appear frozen at their last known status, and status pages will display a maintenance indicator. Once the maintenance window ends, monitoring automatically resumes, and alerts will trigger if issues are detected. Maintenance periods do not count against uptime calculations.", - "maintenanceWindowName": "Maintenance Window Name", - "matchMethod": "Match Method", - "matchMethodOptions": { - "equal": "Equal", - "include": "Include", - "regex": "Regex" - }, - "MaxSize": "Maximum Size", - "mb": "MB", - "mem": "Mem", - "memory": "Memory", - "memoryUsage": "Memory usage", - "menu": { - "changelog": "Changelog", - "checks": "Checks", - "discussions": "Discussions", - "docs": "Docs", - "incidents": "Incidents", - "inviteMember": "Invite member", - "infrastructure": "Infrastructure", - "logOut": "Log out", - "logs": "Logs", - "maintenance": "Maintenance", - "notifications": "Notifications", - "pagespeed": "Pagespeed", - "password": "Password", - "profile": "Profile", - "settings": "Settings", - "statusPages": "Status pages", - "support": "Support", - "team": "Team", - "uptime": "Uptime" - }, - "message": "Message", - "monitor": "monitor", - "monitorHooks": { - "failureAddDemoMonitors": "Failed to add demo monitors", - "successAddDemoMonitors": "Successfully added demo monitors" - }, - "monitors": "monitors", - "monitorsToApply": "Monitors to apply maintenance window to", - "ms": "ms", - "navControls": "Controls", - "network": "Network", - "nextWindow": "Next window", - "notFoundButton": "Go to the main dashboard", - "notifications": { - "fallback": { - "actionButton": "Create notification channel!", - "checks": [ - "Alert teams about downtime or performance issues", - "Let engineers know when incidents happen", - "Keep administrators informed of system changes" - ], - "title": "A notification channel is used to:" - }, - "fetch": { - "failed": "Failed to fetch notifications" - }, - "test": { - "success": "Test notification sent successfully" - } - }, - "now": "Now", - "os": "OS", "pages": { "notFound": { "title": "Oh no! You dropped your sushi!", @@ -681,7 +269,32 @@ }, "auth": { "common": { - "passwordRules": {}, + "passwordRules": { + "length": { + "beginning": "Must be at least", + "highlighted": "8 characters long" + }, + "lowercase": { + "beginning": "Must contain at least", + "highlighted": "one lower character" + }, + "match": { + "beginning": "Passwords", + "highlighted": "must match" + }, + "number": { + "beginning": "Must contain at least", + "highlighted": "one number" + }, + "special": { + "beginning": "Must contain at least", + "highlighted": "one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" + }, + "uppercase": { + "beginning": "Must contain at least", + "highlighted": "one upper character" + } + }, "form": { "option": { "email": { @@ -798,7 +411,10 @@ "label": "JSONPath expression" }, "matchMethod": { - "label": "Match method" + "label": "Match method", + "equal": "Equal", + "include": "Include", + "regex": "Regex" } }, "title": "Advanced settings" @@ -807,7 +423,19 @@ "description": "How often do you want to check the status of this monitor?", "option": { "frequency": { - "label": "Check frequency" + "label": "Check frequency", + "value": { + "fifteenMinutes": "15 minutes", + "fifteenSeconds": "15 seconds", + "fiveMinutes": "5 minutes", + "fourMinutes": "4 minutes", + "oneMinute": "1 minute", + "tenMinutes": "10 minutes", + "thirtyMinutes": "30 minutes", + "thirtySeconds": "30 seconds", + "threeMinutes": "3 minutes", + "twoMinutes": "2 minutes" + } } }, "title": "Check frequency" @@ -833,6 +461,14 @@ "url": { "label": "URL", "placeholder": "https://www.google.com" + }, + "game": { + "label": "Choose game", + "placeholder": "Select a game" + }, + "port": { + "label": "Port to monitor", + "placeholder": 80 } }, "title": "General settings", @@ -884,6 +520,24 @@ "optionPort": "Port", "optionPortDescription": "Monitor if a specific port on a server is open.", "title": "Type" + }, + "thresholds": { + "title": "Alert thresholds", + "description": "Define the thresholds at which alerts should be triggered for this hardware monitor.", + "option": { + "cpuThreshold": { + "label": "CPU alert threshold (%)" + }, + "memoryThreshold": { + "label": "Memory alert threshold (%)" + }, + "diskThreshold": { + "label": "Disk alert threshold (%)" + }, + "tempThreshold": { + "label": "Temperature alert threshold (°C)" + } + } } } }, @@ -897,6 +551,15 @@ }, "incidents": { "dialog": { + "resolveIncident": { + "title": "Resolve incident", + "option": { + "comment": { + "label": "Comment (optional)", + "placeholder": "Add a comment about the resolution..." + } + } + }, "details": { "analysis": "Incident analysis", "comment": "Comment:", @@ -1371,272 +1034,5 @@ "title": "An uptime monitor is used to:" } } - }, - "pageSpeedAddApiKey": "to add your API key.", - "pageSpeedLearnMoreLink": "Click here", - "pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.", - "passwordPanel": { - "confirmNewPassword": "Confirm new password", - "currentPassword": "Current password", - "enterCurrentPassword": "Enter your current password", - "enterNewPassword": "Enter your new password", - "newPassword": "New password", - "passwordChangedSuccess": "Your password was changed successfully.", - "passwordInputIncorrect": "Your password input was incorrect.", - "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)." - }, - "pause": "Pause", - "PhotoDescriptionText": "This photo will be displayed in your profile page.", - "portToMonitor": "Port to monitor", - "publicLink": "Public link", - "queuePage": { - "failedJobTable": { - "failCountHeader": "Fail count", - "failedAtHeader": "Last failed at", - "failReasonHeader": "Fail reason", - "monitorIdHeader": "Monitor ID", - "monitorUrlHeader": "Monitor URL", - "title": "Failed jobs" - }, - "flushButton": "Flush queue", - "jobTable": { - "activeHeader": "Active", - "failCountHeader": "Fail count", - "idHeader": "Monitor ID", - "intervalHeader": "Interval", - "lastFinishedAtHeader": "Last finished at", - "lastRunHeader": "Last run at", - "lastRunTookHeader": "Last run took", - "lockedAtHeader": "Locked at", - "runCountHeader": "Run count", - "title": "Jobs currently in queue", - "typeHeader": "Type", - "urlHeader": "URL" - }, - "metricsTable": { - "metricHeader": "Metric", - "title": "Queue metrics", - "valueHeader": "Value" - }, - "refreshButton": "Refresh" - }, - "rate": "Rate", - "remove": "Remove", - "repeat": "Repeat", - "reset": "Reset", - "response": "RESPONSE", - "responseTime": "Response time", - "resume": "Resume", - "roles": { - "admin": "Admin", - "demoUser": "Demo user", - "superAdmin": "Super admin", - "teamMember": "Team member" - }, - "save": "Save", - "sendInvite": "Send invite", - "selectAll": "Select all", - "settingsFailedToClearStats": "Failed to clear stats", - "settingsFailedToDeleteMonitors": "Failed to delete all monitors", - "settingsFailedToSave": "Failed to save settings", - "settingsGeneralSettings": "General settings", - "settingsMonitorsDeleted": "Successfully deleted all monitors", - "settingsPage": { - "aboutSettings": { - "labelDevelopedBy": "Developed by Bluewave Labs", - "title": "About" - }, - "demoMonitorsSettings": { - "buttonAddMonitors": "Add demo monitors", - "description": "Add sample monitors for demonstration purposes.", - "title": "Demo monitors" - }, - "emailSettings": { - "buttonSendTestEmail": "Send test e-mail", - "description": "Configure the email settings for your system. This is used to send notifications and alerts.", - "descriptionTransport": "This builds an SMTP transport for NodeMailer", - "labelAddress": "Email address - Used for authentication", - "labelConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting", - "labelHost": "Email host - Hostname or IP address to connect to", - "labelIgnoreTLS": "Disable STARTTLS: Don't use TLS even if the server supports it", - "labelPassword": "Email password - Password for authentication", - "labelPasswordSet": "Password is set. Click Reset to change it.", - "labelPool": "Enable connection pooling: Reuse existing connections to improve performance", - "labelPort": "Email port - Port to connect to", - "labelRejectUnauthorized": "Reject invalid certificates: Reject connections with self-signed or untrusted certificates", - "labelRequireTLS": "Force STARTTLS: Require TLS upgrade, fail if not supported", - "labelSecure": "Use SSL (recommended): Encrypt the connection using SSL/TLS", - "labelTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP", - "labelUser": "Email user - Username for authentication, overrides email address if specified", - "linkTransport": "See specifications here", - "placeholderUser": "Leave empty if not required", - "title": "Email", - "toastEmailRequiredFieldsError": "Email address, host, port and password are required" - }, - "globalThresholds": { - "description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring.", - "title": "Global Thresholds" - }, - "pageSpeedSettings": { - "description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.", - "labelApiKey": "PageSpeed API key", - "labelApiKeySet": "API key is set. Click Reset to change it.", - "title": "Google PageSpeed API key" - }, - "saveButtonLabel": "Save", - "statsSettings": { - "clearAllStatsButton": "Clear all stats", - "clearAllStatsDescription": "Clear all stats. This is irreversible.", - "clearAllStatsDialogConfirm": "Yes, clear all stats", - "clearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.", - "clearAllStatsDialogTitle": "Do you want to clear all stats?", - "description": "Define how long you want to retain historical data. You can also clear all existing data.", - "title": "Monitor history" - }, - "systemResetSettings": { - "buttonRemoveAllMonitors": "Remove all monitors", - "description": "Remove all monitors from your system.", - "dialogConfirm": "Yes, remove all monitors", - "dialogTitle": "Do you want to remove all monitors?", - "title": "System reset" - }, - "timezoneSettings": { - "description": "Select the timezone used to display dates and times throughout the application.", - "title": "Display timezone" - }, - "title": "Settings", - "uiSettings": { - "chartTypeHeatmap": "Heatmap", - "chartTypeHistogram": "Histogram", - "description": "Switch between light and dark mode, or change user interface language.", - "labelChartType": "Chart type", - "labelLanguage": "Language", - "labelTheme": "Theme mode", - "title": "Appearance" - }, - "urlSettings": { - "description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.", - "label": "Display IP/URL on status page", - "selectDisabled": "Disabled", - "selectEnabled": "Enabled", - "title": "Monitor IP/URL on Status Page" - } - }, - "settingsStatsCleared": "Stats cleared successfully", - "settingsSuccessSaved": "Settings saved successfully", - "settingsTestEmailFailed": "Failed to send test email", - "settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}", - "settingsTestEmailSuccess": "Test email sent successfully", - "settingsTestEmailUnknownError": "Unknown error", - "shown": "Shown", - "starPromptDescription": "See the latest releases and help grow the community on GitHub", - "starPromptTitle": "Star Checkmate", - "startTime": "Start time", - "state": "State", - "status": "Status", - "statusCode": "Status code", - "statusPage": { - "contents": "Contents", - "deleteFailed": "Failed to delete status page", - "deleteSuccess": "Status page deleted successfully", - "generalSettings": "General settings", - "details": { - "statusHeader": { - "allUp": "All systems operational", - "allDown": "All systems are experiencing issues", - "anyDown": "Some systems are experiencing issues" - } - } - }, - "statusPageCreate": { - "buttonSave": "Save" - }, - "statusPageStatusServiceStatus": "Service status", - "submit": "Submit", - "SupportedFormats": "Supported formats", - "teamPanel": { - "addTeamMember": { - "addButton": "Add Member", - "addMemberMenu": "Add Team Member", - "description": "Create a new user and share the credentials with them. This method gives the member immediate access to all monitors.", - "title": "Register new team member" - }, - "addMember": "Add member", - "cancel": "Cancel", - "changeTeamPassword": { - "changePasswordMenu": "Reset Password", - "description": "Create a new password for this team member. You will need to share the password with them securely.", - "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way.", - "title": "Reset team member password" - }, - "email": "Email", - "emailToken": "E-mail token", - "filter": { - "all": "All", - "member": "Member" - }, - "getToken": "Get token", - "inviteDescription": "When you add a new team member, they will get access to all monitors.", - "inviteLink": "Invite link", - "inviteNewTeamMember": "Invite new team member", - "inviteTeamMember": "Invite a team member", - "noMembers": "There are no team members with this role", - "register": "Register a team member", - "registerTeamMember": { - "auth": { - "common": { - "inputs": { - "role": { - "errors": { - "empty": "Role is required" - } - } - } - } - } - }, - "registerToast": { - "success": "User created, share credentials with the member securely." - }, - "role": "Role", - "selectRole": "Select role", - "table": { - "created": "Created", - "email": "Email", - "name": "Name", - "role": "Role" - }, - "teamMembers": "Team members" - }, - "time": { - "fifteenMinutes": "15 minutes", - "fifteenSeconds": "15 seconds", - "fiveMinutes": "5 minutes", - "fourMinutes": "4 minutes", - "oneMinute": "1 minute", - "tenMinutes": "10 minutes", - "thirtyMinutes": "30 minutes", - "thirtySeconds": "30 seconds", - "threeMinutes": "3 minutes", - "twoMinutes": "2 minutes" - }, - "timezone": "Timezone", - "timeZoneInfo": "All dates and times are in GMT+0 time zone.", - "title": "Title", - "total": "Total", - "type": "Type", - "update": "Update", - "uptime": "Uptime", - "url": "URL", - "used": "Used", - "window": "window", - "YourPhoto": "Profile photo", - "failedDeleteMonitor": "Failed to delete monitor", - "failedPauseMonitor": "Failed to pause monitor", - "hour": "hour", - "minute": "minute", - "monitorDeleted": "Monitor deleted successfully", - "monitorPaused": "Monitor paused successfully", - "monitorResumed": "Monitor resumed successfully", - "name": "Name" + } } diff --git a/client/src/main.tsx b/client/src/main.tsx index 1e339f87b..f075886d3 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -5,7 +5,7 @@ import { BrowserRouter as Router } from "react-router-dom"; import { Provider } from "react-redux"; import { persistor, store } from "@/store.js"; import { PersistGate } from "redux-persist/integration/react"; -import I18nLoader from "./Components/v1/I18nLoader/index.jsx"; +import I18nLoader from "./Components/v2/i18nLoader"; import { initApiClient } from "./Utils/ApiClient.js"; initApiClient(store); diff --git a/scripts/flatten-locales.js b/scripts/flatten-locales.js new file mode 100644 index 000000000..ba8d5d16c --- /dev/null +++ b/scripts/flatten-locales.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +function flattenKeys(obj, prefix = "", result = []) { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const newKey = prefix ? `${prefix}.${key}` : key; + + if ( + typeof obj[key] === "object" && + obj[key] !== null && + !Array.isArray(obj[key]) + ) { + flattenKeys(obj[key], newKey, result); + } else { + result.push(newKey); + } + } + } + return result; +} + +// Paths +const localesDir = path.join(__dirname, "../client/src/locales"); +const inputPath = path.join(localesDir, "en.json"); +const outputPath = path.join(localesDir, "keys.json"); + +try { + const jsonContent = fs.readFileSync(inputPath, "utf8"); + const jsonData = JSON.parse(jsonContent); + + const flattenedKeys = flattenKeys(jsonData); + flattenedKeys.sort(); + + fs.writeFileSync(outputPath, JSON.stringify(flattenedKeys, null, 2), "utf8"); + + console.log(`Keys written to: ${outputPath}`); +} catch (error) { + console.error(error.message); + process.exit(1); +} diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 7a20ed93c..8a83a0f63 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -30,7 +30,7 @@ export const initializeControllers = (services: InitializedServices): Initialize return { authController: new AuthController(services.userService), monitorController: new MonitorController(services.monitorService), - settingsController: new SettingsController(services.settingsService, services.emailService, services.db), + settingsController: new SettingsController(services.settingsService, services.emailService), checkController: new CheckController(services.checkService), inviteController: new InviteController(services.inviteService), maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService), diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 9bf18ff1d..f88bc2584 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -41,18 +41,6 @@ import crypto from "crypto"; import { games, GameDig } from "gamedig"; import jmespath from "jmespath"; -// DB Modules -import { GenerateAvatarImage } from "../utils/imageProcessing.js"; -import { ParseBoolean } from "../utils/utils.js"; - -// Models -import InviteToken from "../db/models/Invite.js"; -import Team from "../db/models/Team.js"; -import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; -import MonitorStats from "../db/models/MonitorStats.js"; -import NotificationModel from "../db/models/Notification.js"; -import RecoveryToken from "../db/models/RecoveryToken.js"; - // repositories import { MongoMonitorsRepository, @@ -176,7 +164,7 @@ export const initializeServices = async ({ const bufferService = new BufferService({ logger, checkService, settingsService }); - const statusService = new StatusService({ logger, buffer: bufferService, monitorsRepository }); + const statusService = new StatusService(logger, bufferService, monitorsRepository, monitorStatsRepository, checksRepository); const webhookProvider = new WebhookProvider(logger); const slackProvider = new SlackProvider(logger); diff --git a/server/src/controllers/settingsController.ts b/server/src/controllers/settingsController.ts index 0a01daa0c..3cbfd3f6d 100644 --- a/server/src/controllers/settingsController.ts +++ b/server/src/controllers/settingsController.ts @@ -8,11 +8,9 @@ class SettingsController { static SERVICE_NAME = SERVICE_NAME; private settingsService: any; private emailService: any; - private db: any; - constructor(settingsService: any, emailService: any, db: any) { + constructor(settingsService: any, emailService: any) { this.settingsService = settingsService; this.emailService = emailService; - this.db = db; } get serviceName() { diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 74def03c2..b75b18f13 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -11,6 +11,7 @@ import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory export interface IChecksRepository { // create + create(check: Check): Promise; createChecks(checks: Check[]): Promise; // single fetch diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index a36788e2c..c9cdbffd6 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -227,6 +227,11 @@ class MongoChecksRepository implements IChecksRepository { } as unknown as CheckDocument; }; + create = async (check: Check) => { + const savedCheck = await CheckModel.create(check); + return this.toEntity(savedCheck); + }; + createChecks = async (checks: Check[]) => { const docs = checks.map((check) => this.toDocument(check)); const inserted = await CheckModel.insertMany(docs); diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts index 08309b4ee..c718e7eb3 100644 --- a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -1,6 +1,7 @@ import type { MonitorStats } from "@/types/index.js"; export interface IMonitorStatsRepository { // create + create(data: Omit): Promise; // single fetch findByMonitorId(monitorId: string): Promise; // update diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts index 52dbe8aa5..7211c7d89 100644 --- a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -32,6 +32,11 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository { }; }; + create = async (data: Omit): Promise => { + const created = await MonitorStatsModel.create(data); + return this.toEntity(created); + }; + findByMonitorId = async (monitorId: string): Promise => { const monitorStats = await MonitorStatsModel.findOne({ monitorId: new mongoose.Types.ObjectId(monitorId) }); if (!monitorStats) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 6cd1a7675..6c9e9264a 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -86,7 +86,15 @@ class SuperSimpleQueueHelper { // Step 3. Build check const check = await this.checkService.buildCheck(status); - + if (!check) { + this.logger.warn({ + message: `No check could be built for monitor ${monitorId}`, + service: SERVICE_NAME, + method: "getMonitorJob", + details: { code: status.code, message: status.message }, + }); + return; + } // Step 4 Add check to buffer this.buffer.addToBuffer({ check }); // Step 4. Update monitor status diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 461001800..415c22281 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -2,7 +2,14 @@ import type { Check } from "@/types/index.js"; const SERVICE_NAME = "BufferService"; -class BufferService { +export interface IBufferService { + addToBuffer(check: Check): void; + removeCheckFromBuffer(check: Check): boolean; + scheduleNextFlush(): void; + flushBuffer(): Promise; +} + +class BufferService implements IBufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; private logger: any; @@ -13,7 +20,6 @@ class BufferService { constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) { this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute - console.log(this.BUFFER_TIMEOUT); this.logger = logger; this.checksService = checkService; this.SERVICE_NAME = SERVICE_NAME; @@ -30,7 +36,7 @@ class BufferService { return BufferService.SERVICE_NAME; } - addToBuffer({ check }: { check: Check }) { + addToBuffer(check: Check) { try { this.buffer.push(check); } catch (error: any) { diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 284fd3ff7..0ab47f85b 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -1,6 +1,4 @@ -import { IMonitorsRepository } from "@/repositories/index.js"; -import MonitorStats from "../../db/models/MonitorStats.js"; -import { CheckModel } from "@/db/models/index.js"; +import { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js"; import type { Monitor, MonitorStatus, @@ -10,7 +8,9 @@ import type { HardwareStatusPayload, PageSpeedStatusPayload, CheckSnapshot, + MonitorStats, } from "@/types/index.js"; +import { ILogger } from "@/utils/logger.js"; const SERVICE_NAME = "StatusService"; export interface IStatusService { @@ -27,11 +27,21 @@ export class StatusService implements IStatusService { private logger: any; private buffer: any; private monitorsRepository: IMonitorsRepository; + private monitorStatsRepository: IMonitorStatsRepository; + private checksRepository: IChecksRepository; - constructor({ logger, buffer, monitorsRepository }: { logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) { + constructor( + logger: ILogger, + buffer: any, + monitorsRepository: IMonitorsRepository, + monitorStatsRepository: IMonitorStatsRepository, + checksRepository: IChecksRepository + ) { this.logger = logger; this.buffer = buffer; this.monitorsRepository = monitorsRepository; + this.monitorStatsRepository = monitorStatsRepository; + this.checksRepository = checksRepository; } get serviceName() { @@ -42,18 +52,29 @@ export class StatusService implements IStatusService { try { const monitorId = monitor.id; const { responseTime, status } = networkResponse; - // Get stats - let stats = await MonitorStats.findOne({ monitorId }); + let stats: Omit | null = null; + stats = await this.monitorStatsRepository + .findByMonitorId(monitorId) + .then((result) => result) + .catch(() => { + this.logger.debug({ + service: SERVICE_NAME, + method: "updateRunningStats", + message: `No existing stats found for monitor ${monitorId}, initializing new stats.`, + }); + return null; + }); if (!stats) { - stats = new MonitorStats({ + stats = { monitorId, avgResponseTime: 0, totalChecks: 0, totalUpChecks: 0, totalDownChecks: 0, uptimePercentage: 0, - lastCheck: null, - }); + lastResponseTime: 0, + lastCheckTimestamp: 0, + }; } // Update stats @@ -96,8 +117,7 @@ export class StatusService implements IStatusService { // latest check stats.lastCheckTimestamp = new Date().getTime(); - - await stats.save(); + await this.monitorStatsRepository.create(stats); return true; } catch (error: any) { this.logger.error({ @@ -112,11 +132,10 @@ export class StatusService implements IStatusService { handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => { try { - let savedCheck; + let savedCheck: Check | null = null; if (!check.id) { try { - const checkModel = new CheckModel(check); - savedCheck = await checkModel.save(); + savedCheck = await this.checksRepository.create(check); this.buffer.removeCheckFromBuffer(check); } catch (checkError: any) {